Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package meteordevelopment.meteorclient.renderer.text;

import meteordevelopment.meteorclient.utils.render.FontUtils;
import org.jspecify.annotations.NullMarked;

import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

public class BuiltinFontFace extends FontFace {
@NullMarked
public non-sealed class BuiltinFontFace extends FontFace {
private final String name;

public BuiltinFontFace(FontInfo info, String name) {
Expand All @@ -14,10 +18,12 @@ public BuiltinFontFace(FontInfo info, String name) {
}

@Override
public InputStream toStream() {
InputStream in = FontUtils.stream(name);
if (in == null) throw new RuntimeException("Failed to load builtin font " + name + ".");
return in;
public ReadableByteChannel byteChannelForRead() {
InputStream inputStream = FontUtils.builtinFontStream(this.name);
if (inputStream == null) {
throw new IllegalArgumentException("Builtin font '" + this.name + "' not found");
}
return Channels.newChannel(inputStream);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
import meteordevelopment.meteorclient.renderer.MeshBuilder;
import meteordevelopment.meteorclient.renderer.MeshRenderer;
import meteordevelopment.meteorclient.renderer.MeteorRenderPipelines;
import meteordevelopment.meteorclient.utils.Utils;
import meteordevelopment.meteorclient.utils.render.color.Color;
import net.minecraft.client.MinecraftClient;
import org.lwjgl.BufferUtils;

import java.io.IOException;
import java.nio.ByteBuffer;

public class CustomTextRenderer implements TextRenderer {
Expand All @@ -30,11 +29,10 @@ public class CustomTextRenderer implements TextRenderer {
private double fontScale = 1;
private double scale = 1;

public CustomTextRenderer(FontFace fontFace) {
public CustomTextRenderer(FontFace fontFace) throws IOException {
this.fontFace = fontFace;

byte[] bytes = Utils.readBytes(fontFace.toStream());
ByteBuffer buffer = BufferUtils.createByteBuffer(bytes.length).put(bytes).flip();
ByteBuffer buffer = fontFace.readToDirectByteBuffer();

fonts = new Font[5];
for (int i = 0; i < fonts.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
package meteordevelopment.meteorclient.renderer.text;

import java.io.InputStream;
import meteordevelopment.meteorclient.utils.files.ByteBufferUtils;
import org.jspecify.annotations.NullMarked;
import org.lwjgl.BufferUtils;

public abstract class FontFace {
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;

@NullMarked
public abstract sealed class FontFace permits BuiltinFontFace, SystemFontFace {
public final FontInfo info;

protected FontFace(FontInfo info) {
this.info = info;
}

public abstract InputStream toStream();
public abstract ReadableByteChannel byteChannelForRead() throws IOException;

public final ByteBuffer readToDirectByteBuffer() throws IOException {
try (ReadableByteChannel channel = byteChannelForRead()) {
return ByteBufferUtils.readFully(channel, BufferUtils::createByteBuffer);
}
}

@Override
public String toString() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package meteordevelopment.meteorclient.renderer.text;

import meteordevelopment.meteorclient.utils.render.FontUtils;
import org.jspecify.annotations.NullMarked;

import java.io.InputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class SystemFontFace extends FontFace {
@NullMarked
public final class SystemFontFace extends FontFace {
private final Path path;

public SystemFontFace(FontInfo info, Path path) {
Expand All @@ -15,14 +19,8 @@ public SystemFontFace(FontInfo info, Path path) {
}

@Override
public InputStream toStream() {
if (!path.toFile().exists()) {
throw new RuntimeException("Tried to load font that no longer exists.");
}

InputStream in = FontUtils.stream(path.toFile());
if (in == null) throw new RuntimeException("Failed to load font from " + path + ".");
return in;
public ReadableByteChannel byteChannelForRead() throws IOException {
return FileChannel.open(this.path, StandardOpenOption.READ);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
import net.minecraft.util.Identifier;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.lwjgl.BufferUtils;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
Expand Down Expand Up @@ -298,10 +298,12 @@ private void onCustomFontChanged(CustomFontChangedEvent event) {
}

private static FontHolder loadFont(int height) {
byte[] data = Utils.readBytes(Fonts.RENDERER.fontFace.toStream());
ByteBuffer buffer = BufferUtils.createByteBuffer(data.length).put(data).flip();

return new FontHolder(new Font(buffer, height));
try {
ByteBuffer buffer = Fonts.RENDERER.fontFace.readToDirectByteBuffer();
return new FontHolder(new Font(buffer, height));
} catch (IOException e) {
throw new RuntimeException("Failed to load font: " + Fonts.RENDERER.fontFace, e);
}
}

private static class FontHolder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.utils.files;

import org.jspecify.annotations.NullMarked;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.function.IntFunction;

import static java.nio.file.Files.*;

@NullMarked
public final class ByteBufferUtils {
private ByteBufferUtils() {}

public static ByteBuffer readFully(Path path, IntFunction<ByteBuffer> allocator) throws IOException {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
long size = size(path);
if (size > Integer.MAX_VALUE) {
throw new IOException("File too large to read into ByteBuffer: " + path);
}
ByteBuffer buffer = allocator.apply((int) size);
while (buffer.hasRemaining()) {
int bytesRead = channel.read(buffer);
if (bytesRead == -1) break; // EOF
}
buffer.flip();
return buffer;
}
}

public static ByteBuffer readFully(ReadableByteChannel channel, IntFunction<ByteBuffer> allocator) throws IOException {
ByteBuffer buffer = requireCapacity(allocator.apply(8192), 8192);

while (true) {
int bytesRead = channel.read(buffer);

if (bytesRead == -1) break;

if (bytesRead == 0) {
// Avoid busy-spin on non-blocking channels.
// If buffer is full, grow; otherwise caller should probably be using blocking I/O.
if (!buffer.hasRemaining()) {
buffer = grow(buffer, allocator);
continue;
}
// In a "readFully" API, returning early is usually better than spinning forever.
// Alternative: Thread.onSpinWait(); continue;
break;
}

if (!buffer.hasRemaining()) {
buffer = grow(buffer, allocator);
}
}

buffer.flip();
return buffer;
}

private static ByteBuffer grow(ByteBuffer buffer, IntFunction<ByteBuffer> allocator) {
int oldCap = buffer.capacity();
int newCap = oldCap << 1;
if (newCap <= 0) throw new OutOfMemoryError("Buffer too large (overflow): " + oldCap);

ByteBuffer newBuffer = requireCapacity(allocator.apply(newCap), newCap);
buffer.flip();
newBuffer.put(buffer);
return newBuffer;
}

private static ByteBuffer requireCapacity(ByteBuffer buf, int minCap) {
if (buf.capacity() < minCap) {
throw new IllegalArgumentException("Allocator returned capacity " + buf.capacity() + " < " + minCap);
}
return buf;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,86 @@
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
* Copyright (c) Meteor Development.
*/

package meteordevelopment.meteorclient.utils.render;

import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import meteordevelopment.meteorclient.MeteorClient;
import meteordevelopment.meteorclient.renderer.Fonts;
import meteordevelopment.meteorclient.renderer.text.*;
import meteordevelopment.meteorclient.utils.Utils;
import meteordevelopment.meteorclient.utils.files.ByteBufferUtils;
import net.minecraft.util.Util;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.lwjgl.BufferUtils;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.stb.STBTruetype;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class FontUtils {
private FontUtils() {
@NullMarked
public final class FontUtils {
private FontUtils() {}

public static @Nullable FontInfo getSysFontInfo(File file) {
return getFontInfo(file);
}

public static FontInfo getSysFontInfo(File file) {
return getFontInfo(stream(file));
public static @Nullable FontInfo getBuiltinFontInfo(String builtin) {
return getFontInfo(builtinFontStream(builtin));
}

public static FontInfo getBuiltinFontInfo(String builtin) {
return getFontInfo(stream(builtin));
/**
* System font path: avoid heap byte[] by reading the file into a direct buffer.
*/
private static @Nullable FontInfo getFontInfo(@Nullable File file) {
if (file == null || !file.isFile()) return null;

try {
return getFontInfo(ByteBufferUtils.readFully(file.toPath(), BufferUtils::createByteBuffer));
} catch (Exception e) {
MeteorClient.LOG.warn("Failed to read font file: {}", file, e);
return null;
}
}

public static FontInfo getFontInfo(InputStream stream) {
/**
* Builtin/resource path: stream into a direct buffer (no byte[] intermediate).
*/
public static @Nullable FontInfo getFontInfo(@Nullable InputStream stream) {
if (stream == null) return null;

byte[] bytes = Utils.readBytes(stream);
if (bytes.length < 5) return null;
try (ReadableByteChannel ch = Channels.newChannel(stream)) {
ByteBuffer buf = ByteBufferUtils.readFully(ch, BufferUtils::createByteBuffer);
return getFontInfo(buf);
} catch (Exception e) {
MeteorClient.LOG.warn("Failed to read font stream.", e);
return null;
}
}

/**
* Core logic: interpret font data from a ByteBuffer.
* NOTE: This preserves your original 5-byte header check exactly.
*/
private static @Nullable FontInfo getFontInfo(ByteBuffer buffer) {
if (buffer.remaining() < 5) return null;

// Preserve existing check: 00 01 00 00 00
if (
bytes[0] != 0 ||
bytes[1] != 1 ||
bytes[2] != 0 ||
bytes[3] != 0 ||
bytes[4] != 0
buffer.get(0) != 0 ||
buffer.get(1) != 1 ||
buffer.get(2) != 0 ||
buffer.get(3) != 0 ||
buffer.get(4) != 0
) return null;

ByteBuffer buffer = BufferUtils.createByteBuffer(bytes.length).put(bytes).flip();
STBTTFontinfo fontInfo = STBTTFontinfo.create();
if (!STBTruetype.stbtt_InitFont(fontInfo, buffer)) return null;

Expand All @@ -65,7 +96,7 @@ public static FontInfo getFontInfo(InputStream stream) {
}

public static Set<String> getSearchPaths() {
Set<String> paths = new HashSet<>();
Set<String> paths = new ObjectOpenHashSet<>();
paths.add(System.getProperty("java.home") + "/lib/fonts");

for (File dir : getUFontDirs()) {
Expand Down Expand Up @@ -136,7 +167,7 @@ public static void loadSystem(List<FontFamily> fontList, File dir) {
}
}

public static boolean addFont(List<FontFamily> fontList, FontFace font) {
private static boolean addFont(List<FontFamily> fontList, @Nullable FontFace font) {
if (font == null) return false;

FontInfo info = font.info;
Expand All @@ -152,17 +183,8 @@ public static boolean addFont(List<FontFamily> fontList, FontFace font) {
return family.addFont(font);
}

public static InputStream stream(String builtin) {
return FontUtils.class.getResourceAsStream("/assets/" + MeteorClient.MOD_ID + "/fonts/" + builtin + ".ttf");
public static @Nullable InputStream builtinFontStream(String name) {
return FontUtils.class.getResourceAsStream("/assets/" + MeteorClient.MOD_ID + "/fonts/" + name + ".ttf");
}

public static InputStream stream(File file) {
try {
return new FileInputStream(file);
}
catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
}