mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
Backport modern F3 debug screen + debug overlays (#2301)
This commit is contained in:
1 parent
a019b7eed8
commit
9bbb0537ee
184 files changed
+14703
-886
No files matched your search
@@ -62,6 +62,7 @@ DependencyHandlerScope.of(dependencies).apply {
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterLoadingScreen:1.7.9-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.72-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.63:dev")
|
||||
compileOnly("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev") { isTransitive = false }
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:LogisticsPipes:1.5.32-GTNH:dev")
|
||||
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.64-GTNH:dev")
|
||||
|
||||
@@ -69,7 +69,13 @@ public final class SystemProperties {
|
||||
public static final boolean FLYBY_JFR = Boolean.getBoolean("angelica.flyby.jfr");
|
||||
public static final float FLYBY_PITCH = parseFloat("angelica.flyby.pitch");
|
||||
public static final int FLYBY_SCREENSHOTS = Integer.getInteger("angelica.flyby.screenshots", 0);
|
||||
public static final boolean FLYBY_DEBUG_HUD = Boolean.getBoolean("angelica.flyby.debugHud");
|
||||
public static final Boolean FLYBY_DEBUG_HUD = parseBooleanOverride("angelica.flyby.debugHud");
|
||||
public static final String FLYBY_DEBUG_PROFILE = System.getProperty("angelica.flyby.debugProfile", "");
|
||||
public static final String FLYBY_DEBUG_ENTRIES = System.getProperty("angelica.flyby.debugEntries", "");
|
||||
public static final Boolean FLYBY_DEBUG_FLAT = parseBooleanOverride("angelica.flyby.debugFlat");
|
||||
public static final Boolean FLYBY_HUD_CACHING = parseBooleanOverride("angelica.flyby.hudCaching");
|
||||
public static final Boolean FLYBY_FPS_GRAPH = parseBooleanOverride("angelica.flyby.fpsGraph");
|
||||
public static final boolean FLYBY_DEBUG_DUMP = Boolean.getBoolean("angelica.flyby.debugDump");
|
||||
public static final String FLYBY_CRASH_TEST = System.getProperty("angelica.flyby.crashTest", "");
|
||||
|
||||
// Debug
|
||||
|
||||
+6
-1
@@ -42,6 +42,9 @@ import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetAddress;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.Pointer.POINTER_SIZE;
|
||||
import static org.taumc.celeritas.lwjgl.LWJGLServiceProvider.LWJGL;
|
||||
|
||||
/**
|
||||
@@ -380,7 +383,9 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
|
||||
@Override
|
||||
public void multiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
|
||||
throw new UnsupportedOperationException("GLRenderBackend.multiDrawElementsBaseVertex should not be called — LWJGL service handles this");
|
||||
for (int i = 0; i < drawcount; i++) {
|
||||
GL32.glDrawElementsBaseVertex(mode, memGetInt(pCount + 4L * i), type, memGetAddress(pIndices + (long) i * POINTER_SIZE), memGetInt(pBaseVertex + 4L * i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public abstract class RenderBackend {
|
||||
|
||||
public static final int MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
public void onPostWindowCreate(long window) {}
|
||||
|
||||
public abstract void init();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
|
||||
/**
|
||||
* Generates GLSL 330 core vertex shaders for FFP emulation.
|
||||
@@ -19,6 +20,9 @@ public final class VertexShaderGenerator {
|
||||
emitAttributes(sb, key);
|
||||
emitUniforms(sb, key);
|
||||
emitOutputs(sb, key);
|
||||
if (key.lineQuads()) {
|
||||
sb.append(ShaderLoader.getShaderSource("angelica:include/line_quads.glsl")).append('\n');
|
||||
}
|
||||
|
||||
sb.append("void main() {\n");
|
||||
if (key.instancing() == Instancing.WEATHER) {
|
||||
@@ -142,7 +146,7 @@ public final class VertexShaderGenerator {
|
||||
} else {
|
||||
sb.append(" gl_Position = u_MVPMatrix * pos4;\n");
|
||||
if (key.lineQuads()) {
|
||||
emitLineQuadWidening(sb);
|
||||
emitLineQuadWidening(sb, key);
|
||||
}
|
||||
|
||||
// Eye position needed for lighting, fog, EYE_LINEAR texgen, and clip planes
|
||||
@@ -156,42 +160,15 @@ public final class VertexShaderGenerator {
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
private static void emitLineQuadWidening(StringBuilder sb) {
|
||||
private static void emitLineQuadWidening(StringBuilder sb, VertexKey key) {
|
||||
sb.append(" int lineCorner = gl_VertexID % 6;\n");
|
||||
sb.append(" bool lineStart = lineCorner == 0 || lineCorner == 1 || lineCorner == 3;\n");
|
||||
sb.append(" bool lineStart = angelica_lineStart(lineCorner);\n");
|
||||
sb.append(" vec4 lineOther = u_MVPMatrix * vec4(a_LineOtherEnd, 1.0);\n");
|
||||
sb.append(" vec4 lineC0 = lineStart ? gl_Position : lineOther;\n");
|
||||
sb.append(" vec4 lineC1 = lineStart ? lineOther : gl_Position;\n");
|
||||
sb.append(" vec4 lineCenter = lineC0;\n");
|
||||
sb.append(" float lineT = 0.0;\n");
|
||||
sb.append(" float lineD0 = lineC0.z + lineC0.w;\n");
|
||||
sb.append(" float lineD1 = lineC1.z + lineC1.w;\n");
|
||||
sb.append(" if (lineD0 < 0.0 && lineD1 < 0.0) {\n");
|
||||
sb.append(" gl_Position = vec4(0.0, 0.0, 2.0, 1.0);\n");
|
||||
sb.append(" } else {\n");
|
||||
sb.append(" float lineT0 = lineD0 < 0.0 ? lineD0 / (lineD0 - lineD1) : 0.0;\n");
|
||||
sb.append(" float lineT1 = lineD1 < 0.0 ? lineD0 / (lineD0 - lineD1) : 1.0;\n");
|
||||
sb.append(" lineT = lineStart ? lineT0 : lineT1;\n");
|
||||
sb.append(" vec4 lineP0 = mix(lineC0, lineC1, lineT0);\n");
|
||||
sb.append(" vec4 lineP1 = mix(lineC0, lineC1, lineT1);\n");
|
||||
sb.append(" vec2 lineN0 = lineP0.xy / lineP0.w;\n");
|
||||
sb.append(" vec2 lineN1 = lineP1.xy / lineP1.w;\n");
|
||||
sb.append(" vec2 linePixel = 2.0 / u_ViewportSize;\n");
|
||||
sb.append(" float lineHalfWidth = 0.5 * u_LineWidth;\n");
|
||||
sb.append(" vec2 lineOffset;\n");
|
||||
sb.append(" vec2 lineShift;\n");
|
||||
sb.append(" vec2 lineSpan = abs(lineN1 - lineN0) * u_ViewportSize;\n");
|
||||
sb.append(" if (lineSpan.x > lineSpan.y) {\n");
|
||||
sb.append(" lineOffset = vec2(0.0, lineHalfWidth * linePixel.y);\n");
|
||||
sb.append(" lineShift = vec2(lineN0.x < lineN1.x ? -0.5 : 0.5, -0.125) * linePixel;\n");
|
||||
sb.append(" } else {\n");
|
||||
sb.append(" lineOffset = vec2(lineHalfWidth * linePixel.x, 0.0);\n");
|
||||
sb.append(" lineShift = vec2(0.125, lineN0.y < lineN1.y ? -0.5 : 0.5) * linePixel;\n");
|
||||
sb.append(" }\n");
|
||||
sb.append(" vec4 lineP = lineStart ? lineP0 : lineP1;\n");
|
||||
sb.append(" vec2 lineN = lineStart ? lineN0 : lineN1;\n");
|
||||
sb.append(" gl_Position = vec4((lineN + (lineCorner % 2 == 0 ? lineOffset : -lineOffset) + lineShift) * lineP.w, lineP.z, lineP.w);\n");
|
||||
sb.append(" }\n");
|
||||
if (key.lineStipple()) {
|
||||
sb.append(" vec4 lineCenter = lineStart ? gl_Position : lineOther;\n");
|
||||
}
|
||||
sb.append(" float lineT;\n");
|
||||
sb.append(" gl_Position = angelica_lineQuad(gl_Position, lineOther, lineCorner, u_ViewportSize, u_LineWidth, lineT);\n");
|
||||
}
|
||||
|
||||
private static void emitNormalTransform(StringBuilder sb, VertexKey key) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
|
||||
import com.gtnewhorizons.angelica.glsm.GLContextState;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
|
||||
@@ -122,18 +121,14 @@ public final class DisplayListVBO {
|
||||
}
|
||||
|
||||
private void renderLineQuads() {
|
||||
final GLContextState glCtx = GLStateManager.ctx();
|
||||
lineQuads.bind();
|
||||
VAOManager.setCurrentVertexFlags(lineQuadFlags);
|
||||
final boolean culled = LineQuads.disableCulling();
|
||||
glCtx.lineQuadsActive = true;
|
||||
final boolean culled = LineQuads.beginDraw(lineQuadFlags);
|
||||
FfpExtendedAttribs.beginInternalDraw();
|
||||
try {
|
||||
lineQuads.draw(GL11.GL_TRIANGLES, lineQuadStart, lineQuadCount);
|
||||
} finally {
|
||||
FfpExtendedAttribs.endInternalDraw();
|
||||
glCtx.lineQuadsActive = false;
|
||||
LineQuads.restoreCulling(culled);
|
||||
LineQuads.endDraw(culled);
|
||||
}
|
||||
lineQuads.unbind();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
|
||||
@@ -72,6 +73,18 @@ public final class LineQuads {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean beginDraw(int vertexFlags) {
|
||||
VAOManager.setCurrentVertexFlags(vertexFlags);
|
||||
final boolean culled = disableCulling();
|
||||
GLStateManager.ctx().lineQuadsActive = true;
|
||||
return culled;
|
||||
}
|
||||
|
||||
public static void endDraw(boolean culled) {
|
||||
GLStateManager.ctx().lineQuadsActive = false;
|
||||
restoreCulling(culled);
|
||||
}
|
||||
|
||||
public static VertexFormat quadFormat(VertexFormat lineFormat) {
|
||||
return QUAD_FORMATS[lineFormat.getVertexFlags() | VertexFlags.NORMAL_BIT];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
bool angelica_lineStart(int corner) {
|
||||
return corner == 0 || corner == 1 || corner == 3;
|
||||
}
|
||||
|
||||
vec4 angelica_lineQuad(vec4 clipThis, vec4 clipOther, int corner, vec2 viewport, float width, out float lineT) {
|
||||
bool lineStart = angelica_lineStart(corner);
|
||||
vec4 lineC0 = lineStart ? clipThis : clipOther;
|
||||
vec4 lineC1 = lineStart ? clipOther : clipThis;
|
||||
lineT = 0.0;
|
||||
float lineD0 = lineC0.z + lineC0.w;
|
||||
float lineD1 = lineC1.z + lineC1.w;
|
||||
if (lineD0 < 0.0 && lineD1 < 0.0) {
|
||||
return vec4(0.0, 0.0, 2.0, 1.0);
|
||||
} else {
|
||||
float lineT0 = lineD0 < 0.0 ? lineD0 / (lineD0 - lineD1) : 0.0;
|
||||
float lineT1 = lineD1 < 0.0 ? lineD0 / (lineD0 - lineD1) : 1.0;
|
||||
lineT = lineStart ? lineT0 : lineT1;
|
||||
vec4 lineP0 = mix(lineC0, lineC1, lineT0);
|
||||
vec4 lineP1 = mix(lineC0, lineC1, lineT1);
|
||||
vec2 lineN0 = lineP0.xy / lineP0.w;
|
||||
vec2 lineN1 = lineP1.xy / lineP1.w;
|
||||
vec2 linePixel = 2.0 / viewport;
|
||||
float lineHalfWidth = 0.5 * width;
|
||||
vec2 lineOffset;
|
||||
vec2 lineShift;
|
||||
vec2 lineSpan = abs(lineN1 - lineN0) * viewport;
|
||||
if (lineSpan.x > lineSpan.y) {
|
||||
lineOffset = vec2(0.0, lineHalfWidth * linePixel.y);
|
||||
lineShift = vec2(lineN0.x < lineN1.x ? -0.5 : 0.5, -0.125) * linePixel;
|
||||
} else {
|
||||
lineOffset = vec2(lineHalfWidth * linePixel.x, 0.0);
|
||||
lineShift = vec2(0.125, lineN0.y < lineN1.y ? -0.5 : 0.5) * linePixel;
|
||||
}
|
||||
vec4 lineP = lineStart ? lineP0 : lineP1;
|
||||
vec2 lineN = lineStart ? lineN0 : lineN1;
|
||||
return vec4((lineN + (corner % 2 == 0 ? lineOffset : -lineOffset) + lineShift) * lineP.w, lineP.z, lineP.w);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
# ALSO_JFR=1 - with a .tracy output, also record a JFR alongside it (timings inflated, correlation only).
|
||||
# ASPROF_OPTS - comma separated async-profiler agent opts, passed through when a JFR is recorded.
|
||||
#
|
||||
# FLYBY_FULLSCREEN=0 - keep options.txt's window mode. Default: fullscreen:true for the run (avoids vsync pacing), restored on exit.
|
||||
#
|
||||
# The output extension selects the profiler: .tracy for Tracy, .jfr for an in-process async-profiler
|
||||
# JFR that starts near the end of warmup and stops when the measured run ends.
|
||||
#
|
||||
@@ -32,6 +34,9 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$(dirname "$0")/flyby-lib.sh"
|
||||
|
||||
OUT="${1:?usage: flyby-capture.sh <output.tracy|output.jfr> [route] [length] [extra gradle args...]}"
|
||||
ROUTE="${2:-straight}"
|
||||
LENGTH="${3:-0}"
|
||||
@@ -148,12 +153,40 @@ if [ "$MODE" = "tracy" ]; then
|
||||
if kill -0 "$CAPTURE_PID" 2>/dev/null; then
|
||||
echo "==> stopping capture"
|
||||
kill -INT "$CAPTURE_PID" 2>/dev/null || true
|
||||
local waited=0
|
||||
while kill -0 "$CAPTURE_PID" 2>/dev/null && { { [ -e "$OUT" ] && [ "$waited" -lt 600 ]; } || [ "$waited" -lt 120 ]; }; do
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
if kill -0 "$CAPTURE_PID" 2>/dev/null; then
|
||||
echo "==> capture did not stop after ${waited}s (limit 120s before output exists, 600s after), killing it" >&2
|
||||
kill -9 "$CAPTURE_PID" 2>/dev/null || true
|
||||
fi
|
||||
wait "$CAPTURE_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OPTIONS_TXT="$ROOT/run/client/options.txt"
|
||||
FS_DIR="$(mktemp -d)"
|
||||
|
||||
on_exit() {
|
||||
if [ -n "$CAPTURE_PID" ]; then
|
||||
cleanup
|
||||
fi
|
||||
if [ "$FULLSCREEN" != 0 ]; then
|
||||
flyby_fullscreen_restore "$OPTIONS_TXT" "$FS_DIR/options.txt.bak" || true
|
||||
fi
|
||||
rm -rf "$FS_DIR"
|
||||
}
|
||||
FULLSCREEN="${FLYBY_FULLSCREEN:-1}"
|
||||
if [ "$FULLSCREEN" != 0 ]; then
|
||||
flyby_fullscreen_on "$OPTIONS_TXT" "$FS_DIR/options.txt.bak"
|
||||
fi
|
||||
trap on_exit EXIT
|
||||
|
||||
echo "==> launching game (route=$ROUTE warmup=$WARMUP length=${LENGTH:-default} scene=${SCENE_ABS:-none})"
|
||||
if [ -n "$JFR_OUT" ]; then
|
||||
echo "==> recording JFR -> $JFR_ABS"
|
||||
@@ -178,7 +211,6 @@ fi
|
||||
echo "==> game exited, finalising capture"
|
||||
if [ "$MODE" = "tracy" ]; then
|
||||
cleanup
|
||||
trap - EXIT
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "jfr" ]; then
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared file helpers for the flyby-*.sh scripts. Sourced, not executed: . "$(dirname "$0")/flyby-lib.sh"
|
||||
#
|
||||
# flyby_set_key <file> <key> <value> [sep] replace the first line starting with <key><sep> (default sep ":"), else append; creates the file
|
||||
# flyby_copy <src> <dst> copy bytes and verify with a compare
|
||||
# flyby_backup_file <file> <backup> copy to <backup>; if <file> is absent, remove any stale <backup> (records absence)
|
||||
# flyby_restore_file <backup> <file> flyby_backup_file with swapped arguments; prints one error line and returns 1 on failure.
|
||||
# Trap callers append '|| true'. Removes <file> if <backup> is absent: only call it after the backup was made.
|
||||
# flyby_fullscreen_on <options.txt> <backup> backs up, sets fullscreen:true
|
||||
# flyby_fullscreen_restore <options.txt> <backup> restores the backup; idempotent, call only after flyby_fullscreen_on
|
||||
# flyby_fresh_dir <dir> remove <dir> and recreate it empty
|
||||
# flyby_collect <run_dir> <variant_dir> <log>... copy <run_dir>/logs/<log> and <run_dir>/screenshots/flyby-shot-*.png into <variant_dir>
|
||||
# flyby_crash_list <dir> sorted names in <dir>, nothing if it is absent
|
||||
# flyby_new_crashes <dir> <before> <after> write the current crash list to <after>, print how many names are not in <before>
|
||||
|
||||
flyby_set_key() {
|
||||
python3 - "$1" "$2" "$3" "${4:-:}" <<'PY'
|
||||
import os, re, sys
|
||||
path, key, value, sep = sys.argv[1:5]
|
||||
text = ""
|
||||
if os.path.isfile(path):
|
||||
with open(path, newline="") as f:
|
||||
text = f.read()
|
||||
pat = re.compile(r"^" + re.escape(key + sep) + r"[^\r\n]*", re.M)
|
||||
line = key + sep + value
|
||||
if pat.search(text):
|
||||
text = pat.sub(lambda _: line, text, count=1)
|
||||
else:
|
||||
if text and not text.endswith("\n"):
|
||||
text += "\n"
|
||||
text += line + "\n"
|
||||
with open(path, "w", newline="") as f:
|
||||
f.write(text)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_copy() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import filecmp, shutil, sys
|
||||
src, dst = sys.argv[1:3]
|
||||
shutil.copyfile(src, dst)
|
||||
if not filecmp.cmp(src, dst, shallow=False):
|
||||
sys.exit("error: copy differs: " + dst)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_backup_file() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import filecmp, os, shutil, sys
|
||||
path, backup = sys.argv[1:3]
|
||||
if os.path.isfile(path):
|
||||
shutil.copyfile(path, backup)
|
||||
if not filecmp.cmp(path, backup, shallow=False):
|
||||
sys.exit("error: backup differs: " + backup)
|
||||
elif os.path.isfile(backup):
|
||||
os.remove(backup)
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_restore_file() {
|
||||
flyby_backup_file "$1" "$2" || { echo "error: restore failed or differs: $2" >&2; return 1; }
|
||||
}
|
||||
|
||||
flyby_fullscreen_on() {
|
||||
flyby_backup_file "$1" "$2"
|
||||
flyby_set_key "$1" fullscreen true :
|
||||
}
|
||||
|
||||
flyby_fullscreen_restore() {
|
||||
flyby_restore_file "$2" "$1"
|
||||
}
|
||||
|
||||
flyby_fresh_dir() {
|
||||
python3 -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' "$1"
|
||||
mkdir -p "$1"
|
||||
}
|
||||
|
||||
flyby_collect() {
|
||||
python3 - "$@" <<'PY'
|
||||
import glob, os, shutil, sys
|
||||
run, vdir = sys.argv[1:3]
|
||||
for name in sys.argv[3:]:
|
||||
src = os.path.join(run, "logs", name)
|
||||
if os.path.isfile(src):
|
||||
shutil.copyfile(src, os.path.join(vdir, name))
|
||||
for src in glob.glob(os.path.join(run, "screenshots", "flyby-shot-*.png")):
|
||||
shutil.copyfile(src, os.path.join(vdir, os.path.basename(src)))
|
||||
PY
|
||||
}
|
||||
|
||||
flyby_crash_list() {
|
||||
if [ -d "$1" ]; then
|
||||
ls -1 "$1" | sort
|
||||
fi
|
||||
}
|
||||
|
||||
flyby_new_crashes() {
|
||||
flyby_crash_list "$1" >| "$3"
|
||||
comm -13 "$2" "$3" | wc -l | tr -d ' '
|
||||
}
|
||||
Executable
+480
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env bash
|
||||
# Unattended flyby run of a test Angelica jar inside the real Prism GTNH instance, one launch per variant
|
||||
#
|
||||
# Usage: scripts/flyby-prism.sh <name> <variant>...
|
||||
# Variants: sdl-off | sdl-pack | gl-off | gl-pack (SDL-GPU vs OpenGL x shader pack off vs on)
|
||||
# gl-pack writes <pack>.txt from the pack's profile.HIGH first (never Ultra on OpenGL) and fails before launching without it.
|
||||
# Output: tmp/prism/<name>/<variant>/ (fml-client-latest.log, flyby-shot-*.png, console from the checker).
|
||||
# Prints one PASS/FAIL line per variant; exits non-zero if any fails. A run that never reaches the world
|
||||
# (no 'Flyby: world saving is off' in the log) is FAILED and stops the batch.
|
||||
# Needs a hands-off machine: the game must keep focus (waitForFocus) and runs fullscreen.
|
||||
#
|
||||
# PRISM_INSTANCE - instance id (default: GTNH-daily-2026-10-01+766-mmcprism-java17-26).
|
||||
# PRISM_BIN - launcher binary (default: /Applications/Prism Launcher.app/Contents/MacOS/prismlauncher).
|
||||
# PRISM_TIMEOUT - seconds before a variant's game is killed (default: 900).
|
||||
# PRISM_KEEP_JAR - 1 leaves the test jar installed and the old angelica jar(s) disabled after the script ends (default: 0).
|
||||
# PRISM_WORLD - save for QuickPlay (default: FlybyGTNH). No spaces.
|
||||
# PRISM_ENTRIES - id=status,... passed as -Dangelica.flyby.debugEntries on every variant and graded with
|
||||
# --require-renderers (default: all nine world renderers inOverlay).
|
||||
#
|
||||
# Sets fullscreen:true in the instance options.txt per variant (avoids vsync pacing).
|
||||
# Restores instance.cfg, options.txt, config/shaders.properties and the pack option file on exit (cmp-verified).
|
||||
# scripts/flyby-prism.sh --helper <jvmargs|profile> ... runs one pure step in isolation.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$(dirname "$0")/flyby-lib.sh"
|
||||
|
||||
HELPER="$(cat <<'PY'
|
||||
import re, sys
|
||||
|
||||
|
||||
def read(path):
|
||||
with open(path, newline="") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write(path, text):
|
||||
with open(path, "w", newline="") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
def unquote(raw):
|
||||
if len(raw) >= 2 and raw[0] == '"' and raw[-1] == '"':
|
||||
return True, raw[1:-1].replace('\\"', '"').replace("\\\\", "\\")
|
||||
return False, raw
|
||||
|
||||
|
||||
def requote(value, quoted):
|
||||
if quoted or re.search(r"[\s,;\"]", value):
|
||||
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
return value
|
||||
|
||||
|
||||
def key_of(token):
|
||||
return token.split("=", 1)[0]
|
||||
|
||||
|
||||
def jvmargs(cfg, original, backend, extra):
|
||||
src = read(original)
|
||||
m = re.search(r"^JvmArgs=([^\r\n]*)", src, re.M)
|
||||
quoted, value = unquote(m.group(1)) if m else (True, "")
|
||||
new = ["-Dangelica.sdlgpu.enable=" + backend] + extra
|
||||
for t in new:
|
||||
if re.search(r"\s", t) or '"' in t:
|
||||
raise SystemExit("error: jvm arg contains whitespace or a quote: " + repr(t))
|
||||
drop = {key_of(t) for t in new}
|
||||
kept = [t for t in value.split() if key_of(t) not in drop]
|
||||
line = "JvmArgs=" + requote(" ".join(kept + new), quoted)
|
||||
text = read(cfg)
|
||||
if re.search(r"^JvmArgs=[^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^JvmArgs=[^\r\n]*", lambda _: line, text, count=1, flags=re.M)
|
||||
elif re.search(r"^\[General\][^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^(\[General\][^\r\n]*)(\r?\n)", lambda g: g.group(1) + g.group(2) + line + g.group(2), text, count=1, flags=re.M)
|
||||
else:
|
||||
text = line + "\n" + text
|
||||
if re.search(r"^OverrideJavaArgs=[^\r\n]*", text, re.M):
|
||||
text = re.sub(r"^OverrideJavaArgs=[^\r\n]*", "OverrideJavaArgs=true", text, count=1, flags=re.M)
|
||||
else:
|
||||
text = re.sub(r"^(JvmArgs=[^\r\n]*)(\r?\n|$)", lambda g: g.group(1) + (g.group(2) or "\n") + "OverrideJavaArgs=true" + g.group(2), text, count=1, flags=re.M)
|
||||
write(cfg, text)
|
||||
print(line)
|
||||
|
||||
|
||||
def logical_lines(text):
|
||||
out = []
|
||||
buf = ""
|
||||
for raw in text.splitlines():
|
||||
if buf:
|
||||
piece = raw.strip()
|
||||
else:
|
||||
piece = raw.rstrip()
|
||||
if piece.endswith("\\"):
|
||||
buf += piece[:-1] + " "
|
||||
continue
|
||||
out.append(buf + piece)
|
||||
buf = ""
|
||||
if buf:
|
||||
out.append(buf)
|
||||
return out
|
||||
|
||||
|
||||
def profile_tokens(defs, name, seen):
|
||||
if name in seen:
|
||||
raise SystemExit("error: profile cycle at " + name)
|
||||
if name not in defs:
|
||||
raise SystemExit("error: profile." + name + " not defined")
|
||||
out = []
|
||||
for tok in defs[name].split():
|
||||
if tok.startswith("profile."):
|
||||
out.extend(profile_tokens(defs, tok[len("profile."):], seen | {name}))
|
||||
else:
|
||||
out.append(tok)
|
||||
return out
|
||||
|
||||
|
||||
def profile(props, out_txt, name):
|
||||
defs = {}
|
||||
for line in logical_lines(read(props)):
|
||||
s = line.strip()
|
||||
if s.startswith("#") or s.startswith("!"):
|
||||
continue
|
||||
m = re.match(r"profile\.(\w+)\s*=(.*)$", s)
|
||||
if m:
|
||||
defs[m.group(1)] = m.group(2)
|
||||
if name not in defs:
|
||||
raise SystemExit("error: profile." + name + " not found in " + props)
|
||||
options = {}
|
||||
for tok in profile_tokens(defs, name, frozenset()):
|
||||
if "=" in tok:
|
||||
k, v = tok.split("=", 1)
|
||||
elif tok.startswith("!"):
|
||||
k, v = tok[1:], "false"
|
||||
else:
|
||||
k, v = tok, "true"
|
||||
if not re.match(r"^[A-Za-z_][A-Za-z0-9_.]*$", k) or v == "":
|
||||
raise SystemExit("error: cannot parse profile token " + repr(tok))
|
||||
options[k] = v
|
||||
if not options:
|
||||
raise SystemExit("error: profile." + name + " is empty")
|
||||
write(out_txt, "".join(k + "=" + v + "\n" for k, v in options.items()))
|
||||
print(len(options))
|
||||
|
||||
|
||||
def main(argv):
|
||||
cmd = argv[1]
|
||||
if cmd == "jvmargs":
|
||||
jvmargs(argv[2], argv[3], argv[4], argv[5:])
|
||||
elif cmd == "profile":
|
||||
profile(argv[2], argv[3], argv[4] if len(argv) > 4 else "HIGH")
|
||||
else:
|
||||
raise SystemExit("error: unknown helper " + cmd)
|
||||
|
||||
|
||||
main(sys.argv)
|
||||
PY
|
||||
)"
|
||||
|
||||
helper() {
|
||||
python3 -c "$HELPER" "$@"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--helper" ]; then
|
||||
shift
|
||||
helper "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
NAME="${1:?usage: flyby-prism.sh <name> <variant>...}"
|
||||
shift
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "usage: flyby-prism.sh <name> <sdl-off|sdl-pack|gl-off|gl-pack>..." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PRISM_INSTANCE="${PRISM_INSTANCE:-GTNH-daily-2026-10-01+766-mmcprism-java17-26}"
|
||||
PRISM_BIN="${PRISM_BIN:-/Applications/Prism Launcher.app/Contents/MacOS/prismlauncher}"
|
||||
PRISM_TIMEOUT="${PRISM_TIMEOUT:-900}"
|
||||
PRISM_KEEP_JAR="${PRISM_KEEP_JAR:-0}"
|
||||
PRISM_WORLD="${PRISM_WORLD:-FlybyGTNH}"
|
||||
PRISM_ENTRIES="${PRISM_ENTRIES:-minecraft:chunk_borders=inOverlay,minecraft:chunk_section_visibility=inOverlay,minecraft:visualize_block_light_levels=inOverlay,minecraft:visualize_sky_light_levels=inOverlay,minecraft:visualize_heightmap=inOverlay,minecraft:visualize_collision_boxes=inOverlay,minecraft:visualize_solid_faces=inOverlay,minecraft:visualize_water_levels=inOverlay,minecraft:visualize_entity_supporting_block=inOverlay}"
|
||||
|
||||
for v in "$@"; do
|
||||
case "$v" in
|
||||
sdl-off|sdl-pack|gl-off|gl-pack) ;;
|
||||
*) echo "error: variant must be sdl-off, sdl-pack, gl-off or gl-pack, got: $v" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
case "$PRISM_WORLD" in
|
||||
*[[:space:]\"]*|"") echo "error: PRISM_WORLD must be non-empty without spaces or quotes: '$PRISM_WORLD'" >&2; exit 2 ;;
|
||||
esac
|
||||
case "$PRISM_ENTRIES" in
|
||||
*[[:space:]\"]*) echo "error: PRISM_ENTRIES must not contain spaces or quotes" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
INST="$HOME/Library/Application Support/PrismLauncher/instances/$PRISM_INSTANCE"
|
||||
MC="$INST/.minecraft"
|
||||
CFG="$INST/instance.cfg"
|
||||
OPTS="$MC/options.txt"
|
||||
SHADERS="$MC/config/shaders.properties"
|
||||
for f in "$CFG" "$OPTS" "$SHADERS"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "error: missing $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ! -x "$PRISM_BIN" ]; then
|
||||
echo "error: launcher not executable: $PRISM_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INST_TAIL="${PRISM_INSTANCE#*+}"
|
||||
GAME_PAT="java.*$INST_TAIL"
|
||||
if pgrep -x prismlauncher >/dev/null 2>&1; then
|
||||
echo "error: Prism Launcher is already running; quit it first" >&2
|
||||
exit 1
|
||||
fi
|
||||
if pgrep -f "$GAME_PAT" >/dev/null 2>&1; then
|
||||
echo "error: a process matching '$GAME_PAT' (the instance's game?) is running" >&2
|
||||
exit 1
|
||||
fi
|
||||
if pgrep -f 'GradleStart|retrofuturabootstrap' >/dev/null 2>&1; then
|
||||
echo "error: a dev client is running" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./gradlew assemble
|
||||
JAR=""
|
||||
for f in "$ROOT"/build/libs/angelica-*.jar; do
|
||||
case "$f" in
|
||||
*-dev.jar|*-sources.jar|*-api.jar|*-dev-preshadow.jar|*-predowngrade.jar|*shadow-stripped.jar) continue ;;
|
||||
esac
|
||||
if [ -f "$f" ] && { [ -z "$JAR" ] || [ "$f" -nt "$JAR" ]; }; then
|
||||
JAR="$f"
|
||||
fi
|
||||
done
|
||||
if [ -z "$JAR" ]; then
|
||||
echo "error: no production angelica jar in build/libs" >&2
|
||||
exit 1
|
||||
fi
|
||||
JAR_NAME="$(basename "$JAR")"
|
||||
echo "==> test jar: $JAR"
|
||||
|
||||
OUT="$ROOT/tmp/prism/$NAME"
|
||||
BAK="$OUT/backup"
|
||||
|
||||
PACK="$(awk '/^shaderPack=/ { sub(/^[^=]*=/, ""); sub(/\r$/, ""); print; exit }' "$SHADERS")"
|
||||
PACK_TXT=""
|
||||
if [ -n "$PACK" ]; then
|
||||
PACK_TXT="$MC/shaderpacks/$PACK.txt"
|
||||
fi
|
||||
for v in "$@"; do
|
||||
if [ "$v" = "gl-pack" ] || [ "$v" = "sdl-pack" ]; then
|
||||
if [ -z "$PACK" ] || [ ! -d "$MC/shaderpacks/$PACK" ]; then
|
||||
echo "error: shaderPack '$PACK' missing from $MC/shaderpacks" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
STALE_BAK=""
|
||||
if [ -d "$BAK" ]; then
|
||||
STALE_BAK="$(find "$BAK" -mindepth 1 -maxdepth 1 | head -n 1)"
|
||||
fi
|
||||
STALE_DISABLED=""
|
||||
for f in "$MC"/mods/angelica-*.jar.disabled; do
|
||||
if [ -e "$f" ]; then
|
||||
STALE_DISABLED="$STALE_DISABLED $f"
|
||||
fi
|
||||
done
|
||||
if [ -n "$STALE_BAK" ] || [ -n "$STALE_DISABLED" ]; then
|
||||
echo "error: leftovers from an interrupted run; nothing was touched" >&2
|
||||
echo "undo steps:" >&2
|
||||
echo " 1. copy back from $BAK (if present): instance.cfg -> $CFG, options.txt -> $OPTS, shaders.properties -> $SHADERS, pack.txt -> ${PACK_TXT:-n/a}" >&2
|
||||
echo " 2. remove the installed test jar from $MC/mods (originals are listed in $BAK/renamed.list)" >&2
|
||||
echo " 3. drop the .disabled suffix from:${STALE_DISABLED:- (none found)}" >&2
|
||||
echo " 4. rm -rf '$BAK'" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$BAK"
|
||||
|
||||
flyby_copy "$CFG" "$BAK/instance.cfg"
|
||||
flyby_copy "$OPTS" "$BAK/options.txt"
|
||||
flyby_copy "$SHADERS" "$BAK/shaders.properties"
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_backup_file "$PACK_TXT" "$BAK/pack.txt"
|
||||
fi
|
||||
|
||||
RENAMED="$BAK/renamed.list"
|
||||
: >| "$RENAMED"
|
||||
|
||||
PRISM_PID=""
|
||||
GAME_PID=""
|
||||
|
||||
kill_game() {
|
||||
local pids
|
||||
pids="$(pgrep -f "$GAME_PAT" || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
kill $pids 2>/dev/null || true
|
||||
sleep 3
|
||||
pids="$(pgrep -f "$GAME_PAT" || true)"
|
||||
if [ -n "$pids" ]; then
|
||||
# shellcheck disable=SC2086
|
||||
kill -9 $pids 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
if [ -n "$PRISM_PID" ] && kill -0 "$PRISM_PID" 2>/dev/null; then
|
||||
kill "$PRISM_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
kill -9 "$PRISM_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
RESTORED=0
|
||||
restore() {
|
||||
set +e
|
||||
if [ "$RESTORED" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
RESTORED=1
|
||||
trap - EXIT INT TERM HUP
|
||||
kill_game
|
||||
local ok=1
|
||||
flyby_restore_file "$BAK/instance.cfg" "$CFG" || ok=0
|
||||
flyby_restore_file "$BAK/options.txt" "$OPTS" || ok=0
|
||||
flyby_restore_file "$BAK/shaders.properties" "$SHADERS" || ok=0
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT" || ok=0
|
||||
fi
|
||||
if [ "$PRISM_KEEP_JAR" != "1" ]; then
|
||||
rm -f "$MC/mods/$JAR_NAME" || { echo "warning: could not remove $MC/mods/$JAR_NAME" >&2; ok=0; }
|
||||
while IFS= read -r f; do
|
||||
if [ -n "$f" ] && [ -f "$f.disabled" ]; then
|
||||
mv -f "$f.disabled" "$f" || { echo "warning: could not re-enable $f" >&2; ok=0; }
|
||||
fi
|
||||
done < "$RENAMED"
|
||||
if [ "$ok" = "1" ]; then
|
||||
rm -rf "$BAK"
|
||||
fi
|
||||
echo "==> test jar removed, original angelica jar(s) re-enabled"
|
||||
else
|
||||
echo "==> test jar LEFT installed: $MC/mods/$JAR_NAME"
|
||||
echo "==> to undo: rm -f '$MC/mods/$JAR_NAME'; then drop the .disabled suffix from: $(tr '\n' ' ' < "$RENAMED")"
|
||||
fi
|
||||
}
|
||||
trap restore EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 129' HUP
|
||||
|
||||
for f in "$MC"/mods/angelica-*.jar; do
|
||||
if [ -f "$f" ]; then
|
||||
printf '%s\n' "$f" >> "$RENAMED"
|
||||
mv -n "$f" "$f.disabled"
|
||||
fi
|
||||
done
|
||||
flyby_copy "$JAR" "$MC/mods/$JAR_NAME"
|
||||
echo "==> installed $JAR_NAME; disabled: $(tr '\n' ' ' < "$RENAMED")"
|
||||
|
||||
game_pid() {
|
||||
pgrep -f "$GAME_PAT" | head -n 1 || true
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
for VARIANT in "$@"; do
|
||||
VDIR="$OUT/$VARIANT"
|
||||
flyby_fresh_dir "$VDIR"
|
||||
|
||||
case "$VARIANT" in
|
||||
sdl-*) BACKEND=true ;;
|
||||
*) BACKEND=false ;;
|
||||
esac
|
||||
case "$VARIANT" in
|
||||
*-pack) SHADERS_ON=true ;;
|
||||
*) SHADERS_ON=false ;;
|
||||
esac
|
||||
|
||||
EXTRA=(
|
||||
-Dangelica.debug.tooling=true
|
||||
-Dangelica.flyby.route=static
|
||||
-Dangelica.flyby.length=400
|
||||
-Dangelica.flyby.warmupTicks=200
|
||||
-Dangelica.flyby.exitWhenDone=true
|
||||
-Dangelica.flyby.waitForFocus=true
|
||||
-Dangelica.flyby.screenshots=4
|
||||
-Dangelica.flyby.debugHud=true
|
||||
-Dangelica.flyby.debugDump=true
|
||||
"-DquickPlaySingleplayer=$PRISM_WORLD"
|
||||
)
|
||||
CHECK=(--modpack --shots 4 --expect-mode OVERLAY --require-group botania:botania --require-group journeymap:journeymap --forbid-dup --require-mod etfuturum)
|
||||
EXTRA+=("-Dangelica.flyby.debugEntries=$PRISM_ENTRIES")
|
||||
RENDERERS="$(printf '%s\n' "$PRISM_ENTRIES" | tr ',' '\n' | sed -e '/^$/d' -e 's/=[^=]*$//' | paste -sd, -)"
|
||||
CHECK+=(--require-renderers "$RENDERERS")
|
||||
|
||||
flyby_restore_file "$BAK/instance.cfg" "$CFG"
|
||||
echo "==> $NAME/$VARIANT: JvmArgs"
|
||||
helper jvmargs "$CFG" "$BAK/instance.cfg" "$BACKEND" "${EXTRA[@]}" | tee "$VDIR/jvmargs.txt"
|
||||
|
||||
flyby_restore_file "$BAK/shaders.properties" "$SHADERS"
|
||||
flyby_set_key "$SHADERS" enableShaders "$SHADERS_ON" =
|
||||
|
||||
if [ -n "$PACK_TXT" ]; then
|
||||
flyby_restore_file "$BAK/pack.txt" "$PACK_TXT"
|
||||
fi
|
||||
if [ "$VARIANT" = "gl-pack" ]; then
|
||||
PROPS="$MC/shaderpacks/$PACK/shaders/shaders.properties"
|
||||
if [ ! -f "$PROPS" ] || ! COUNT="$(helper profile "$PROPS" "$PACK_TXT" HIGH)"; then
|
||||
echo "FAIL $VARIANT: pack profile.HIGH could not be read from $PROPS; not launching"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
echo "==> $NAME/$VARIANT: wrote $COUNT HIGH options to $PACK_TXT"
|
||||
fi
|
||||
|
||||
flyby_restore_file "$BAK/options.txt" "$OPTS"
|
||||
flyby_set_key "$OPTS" fullscreen true :
|
||||
|
||||
mkdir -p "$MC/screenshots"
|
||||
rm -f "$MC"/screenshots/flyby-shot-*.png "$MC/logs/fml-client-latest.log"
|
||||
flyby_crash_list "$MC/crash-reports" >| "$VDIR/crashes.before"
|
||||
|
||||
echo "==> $NAME/$VARIANT: launching (timeout ${PRISM_TIMEOUT}s)"
|
||||
"$PRISM_BIN" -l "$PRISM_INSTANCE" >| "$VDIR/launcher.log" 2>&1 &
|
||||
PRISM_PID=$!
|
||||
|
||||
GAME_PID=""
|
||||
WAITED=0
|
||||
while [ "$WAITED" -lt 240 ]; do
|
||||
GAME_PID="$(game_pid)"
|
||||
if [ -n "$GAME_PID" ]; then
|
||||
break
|
||||
fi
|
||||
if ! kill -0 "$PRISM_PID" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
|
||||
STATUS="0"
|
||||
if [ -z "$GAME_PID" ]; then
|
||||
STATUS="nogame"
|
||||
echo "==> $NAME/$VARIANT: no game process appeared within ${WAITED}s"
|
||||
else
|
||||
DEADLINE=$(( $(date +%s) + PRISM_TIMEOUT ))
|
||||
while kill -0 "$GAME_PID" 2>/dev/null; do
|
||||
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
||||
echo "==> $NAME/$VARIANT: timeout, killing game"
|
||||
STATUS="timeout"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
kill_game
|
||||
PRISM_PID=""
|
||||
sleep 2
|
||||
|
||||
NEW_CRASHES="$(flyby_new_crashes "$MC/crash-reports" "$VDIR/crashes.before" "$VDIR/crashes.after")"
|
||||
flyby_collect "$MC" "$VDIR" fml-client-latest.log
|
||||
|
||||
if ! grep -qF "Flyby: world saving is off" "$VDIR/fml-client-latest.log" 2>/dev/null; then
|
||||
echo "FAIL $VARIANT: never entered the world (no 'Flyby: world saving is off' in the log; QuickPlay did not fire?); stopping the batch"
|
||||
FAILED=1
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$STATUS" = "nogame" ]; then
|
||||
STATUS="1"
|
||||
fi
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES" "${CHECK[@]}"; then
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$FAILED"
|
||||
+117
-11
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
@@ -15,8 +16,18 @@ CATCH_MARK = "Caught an exception during ISBRH rendering"
|
||||
BAD_LINE = re.compile(
|
||||
r"InvalidInjectionException|InjectionError|MixinApplyError|MixinTransformerError|"
|
||||
r"InvalidMixinException|MixinPrepareError|skipped: the surrounded call was removed|"
|
||||
r"flyby scene command did not execute"
|
||||
r"flyby scene command did not execute|Grouped debug screen failed|Failed to set up the debug screen|"
|
||||
r"Debug screen entry \S+ failed and is disabled|needs the batching font renderer|"
|
||||
r"Flyby debugEntries:|Flyby: debug entry list is not available|Flyby: unknown debugProfile|"
|
||||
r"Debug screen compat entry for \S+ failed to register|"
|
||||
r"Failed to (?:load|save|set aside the unreadable) debug screen profile|toggle listeners kept changing"
|
||||
)
|
||||
OVERLAY_RE = re.compile(r"Flyby debug overlay shot=(\d+) mode=(\w+)(?:.* renderers=(\S*))?")
|
||||
LINE_RE = re.compile(r"Flyby debug line shot=(\d+) id=(\S+) text=(.*)")
|
||||
GROUP_RE = re.compile(r"Flyby debug group shot=(\d+) id=(\S+) ")
|
||||
DUP_IDS = ("angelica:other_left", "angelica:other_right")
|
||||
DUP_PREFIXES = ("Allocation rate:", "Java:", "GPU:", "OpenGL:", "CPU Cores:", "OS:", "LWJGL:", "IntCache:",
|
||||
"[Botania]", "[JM]", "[ChunkGen]", "hunger:", "HydroEnergy GPU RAM:", "renderDebugMode:")
|
||||
ANGELICA_FRAME = re.compile(
|
||||
r"com\.gtnewhorizons\.angelica|net\.coderbot|org\.embeddedt|org\.taumc|com\.prupe|"
|
||||
r"jss\.notfine|\$surround|angelica\$"
|
||||
@@ -44,7 +55,44 @@ def split_traces(lines):
|
||||
return traces
|
||||
|
||||
|
||||
def check(variant_dir, exit_status, new_crashes):
|
||||
def check_overlay(lines, opts, reasons):
|
||||
overlays = [m for m in (OVERLAY_RE.search(l) for l in lines) if m]
|
||||
mode = opts.get("expect_mode")
|
||||
if mode:
|
||||
if not overlays:
|
||||
reasons.append("no 'Flyby debug overlay' line")
|
||||
else:
|
||||
wrong = sorted({m.group(2) for m in overlays if m.group(2) != mode})
|
||||
if wrong:
|
||||
reasons.append("overlay mode " + ",".join(wrong) + " != expected " + mode)
|
||||
|
||||
dbg_lines = [m for m in (LINE_RE.search(l) for l in lines) if m]
|
||||
groups = [m for m in (GROUP_RE.search(l) for l in lines) if m]
|
||||
for ident in opts.get("require_group") or []:
|
||||
if not any(m.group(2) == ident for m in groups):
|
||||
reasons.append("missing debug group " + ident)
|
||||
|
||||
wanted = [r for r in (opts.get("require_renderers") or "").split(",") if r]
|
||||
if wanted:
|
||||
if not any(m.group(3) is not None and set(wanted) <= set(m.group(3).split(",")) for m in overlays):
|
||||
reasons.append("no overlay line with renderers " + ",".join(wanted))
|
||||
|
||||
for name in opts.get("require_mod") or []:
|
||||
if not any(name.lower() in l.lower() for l in lines):
|
||||
reasons.append("mod " + name + " not in log")
|
||||
|
||||
if opts.get("forbid_dup"):
|
||||
if not dbg_lines:
|
||||
reasons.append("no debug lines dumped")
|
||||
dups = sorted({m.group(3).lstrip().split(":")[0] + ":" for m in dbg_lines
|
||||
if m.group(2) in DUP_IDS and m.group(3).lstrip().startswith(DUP_PREFIXES)})
|
||||
if dups:
|
||||
reasons.append("duplicate vanilla lines in " + "/".join(DUP_IDS) + ": " + ", ".join(dups))
|
||||
|
||||
|
||||
def check(variant_dir, exit_status, new_crashes, opts=None):
|
||||
opts = opts or {}
|
||||
shots_required = opts.get("shots") or SHOTS_REQUIRED
|
||||
variant = os.path.basename(os.path.normpath(variant_dir))
|
||||
reasons = []
|
||||
details = []
|
||||
@@ -71,7 +119,11 @@ def check(variant_dir, exit_status, new_crashes):
|
||||
elif max(frames) == 0:
|
||||
reasons.append("flyby frames is 0")
|
||||
|
||||
bad = [l for l in lines if BAD_LINE.search(l)]
|
||||
if opts.get("modpack"):
|
||||
bad = [l for i, l in enumerate(lines) if BAD_LINE.search(l)
|
||||
and any("angelica" in x.lower() for x in lines[i:i + 4])]
|
||||
else:
|
||||
bad = [l for l in lines if BAD_LINE.search(l)]
|
||||
if bad:
|
||||
reasons.append(str(len(bad)) + " mixin/injection/scene failure line(s)")
|
||||
details.extend([" " + l.strip()[:200] for l in bad[:3]])
|
||||
@@ -94,12 +146,14 @@ def check(variant_dir, exit_status, new_crashes):
|
||||
if CATCH_MARK in l and any(CRASH_MARK in x for x in lines[i:i + 2]):
|
||||
caught = True
|
||||
break
|
||||
if not caught:
|
||||
if not caught and not opts.get("modpack"):
|
||||
reasons.append("no ISBRH catch line for crashtest")
|
||||
|
||||
check_overlay(lines, opts, reasons)
|
||||
|
||||
shots = sorted(glob.glob(os.path.join(variant_dir, "flyby-shot-*.png")))
|
||||
if len(shots) < SHOTS_REQUIRED:
|
||||
reasons.append("only " + str(len(shots)) + " of " + str(SHOTS_REQUIRED) + " screenshots")
|
||||
if len(shots) < shots_required:
|
||||
reasons.append("only " + str(len(shots)) + " of " + str(shots_required) + " screenshots")
|
||||
small = [os.path.basename(p) for p in shots if os.path.getsize(p) < MIN_SHOT_BYTES]
|
||||
if small:
|
||||
reasons.append("small screenshot(s): " + ", ".join(small))
|
||||
@@ -154,12 +208,56 @@ def self_test():
|
||||
("no-isbrh", [l for l in GOOD_LOG if CATCH_MARK not in l], {}, 1),
|
||||
("mixin-error", GOOD_LOG + ["[10:00:05] [main/ERROR]: InvalidInjectionException: bad"], {}, 1),
|
||||
("small-shot", GOOD_LOG, {"shot_size": 100}, 1),
|
||||
("bad-entries", GOOD_LOG + ["[10:00:06] [Client thread/ERROR]: Flyby debugEntries: unknown entry id 'test:nope'"], {}, 1),
|
||||
]
|
||||
for name, log, kw, want in cases:
|
||||
d = make_variant(root, name, log, **kw)
|
||||
got = check(d, 0, 0)
|
||||
if got != want:
|
||||
failures.append(name)
|
||||
no_isbrh = [l for l in GOOD_LOG if CATCH_MARK not in l]
|
||||
ov = [
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug overlay shot=0 mode=OVERLAY groups=2 renderers=vanilla,angelica",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug group shot=0 id=angelica:other_left side=L x=0 y=0 w=1 h=1 facts= lines=2",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_left text=Allocation rate: 1 MB/s",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_left text=Second",
|
||||
"[10:00:06] [Client thread/INFO]: Flyby debug line shot=0 id=angelica:other_right text=Free",
|
||||
"[10:00:07] [main/INFO]: Loading mod Botania",
|
||||
]
|
||||
ov_dup = [l for l in ov if "Allocation" not in l]
|
||||
ov_dup += ["[10:00:06] [Client thread/INFO]: Flyby debug line shot=1 id=angelica:other_right text= GPU: foo"]
|
||||
flag_cases = [
|
||||
("modpack-ok", no_isbrh, {"modpack": True}, 0),
|
||||
("modpack-no-flag", no_isbrh, {}, 1),
|
||||
("modpack-foreign-bad", GOOD_LOG + ["[10:00:05] [main/ERROR]: InvalidInjectionException: x", "other", "other", "other", "angelica late"], {"modpack": True}, 0),
|
||||
("modpack-angelica-bad", no_isbrh + ["[10:00:05] [main/ERROR]: Grouped debug screen failed", "x", "angelica y"], {"modpack": True}, 1),
|
||||
("grouped-failed", GOOD_LOG + ["[10:00:05] [main/ERROR]: Grouped debug screen failed"], {}, 1),
|
||||
("setup-failed", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Failed to set up the debug screen; it stays disabled for this session"], {}, 1),
|
||||
("entry-failed", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Debug screen entry botania:debug failed and is disabled for the rest of this session"], {}, 1),
|
||||
("font-fallback", GOOD_LOG + ["[10:00:05] [Client thread/ERROR] [Angelica]: Grouped debug screen needs the batching font renderer on the HUD font; using the classic layout for the rest of this session"], {}, 1),
|
||||
("shots-ok", GOOD_LOG, {"shots": 3}, 0),
|
||||
("shots-short", GOOD_LOG, {"shots": 9}, 1),
|
||||
("mode-ok", GOOD_LOG + ov, {"expect_mode": "OVERLAY"}, 0),
|
||||
("mode-wrong", GOOD_LOG + ov, {"expect_mode": "FLAT"}, 1),
|
||||
("mode-none", GOOD_LOG, {"expect_mode": "FLAT"}, 1),
|
||||
("group-ok", GOOD_LOG + ov, {"require_group": ["angelica:other_left"]}, 0),
|
||||
("group-missing", GOOD_LOG + ov, {"require_group": ["angelica:nope"]}, 1),
|
||||
("renderers-ok", GOOD_LOG + ov, {"require_renderers": "angelica,vanilla"}, 0),
|
||||
("renderers-missing", GOOD_LOG + ov, {"require_renderers": "angelica,sodium"}, 1),
|
||||
("mod-ok", GOOD_LOG + ov, {"require_mod": ["botania"]}, 0),
|
||||
("mod-missing", GOOD_LOG + ov, {"require_mod": ["thaumcraft"]}, 1),
|
||||
("dup-fail", GOOD_LOG + ov, {"forbid_dup": True}, 1),
|
||||
("dup-ok", GOOD_LOG + ov_dup[:3] + ov_dup[4:5], {"forbid_dup": True}, 0),
|
||||
("dup-spaces-fail", GOOD_LOG + ov_dup, {"forbid_dup": True}, 1),
|
||||
("dup-no-dump", GOOD_LOG, {"forbid_dup": True}, 1),
|
||||
]
|
||||
line = "[10:00:06] [Client thread/INFO]: Flyby debug line shot=1 id=angelica:other_left text= [Botania] x"
|
||||
flag_cases.append(("dup-prefix", GOOD_LOG + ov_dup[:3] + [line], {"forbid_dup": True}, 1))
|
||||
flag_cases.append(("dup-prefix-other-id", GOOD_LOG + ov_dup[:3] + [line.replace("other_left", "other_elsewhere")], {"forbid_dup": True}, 0))
|
||||
for name, log, opts, want in flag_cases:
|
||||
d = make_variant(root, name, log, shots=3 if name == "shots-ok" else 8)
|
||||
if check(d, 0, 0, opts) != want:
|
||||
failures.append(name)
|
||||
d = make_variant(root, "bad-exit", GOOD_LOG)
|
||||
if check(d, "timeout", 1) != 1:
|
||||
failures.append("bad-exit")
|
||||
@@ -175,11 +273,19 @@ def self_test():
|
||||
def main(argv):
|
||||
if len(argv) == 2 and argv[1] == "--self-test":
|
||||
return self_test()
|
||||
if len(argv) != 4:
|
||||
print("usage: flyby-smoke-check.py <variant_dir> <exit_status|timeout> <new_crash_count>", file=sys.stderr)
|
||||
print(" flyby-smoke-check.py --self-test", file=sys.stderr)
|
||||
return 2
|
||||
return check(argv[1], argv[2], int(argv[3]))
|
||||
ap = argparse.ArgumentParser(prog="flyby-smoke-check.py")
|
||||
ap.add_argument("variant_dir")
|
||||
ap.add_argument("exit_status")
|
||||
ap.add_argument("new_crashes", type=int)
|
||||
ap.add_argument("--modpack", action="store_true")
|
||||
ap.add_argument("--shots", type=int)
|
||||
ap.add_argument("--expect-mode")
|
||||
ap.add_argument("--require-group", action="append", metavar="ID")
|
||||
ap.add_argument("--require-renderers", metavar="a,b")
|
||||
ap.add_argument("--require-mod", action="append", metavar="NAME")
|
||||
ap.add_argument("--forbid-dup", action="store_true")
|
||||
a = ap.parse_args(argv[1:])
|
||||
return check(a.variant_dir, a.exit_status, a.new_crashes, vars(a))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+27
-60
@@ -11,11 +11,14 @@
|
||||
# SMOKE_TIMEOUT - seconds before the client is killed (default: 600).
|
||||
# SMOKE_SCENE - scene command file (default: scripts/flyby-scenes/surround-smoke.txt).
|
||||
# SMOKE_PACK - shaderpack name for the pack variant (default: ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1).
|
||||
# SMOKE_DEBUG_HUD - true forces F3 on; false keeps the saved setting (default: true).
|
||||
# SMOKE_DEBUG_HUD - true forces F3 on, false forces it off (default: true).
|
||||
# SMOKE_PITCH - camera pitch in degrees, positive looks down (default: 15).
|
||||
# SMOKE_WARMUP_TICKS - ticks at the first pose before the pan (default: 100).
|
||||
# SMOKE_ARGS - extra -D flags for the client, word-split (default: none).
|
||||
# SMOKE_CHECK_ARGS - extra flyby-smoke-check.py flags, word-split (default: none).
|
||||
# SMOKE_FULLSCREEN - 0 keeps options.txt's window mode (default: 1, fullscreen for the run).
|
||||
#
|
||||
# Runs from this script's own checkout. Restores run/client/config/shaders.properties and angelica-options.json on exit.
|
||||
# Runs from this script's own checkout, fullscreen unless SMOKE_FULLSCREEN=0 (fullscreen avoids vsync pacing). Restores run/client/config/shaders.properties, angelica-options.json and options.txt on exit.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -27,6 +30,8 @@ fi
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
# shellcheck source=flyby-lib.sh
|
||||
. "$ROOT/scripts/flyby-lib.sh"
|
||||
|
||||
QUICKPLAY_WORLD="${QUICKPLAY_WORLD:-Flat World - No Mods}"
|
||||
FLYBY_ORIGIN="${FLYBY_ORIGIN:-1509.5,-924.5,0}"
|
||||
@@ -35,6 +40,9 @@ SMOKE_PACK="${SMOKE_PACK:-ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1}"
|
||||
SMOKE_DEBUG_HUD="${SMOKE_DEBUG_HUD:-true}"
|
||||
SMOKE_PITCH="${SMOKE_PITCH:-15}"
|
||||
SMOKE_WARMUP_TICKS="${SMOKE_WARMUP_TICKS:-100}"
|
||||
SMOKE_ARGS="${SMOKE_ARGS:-}"
|
||||
SMOKE_CHECK_ARGS="${SMOKE_CHECK_ARGS:-}"
|
||||
SMOKE_FULLSCREEN="${SMOKE_FULLSCREEN:-1}"
|
||||
|
||||
SCENE="${SMOKE_SCENE:-$ROOT/scripts/flyby-scenes/surround-smoke.txt}"
|
||||
case "$SCENE" in
|
||||
@@ -47,6 +55,7 @@ OUT="$ROOT/tmp/smoke/$NAME"
|
||||
BACKUP="$OUT/shaders.properties.bak"
|
||||
OPTIONS="$RUN/config/angelica-options.json"
|
||||
OPTIONS_BACKUP="$OUT/angelica-options.json.bak"
|
||||
OPTIONS_TXT="$RUN/options.txt"
|
||||
|
||||
for v in "$@"; do
|
||||
case "$v" in
|
||||
@@ -66,48 +75,26 @@ for v in "$@"; do
|
||||
done
|
||||
|
||||
mkdir -p "$OUT"
|
||||
python3 - "$SHADERS" "$BACKUP" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
python3 - "$OPTIONS" "$OPTIONS_BACKUP" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_copy "$SHADERS" "$BACKUP"
|
||||
flyby_copy "$OPTIONS" "$OPTIONS_BACKUP"
|
||||
|
||||
restore_options() {
|
||||
python3 - "$OPTIONS_BACKUP" "$OPTIONS" <<'PY' || true
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_restore_file "$OPTIONS_BACKUP" "$OPTIONS" || true
|
||||
}
|
||||
|
||||
restore() {
|
||||
python3 - "$BACKUP" "$SHADERS" <<'PY' || true
|
||||
import shutil, sys
|
||||
shutil.copyfile(sys.argv[1], sys.argv[2])
|
||||
PY
|
||||
flyby_restore_file "$BACKUP" "$SHADERS" || true
|
||||
restore_options
|
||||
if [ "$SMOKE_FULLSCREEN" = 1 ]; then flyby_fullscreen_restore "$OPTIONS_TXT" "$OUT/options.txt.bak" || true; fi
|
||||
}
|
||||
if [ "$SMOKE_FULLSCREEN" = 1 ]; then flyby_fullscreen_on "$OPTIONS_TXT" "$OUT/options.txt.bak"; fi
|
||||
trap restore EXIT
|
||||
|
||||
set_shaders() {
|
||||
python3 - "$SHADERS" "$1" "$2" <<'PY'
|
||||
import re, sys
|
||||
path, enable, pack = sys.argv[1:4]
|
||||
text = open(path).read()
|
||||
def put(text, key, value):
|
||||
pat = re.compile(r'^' + key + r'=.*$', re.M)
|
||||
if pat.search(text):
|
||||
return pat.sub(lambda m: key + '=' + value, text)
|
||||
if text and not text.endswith('\n'):
|
||||
text += '\n'
|
||||
return text + key + '=' + value + '\n'
|
||||
text = put(text, 'enableShaders', enable)
|
||||
if pack:
|
||||
text = put(text, 'shaderPack', pack)
|
||||
open(path, 'w').write(text)
|
||||
PY
|
||||
flyby_set_key "$SHADERS" enableShaders "$1" =
|
||||
if [ -n "$2" ]; then
|
||||
flyby_set_key "$SHADERS" shaderPack "$2" =
|
||||
fi
|
||||
}
|
||||
|
||||
set_vao() {
|
||||
@@ -123,20 +110,10 @@ with open(path, "w") as f:
|
||||
PY
|
||||
}
|
||||
|
||||
crash_list() {
|
||||
if [ -d "$RUN/crash-reports" ]; then
|
||||
ls "$RUN/crash-reports" | sort
|
||||
fi
|
||||
}
|
||||
|
||||
FAILED=0
|
||||
for VARIANT in "$@"; do
|
||||
VDIR="$OUT/$VARIANT"
|
||||
python3 - "$VDIR" <<'PY'
|
||||
import shutil, sys
|
||||
shutil.rmtree(sys.argv[1], ignore_errors=True)
|
||||
PY
|
||||
mkdir -p "$VDIR"
|
||||
flyby_fresh_dir "$VDIR"
|
||||
|
||||
restore_options
|
||||
if [ "$VARIANT" = "vao" ]; then
|
||||
@@ -151,7 +128,7 @@ PY
|
||||
|
||||
mkdir -p "$RUN/screenshots"
|
||||
rm -f "$RUN"/screenshots/flyby-shot-*.png
|
||||
crash_list >| "$VDIR/crashes.before"
|
||||
flyby_crash_list "$RUN/crash-reports" >| "$VDIR/crashes.before"
|
||||
|
||||
echo "==> $NAME/$VARIANT: launching (timeout ${SMOKE_TIMEOUT}s)"
|
||||
./gradlew runClient25 --console=plain \
|
||||
@@ -167,6 +144,7 @@ PY
|
||||
-Dangelica.flyby.crashTest=isbrh \
|
||||
-Dangelica.debug.testBlocks=true \
|
||||
-DquickPlaySingleplayer="$QUICKPLAY_WORLD" \
|
||||
$SMOKE_ARGS \
|
||||
>| "$VDIR/console.log" 2>&1 &
|
||||
PID=$!
|
||||
|
||||
@@ -190,21 +168,10 @@ PY
|
||||
STATUS="$RC"
|
||||
fi
|
||||
|
||||
crash_list >| "$VDIR/crashes.after"
|
||||
NEW_CRASHES="$(comm -13 "$VDIR/crashes.before" "$VDIR/crashes.after" | wc -l | tr -d ' ')"
|
||||
NEW_CRASHES="$(flyby_new_crashes "$RUN/crash-reports" "$VDIR/crashes.before" "$VDIR/crashes.after")"
|
||||
flyby_collect "$RUN" "$VDIR" fml-client-latest.log latest.log
|
||||
|
||||
for f in fml-client-latest.log latest.log; do
|
||||
if [ -f "$RUN/logs/$f" ]; then
|
||||
cp -f "$RUN/logs/$f" "$VDIR/$f"
|
||||
fi
|
||||
done
|
||||
for f in "$RUN"/screenshots/flyby-shot-*.png; do
|
||||
if [ -f "$f" ]; then
|
||||
cp -f "$f" "$VDIR/"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES"; then
|
||||
if ! python3 "$ROOT/scripts/flyby-smoke-check.py" "$VDIR" "$STATUS" "$NEW_CRASHES" $SMOKE_CHECK_ARGS; then
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -785,7 +785,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
device.fenceReleaser().drain();
|
||||
if (frameSync != 0L) {
|
||||
frameSyncs.enqueue(frameSync);
|
||||
while (frameSyncs.size() > Device.MAX_FRAMES_IN_FLIGHT - 1) {
|
||||
while (frameSyncs.size() > RenderBackend.MAX_FRAMES_IN_FLIGHT - 1) {
|
||||
final long sync = frameSyncs.dequeueLong();
|
||||
fenceTracker.clientWaitSync(sync, 0, Long.MAX_VALUE);
|
||||
fenceTracker.deleteSync(sync);
|
||||
|
||||
@@ -37,9 +37,7 @@ import static org.lwjgl.system.MemoryUtil.memUTF8;
|
||||
public final class Device {
|
||||
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
|
||||
|
||||
public static final int MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), MAX_FRAMES_IN_FLIGHT);
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), RenderBackend.MAX_FRAMES_IN_FLIGHT);
|
||||
|
||||
public static int framesInFlight() { return FRAMES_IN_FLIGHT; }
|
||||
|
||||
|
||||
+4
-3
@@ -45,6 +45,7 @@ import static org.lwjgl.system.MemoryStack.*;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
@@ -697,7 +698,7 @@ public final class ResourceManager {
|
||||
final GpuBucket b = gpuBufferPool.get(Hashing.packHiLo(usage, bucket));
|
||||
if (b == null || b.handles.isEmpty()) return 0;
|
||||
for (int i = 0, n = b.handles.size(); i < n; i++) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= RenderBackend.MAX_FRAMES_IN_FLIGHT) {
|
||||
final long handle = b.handles.getLong(i);
|
||||
final int last = n - 1;
|
||||
if (i != last) {
|
||||
@@ -1555,7 +1556,7 @@ public final class ResourceManager {
|
||||
for (int i = 0; i < n; i += 2) {
|
||||
final long handle = batchSegmentPending.getLong(i);
|
||||
final long stamp = batchSegmentPending.getLong(i + 1);
|
||||
if (currentFrame - stamp >= Device.MAX_FRAMES_IN_FLIGHT) {
|
||||
if (currentFrame - stamp >= RenderBackend.MAX_FRAMES_IN_FLIGHT) {
|
||||
batchSegmentFree.enqueue(handle);
|
||||
} else {
|
||||
batchSegmentPending.set(w, handle);
|
||||
@@ -2445,7 +2446,7 @@ public final class ResourceManager {
|
||||
}
|
||||
|
||||
private static final int DUMMY_VBO_SIZE = 16 * 16;
|
||||
public static final int ATTRIB_RING_BLOCKS = 2048 * Device.MAX_FRAMES_IN_FLIGHT;
|
||||
public static final int ATTRIB_RING_BLOCKS = 2048 * RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
public static final int ATTRIB_RING_CHUNK_BLOCKS = 64;
|
||||
private static final int ATTRIB_RING_CHUNKS = ATTRIB_RING_BLOCKS / ATTRIB_RING_CHUNK_BLOCKS;
|
||||
private final AtomicLong attribRingCounter = new AtomicLong();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
public interface DebugContext {
|
||||
|
||||
World world();
|
||||
|
||||
EntityClientPlayerMP player();
|
||||
|
||||
int blockX();
|
||||
|
||||
int blockY();
|
||||
|
||||
int blockZ();
|
||||
|
||||
Chunk chunk();
|
||||
|
||||
long nanoTime();
|
||||
|
||||
boolean isSampleDue();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public enum DebugEntryCategory {
|
||||
|
||||
SCREEN_TEXT("angelica.debug.options.category.text"),
|
||||
RENDERER("angelica.debug.options.category.renderer");
|
||||
|
||||
public final String translationKey;
|
||||
|
||||
DebugEntryCategory(String translationKey) {
|
||||
this.translationKey = translationKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugEntryHandle {
|
||||
|
||||
String id();
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
boolean isFailed();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public final class DebugEntrySpec {
|
||||
|
||||
public interface DebugEntryFactory {
|
||||
|
||||
DebugScreenEntry create(DebugEntryHandle entry);
|
||||
}
|
||||
|
||||
public interface DebugToggleListener {
|
||||
|
||||
void onEnabledChanged(DebugEntryHandle entry, boolean enabled);
|
||||
}
|
||||
|
||||
private final String id;
|
||||
private final DebugEntryCategory category;
|
||||
private String name;
|
||||
private DebugScreenEntryStatus defaultStatus;
|
||||
private DebugScreenEntryStatus performanceStatus = DebugScreenEntryStatus.NEVER;
|
||||
private DebugEntryFactory textFactory;
|
||||
private DebugToggleListener toggleListener;
|
||||
private boolean supportsAlways = true;
|
||||
|
||||
public DebugEntrySpec(String id, DebugEntryCategory category) {
|
||||
this.id = id;
|
||||
this.category = category;
|
||||
this.defaultStatus = category == DebugEntryCategory.SCREEN_TEXT ? DebugScreenEntryStatus.IN_OVERLAY : DebugScreenEntryStatus.NEVER;
|
||||
}
|
||||
|
||||
public DebugEntrySpec name(String keyOrLiteral) {
|
||||
this.name = keyOrLiteral;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec defaults(DebugScreenEntryStatus defaultStatus, DebugScreenEntryStatus performanceStatus) {
|
||||
this.defaultStatus = defaultStatus;
|
||||
this.performanceStatus = performanceStatus;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec text(DebugEntryFactory factory) {
|
||||
this.textFactory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec toggle(DebugToggleListener listener) {
|
||||
this.toggleListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugEntrySpec supportsAlways(boolean supportsAlways) {
|
||||
this.supportsAlways = supportsAlways;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public DebugEntryCategory getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus getDefaultStatus() {
|
||||
return defaultStatus;
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus getPerformanceStatus() {
|
||||
return performanceStatus;
|
||||
}
|
||||
|
||||
public DebugEntryFactory getTextFactory() {
|
||||
return textFactory;
|
||||
}
|
||||
|
||||
public DebugToggleListener getToggleListener() {
|
||||
return toggleListener;
|
||||
}
|
||||
|
||||
public boolean getSupportsAlways() {
|
||||
return supportsAlways;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugFact {
|
||||
|
||||
DebugFact text(CharSequence text);
|
||||
|
||||
DebugFact text(char c);
|
||||
|
||||
DebugFact value(CharSequence value);
|
||||
|
||||
DebugFact value(int value);
|
||||
|
||||
DebugFact value(long value);
|
||||
|
||||
DebugFact value(double value, int decimals);
|
||||
|
||||
DebugFact padded(long value, int width);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class DebugFactLines {
|
||||
|
||||
private static final int MAX_LABEL = 24;
|
||||
private static final String COLON = ": ";
|
||||
private static final String DASH = " - ";
|
||||
|
||||
private String[] sources = new String[8];
|
||||
private String[] names = new String[8];
|
||||
private String[] values = new String[8];
|
||||
private int cursor;
|
||||
private int size;
|
||||
private boolean open;
|
||||
|
||||
public void begin() {
|
||||
cursor = 0;
|
||||
open = true;
|
||||
}
|
||||
|
||||
public void end() {
|
||||
open = false;
|
||||
if (cursor < size) {
|
||||
Arrays.fill(sources, cursor, size, null);
|
||||
Arrays.fill(names, cursor, size, null);
|
||||
Arrays.fill(values, cursor, size, null);
|
||||
}
|
||||
size = cursor;
|
||||
}
|
||||
|
||||
public void fact(String name, String value) {
|
||||
requireOpen();
|
||||
if (value == null) throw new IllegalArgumentException("Debug fact value is null");
|
||||
reserve();
|
||||
sources[cursor] = null;
|
||||
names[cursor] = name;
|
||||
values[cursor] = value;
|
||||
cursor++;
|
||||
}
|
||||
|
||||
public void line(String line) {
|
||||
fact(null, line);
|
||||
}
|
||||
|
||||
public void split(String line) {
|
||||
requireOpen();
|
||||
if (line == null) throw new IllegalArgumentException("Debug fact line is null");
|
||||
reserve();
|
||||
if (line.equals(sources[cursor])) {
|
||||
cursor++;
|
||||
return;
|
||||
}
|
||||
sources[cursor] = line;
|
||||
parse(line);
|
||||
cursor++;
|
||||
}
|
||||
|
||||
public void emit(DebugScreenDisplayer displayer, DebugGroupHandle group) {
|
||||
if (open) throw new IllegalStateException("DebugFactLines.emit() called before end()");
|
||||
for (int i = 0; i < size; i++) {
|
||||
final String value = values[i];
|
||||
final String name = names[i];
|
||||
if (name == null) {
|
||||
displayer.line(group).text(value);
|
||||
} else {
|
||||
displayer.fact(group, name).value(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parse(String line) {
|
||||
int at = labelEnd(line, COLON);
|
||||
if (at >= 0) {
|
||||
names[cursor] = name(line, at);
|
||||
values[cursor] = line.substring(at + COLON.length());
|
||||
return;
|
||||
}
|
||||
at = labelEnd(line, DASH);
|
||||
if (at >= 0) {
|
||||
names[cursor] = name(line, at);
|
||||
values[cursor] = line.substring(at + DASH.length());
|
||||
return;
|
||||
}
|
||||
names[cursor] = null;
|
||||
values[cursor] = line;
|
||||
}
|
||||
|
||||
private String name(String line, int end) {
|
||||
final String previous = names[cursor];
|
||||
return previous != null && previous.length() == end && line.startsWith(previous) ? previous : line.substring(0, end);
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (!open) throw new IllegalStateException("DebugFactLines is not open: call begin() first");
|
||||
}
|
||||
|
||||
private void reserve() {
|
||||
if (cursor == sources.length) {
|
||||
final int capacity = cursor * 2;
|
||||
sources = Arrays.copyOf(sources, capacity);
|
||||
names = Arrays.copyOf(names, capacity);
|
||||
values = Arrays.copyOf(values, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
static int labelEnd(String line, String separator) {
|
||||
final int at = line.indexOf(separator);
|
||||
if (at <= 0 || at > MAX_LABEL) return -1;
|
||||
for (int i = 0; i < at; i++) {
|
||||
final char c = line.charAt(i);
|
||||
if (c == '=' || c == '(') return -1;
|
||||
}
|
||||
return at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugGroupHandle {
|
||||
|
||||
int LEFT = 0;
|
||||
int RIGHT = 1;
|
||||
|
||||
String id();
|
||||
|
||||
int side();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public final class DebugGroupSpec {
|
||||
|
||||
public static final int DEFAULT_ORDER = 500;
|
||||
|
||||
private final String id;
|
||||
private String titleKey;
|
||||
private int accent = -1;
|
||||
private int side = DebugGroupHandle.LEFT;
|
||||
private int order = DEFAULT_ORDER;
|
||||
|
||||
public DebugGroupSpec(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public DebugGroupSpec title(String keyOrLiteral) {
|
||||
this.titleKey = keyOrLiteral;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec accent(int rgb) {
|
||||
this.accent = rgb & 0xFFFFFF;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec side(int side) {
|
||||
this.side = side;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugGroupSpec order(int order) {
|
||||
this.order = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitleKey() {
|
||||
return titleKey;
|
||||
}
|
||||
|
||||
public int getAccent() {
|
||||
return accent;
|
||||
}
|
||||
|
||||
public int getSide() {
|
||||
return side;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
|
||||
public final class DebugScreenApi {
|
||||
|
||||
private DebugScreenApi() {}
|
||||
|
||||
public static DebugGroupHandle registerGroup(DebugGroupSpec spec) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.registerGroup(spec);
|
||||
}
|
||||
|
||||
public static DebugEntryHandle registerEntry(DebugEntrySpec spec) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.registerEntry(spec);
|
||||
}
|
||||
|
||||
public static DebugGroupHandle group(String id) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.groupById(id);
|
||||
}
|
||||
|
||||
public static DebugEntryHandle entry(String id) {
|
||||
DebugScreen.bootstrap();
|
||||
return DebugRegistry.MAIN.entryById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugScreenDisplayer {
|
||||
|
||||
DebugFact line(DebugGroupHandle group);
|
||||
|
||||
DebugFact fact(DebugGroupHandle group, String name);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public interface DebugScreenEntry {
|
||||
|
||||
void display(DebugScreenDisplayer displayer, DebugContext context);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
public enum DebugScreenEntryStatus {
|
||||
|
||||
ALWAYS_ON("alwaysOn"),
|
||||
IN_OVERLAY("inOverlay"),
|
||||
NEVER("never");
|
||||
|
||||
private static final DebugScreenEntryStatus[] VALUES = values();
|
||||
|
||||
public final String serializedName;
|
||||
|
||||
DebugScreenEntryStatus(String serializedName) {
|
||||
this.serializedName = serializedName;
|
||||
}
|
||||
|
||||
public static DebugScreenEntryStatus bySerializedName(String name) {
|
||||
for (final DebugScreenEntryStatus status : VALUES) {
|
||||
if (status.serializedName.equals(name)) return status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@API(owner = "angelica", provides = "angelica|debug", apiVersion = "1")
|
||||
package com.gtnewhorizons.angelica.api.debug;
|
||||
|
||||
import cpw.mods.fml.common.API;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -48,10 +48,6 @@ public final class FontEffectRegistry {
|
||||
return anyRegistered && lowerCode >= 'a' && lowerCode <= 'z' && EFFECTS[lowerCode - 'a'] != null;
|
||||
}
|
||||
|
||||
static long bit(char lowerCode) {
|
||||
return 1L << (lowerCode - 'a');
|
||||
}
|
||||
|
||||
static CustomGlyphEffect get(int bitIndex) {
|
||||
return EFFECTS[bitIndex];
|
||||
}
|
||||
|
||||
@@ -31,15 +31,19 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
|
||||
private static final ResourceLocation DEFINITION = new ResourceLocation("angelica", "font/modern.json");
|
||||
private static final int DEFAULT_HEIGHT = 8;
|
||||
private static final FontProviderBitmap[] NONE = new FontProviderBitmap[0];
|
||||
static final FontProviderBitmap[] NONE = new FontProviderBitmap[0];
|
||||
|
||||
private static volatile FontProviderBitmap[] loaded;
|
||||
|
||||
public static void registerReloadListener() {
|
||||
((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(manager -> loaded = null);
|
||||
((IReloadableResourceManager) Minecraft.getMinecraft().getResourceManager()).registerReloadListener(manager -> {
|
||||
loaded = null;
|
||||
FontProviderCache.invalidate();
|
||||
});
|
||||
}
|
||||
|
||||
private final ResourceLocation sheet;
|
||||
private int texture;
|
||||
private final int columns;
|
||||
private final float uSize;
|
||||
private final float vSize;
|
||||
@@ -60,7 +64,7 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static FontProviderBitmap[] providers() {
|
||||
static FontProviderBitmap[] providers() {
|
||||
FontProviderBitmap[] local = loaded;
|
||||
if (local == null) {
|
||||
final IResourceManager resources = Minecraft.getMinecraft().getResourceManager();
|
||||
@@ -229,7 +233,12 @@ public final class FontProviderBitmap implements FontProvider {
|
||||
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
return FontStrategist.getIntFromResourceLocation(this.sheet);
|
||||
int texture = this.texture;
|
||||
if (texture == 0) {
|
||||
texture = FontStrategist.getIntFromResourceLocation(this.sheet);
|
||||
this.texture = texture;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
|
||||
final class FontProviderCache {
|
||||
|
||||
private static final byte UNRESOLVED = 0;
|
||||
private static final byte MC = 1;
|
||||
private static final byte UNICODE = 2;
|
||||
private static final byte CUSTOM_PRIMARY = 3;
|
||||
private static final byte CUSTOM_FALLBACK = 4;
|
||||
private static final byte BITMAP = 5;
|
||||
private static final int MAX_BITMAPS = Byte.MAX_VALUE - BITMAP;
|
||||
private static final int VARIANT_COUNT = 8;
|
||||
|
||||
private static FontProviderCache[] variants = new FontProviderCache[VARIANT_COUNT];
|
||||
private static int generation;
|
||||
|
||||
private final boolean custom;
|
||||
private final boolean unicode;
|
||||
private final FontProviderBitmap[] bitmaps;
|
||||
private final byte[][] pages = new byte[256][];
|
||||
|
||||
private FontProviderCache(boolean custom, boolean unicode, boolean modern) {
|
||||
this.custom = custom;
|
||||
this.unicode = unicode;
|
||||
this.bitmaps = modern && !unicode ? FontProviderBitmap.providers() : FontProviderBitmap.NONE;
|
||||
}
|
||||
|
||||
static FontProvider lookup(char chr, boolean custom, boolean unicode) {
|
||||
final int variant = (custom ? 4 : 0) | (unicode ? 2 : 0) | (FontConfig.enableModernFont ? 1 : 0);
|
||||
final FontProviderCache[] tables = variants;
|
||||
FontProviderCache table = tables[variant];
|
||||
if (table == null) {
|
||||
table = new FontProviderCache(custom, unicode, (variant & 1) != 0);
|
||||
tables[variant] = table;
|
||||
}
|
||||
return table.bitmaps.length > MAX_BITMAPS ? null : table.get(chr);
|
||||
}
|
||||
|
||||
static void invalidate() {
|
||||
generation++;
|
||||
variants = new FontProviderCache[VARIANT_COUNT];
|
||||
}
|
||||
|
||||
static int generation() {
|
||||
return generation;
|
||||
}
|
||||
|
||||
private FontProvider get(char chr) {
|
||||
final int hi = chr >>> 8;
|
||||
byte[] page = pages[hi];
|
||||
if (page == null) {
|
||||
page = new byte[256];
|
||||
pages[hi] = page;
|
||||
}
|
||||
final int lo = chr & 0xFF;
|
||||
byte id = page[lo];
|
||||
if (id == UNRESOLVED) {
|
||||
id = resolve(chr);
|
||||
page[lo] = id;
|
||||
}
|
||||
return switch (id) {
|
||||
case MC -> FontProviderMC.get(false);
|
||||
case UNICODE -> FontProviderUnicode.get();
|
||||
case CUSTOM_PRIMARY -> FontProviderCustom.getPrimary();
|
||||
case CUSTOM_FALLBACK -> FontProviderCustom.getFallback();
|
||||
default -> bitmaps[id - BITMAP];
|
||||
};
|
||||
}
|
||||
|
||||
private byte resolve(char chr) {
|
||||
if (custom) {
|
||||
if (FontProviderCustom.getPrimary().isGlyphAvailable(chr)) {
|
||||
return CUSTOM_PRIMARY;
|
||||
}
|
||||
if (FontProviderCustom.getFallback().isGlyphAvailable(chr)) {
|
||||
return CUSTOM_FALLBACK;
|
||||
}
|
||||
final int bitmap = bitmapIndex(chr);
|
||||
return bitmap >= 0 ? (byte) (BITMAP + bitmap) : UNICODE;
|
||||
}
|
||||
final int bitmap = bitmapIndex(chr);
|
||||
if (bitmap >= 0) {
|
||||
return (byte) (BITMAP + bitmap);
|
||||
}
|
||||
return !unicode && FontProviderMC.get(false).isGlyphAvailable(chr) ? MC : UNICODE;
|
||||
}
|
||||
|
||||
private int bitmapIndex(char chr) {
|
||||
for (int i = 0; i < bitmaps.length; i++) {
|
||||
if (bitmaps[i].isGlyphAvailable(chr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public final class FontProviderCustom implements FontProvider {
|
||||
this.atlasTextures[i] = 0;
|
||||
}
|
||||
}
|
||||
FontProviderCache.invalidate();
|
||||
}
|
||||
|
||||
public void reloadFont(int fontID) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import it.unimi.dsi.fastutil.chars.Char2ShortOpenHashMap;
|
||||
import jss.util.RandomXoshiro256StarStar;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FontProviderMC implements FontProvider {
|
||||
|
||||
/**
|
||||
@@ -22,30 +24,38 @@ public final class FontProviderMC implements FontProvider {
|
||||
return (isSGA ? InstLoader.instanceSGA : InstLoader.instance);
|
||||
}
|
||||
|
||||
private static final Char2ShortOpenHashMap MCFONT_ASCII_MAP = new Char2ShortOpenHashMap();
|
||||
private static final short[] MCFONT_SLOTS;
|
||||
ResourceLocation locationFontTexture = null;
|
||||
private record TextureSlot(ResourceLocation location, int id) {}
|
||||
|
||||
private TextureSlot textureSlot = new TextureSlot(null, 0);
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
public int[] charWidth;
|
||||
|
||||
static {
|
||||
MCFONT_ASCII_MAP.defaultReturnValue((short) -1);
|
||||
int maxChar = 0;
|
||||
for (int i = 0; i < MCFONT_CHARS.length(); i++) {
|
||||
maxChar = Math.max(maxChar, MCFONT_CHARS.charAt(i));
|
||||
}
|
||||
MCFONT_SLOTS = new short[maxChar + 1];
|
||||
Arrays.fill(MCFONT_SLOTS, (short) -1);
|
||||
for (short i = 0; i < MCFONT_CHARS.length(); i++) {
|
||||
MCFONT_ASCII_MAP.put(MCFONT_CHARS.charAt(i), i);
|
||||
MCFONT_SLOTS[MCFONT_CHARS.charAt(i)] = i;
|
||||
}
|
||||
}
|
||||
|
||||
public int lookupMcFontPosition(char ch) {
|
||||
return MCFONT_ASCII_MAP.get(ch);
|
||||
return ch < MCFONT_SLOTS.length ? MCFONT_SLOTS[ch] : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlyphAvailable(char chr) {
|
||||
return MCFONT_ASCII_MAP.containsKey(chr);
|
||||
return lookupMcFontPosition(chr) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* A random glyph of the same width, for {@code §k}. Only slots the reverse lookup
|
||||
* agrees with are eligible: {@link #MCFONT_CHARS} repeats {@code U+0000} and the map
|
||||
* agrees with are eligible: {@link #MCFONT_CHARS} repeats {@code U+0000} and the table
|
||||
* keeps the last, so other slots would measure from a different one. Bounded, since
|
||||
* a resource pack may ship widths where nothing matches.
|
||||
*/
|
||||
@@ -61,7 +71,7 @@ public final class FontProviderMC implements FontProvider {
|
||||
continue;
|
||||
}
|
||||
final char replacement = MCFONT_CHARS.charAt(candidate);
|
||||
if (MCFONT_ASCII_MAP.get(replacement) == candidate) {
|
||||
if (lookupMcFontPosition(replacement) == candidate) {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +120,13 @@ public final class FontProviderMC implements FontProvider {
|
||||
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
return FontStrategist.getIntFromResourceLocation(locationFontTexture);
|
||||
final ResourceLocation location = locationFontTexture;
|
||||
TextureSlot slot = textureSlot;
|
||||
if (!Objects.equals(slot.location(), location)) {
|
||||
slot = new TextureSlot(location, FontStrategist.getIntFromResourceLocation(location));
|
||||
textureSlot = slot;
|
||||
}
|
||||
return slot.id();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
public static FontProviderUnicode get() { return FontProviderUnicode.InstLoader.instance; }
|
||||
|
||||
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[256];
|
||||
private static final int[] unicodePageTextures = new int[256];
|
||||
private final byte[] glyphWidth = new byte[65536];
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
|
||||
@@ -129,8 +130,12 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
@Override
|
||||
public int getTexture(char chr) {
|
||||
final int uniPage = chr / 256;
|
||||
ResourceLocation rl = getUnicodePageLocation(uniPage);
|
||||
return FontStrategist.getIntFromResourceLocation(rl);
|
||||
int texture = unicodePageTextures[uniPage];
|
||||
if (texture == 0) {
|
||||
texture = FontStrategist.getIntFromResourceLocation(getUnicodePageLocation(uniPage));
|
||||
unicodePageTextures[uniPage] = texture;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -161,6 +161,14 @@ public class FontStrategist {
|
||||
if (me.bookMode) {
|
||||
return FontProviderUnicode.get();
|
||||
}
|
||||
if (!me.isSplash && GLStateManager.isMainThread()) {
|
||||
final FontProvider cached = FontProviderCache.lookup(chr, customFontEnabled, forceUnicode);
|
||||
if (cached != null) { return cached; }
|
||||
}
|
||||
return searchFontProvider(me, chr, customFontEnabled, forceUnicode);
|
||||
}
|
||||
|
||||
static FontProvider searchFontProvider(BatchingFontRenderer me, char chr, boolean customFontEnabled, boolean forceUnicode) {
|
||||
if (customFontEnabled && !me.isSplash) {
|
||||
FontProvider fp;
|
||||
fp = FontProviderCustom.getPrimary();
|
||||
@@ -235,23 +243,30 @@ public class FontStrategist {
|
||||
static final int INVALID_TEXTURE = Integer.MIN_VALUE;
|
||||
private static int remap(int n) { return -1 - n; } // involution
|
||||
|
||||
private static final Object RESOURCE_LOCK = new Object();
|
||||
private static final List<ResourceLocation> intToResourceLocation = new ArrayList<>();
|
||||
private static final Map<ResourceLocation, Integer> resourceLocationToInt = new HashMap<>();
|
||||
|
||||
static int getIntFromResourceLocation(ResourceLocation rl) {
|
||||
Integer i = resourceLocationToInt.get(rl);
|
||||
if (i != null) { return i; }
|
||||
int idx = remap(intToResourceLocation.size());
|
||||
resourceLocationToInt.put(rl, idx);
|
||||
intToResourceLocation.add(rl);
|
||||
return idx;
|
||||
synchronized (RESOURCE_LOCK) {
|
||||
Integer i = resourceLocationToInt.get(rl);
|
||||
if (i != null) { return i; }
|
||||
int idx = remap(intToResourceLocation.size());
|
||||
resourceLocationToInt.put(rl, idx);
|
||||
intToResourceLocation.add(rl);
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
static void bindIntTexture(BatchingFontRenderer owner, int texture) {
|
||||
if (texture > 0) {
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||
} else if (texture < 0) {
|
||||
((FontRendererAccessor) owner.underlying).angelica$bindTexture(intToResourceLocation.get(remap(texture)));
|
||||
final ResourceLocation location;
|
||||
synchronized (RESOURCE_LOCK) {
|
||||
location = intToResourceLocation.get(remap(texture));
|
||||
}
|
||||
((FontRendererAccessor) owner.underlying).angelica$bindTexture(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
|
||||
|
||||
public final class FormatCodes {
|
||||
|
||||
public static final int NONE = 0;
|
||||
public static final int HEX = 1;
|
||||
public static final int COLOR = 2;
|
||||
public static final int OBFUSCATED = 3;
|
||||
public static final int BOLD = 4;
|
||||
public static final int STRIKETHROUGH = 5;
|
||||
public static final int UNDERLINE = 6;
|
||||
public static final int ITALIC = 7;
|
||||
public static final int RAINBOW = 8;
|
||||
public static final int WAVE = 9;
|
||||
public static final int DINNERBONE = 10;
|
||||
public static final int SHADOW_COLOR = 11;
|
||||
public static final int SHADOW_TOGGLE = 12;
|
||||
public static final int GRADIENT = 13;
|
||||
public static final int CUSTOM = 14;
|
||||
public static final int RESET = 15;
|
||||
|
||||
private static final int EXTRA_SHIFT = 8;
|
||||
private static final int PAYLOAD_SHIFT = 16;
|
||||
private static final long RGB_MASK = 0xFFFFFFL;
|
||||
|
||||
private FormatCodes() {}
|
||||
|
||||
public static long parse(CharSequence s, int codeIndex, int end) {
|
||||
final char code = Character.toLowerCase(s.charAt(codeIndex));
|
||||
return switch (code) {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> pack(COLOR, 0, code - '0');
|
||||
case 'a', 'b', 'c', 'd', 'e', 'f' -> pack(COLOR, 0, code - 'a' + 10);
|
||||
case 'k' -> OBFUSCATED;
|
||||
case 'l' -> BOLD;
|
||||
case 'm' -> STRIKETHROUGH;
|
||||
case 'n' -> UNDERLINE;
|
||||
case 'o' -> ITALIC;
|
||||
case 'r' -> RESET;
|
||||
case 'q' -> AngelicaConfig.enableRainbow ? RAINBOW : NONE;
|
||||
case 'z' -> AngelicaConfig.enableWaveText ? WAVE : NONE;
|
||||
case 'v' -> AngelicaConfig.enableDinnerboneText ? DINNERBONE : NONE;
|
||||
case 'x' -> {
|
||||
if (!AngelicaConfig.enableRGBColors || codeIndex + SECTION_X_PAYLOAD >= end) yield NONE;
|
||||
final int rgb = ColorCodeUtils.parseHexPairs(s, codeIndex + 1, 6);
|
||||
yield rgb == -1 ? NONE : pack(HEX, SECTION_X_PAYLOAD, rgb);
|
||||
}
|
||||
case 'u' -> {
|
||||
if (!AngelicaConfig.enableDropShadow) yield NONE;
|
||||
final int rgb = codeIndex + SECTION_X_LENGTH < end ? ColorCodeUtils.parseSectionXAt(s, codeIndex + 1) : -1;
|
||||
yield rgb == -1 ? SHADOW_TOGGLE : pack(SHADOW_COLOR, SECTION_X_LENGTH, rgb);
|
||||
}
|
||||
case 'g' -> {
|
||||
if (!AngelicaConfig.enableGradients || codeIndex + GRADIENT_PAYLOAD >= end) yield NONE;
|
||||
final int from = ColorCodeUtils.parseSectionXAt(s, codeIndex + 1);
|
||||
final int to = ColorCodeUtils.parseSectionXAt(s, codeIndex + 1 + SECTION_X_LENGTH);
|
||||
yield from == -1 || to == -1 ? NONE : pack(GRADIENT, GRADIENT_PAYLOAD, ((long) from << 24) | to);
|
||||
}
|
||||
default -> FontEffectRegistry.isRegistered(code) ? pack(CUSTOM, 0, code - 'a') : NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private static long pack(int kind, int extraChars, long payload) {
|
||||
return kind | ((long) extraChars << EXTRA_SHIFT) | (payload << PAYLOAD_SHIFT);
|
||||
}
|
||||
|
||||
public static int kind(long code) {
|
||||
return (int) (code & 0xFF);
|
||||
}
|
||||
|
||||
public static int extraChars(long code) {
|
||||
return (int) ((code >>> EXTRA_SHIFT) & 0xFF);
|
||||
}
|
||||
|
||||
public static int rgb(long code) {
|
||||
return (int) ((code >>> PAYLOAD_SHIFT) & RGB_MASK);
|
||||
}
|
||||
|
||||
public static int colorIndex(long code) {
|
||||
return (int) ((code >>> PAYLOAD_SHIFT) & 0xF);
|
||||
}
|
||||
|
||||
public static int gradientFrom(long code) {
|
||||
return (int) ((code >>> (PAYLOAD_SHIFT + 24)) & RGB_MASK);
|
||||
}
|
||||
|
||||
public static int gradientTo(long code) {
|
||||
return rgb(code);
|
||||
}
|
||||
|
||||
public static long customBit(long code) {
|
||||
return 1L << ((code >>> PAYLOAD_SHIFT) & 0x1F);
|
||||
}
|
||||
|
||||
public static boolean endsBold(long code) {
|
||||
return switch (kind(code)) {
|
||||
case COLOR, RESET -> true;
|
||||
case HEX -> FontRendering.hexColorResetsStyles();
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
|
||||
record GlyphScale(int scaleBits, int aspectBits, float x, float y) {
|
||||
|
||||
private static GlyphScale current = of(FontConfig.glyphScale, FontConfig.glyphAspect);
|
||||
|
||||
private static GlyphScale of(float scale, float aspect) {
|
||||
final double pow = Math.pow(2, aspect);
|
||||
return new GlyphScale(Float.floatToRawIntBits(scale), Float.floatToRawIntBits(aspect), (float) (scale * pow), (float) (scale / pow));
|
||||
}
|
||||
|
||||
static GlyphScale current() {
|
||||
final float scale = FontConfig.glyphScale;
|
||||
final float aspect = FontConfig.glyphAspect;
|
||||
GlyphScale holder = current;
|
||||
if (holder.scaleBits != Float.floatToRawIntBits(scale) || holder.aspectBits != Float.floatToRawIntBits(aspect)) {
|
||||
holder = of(scale, aspect);
|
||||
current = holder;
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
|
||||
public record GlyphSettings(int generation, int fontAAMode, int fontAAStrength, int shadowCopies, int boldCopies, int fontShadowOffset, int fontShadowOffsetUC,
|
||||
int glyphScale, int glyphAspect, int glyphSpacing, int whitespaceScale, int customFontScale, int waveAmplitude, int flags) {
|
||||
|
||||
private static final int CUSTOM_FONT = 1;
|
||||
private static final int MODERN_FONT = 1 << 1;
|
||||
private static final int GLYPH_REPLACEMENTS = 1 << 2;
|
||||
private static final int RGB_COLORS = 1 << 3;
|
||||
private static final int GRADIENTS = 1 << 4;
|
||||
private static final int RAINBOW = 1 << 5;
|
||||
private static final int WAVE = 1 << 6;
|
||||
private static final int DINNERBONE = 1 << 7;
|
||||
private static final int DROP_SHADOW = 1 << 8;
|
||||
private static final int HEX_RESETS_STYLES = 1 << 9;
|
||||
private static final int CACHE_OVERRIDE = 1 << 10;
|
||||
private static final int METRIC_FLAGS = CUSTOM_FONT | MODERN_FONT | GLYPH_REPLACEMENTS | RGB_COLORS | GRADIENTS | DROP_SHADOW | HEX_RESETS_STYLES;
|
||||
|
||||
// One slot per renderingCacheOverride value: the HUD-cache redraw flips it every refresh, and a single slot would allocate on each flip.
|
||||
private static final GlyphSettings[] CURRENT = new GlyphSettings[2];
|
||||
|
||||
public static GlyphSettings current() {
|
||||
final boolean cacheOverride = HUDCaching.renderingCacheOverride;
|
||||
final int generation = FontProviderCache.generation();
|
||||
final int fontAAMode = FontConfig.fontAAMode;
|
||||
final int fontAAStrength = FontConfig.fontAAStrength;
|
||||
final int shadowCopies = FontConfig.shadowCopies;
|
||||
final int boldCopies = FontConfig.boldCopies;
|
||||
final int fontShadowOffset = Float.floatToIntBits(FontConfig.fontShadowOffset);
|
||||
final int fontShadowOffsetUC = Float.floatToIntBits(FontConfig.fontShadowOffsetUC);
|
||||
final int glyphScale = Float.floatToIntBits(FontConfig.glyphScale);
|
||||
final int glyphAspect = Float.floatToIntBits(FontConfig.glyphAspect);
|
||||
final int glyphSpacing = Float.floatToIntBits(FontConfig.glyphSpacing);
|
||||
final int whitespaceScale = Float.floatToIntBits(FontConfig.whitespaceScale);
|
||||
final int customFontScale = Float.floatToIntBits(FontConfig.customFontScale);
|
||||
final int waveAmplitude = Float.floatToIntBits(AngelicaConfig.waveAmplitude);
|
||||
final int flags = (FontConfig.enableCustomFont ? CUSTOM_FONT : 0)
|
||||
| (FontConfig.enableModernFont ? MODERN_FONT : 0)
|
||||
| (FontConfig.enableGlyphReplacements ? GLYPH_REPLACEMENTS : 0)
|
||||
| (AngelicaConfig.enableRGBColors ? RGB_COLORS : 0)
|
||||
| (AngelicaConfig.enableGradients ? GRADIENTS : 0)
|
||||
| (AngelicaConfig.enableRainbow ? RAINBOW : 0)
|
||||
| (AngelicaConfig.enableWaveText ? WAVE : 0)
|
||||
| (AngelicaConfig.enableDinnerboneText ? DINNERBONE : 0)
|
||||
| (AngelicaConfig.enableDropShadow ? DROP_SHADOW : 0)
|
||||
| (FontRendering.hexColorResetsStyles() ? HEX_RESETS_STYLES : 0)
|
||||
| (cacheOverride ? CACHE_OVERRIDE : 0);
|
||||
final int slot = cacheOverride ? 1 : 0;
|
||||
final GlyphSettings cached = CURRENT[slot];
|
||||
if (cached != null
|
||||
&& cached.generation == generation
|
||||
&& cached.fontAAMode == fontAAMode
|
||||
&& cached.fontAAStrength == fontAAStrength
|
||||
&& cached.shadowCopies == shadowCopies
|
||||
&& cached.boldCopies == boldCopies
|
||||
&& cached.fontShadowOffset == fontShadowOffset
|
||||
&& cached.fontShadowOffsetUC == fontShadowOffsetUC
|
||||
&& cached.glyphScale == glyphScale
|
||||
&& cached.glyphAspect == glyphAspect
|
||||
&& cached.glyphSpacing == glyphSpacing
|
||||
&& cached.whitespaceScale == whitespaceScale
|
||||
&& cached.customFontScale == customFontScale
|
||||
&& cached.waveAmplitude == waveAmplitude
|
||||
&& cached.flags == flags) {
|
||||
return cached;
|
||||
}
|
||||
final GlyphSettings live = new GlyphSettings(generation, fontAAMode, fontAAStrength, shadowCopies, boldCopies, fontShadowOffset, fontShadowOffsetUC,
|
||||
glyphScale, glyphAspect, glyphSpacing, whitespaceScale, customFontScale, waveAmplitude, flags);
|
||||
CURRENT[slot] = live;
|
||||
return live;
|
||||
}
|
||||
|
||||
public boolean sameMetricsAs(GlyphSettings other) {
|
||||
return generation == other.generation
|
||||
&& glyphScale == other.glyphScale
|
||||
&& glyphAspect == other.glyphAspect
|
||||
&& glyphSpacing == other.glyphSpacing
|
||||
&& whitespaceScale == other.whitespaceScale
|
||||
&& customFontScale == other.customFontScale
|
||||
&& (flags & METRIC_FLAGS) == (other.flags & METRIC_FLAGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class QuadRegionAllocator {
|
||||
|
||||
private int[] freeStart = new int[16];
|
||||
private int[] freeLength = new int[16];
|
||||
private int freeCount;
|
||||
|
||||
private int[] retiredStart = new int[16];
|
||||
private int[] retiredLength = new int[16];
|
||||
private long[] retiredFrame = new long[16];
|
||||
private int retiredCount;
|
||||
|
||||
QuadRegionAllocator(int capacity) {
|
||||
reset(capacity);
|
||||
}
|
||||
|
||||
void reset(int capacity) {
|
||||
freeCount = 0;
|
||||
retiredCount = 0;
|
||||
if (capacity > 0) {
|
||||
freeStart[0] = 0;
|
||||
freeLength[0] = capacity;
|
||||
freeCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int allocate(int quads) {
|
||||
for (int i = 0; i < freeCount; i++) {
|
||||
final int length = freeLength[i];
|
||||
if (length < quads) continue;
|
||||
final int start = freeStart[i];
|
||||
if (length == quads) {
|
||||
System.arraycopy(freeStart, i + 1, freeStart, i, freeCount - i - 1);
|
||||
System.arraycopy(freeLength, i + 1, freeLength, i, freeCount - i - 1);
|
||||
freeCount--;
|
||||
} else {
|
||||
freeStart[i] = start + quads;
|
||||
freeLength[i] = length - quads;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void retire(int start, int quads, long frame) {
|
||||
if (retiredCount == retiredStart.length) {
|
||||
final int n = retiredCount * 2;
|
||||
retiredStart = Arrays.copyOf(retiredStart, n);
|
||||
retiredLength = Arrays.copyOf(retiredLength, n);
|
||||
retiredFrame = Arrays.copyOf(retiredFrame, n);
|
||||
}
|
||||
retiredStart[retiredCount] = start;
|
||||
retiredLength[retiredCount] = quads;
|
||||
retiredFrame[retiredCount] = frame;
|
||||
retiredCount++;
|
||||
}
|
||||
|
||||
void advance(long frame) {
|
||||
final long reusableUpTo = frame - RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
int kept = 0;
|
||||
for (int i = 0; i < retiredCount; i++) {
|
||||
if (retiredFrame[i] <= reusableUpTo) {
|
||||
free(retiredStart[i], retiredLength[i]);
|
||||
} else {
|
||||
retiredStart[kept] = retiredStart[i];
|
||||
retiredLength[kept] = retiredLength[i];
|
||||
retiredFrame[kept] = retiredFrame[i];
|
||||
kept++;
|
||||
}
|
||||
}
|
||||
retiredCount = kept;
|
||||
}
|
||||
|
||||
private void free(int start, int quads) {
|
||||
int at = 0;
|
||||
while (at < freeCount && freeStart[at] < start) at++;
|
||||
final boolean joinsPrev = at > 0 && freeStart[at - 1] + freeLength[at - 1] == start;
|
||||
final boolean joinsNext = at < freeCount && start + quads == freeStart[at];
|
||||
if (joinsPrev && joinsNext) {
|
||||
freeLength[at - 1] += quads + freeLength[at];
|
||||
System.arraycopy(freeStart, at + 1, freeStart, at, freeCount - at - 1);
|
||||
System.arraycopy(freeLength, at + 1, freeLength, at, freeCount - at - 1);
|
||||
freeCount--;
|
||||
} else if (joinsPrev) {
|
||||
freeLength[at - 1] += quads;
|
||||
} else if (joinsNext) {
|
||||
freeStart[at] = start;
|
||||
freeLength[at] += quads;
|
||||
} else {
|
||||
if (freeCount == freeStart.length) {
|
||||
freeStart = Arrays.copyOf(freeStart, freeCount * 2);
|
||||
freeLength = Arrays.copyOf(freeLength, freeCount * 2);
|
||||
}
|
||||
System.arraycopy(freeStart, at, freeStart, at + 1, freeCount - at);
|
||||
System.arraycopy(freeLength, at, freeLength, at + 1, freeCount - at);
|
||||
freeStart[at] = start;
|
||||
freeLength[at] = quads;
|
||||
freeCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.FeedbackManager;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
final class RetainedTextStore {
|
||||
|
||||
private static final int INITIAL_QUADS = 4096;
|
||||
private static final long EVICT_AFTER_NANOS = 5_000_000_000L;
|
||||
|
||||
// Retired regions and buffers wait MAX_FRAMES_IN_FLIGHT frames: SDL-GPU runs a frame's uploads before its draws, so reusing one sooner overwrites bytes an in-flight draw still reads.
|
||||
private static final QuadRegionAllocator allocator = new QuadRegionAllocator(0);
|
||||
private static final ObjectArrayList<TextMesh> resident = new ObjectArrayList<>();
|
||||
private static int buffer;
|
||||
private static int capacityQuads = INITIAL_QUADS;
|
||||
private static int generation;
|
||||
private static long frame;
|
||||
private static long frameNanos = System.nanoTime();
|
||||
private static int[] retiredBuffers = new int[4];
|
||||
private static long[] retiredBufferFrames = new long[4];
|
||||
private static int retiredBufferCount;
|
||||
|
||||
private RetainedTextStore() {}
|
||||
|
||||
static int buffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static boolean ensure(TextMesh mesh) {
|
||||
if (!GLStateManager.isMainThread() || mesh.quadCount > BatchingFontRenderer.QUAD_BUDGET || GLStateManager.isRecordingDisplayList() || FeedbackManager.isFeedbackMode()) {
|
||||
return false;
|
||||
}
|
||||
if (mesh.regionStart >= 0 && mesh.regionGeneration == generation) {
|
||||
mesh.lastUsedNanos = frameNanos;
|
||||
return true;
|
||||
}
|
||||
final int quads = mesh.quadCount;
|
||||
if (buffer == 0) {
|
||||
create(Math.max(capacityQuads, quads));
|
||||
}
|
||||
int start = allocator.allocate(quads);
|
||||
if (start < 0) {
|
||||
int capacity = capacityQuads * 2;
|
||||
while (capacity < quads) capacity *= 2;
|
||||
retireBuffer();
|
||||
create(capacity);
|
||||
start = allocator.allocate(quads);
|
||||
}
|
||||
upload(mesh, start);
|
||||
mesh.regionStart = start;
|
||||
mesh.regionQuads = quads;
|
||||
mesh.regionGeneration = generation;
|
||||
mesh.lastUsedNanos = frameNanos;
|
||||
resident.add(mesh);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void release(TextMesh mesh) {
|
||||
if (mesh.regionStart >= 0 && mesh.regionGeneration == generation) {
|
||||
final int i = resident.indexOf(mesh);
|
||||
if (i >= 0) {
|
||||
evict(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
mesh.regionStart = -1;
|
||||
mesh.regionQuads = 0;
|
||||
}
|
||||
|
||||
static void endFrame() {
|
||||
frame++;
|
||||
final long now = System.nanoTime();
|
||||
frameNanos = now;
|
||||
allocator.advance(frame);
|
||||
if (retiredBufferCount > 0) {
|
||||
deleteExpiredBuffers();
|
||||
}
|
||||
if (resident.isEmpty()) {
|
||||
retireBuffer();
|
||||
return;
|
||||
}
|
||||
for (int i = resident.size() - 1; i >= 0; i--) {
|
||||
if (now - resident.get(i).lastUsedNanos > EVICT_AFTER_NANOS) {
|
||||
evict(i);
|
||||
}
|
||||
}
|
||||
if (resident.isEmpty()) {
|
||||
retireBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private static void evict(int index) {
|
||||
final TextMesh mesh = resident.get(index);
|
||||
allocator.retire(mesh.regionStart, mesh.regionQuads, frame);
|
||||
final TextMesh last = resident.pop();
|
||||
if (index < resident.size()) {
|
||||
resident.set(index, last);
|
||||
}
|
||||
mesh.regionStart = -1;
|
||||
mesh.regionQuads = 0;
|
||||
}
|
||||
|
||||
private static void create(int capacity) {
|
||||
final int previous = GLStateManager.ctx().boundVBO;
|
||||
buffer = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, buffer);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, (long) capacity * BatchingFontRenderer.QUAD_BYTES, GL15.GL_DYNAMIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, previous);
|
||||
capacityQuads = capacity;
|
||||
allocator.reset(capacity);
|
||||
resident.clear();
|
||||
generation++;
|
||||
}
|
||||
|
||||
private static void upload(TextMesh mesh, int start) {
|
||||
final int previous = GLStateManager.ctx().boundVBO;
|
||||
final ByteBuffer vertices = mesh.vertices;
|
||||
vertices.position(0);
|
||||
vertices.limit(mesh.quadCount * BatchingFontRenderer.QUAD_BYTES);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, buffer);
|
||||
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, (long) start * BatchingFontRenderer.QUAD_BYTES, vertices);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, previous);
|
||||
vertices.clear();
|
||||
}
|
||||
|
||||
private static void retireBuffer() {
|
||||
if (buffer == 0) return;
|
||||
if (retiredBufferCount == retiredBuffers.length) {
|
||||
retiredBuffers = Arrays.copyOf(retiredBuffers, retiredBufferCount * 2);
|
||||
retiredBufferFrames = Arrays.copyOf(retiredBufferFrames, retiredBufferCount * 2);
|
||||
}
|
||||
retiredBuffers[retiredBufferCount] = buffer;
|
||||
retiredBufferFrames[retiredBufferCount] = frame;
|
||||
retiredBufferCount++;
|
||||
buffer = 0;
|
||||
resident.clear();
|
||||
generation++;
|
||||
}
|
||||
|
||||
private static void deleteExpiredBuffers() {
|
||||
final long deletableUpTo = frame - RenderBackend.MAX_FRAMES_IN_FLIGHT;
|
||||
int kept = 0;
|
||||
for (int i = 0; i < retiredBufferCount; i++) {
|
||||
final int id = retiredBuffers[i];
|
||||
if (retiredBufferFrames[i] <= deletableUpTo) {
|
||||
BatchingFontRenderer.forgetVertexSource(id);
|
||||
GLStateManager.glDeleteBuffers(id);
|
||||
} else {
|
||||
retiredBuffers[kept] = id;
|
||||
retiredBufferFrames[kept] = retiredBufferFrames[i];
|
||||
kept++;
|
||||
}
|
||||
}
|
||||
retiredBufferCount = kept;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLContextState;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ColorMask;
|
||||
import com.gtnewhorizons.angelica.glsm.states.PolygonState;
|
||||
@@ -28,23 +29,23 @@ final class TextDrawState {
|
||||
float offsetUnits;
|
||||
float alphaRef;
|
||||
|
||||
void captureLive() {
|
||||
depthTest = GLStateManager.getDepthTest().isEnabled();
|
||||
depthFunc = GLStateManager.getDepthState().getFunc();
|
||||
depthMask = GLStateManager.isEffectiveDepthMaskEnabled();
|
||||
final ColorMask colorMask = GLStateManager.getEffectiveColorMask(colorMaskScratch);
|
||||
void captureLive(GLContextState ctx) {
|
||||
depthTest = ctx.depthTest.isEnabled();
|
||||
depthFunc = ctx.depthState.getFunc();
|
||||
depthMask = ctx.depthState.isEffectiveMaskEnabled();
|
||||
final ColorMask colorMask = ctx.colorMask.readEffective(colorMaskScratch);
|
||||
colorRed = colorMask.red;
|
||||
colorGreen = colorMask.green;
|
||||
colorBlue = colorMask.blue;
|
||||
colorAlpha = colorMask.alpha;
|
||||
final PolygonState polygon = GLStateManager.getPolygonState();
|
||||
cull = GLStateManager.getCullState().isEnabled();
|
||||
final PolygonState polygon = ctx.polygonState;
|
||||
cull = ctx.cullState.isEnabled();
|
||||
cullFace = polygon.getCullFaceMode();
|
||||
frontFace = polygon.getFrontFace();
|
||||
polygonOffset = GLStateManager.getPolygonOffsetFillState().isEnabled();
|
||||
polygonOffset = ctx.polygonOffsetFillState.isEnabled();
|
||||
offsetFactor = polygon.getOffsetFactor();
|
||||
offsetUnits = polygon.getOffsetUnits();
|
||||
alphaRef = GLStateManager.getAlphaState().getReference();
|
||||
alphaRef = ctx.alphaState.getReference();
|
||||
}
|
||||
|
||||
boolean sameAs(TextDrawState other) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
|
||||
|
||||
public final class TextMesh {
|
||||
|
||||
private static final int INITIAL_COMMANDS = 8;
|
||||
|
||||
GlyphSettings settings;
|
||||
ByteBuffer vertices;
|
||||
long address;
|
||||
int quadCount;
|
||||
int[] commandStart = new int[INITIAL_COMMANDS];
|
||||
int[] commandCount = new int[INITIAL_COMMANDS];
|
||||
int[] commandTexture = new int[INITIAL_COMMANDS];
|
||||
int[] commandLayer = new int[INITIAL_COMMANDS];
|
||||
boolean[] commandUnicode = new boolean[INITIAL_COMMANDS];
|
||||
int commands;
|
||||
boolean captured;
|
||||
boolean animated;
|
||||
int regionStart = -1;
|
||||
int regionQuads;
|
||||
int regionGeneration;
|
||||
long lastUsedNanos;
|
||||
|
||||
public boolean isValid(GlyphSettings live) {
|
||||
return captured && !BatchingFontRenderer.recolorActive() && live.equals(settings);
|
||||
}
|
||||
|
||||
public boolean isAnimated() {
|
||||
return animated;
|
||||
}
|
||||
|
||||
void reserveVertexBytes(int bytes) {
|
||||
if (vertices == null) {
|
||||
vertices = memAlloc(Math.max(bytes, 1));
|
||||
} else if (vertices.capacity() < bytes) {
|
||||
vertices = memRealloc(vertices, Math.max(bytes, vertices.capacity() * 2));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
address = memAddress0(vertices);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
RetainedTextStore.release(this);
|
||||
memFree(vertices);
|
||||
vertices = null;
|
||||
address = 0L;
|
||||
captured = false;
|
||||
}
|
||||
|
||||
void addCommand(int start, int count, int texture, boolean unicode, int layer) {
|
||||
if (commands == commandStart.length) {
|
||||
final int n = commands * 2;
|
||||
commandStart = Arrays.copyOf(commandStart, n);
|
||||
commandCount = Arrays.copyOf(commandCount, n);
|
||||
commandTexture = Arrays.copyOf(commandTexture, n);
|
||||
commandLayer = Arrays.copyOf(commandLayer, n);
|
||||
commandUnicode = Arrays.copyOf(commandUnicode, n);
|
||||
}
|
||||
commandStart[commands] = start;
|
||||
commandCount[commands] = count;
|
||||
commandTexture[commands] = texture;
|
||||
commandLayer[commands] = layer;
|
||||
commandUnicode[commands] = unicode;
|
||||
commands++;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,13 @@ package com.gtnewhorizons.angelica.commands;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.ChunkDebugMinimap;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugFactImpl;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntryList;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.AngelicaEntries;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRoute;
|
||||
import com.gtnewhorizons.angelica.debug.flyby.FlybyRunner;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
|
||||
@@ -13,7 +19,6 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
|
||||
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.RenderRecovery;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.command.CommandBase;
|
||||
@@ -38,7 +43,7 @@ public class AngelicaCommand extends CommandBase {
|
||||
final Map<String, String> help = new LinkedHashMap<>();
|
||||
if (SystemProperties.debugTooling()) {
|
||||
help.put("wireframe", helpLine("wireframe", "Toggle wireframe rendering"));
|
||||
help.put("fog", helpLine("fog", "Toggle fog debug on F3"));
|
||||
help.put("fog", helpLine("fog", "Toggle always-on fog debug info"));
|
||||
help.put("minimap", helpLine("minimap", "Toggle chunk debug overlay"));
|
||||
help.put("flyby", helpLine("flyby <" + FlybyRoute.ids() + ">", "Run a deterministic benchmark route"));
|
||||
help.put("profile", helpLine("profile <start|stop|status>", "Control async-profiler (JFR) recording"));
|
||||
@@ -154,19 +159,33 @@ public class AngelicaCommand extends CommandBase {
|
||||
}
|
||||
|
||||
private void handleFog(ICommandSender sender) {
|
||||
CeleritasDebugScreenHandler.showFogDebug = !CeleritasDebugScreenHandler.showFogDebug;
|
||||
final String f3State = CeleritasDebugScreenHandler.showFogDebug ? "ON" : "OFF";
|
||||
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Fog debug (F3): " + f3State));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + CeleritasDebugScreenHandler.getFogDebugString()));
|
||||
if (debugScreenUnavailable(sender)) return;
|
||||
final boolean on = toggleAlways(DebugScreenEntries.ANGELICA_FOG);
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Fog debug: " + (on ? "ON" : "OFF")));
|
||||
final StringBuilder fogText = new StringBuilder();
|
||||
AngelicaEntries.writeFog(new DebugFactImpl().intoPlain(fogText));
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + " " + fogText));
|
||||
}
|
||||
|
||||
private void handleMinimap(ICommandSender sender) {
|
||||
ChunkDebugMinimap.toggle();
|
||||
final String state = ChunkDebugMinimap.isEnabled() ? "ON" : "OFF";
|
||||
if (debugScreenUnavailable(sender)) return;
|
||||
final String state = toggleAlways(DebugScreenEntries.ANGELICA_CHUNK_MINIMAP) ? "ON" : "OFF";
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.WHITE + "Chunk debug minimap: " + state));
|
||||
}
|
||||
|
||||
private static boolean debugScreenUnavailable(ICommandSender sender) {
|
||||
if (DebugScreen.ENTRIES != null) return false;
|
||||
sender.addChatMessage(new ChatComponentText(EnumChatFormatting.AQUA + "[Angelica] " + EnumChatFormatting.RED + "The debug screen failed to start; see the log"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean toggleAlways(DebugEntryDef def) {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
final boolean on = entries.effectiveStatus(def) != DebugScreenEntryStatus.ALWAYS_ON;
|
||||
entries.overrideForSession(def, on ? DebugScreenEntryStatus.ALWAYS_ON : DebugScreenEntryStatus.NEVER);
|
||||
return on;
|
||||
}
|
||||
|
||||
private void handleCrashTest(ICommandSender sender, String[] args) {
|
||||
if (args.length >= 2 && "isbrh".equalsIgnoreCase(args[1])) {
|
||||
IsbrhTestRenderer.armCrashTest();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.world.World;
|
||||
import org.lwjgl.opengl.ContextCapabilities;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import vazkii.botania.client.fx.ParticleRenderDispatcher;
|
||||
import vazkii.botania.common.core.handler.ConfigHandler;
|
||||
import vazkii.botania.common.core.handler.ManaNetworkHandler;
|
||||
import vazkii.botania.common.lib.LibMisc;
|
||||
|
||||
public final class BotaniaDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "botania:botania";
|
||||
static final String ENTRY_ID = "botania:debug";
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private final String version = displayVersion(runtimeVersion());
|
||||
private int collectors;
|
||||
private int pools;
|
||||
private String glVersion;
|
||||
private String glRenderer;
|
||||
private String glslVersion;
|
||||
|
||||
private BotaniaDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "Botania", 0x00AA00, entry -> new BotaniaDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!ConfigHandler.showF3Debug) return;
|
||||
if (context.isSampleDue()) {
|
||||
final World world = context.world();
|
||||
collectors = ManaNetworkHandler.instance.getAllCollectorsInWorld(world).size();
|
||||
pools = ManaNetworkHandler.instance.getAllPoolsInWorld(world).size();
|
||||
}
|
||||
writeFacts(displayer, group, ParticleRenderDispatcher.sparkleFxCount, ParticleRenderDispatcher.fakeSparkleFxCount, ParticleRenderDispatcher.wispFxCount, ParticleRenderDispatcher.depthIgnoringWispFxCount, ParticleRenderDispatcher.lightningCount, collectors, pools, version);
|
||||
if (!GuiScreen.isCtrlKeyDown() || !GuiScreen.isShiftKeyDown()) {
|
||||
writeContextHint(displayer, group, Minecraft.isRunningOnMac);
|
||||
return;
|
||||
}
|
||||
if (glVersion == null) {
|
||||
glVersion = GLStateManager.glGetString(GL11.GL_VERSION);
|
||||
glRenderer = GLStateManager.glGetString(GL11.GL_RENDERER);
|
||||
glslVersion = GLStateManager.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION);
|
||||
}
|
||||
final ContextCapabilities caps = GLStateManager.capabilities;
|
||||
writeContext(displayer, group, ConfigHandler.useShaders, ConfigHandler.glSecondaryTextureUnit, glVersion, glRenderer, glslVersion, GLStateManager.MAX_TEXTURE_UNITS, caps.GL_ARB_multitexture, caps.GL_ARB_texture_non_power_of_two, caps.OpenGL13);
|
||||
}
|
||||
|
||||
private static String runtimeVersion() {
|
||||
try {
|
||||
return (String) LibMisc.class.getField("VERSION").get(null);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String displayVersion(String version) {
|
||||
return version == null || version.contains("GRADLE") ? "N/A" : version;
|
||||
}
|
||||
|
||||
static void writeFacts(DebugScreenDisplayer displayer, DebugGroupHandle group, int sparkles, int fakeSparkles, int wisps, int depthIgnoringWisps, int lightning, int collectors, int pools, String version) {
|
||||
displayer.fact(group, "Sparkles").value(sparkles).text(" (").value(fakeSparkles).text(" fake)");
|
||||
displayer.fact(group, "Wisps").value(wisps).text(" (").value(depthIgnoringWisps).text(" depth-ignoring)");
|
||||
displayer.fact(group, "Lightning").value(lightning);
|
||||
displayer.fact(group, "Mana Network").value(collectors).text(" collectors, ").value(pools).text(" pools");
|
||||
displayer.fact(group, "Version").value(version);
|
||||
}
|
||||
|
||||
static void writeContextHint(DebugScreenDisplayer displayer, DebugGroupHandle group, boolean mac) {
|
||||
displayer.line(group).text(mac ? "SHIFT+CMD for context" : "SHIFT+CTRL for context");
|
||||
}
|
||||
|
||||
static void writeContext(DebugScreenDisplayer displayer, DebugGroupHandle group, boolean useShaders, int secondaryUnit, String glVersion, String glRenderer, String glslVersion, int maxTextureUnits, boolean multitexture, boolean nonPowerOfTwo, boolean openGl13) {
|
||||
displayer.line(group).text("Config Context");
|
||||
displayer.fact(group, "shaders.enabled").value(String.valueOf(useShaders));
|
||||
displayer.fact(group, "shaders.secondaryUnit").value(secondaryUnit);
|
||||
displayer.line(group).text("OpenGL Context");
|
||||
displayer.fact(group, "GL_VERSION").value(String.valueOf(glVersion));
|
||||
displayer.fact(group, "GL_RENDERER").value(String.valueOf(glRenderer));
|
||||
displayer.fact(group, "GL_SHADING_LANGUAGE_VERSION").value(String.valueOf(glslVersion));
|
||||
displayer.fact(group, "GL_MAX_TEXTURE_IMAGE_UNITS_ARB").value(maxTextureUnits);
|
||||
displayer.fact(group, "GL_ARB_multitexture").value(String.valueOf(multitexture));
|
||||
displayer.fact(group, "GL_ARB_texture_non_power_of_two").value(String.valueOf(nonPowerOfTwo));
|
||||
displayer.fact(group, "OpenGL13").value(String.valueOf(openGl13));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugEntryFactory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupSpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public final class DebugCompat {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
|
||||
private DebugCompat() {}
|
||||
|
||||
public static void registerMissing() {
|
||||
if (Loader.isModLoaded("Botania")) registerMissing("botania", BotaniaDebugEntry::register);
|
||||
if (Loader.isModLoaded("journeymap")) registerMissing("journeymap", JourneyMapDebugEntry::register);
|
||||
if (Loader.isModLoaded("hodgepodge")) registerMissing("hodgepodge", HodgepodgeDebugEntry::register);
|
||||
}
|
||||
|
||||
static void registerMissing(String namespace, Runnable register) {
|
||||
if (DebugRegistry.MAIN.hasNamespace(namespace)) return;
|
||||
try {
|
||||
register.run();
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
LOGGER.error("Debug screen compat entry for {} failed to register", namespace, e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean active(DebugEntryHandle handle) {
|
||||
return handle != null && !handle.isFailed();
|
||||
}
|
||||
|
||||
static DebugEntryHandle register(String groupId, String entryId, String name, int accent, DebugEntryFactory factory) {
|
||||
DebugScreenApi.registerGroup(new DebugGroupSpec(groupId).title(name).accent(accent).side(DebugGroupHandle.LEFT));
|
||||
return DebugScreenApi.registerEntry(new DebugEntrySpec(entryId, DebugEntryCategory.SCREEN_TEXT)
|
||||
.name(name)
|
||||
.defaults(DebugScreenEntryStatus.IN_OVERLAY, DebugScreenEntryStatus.NEVER)
|
||||
.text(factory));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.mitchej123.hodgepodge.config.DebugConfig;
|
||||
import com.mitchej123.hodgepodge.mixins.hooks.ChunkGenScheduler;
|
||||
import com.mitchej123.hodgepodge.mixins.hooks.ChunkGenStats;
|
||||
import net.minecraftforge.classloading.FMLForgePlugin;
|
||||
|
||||
public final class HodgepodgeDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "hodgepodge:chunkgen";
|
||||
static final String ENTRY_ID = "hodgepodge:chunkgen";
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private boolean scheduled;
|
||||
private double budgetUsedMs;
|
||||
private double budgetAvailMs;
|
||||
private double terrainGenEmaMs;
|
||||
private double populateEmaMs;
|
||||
private int genQueue;
|
||||
private int popQueue;
|
||||
private int pipelineBacklog;
|
||||
private int terrainGenCount;
|
||||
private int popCount;
|
||||
private int blockedLoads;
|
||||
private int stuckCount;
|
||||
private int neighborFills;
|
||||
private double debtMs;
|
||||
|
||||
private HodgepodgeDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
if (!DebugConfig.showChunkGenDebug && FMLForgePlugin.RUNTIME_DEOBF) return;
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "Chunk Generation", 0xFFAA00, entry -> new HodgepodgeDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) sample(context);
|
||||
if (!scheduled) return;
|
||||
writeFacts(displayer, group, budgetUsedMs, budgetAvailMs, terrainGenEmaMs, populateEmaMs, genQueue, popQueue, pipelineBacklog, terrainGenCount, popCount, blockedLoads, stuckCount, neighborFills, debtMs);
|
||||
}
|
||||
|
||||
private void sample(DebugContext context) {
|
||||
final ChunkGenScheduler scheduler = ChunkGenScheduler.getForDimension(context.player().dimension);
|
||||
scheduled = scheduler != null;
|
||||
if (scheduler == null) return;
|
||||
final ChunkGenStats stats = scheduler.getStats();
|
||||
budgetUsedMs = stats.getSnapBudgetUsedMs();
|
||||
budgetAvailMs = stats.getSnapBudgetAvailMs();
|
||||
terrainGenEmaMs = stats.getTerrainGenEmaMs();
|
||||
populateEmaMs = stats.getPopulateEmaMs();
|
||||
genQueue = stats.getSnapGenQueue();
|
||||
popQueue = stats.getSnapPopQueue();
|
||||
pipelineBacklog = stats.getSnapPipelineBacklog();
|
||||
terrainGenCount = stats.getSnapTerrainGenCount();
|
||||
popCount = stats.getSnapPopCount();
|
||||
blockedLoads = stats.getSnapBlockedLoads();
|
||||
stuckCount = stats.getSnapStuckCount();
|
||||
neighborFills = stats.getSnapNeighborFills();
|
||||
debtMs = stats.getDebtMs();
|
||||
}
|
||||
|
||||
static void writeFacts(DebugScreenDisplayer displayer, DebugGroupHandle group, double budgetUsedMs, double budgetAvailMs, double terrainGenEmaMs, double populateEmaMs, int genQueue, int popQueue, int pipelineBacklog, int terrainGenCount, int popCount, int blockedLoads, int stuckCount, int neighborFills, double debtMs) {
|
||||
displayer.fact(group, "Budget").value(budgetUsedMs, 1).text(" / ").value(budgetAvailMs, 1).text(" ms");
|
||||
displayer.fact(group, "Average").text("terrain ").value(terrainGenEmaMs, 1).text(" ms, populate ").value(populateEmaMs, 1).text(" ms");
|
||||
displayer.fact(group, "Queues").text("gen ").value(genQueue).text(", populate ").value(popQueue).text(", pipeline ").value(pipelineBacklog);
|
||||
displayer.fact(group, "Active").text("gen ").value(terrainGenCount).text(", populate ").value(popCount);
|
||||
displayer.fact(group, "Blocked").value(blockedLoads).text(" blocked, ").value(stuckCount).text(" stuck, ").value(neighborFills).text(" fills");
|
||||
displayer.fact(group, "Debt").value(debtMs, 1).text(" ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.gtnewhorizons.angelica.compat.debug;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFactLines;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenApi;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class JourneyMapDebugEntry implements DebugScreenEntry {
|
||||
|
||||
static final String GROUP_ID = "journeymap:journeymap";
|
||||
static final String ENTRY_ID = "journeymap:debug";
|
||||
|
||||
private static final String MAPPING_DISABLED_KEY = "jm.common.enable_mapping_false_text";
|
||||
private static final long REPORT_INTERVAL_NS = TimeUnit.SECONDS.toNanos(3);
|
||||
|
||||
private static DebugEntryHandle handle;
|
||||
|
||||
private final DebugGroupHandle group;
|
||||
private final DebugFactLines facts = new DebugFactLines();
|
||||
private MethodHandle stats;
|
||||
private MethodHandle mappingEnabled;
|
||||
private MethodHandle timerReport;
|
||||
private List<String> report = Collections.emptyList();
|
||||
private long reportAt;
|
||||
private boolean reported;
|
||||
|
||||
private JourneyMapDebugEntry(DebugGroupHandle group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
static void register() {
|
||||
handle = DebugCompat.register(GROUP_ID, ENTRY_ID, "JourneyMap", 0x55FFFF, entry -> new JourneyMapDebugEntry(DebugScreenApi.group(GROUP_ID)));
|
||||
}
|
||||
|
||||
public static boolean active() {
|
||||
return DebugCompat.active(handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) sample(context.nanoTime());
|
||||
facts.emit(displayer, group);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void sample(long now) {
|
||||
try {
|
||||
if (stats == null) resolve();
|
||||
final boolean mapping = ((AtomicBoolean) mappingEnabled.invokeExact()).get();
|
||||
final boolean profiler = Minecraft.getMinecraft().gameSettings.showDebugProfilerChart;
|
||||
if (profiler && (!reported || now - reportAt > REPORT_INTERVAL_NS)) {
|
||||
report = (List<String>) timerReport.invokeExact("", "");
|
||||
reportAt = now;
|
||||
reported = true;
|
||||
}
|
||||
writeFacts(facts, mapping ? (String[]) stats.invokeExact() : null, mapping ? null : I18n.format(MAPPING_DISABLED_KEY), profiler ? report : null);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalStateException("JourneyMap debug stats are unavailable", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolve() throws ReflectiveOperationException {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
|
||||
final Class<?> properties = Class.forName("journeymap.client.properties.CoreProperties");
|
||||
final MethodHandle coreProperties = lookup.findStatic(Class.forName("journeymap.client.JourneymapClient"), "getCoreProperties", MethodType.methodType(properties));
|
||||
mappingEnabled = MethodHandles.filterReturnValue(coreProperties, lookup.findGetter(properties, "mappingEnabled", AtomicBoolean.class));
|
||||
timerReport = lookup.findStatic(Class.forName("journeymap.client.log.StatTimer"), "getReportByTotalTime", MethodType.methodType(List.class, String.class, String.class));
|
||||
stats = lookup.findStatic(Class.forName("journeymap.client.task.multi.MapPlayerTask"), "getDebugStats", MethodType.methodType(String[].class));
|
||||
}
|
||||
|
||||
static void writeFacts(DebugFactLines facts, String[] stats, String mappingDisabled, List<String> report) {
|
||||
facts.begin();
|
||||
if (mappingDisabled != null) facts.line(mappingDisabled);
|
||||
if (stats != null) {
|
||||
for (int i = 0; i < stats.length; i++) {
|
||||
final String line = stats[i];
|
||||
if (line != null) facts.split(line);
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
for (int i = 0, n = report.size(); i < n; i++) {
|
||||
final String line = report.get(i);
|
||||
if (line != null) facts.line(line);
|
||||
}
|
||||
}
|
||||
facts.end();
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,6 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean modernizeF3Screen;
|
||||
|
||||
@Config.Comment("Show block registry name and meta value in F3, similar to 1.8+. [From ArchaicFix]")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean showBlockDebugInfo;
|
||||
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.Comment("Hide downloading terrain screen. [From ArchaicFix]")
|
||||
public static boolean hideDownloadingTerrainScreen;
|
||||
@@ -460,13 +456,14 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean useVanillaChunkTracking;
|
||||
|
||||
@Config.Comment("Disables additional F3 information added by Angelica.")
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean disableF3Additions;
|
||||
|
||||
@Config.Comment("Shows developer counters (FFP, streaming, TESR, transfer) on the F3 screen. Toggle in game with F3+V.")
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean verboseF3;
|
||||
@Config.Comment({"Size of the F3 text.",
|
||||
"-1 = same as your GUI Scale (default, the size vanilla F3 uses)",
|
||||
"0 = Auto: half the largest size that fits the window",
|
||||
"1-16 = that size",
|
||||
"F3 is never drawn larger than your GUI Scale."})
|
||||
@Config.DefaultInt(-1)
|
||||
@Config.RangeInt(min = -1, max = 16)
|
||||
public static int debugGuiScale;
|
||||
|
||||
@Config.Comment("Replaces various FFP uploads with statically allocated VBO's.")
|
||||
@Config.DefaultBoolean(true)
|
||||
|
||||
@@ -50,4 +50,13 @@ public class CompatConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean fixNTMSpace;
|
||||
|
||||
@Config.Comment({"Rules for F3 lines that other mods add without Angelica's F3 API, one rule per entry.",
|
||||
"A rule matches lines that start with the quoted text (color codes ignored), in the left or right column.",
|
||||
" drop left \"Text: \" hides the line",
|
||||
" fact left \"Text: \" group=<panel id> moves the line into that F3 panel; add name=\"Label\" to rename it",
|
||||
"Panel ids are listed under Panels in the F3+F6 screen; entry ids show when you hover an entry there.",
|
||||
"Add when=<entry id> to apply a rule only while that F3 entry is enabled."})
|
||||
@Config.DefaultStringList({})
|
||||
public static String[] debugLineRules;
|
||||
|
||||
}
|
||||
@@ -22,33 +22,48 @@
|
||||
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.AngelicaRenderSectionManager;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ChunkProviderClient;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.world.chunk.EmptyChunk;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
* Debug overlay that renders a minimap showing chunk loading and rendering state. Toggle via /angelica minimap
|
||||
* Debug overlay that renders a minimap showing chunk loading and rendering state.
|
||||
*/
|
||||
public class ChunkDebugMinimap {
|
||||
private static final ChunkDebugMinimap INSTANCE = new ChunkDebugMinimap();
|
||||
@Getter
|
||||
private static volatile boolean enabled = false;
|
||||
|
||||
private static final int RANGE = 32; // Chunks in each direction from player
|
||||
|
||||
public static synchronized void toggle() {
|
||||
if (enabled) disable();
|
||||
else enable();
|
||||
}
|
||||
private static final int FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat FORMAT = DefaultVertexFormat.ALL_FORMATS[FLAGS];
|
||||
private static final int STRIDE = FORMAT.getVertexSize();
|
||||
private static final int COLOR_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int CHUNKS = RANGE * 2 * RANGE * 2;
|
||||
private static final int ALPHA = (int) (0.8F * 255.0F);
|
||||
private static final int BYTES = CHUNKS * 4 * STRIDE;
|
||||
private static final ChunkDebugMinimap INSTANCE = new ChunkDebugMinimap();
|
||||
private static volatile boolean enabled = false;
|
||||
|
||||
private final DebugDrawScope scope = new DebugDrawScope();
|
||||
private final NativeScratch scratch = new NativeScratch(BYTES);
|
||||
private final Matrix4f mvp = new Matrix4f();
|
||||
private long cursor;
|
||||
|
||||
public static synchronized void enable() {
|
||||
if (enabled) return;
|
||||
@@ -60,6 +75,7 @@ public class ChunkDebugMinimap {
|
||||
if (!enabled) return;
|
||||
enabled = false;
|
||||
MinecraftForge.EVENT_BUS.unregister(INSTANCE);
|
||||
INSTANCE.scratch.free();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
@@ -84,39 +100,31 @@ public class ChunkDebugMinimap {
|
||||
final int playerChunkX = ((int) Math.floor(pX)) >> 4;
|
||||
final int playerChunkZ = ((int) Math.floor(pZ)) >> 4;
|
||||
|
||||
// Save GL state
|
||||
GLStateManager.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GLStateManager.glPushMatrix();
|
||||
try {
|
||||
// Setup rendering
|
||||
GLStateManager.glScaled(2, 2, 1);
|
||||
GLStateManager.glTranslated(32, 32, 0);
|
||||
scratch.ensure(BYTES);
|
||||
cursor = scratch.address();
|
||||
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
|
||||
|
||||
GLStateManager.disableLighting();
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
final Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawingQuads();
|
||||
|
||||
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
|
||||
|
||||
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
|
||||
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
|
||||
drawChunk(tess, x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
|
||||
}
|
||||
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
|
||||
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
|
||||
drawChunk(x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
|
||||
}
|
||||
}
|
||||
|
||||
tess.draw();
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), mvp).scale(2, 2, 1).translate(32, 32, 0);
|
||||
|
||||
scope.beginHud();
|
||||
try {
|
||||
final DebugProgram color = DebugPrograms.color();
|
||||
color.use();
|
||||
color.mvp(mvp);
|
||||
final int bytes = (int) (cursor - scratch.address());
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(bytes), GL11.GL_QUADS, FLAGS, bytes / STRIDE);
|
||||
} finally {
|
||||
GLStateManager.glPopMatrix();
|
||||
GLStateManager.glPopAttrib();
|
||||
scope.end();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawChunk(Tessellator tess, int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
|
||||
private void drawChunk(int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
|
||||
boolean initialized = false;
|
||||
float r = 0, g = 0, b = 0;
|
||||
|
||||
@@ -142,16 +150,23 @@ public class ChunkDebugMinimap {
|
||||
}
|
||||
}
|
||||
|
||||
tess.setColorRGBA_F(r, g, b, 0.8f);
|
||||
final int argb = ALPHA << 24 | (int) (r * 255) << 16 | (int) (g * 255) << 8 | (int) (b * 255);
|
||||
|
||||
final int xStart = chunkX - playerChunkX;
|
||||
final int zStart = chunkZ - playerChunkZ;
|
||||
final int xEnd = xStart + 1;
|
||||
final int zEnd = zStart + 1;
|
||||
|
||||
tess.addVertex(xStart, zStart, 0);
|
||||
tess.addVertex(xStart, zEnd, 0);
|
||||
tess.addVertex(xEnd, zEnd, 0);
|
||||
tess.addVertex(xEnd, zStart, 0);
|
||||
vertex(xStart, zStart, argb);
|
||||
vertex(xStart, zEnd, argb);
|
||||
vertex(xEnd, zEnd, argb);
|
||||
vertex(xEnd, zStart, argb);
|
||||
}
|
||||
|
||||
private void vertex(float x, float y, int argb) {
|
||||
final long p = cursor;
|
||||
DebugGeometry.putPosition(p, x, y, 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
cursor = p + STRIDE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
public final class DebugF3Toggle {
|
||||
|
||||
private boolean armed;
|
||||
private boolean comboUsed;
|
||||
private boolean shift;
|
||||
private boolean alt;
|
||||
|
||||
public void arm(boolean shift, boolean alt) {
|
||||
armed = true;
|
||||
comboUsed = false;
|
||||
this.shift = shift;
|
||||
this.alt = alt;
|
||||
}
|
||||
|
||||
public void disarm() {
|
||||
armed = false;
|
||||
comboUsed = false;
|
||||
}
|
||||
|
||||
public boolean isArmed() {
|
||||
return armed;
|
||||
}
|
||||
|
||||
public boolean shift() {
|
||||
return shift;
|
||||
}
|
||||
|
||||
public boolean alt() {
|
||||
return alt;
|
||||
}
|
||||
|
||||
public void onKeyPress(int key) {
|
||||
if (!armed) return;
|
||||
switch (key) {
|
||||
case Keyboard.KEY_F3, Keyboard.KEY_LSHIFT, Keyboard.KEY_RSHIFT, Keyboard.KEY_LMENU, Keyboard.KEY_RMENU -> {}
|
||||
default -> comboUsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onF3Release() {
|
||||
final boolean toggle = armed && !comboUsed;
|
||||
disarm();
|
||||
return toggle;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,70 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.GTNHLib;
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugOptionsScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.InputEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
|
||||
/** F3+V toggles {@link AngelicaConfig#verboseF3} */
|
||||
public class DebugKeyHandler {
|
||||
|
||||
public static final DebugF3Toggle F3_TOGGLE = new DebugF3Toggle();
|
||||
|
||||
@SubscribeEvent
|
||||
public void onKey(InputEvent.KeyInputEvent event) {
|
||||
if (!Keyboard.getEventKeyState() || Keyboard.getEventKey() != Keyboard.KEY_V || !Keyboard.isKeyDown(Keyboard.KEY_F3)
|
||||
|| Minecraft.getMinecraft().currentScreen != null) {
|
||||
final int key = Keyboard.getEventKey();
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
if (mc.currentScreen != null) {
|
||||
if (mc.currentScreen instanceof DebugOptionsScreen && key == Keyboard.KEY_F6 && Keyboard.getEventKeyState()
|
||||
&& !Keyboard.isRepeatEvent() && Keyboard.isKeyDown(Keyboard.KEY_F3)) {
|
||||
mc.displayGuiScreen(null);
|
||||
}
|
||||
F3_TOGGLE.disarm();
|
||||
return;
|
||||
}
|
||||
|
||||
AngelicaConfig.verboseF3 = !AngelicaConfig.verboseF3;
|
||||
ConfigurationManager.save(AngelicaConfig.class);
|
||||
CeleritasDebugScreenHandler.invalidate();
|
||||
GTNHLib.proxy.addDebugToChat(StatCollector.translateToLocal(AngelicaConfig.verboseF3 ? "angelica.debug.verbose.on" : "angelica.debug.verbose.off"));
|
||||
if (!Keyboard.getEventKeyState()) {
|
||||
if (key == Keyboard.KEY_F3 && F3_TOGGLE.onF3Release()) {
|
||||
mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo;
|
||||
mc.gameSettings.showDebugProfilerChart = F3_TOGGLE.shift();
|
||||
((IGameSettingsExt) mc.gameSettings).angelica$setShowFpsGraph(F3_TOGGLE.alt());
|
||||
}
|
||||
if (key == Keyboard.KEY_F3) DebugScreen.syncOverlayVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
F3_TOGGLE.onKeyPress(key);
|
||||
|
||||
if (!Keyboard.isKeyDown(Keyboard.KEY_F3)) {
|
||||
return;
|
||||
}
|
||||
if (!Keyboard.isRepeatEvent()) {
|
||||
switch (key) {
|
||||
case Keyboard.KEY_F6 -> DebugOptionsScreen.open(mc);
|
||||
case Keyboard.KEY_V -> toggleAndReport(DebugScreenEntries.ANGELICA_RENDERER_VERBOSE, "angelica.debug.verbose");
|
||||
case Keyboard.KEY_G -> toggleAndReport(DebugScreenEntries.CHUNK_BORDERS, "angelica.debug.chunk_borders");
|
||||
case Keyboard.KEY_B -> {
|
||||
if (DebugScreen.ENTRIES != null) DebugScreen.ENTRIES.toggleStatus(DebugScreenEntries.ENTITY_HITBOXES);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
if (key == Keyboard.KEY_B && DebugScreen.ENTRIES != null) {
|
||||
RenderManager.debugBoundingBox = DebugScreenEntries.ENTITY_HITBOXES.isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
private static void toggleAndReport(DebugEntryDef def, String key) {
|
||||
if (DebugScreen.ENTRIES == null) return;
|
||||
final boolean on = DebugScreen.ENTRIES.toggleStatus(def);
|
||||
GTNHLib.proxy.addDebugToChat(StatCollector.translateToLocal(key + (on ? ".on" : ".off")));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
public class F3Direction {
|
||||
private static final float AXIS_LENGTH = 10.0F;
|
||||
private static final float LINE_WIDTH = 2.0F;
|
||||
private static final int SEGMENTS = 3;
|
||||
private static final int LINE_VERTICES = SEGMENTS * 2;
|
||||
private static final int QUAD_VERTICES = SEGMENTS * 6;
|
||||
private static final int FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat FORMAT = DefaultVertexFormat.ALL_FORMATS[FLAGS];
|
||||
private static final VertexFormat QUAD_FORMAT = LineQuads.quadFormat(FORMAT);
|
||||
private static final int STRIDE = FORMAT.getVertexSize();
|
||||
private static final int COLOR_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
|
||||
private static final DebugDrawScope SCOPE = new DebugDrawScope();
|
||||
private static final Matrix4f MVP = new Matrix4f();
|
||||
private static int vao;
|
||||
|
||||
public static void renderWorldDirectionsEvent(Minecraft mc, RenderGameOverlayEvent.Pre event) {
|
||||
if (mc.gameSettings.showDebugInfo && mc.gameSettings.thirdPersonView == 0) {
|
||||
final ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
final int width = scaledresolution.getScaledWidth();
|
||||
final int height = scaledresolution.getScaledHeight();
|
||||
if (mc.gameSettings.thirdPersonView == 0) {
|
||||
final int width = event.resolution.getScaledWidth();
|
||||
final int height = event.resolution.getScaledHeight();
|
||||
|
||||
GLStateManager.glPushMatrix();
|
||||
|
||||
@@ -30,33 +55,57 @@ public class F3Direction {
|
||||
GLStateManager.glPopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderWorldDirections() {
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glDepthMask(false);
|
||||
final Tessellator tessellator = Tessellator.instance;
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), MVP);
|
||||
final ViewportState viewport = GLStateManager.getViewportState();
|
||||
|
||||
GLStateManager.glLineWidth(2.0F);
|
||||
tessellator.startDrawing(GL11.GL_LINES);
|
||||
SCOPE.beginHud();
|
||||
try {
|
||||
if (vao == 0) build();
|
||||
final DebugProgram lines = DebugPrograms.lines();
|
||||
lines.use();
|
||||
lines.mvp(MVP);
|
||||
lines.lineWidth(LINE_WIDTH);
|
||||
lines.viewport(viewport.width, viewport.height);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, QUAD_VERTICES);
|
||||
} finally {
|
||||
SCOPE.end();
|
||||
}
|
||||
}
|
||||
|
||||
//X
|
||||
tessellator.setColorRGBA_F(255, 0, 0, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(10, 0.0D, 0.0D);
|
||||
private static void build() {
|
||||
final int quadBytes = QUAD_VERTICES * QUAD_FORMAT.getVertexSize();
|
||||
final NativeScratch source = new NativeScratch(LINE_VERTICES * STRIDE);
|
||||
final NativeScratch expanded = new NativeScratch(quadBytes);
|
||||
try {
|
||||
source.ensure(LINE_VERTICES * STRIDE);
|
||||
expanded.ensure(quadBytes);
|
||||
long p = source.address();
|
||||
p = putAxis(p, AXIS_LENGTH, 0.0F, 0.0F, 0xFFFF0000);
|
||||
p = putAxis(p, 0.0F, 0.0F, AXIS_LENGTH, 0xFF0000FF);
|
||||
putAxis(p, 0.0F, AXIS_LENGTH, 0.0F, 0xFF00FF00);
|
||||
LineQuads.expandWithOtherEnd(source.address(), FORMAT, GL11.GL_LINES, LINE_VERTICES, expanded.address());
|
||||
|
||||
//Z
|
||||
tessellator.setColorRGBA_F(0, 0, 255, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(0.0D, 0.0D, 10);
|
||||
final int vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, expanded.window(quadBytes), GL15.GL_STATIC_DRAW);
|
||||
vao = DebugGeometry.createVao(vbo, QUAD_FORMAT);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
} finally {
|
||||
source.free();
|
||||
expanded.free();
|
||||
}
|
||||
}
|
||||
|
||||
//Y
|
||||
tessellator.setColorRGBA_F(0, 255, 0, 1);
|
||||
tessellator.addVertex(0.0D, 0.0D, 0.0D);
|
||||
tessellator.addVertex(0.0D, 10, 0.0D);
|
||||
|
||||
tessellator.draw();
|
||||
|
||||
GLStateManager.glLineWidth(1.0F);
|
||||
GLStateManager.glDepthMask(true);
|
||||
GLStateManager.enableTexture();
|
||||
private static long putAxis(long address, float x, float y, float z, int argb) {
|
||||
long p = address;
|
||||
DebugGeometry.putPosition(p, 0.0F, 0.0F, 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
p += STRIDE;
|
||||
DebugGeometry.putPosition(p, x, y, z);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
return p + STRIDE;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
package com.gtnewhorizons.angelica.debug;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.shader.ShaderProgram;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugDrawScope;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugNumbers;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static org.lwjgl.opengl.GL11.GL_FLOAT;
|
||||
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
|
||||
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
|
||||
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
|
||||
@@ -29,14 +39,25 @@ public abstract class F3Graph {
|
||||
private static final int HEIGHT = 60;
|
||||
private static final int WIDTH = NUM_SAMPLES;
|
||||
private static final int FONT_COLOR = 0xFFE0E0E0;
|
||||
private static final int BACKGROUND_ARGB = 0x90505050;
|
||||
private static final int FOREGROUND_ARGB = 0xFFFFFFFF;
|
||||
private static final int COLOR_FLAGS = VertexFlags.COLOR_BIT;
|
||||
private static final int TEXTURED_FLAGS = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat COLOR_FORMAT = DefaultVertexFormat.ALL_FORMATS[COLOR_FLAGS];
|
||||
private static final VertexFormat TEXTURED_FORMAT = DefaultVertexFormat.ALL_FORMATS[TEXTURED_FLAGS];
|
||||
private static final int COLOR_STRIDE = COLOR_FORMAT.getVertexSize();
|
||||
private static final int TEXTURED_STRIDE = TEXTURED_FORMAT.getVertexSize();
|
||||
private static final int TEXTURED_UV_OFFSET = DebugGeometry.offsetOf(TEXTURED_FORMAT, VertexFormatElement.Usage.PRIMARY_UV);
|
||||
private static final int TEXTURED_COLOR_OFFSET = DebugGeometry.offsetOf(TEXTURED_FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int COLOR_COLOR_OFFSET = DebugGeometry.offsetOf(COLOR_FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int QUAD_BYTES = 4 * Math.max(COLOR_STRIDE, TEXTURED_STRIDE);
|
||||
// Due to GLSL 120 limitations, it's just easier to use floats
|
||||
private final FloatBuffer sampleBuf = BufferUtils.createFloatBuffer(NUM_SAMPLES);
|
||||
private final long[] samples = new long[NUM_SAMPLES]; // long version for calculations
|
||||
// Circular buffer holding the last 240 samples, in nanoseconds
|
||||
private int samplesHead = 0; // one ahead of the position of the last sample
|
||||
private boolean initialized = false;
|
||||
private ShaderProgram shader;
|
||||
private int aPos;
|
||||
private int program;
|
||||
private int uFBWidth;
|
||||
private int uFBHeight;
|
||||
private int uScaleFactor;
|
||||
@@ -44,10 +65,20 @@ public abstract class F3Graph {
|
||||
private int uSamples;
|
||||
private int vertBuf;
|
||||
private int vao;
|
||||
private final DebugDrawScope scope = new DebugDrawScope();
|
||||
private final NativeScratch scratch = new NativeScratch(QUAD_BYTES);
|
||||
private final Matrix4f mvp = new Matrix4f();
|
||||
private final ResourceLocation texture;
|
||||
private final float pxPerNs;
|
||||
private final boolean left;
|
||||
private long sum;
|
||||
private final StringBuilder label = new StringBuilder(16);
|
||||
private String minString;
|
||||
private String avgString;
|
||||
private String maxString;
|
||||
private int labelTick = Integer.MIN_VALUE;
|
||||
private int avgWidth;
|
||||
private int maxWidth;
|
||||
|
||||
protected F3Graph(ResourceLocation texture, float pxPerNs, boolean left) {
|
||||
this.texture = texture;
|
||||
@@ -65,6 +96,10 @@ public abstract class F3Graph {
|
||||
samplesHead = (samplesHead + 1) % NUM_SAMPLES;
|
||||
}
|
||||
|
||||
public static int reservedHeight(FontRenderer fontRenderer) {
|
||||
return HEIGHT + BORDER * 2 + fontRenderer.FONT_HEIGHT;
|
||||
}
|
||||
|
||||
private int getVertX(ScaledResolution sres, int idx) {
|
||||
if (left) {
|
||||
return switch (idx) {
|
||||
@@ -92,23 +127,17 @@ public abstract class F3Graph {
|
||||
}
|
||||
|
||||
private void init() {
|
||||
shader = new ShaderProgram(
|
||||
"angelica",
|
||||
"shaders/debug_graph.vert.glsl",
|
||||
"shaders/debug_graph.frag.glsl");
|
||||
shader.use();
|
||||
final int handle = DebugPrograms.link("angelica:debug_graph", "angelica:debug_graph.vert.glsl", "angelica:debug_graph.frag.glsl", ShaderConstants.EMPTY, new String[]{"pos"}, new int[]{0});
|
||||
program = handle;
|
||||
GLStateManager.glUseProgram(handle);
|
||||
|
||||
// Attribute location is declared in shader via layout(location = 0)
|
||||
aPos = 0;
|
||||
|
||||
// Register uniforms
|
||||
uFBWidth = shader.getUniformLocation("fbWidth");
|
||||
uFBHeight = shader.getUniformLocation("fbHeight");
|
||||
uScaleFactor = shader.getUniformLocation("scaleFactor");
|
||||
uSamples = shader.getUniformLocation("samples");
|
||||
uHeadIdx = shader.getUniformLocation("headIdx");
|
||||
final int uPxPerNs = shader.getUniformLocation("pxPerNs");
|
||||
final int uLeft = shader.getUniformLocation("left");
|
||||
uFBWidth = GLStateManager.glGetUniformLocation(handle, "fbWidth");
|
||||
uFBHeight = GLStateManager.glGetUniformLocation(handle, "fbHeight");
|
||||
uScaleFactor = GLStateManager.glGetUniformLocation(handle, "scaleFactor");
|
||||
uSamples = GLStateManager.glGetUniformLocation(handle, "samples");
|
||||
uHeadIdx = GLStateManager.glGetUniformLocation(handle, "headIdx");
|
||||
final int uPxPerNs = GLStateManager.glGetUniformLocation(handle, "pxPerNs");
|
||||
final int uLeft = GLStateManager.glGetUniformLocation(handle, "left");
|
||||
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
@@ -131,33 +160,17 @@ public abstract class F3Graph {
|
||||
}
|
||||
|
||||
// Configure vertex attribute in the VAO
|
||||
GLStateManager.glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(aPos);
|
||||
GLStateManager.glVertexAttribPointer(0, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
|
||||
GLStateManager.glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
|
||||
// Load initial value for uniforms
|
||||
GLStateManager.glUniform1f(uFBWidth, mc.displayWidth);
|
||||
GLStateManager.glUniform1f(uFBHeight, mc.displayHeight);
|
||||
GLStateManager.glUniform1f(uScaleFactor, sr.getScaleFactor());
|
||||
GLStateManager.glUniform1i(uHeadIdx, samplesHead);
|
||||
GLStateManager.glUniform1(uSamples, sampleBuf);
|
||||
GLStateManager.glUniform1f(uPxPerNs, pxPerNs);
|
||||
GLStateManager.glUniform1i(uLeft, left ? 1 : 0); // this is how you load bool uniforms
|
||||
|
||||
ShaderProgram.clear();
|
||||
GLStateManager.glUniform1i(uLeft, left ? 1 : 0);
|
||||
}
|
||||
|
||||
public void render() {
|
||||
if (!initialized) {
|
||||
init();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public void render(ScaledResolution sr, int tick) {
|
||||
/*
|
||||
* We try to copy modern vanilla's tracker.
|
||||
* It is 240 (FRAME_TIMES) wide by 60 tall, including the 1px borders.
|
||||
@@ -171,88 +184,98 @@ public abstract class F3Graph {
|
||||
*/
|
||||
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
|
||||
// Setup GL state
|
||||
final Tessellator tess = Tessellator.instance;
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.enableAlphaTest();
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), mvp);
|
||||
|
||||
// Draw background
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
|
||||
tess.startDrawingQuads();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tess.addVertex(getVertX(sr, i), getVertY(sr, i), 0);
|
||||
scope.beginHud();
|
||||
try {
|
||||
if (!initialized) {
|
||||
init();
|
||||
initialized = true;
|
||||
}
|
||||
drawGraph(mc, sr);
|
||||
} finally {
|
||||
scope.end();
|
||||
}
|
||||
tess.draw();
|
||||
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
GLStateManager.enableTexture();
|
||||
|
||||
|
||||
// Draw samples
|
||||
shader.use();
|
||||
// Draw text
|
||||
final FontRenderer fr = mc.fontRenderer;
|
||||
if (tick != labelTick) {
|
||||
labelTick = tick;
|
||||
updateLabels(fr);
|
||||
}
|
||||
final int top = sr.getScaledHeight() - reservedHeight(fr);
|
||||
final int scaledWidth = sr.getScaledWidth();
|
||||
final int minX;
|
||||
final int avgX;
|
||||
final int maxX;
|
||||
if (left) {
|
||||
minX = BORDER * 2;
|
||||
avgX = BORDER + WIDTH / 2 - avgWidth / 2;
|
||||
maxX = BORDER * 2 + WIDTH - maxWidth;
|
||||
} else {
|
||||
minX = scaledWidth - (BORDER * 2 + WIDTH);
|
||||
avgX = scaledWidth - (BORDER + WIDTH / 2 + avgWidth / 2);
|
||||
maxX = scaledWidth - (BORDER * 2 + maxWidth);
|
||||
}
|
||||
fr.drawString(minString, minX, top, FONT_COLOR, true);
|
||||
fr.drawString(avgString, avgX, top, FONT_COLOR, true);
|
||||
fr.drawString(maxString, maxX, top, FONT_COLOR, true);
|
||||
}
|
||||
|
||||
// Load uniforms
|
||||
private void drawGraph(Minecraft mc, ScaledResolution sr) {
|
||||
scratch.ensure(QUAD_BYTES);
|
||||
long p = scratch.address();
|
||||
for (int i = 0; i < 4; ++i, p += COLOR_STRIDE) {
|
||||
DebugGeometry.putPosition(p, getVertX(sr, i), getVertY(sr, i), 0.0F);
|
||||
DebugGeometry.putColor(p + COLOR_COLOR_OFFSET, BACKGROUND_ARGB);
|
||||
}
|
||||
final var color = DebugPrograms.color();
|
||||
color.use();
|
||||
color.mvp(mvp);
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(4 * COLOR_STRIDE), GL11.GL_QUADS, COLOR_FLAGS, 4);
|
||||
|
||||
GLStateManager.glUseProgram(program);
|
||||
GLStateManager.glUniform1f(uFBWidth, mc.displayWidth);
|
||||
GLStateManager.glUniform1f(uFBHeight, mc.displayHeight);
|
||||
GLStateManager.glUniform1f(uScaleFactor, sr.getScaleFactor());
|
||||
GLStateManager.glUniform1i(uHeadIdx, samplesHead);
|
||||
GLStateManager.glUniform1(uSamples, sampleBuf);
|
||||
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
// Left side graphs are inverted, so temporarily disable culling
|
||||
if (!left) GLStateManager.disableCull();
|
||||
|
||||
GLStateManager.glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
|
||||
|
||||
if (!left) GLStateManager.enableCull();
|
||||
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
ShaderProgram.clear();
|
||||
|
||||
// Draw foreground
|
||||
|
||||
mc.getTextureManager().bindTexture(texture);
|
||||
tess.startDrawingQuads();
|
||||
tess.addVertexWithUV(getVertX(sr, 0), getVertY(sr, 0), 0.0D, 1, 0);
|
||||
tess.addVertexWithUV(getVertX(sr, 1), getVertY(sr, 1), 0.0D, 0, 0);
|
||||
tess.addVertexWithUV(getVertX(sr, 2), getVertY(sr, 2), 0.0D, 0, 1);
|
||||
tess.addVertexWithUV(getVertX(sr, 3), getVertY(sr, 3), 0.0D, 1, 1);
|
||||
tess.draw();
|
||||
p = scratch.address();
|
||||
for (int i = 0; i < 4; ++i, p += TEXTURED_STRIDE) {
|
||||
DebugGeometry.putPosition(p, getVertX(sr, i), getVertY(sr, i), 0.0F);
|
||||
memPutFloat(p + TEXTURED_UV_OFFSET, (i == 0 || i == 3) ? 1.0F : 0.0F);
|
||||
memPutFloat(p + TEXTURED_UV_OFFSET + 4, i >= 2 ? 1.0F : 0.0F);
|
||||
DebugGeometry.putColor(p + TEXTURED_COLOR_OFFSET, FOREGROUND_ARGB);
|
||||
}
|
||||
final var textured = DebugPrograms.textured();
|
||||
textured.use();
|
||||
textured.mvp(mvp);
|
||||
TessellatorStreamingDrawer.drawPacked(scratch.window(4 * TEXTURED_STRIDE), GL11.GL_QUADS, TEXTURED_FLAGS, 4);
|
||||
}
|
||||
|
||||
// Reset GL state
|
||||
GLStateManager.enableDepthTest();
|
||||
GLStateManager.disableAlphaTest();
|
||||
GLStateManager.disableBlend();
|
||||
|
||||
// Draw text
|
||||
// Ensure running counters are reset on first sample
|
||||
private void updateLabels(FontRenderer fr) {
|
||||
long min = Long.MAX_VALUE;
|
||||
long max = Long.MIN_VALUE;
|
||||
for (int i = 0; i < NUM_SAMPLES; ++i) {
|
||||
min = Math.min(min, samples[i]);
|
||||
max = Math.max(max, samples[i]);
|
||||
}
|
||||
String minStr = String.format("%.1f ms min", min / 1_000_000D);
|
||||
String avgStr = String.format("%.1f ms avg", sum / 1_000_000 / (double) NUM_SAMPLES);
|
||||
String maxStr = String.format("%.1f ms max", max / 1_000_000D);
|
||||
final FontRenderer fr = mc.fontRenderer;
|
||||
final int top = sr.getScaledHeight() - HEIGHT - BORDER * 2 - fr.FONT_HEIGHT;
|
||||
final int scaledWidth = sr.getScaledWidth();
|
||||
minString = label(min / 1_000_000D, " ms min");
|
||||
avgString = label(sum / 1_000_000 / (double) NUM_SAMPLES, " ms avg");
|
||||
maxString = label(max / 1_000_000D, " ms max");
|
||||
avgWidth = fr.getStringWidth(avgString);
|
||||
maxWidth = fr.getStringWidth(maxString);
|
||||
}
|
||||
|
||||
if (left) {
|
||||
fr.drawString(minStr, BORDER * 2, top, FONT_COLOR, true);
|
||||
fr.drawString(avgStr, BORDER + WIDTH / 2 - fr.getStringWidth(avgStr) / 2, top, FONT_COLOR, true);
|
||||
fr.drawString(maxStr, BORDER * 2 + WIDTH - fr.getStringWidth(maxStr), top, FONT_COLOR, true);
|
||||
} else {
|
||||
fr.drawString(minStr, scaledWidth - (BORDER * 2 + WIDTH), top, FONT_COLOR, true);
|
||||
fr.drawString(avgStr, scaledWidth - (BORDER + WIDTH / 2 + fr.getStringWidth(avgStr) / 2), top, FONT_COLOR, true);
|
||||
fr.drawString(maxStr, scaledWidth - (BORDER * 2 + fr.getStringWidth(maxStr)), top, FONT_COLOR, true);
|
||||
}
|
||||
private String label(double millis, String suffix) {
|
||||
label.setLength(0);
|
||||
DebugNumbers.appendFixed(label, millis, 1);
|
||||
return label.append(suffix).toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.gtnewhorizons.angelica.debug.flyby;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugEntryDef;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugRegistry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenEntryList;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenProfile;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_ENTRIES;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_FLAT;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_HUD;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_DEBUG_PROFILE;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_FPS_GRAPH;
|
||||
import static com.gtnewhorizons.angelica.config.SystemProperties.FLYBY_HUD_CACHING;
|
||||
|
||||
final class FlybyDebugOverrides {
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby");
|
||||
|
||||
record Pair(String id, DebugScreenEntryStatus status) {}
|
||||
|
||||
private Boolean hudSaved;
|
||||
private Boolean flatSaved;
|
||||
private Boolean cachingSaved;
|
||||
private Boolean fpsGraphSaved;
|
||||
private boolean sessionOverridden;
|
||||
|
||||
static List<Pair> parseEntries(String raw) {
|
||||
final List<Pair> pairs = new ArrayList<>();
|
||||
if (raw == null) return pairs;
|
||||
for (final String part : raw.split(",")) {
|
||||
final String pair = part.trim();
|
||||
if (pair.isEmpty()) continue;
|
||||
final int eq = pair.lastIndexOf('=');
|
||||
if (eq <= 0 || eq == pair.length() - 1) {
|
||||
LOGGER.error("Flyby debugEntries: malformed pair '{}', expected id=status", pair);
|
||||
continue;
|
||||
}
|
||||
final String id = pair.substring(0, eq).trim();
|
||||
final String name = pair.substring(eq + 1).trim();
|
||||
final DebugScreenEntryStatus status = DebugScreenEntryStatus.bySerializedName(name);
|
||||
if (id.isEmpty() || status == null) {
|
||||
LOGGER.error("Flyby debugEntries: bad {} '{}' in '{}'", id.isEmpty() ? "id" : "status", id.isEmpty() ? id : name, pair);
|
||||
continue;
|
||||
}
|
||||
pairs.add(new Pair(id, status));
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
void apply(Minecraft mc) {
|
||||
if (FLYBY_DEBUG_HUD != null && this.hudSaved == null) {
|
||||
this.hudSaved = mc.gameSettings.showDebugInfo;
|
||||
mc.gameSettings.showDebugInfo = FLYBY_DEBUG_HUD;
|
||||
}
|
||||
if (FLYBY_DEBUG_FLAT != null && this.flatSaved == null) {
|
||||
this.flatSaved = AngelicaConfig.modernizeF3Screen;
|
||||
AngelicaConfig.modernizeF3Screen = !FLYBY_DEBUG_FLAT;
|
||||
}
|
||||
if (FLYBY_HUD_CACHING != null && this.cachingSaved == null) {
|
||||
this.cachingSaved = AngelicaConfig.hudCachingActive;
|
||||
AngelicaConfig.hudCachingActive = FLYBY_HUD_CACHING;
|
||||
}
|
||||
if (FLYBY_FPS_GRAPH != null && this.fpsGraphSaved == null) {
|
||||
final IGameSettingsExt settings = (IGameSettingsExt) mc.gameSettings;
|
||||
this.fpsGraphSaved = settings.angelica$showFpsGraph();
|
||||
settings.angelica$setShowFpsGraph(FLYBY_FPS_GRAPH);
|
||||
}
|
||||
this.applySession();
|
||||
}
|
||||
|
||||
private void applySession() {
|
||||
final boolean wantProfile = !FLYBY_DEBUG_PROFILE.isEmpty();
|
||||
final boolean wantEntries = !FLYBY_DEBUG_ENTRIES.isEmpty();
|
||||
if (!wantProfile && !wantEntries) return;
|
||||
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries == null) {
|
||||
LOGGER.error("Flyby: debug entry list is not available, ignoring debugProfile and debugEntries");
|
||||
return;
|
||||
}
|
||||
final DebugRegistry registry = DebugRegistry.MAIN;
|
||||
this.sessionOverridden = true;
|
||||
|
||||
if (wantProfile) {
|
||||
final DebugScreenProfile profile = DebugScreenProfile.bySerializedName(FLYBY_DEBUG_PROFILE);
|
||||
if (profile == null) {
|
||||
LOGGER.error("Flyby: unknown debugProfile '{}', expected default or performance", FLYBY_DEBUG_PROFILE);
|
||||
} else {
|
||||
for (int i = 0; i < registry.entryCount(); i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
entries.overrideForSession(def, def.profileStatus(profile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Pair pair : parseEntries(FLYBY_DEBUG_ENTRIES)) {
|
||||
final DebugEntryDef def = registry.entryById(pair.id());
|
||||
if (def == null) {
|
||||
LOGGER.error("Flyby debugEntries: unknown entry id '{}'", pair.id());
|
||||
continue;
|
||||
}
|
||||
entries.overrideForSession(def, pair.status());
|
||||
}
|
||||
}
|
||||
|
||||
void restore(Minecraft mc) {
|
||||
if (this.hudSaved != null) {
|
||||
mc.gameSettings.showDebugInfo = this.hudSaved;
|
||||
this.hudSaved = null;
|
||||
}
|
||||
if (this.flatSaved != null) {
|
||||
AngelicaConfig.modernizeF3Screen = this.flatSaved;
|
||||
this.flatSaved = null;
|
||||
}
|
||||
if (this.cachingSaved != null) {
|
||||
AngelicaConfig.hudCachingActive = this.cachingSaved;
|
||||
this.cachingSaved = null;
|
||||
}
|
||||
if (this.fpsGraphSaved != null) {
|
||||
((IGameSettingsExt) mc.gameSettings).angelica$setShowFpsGraph(this.fpsGraphSaved);
|
||||
this.fpsGraphSaved = null;
|
||||
}
|
||||
if (this.sessionOverridden) {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries != null) entries.clearSessionOverrides();
|
||||
this.sessionOverridden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.config.SystemProperties.FlybyPacing;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder.StopResult;
|
||||
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder.StopStatus;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreenOverlay;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
@@ -14,6 +15,7 @@ import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
import com.sun.management.ThreadMXBean;
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -92,8 +94,7 @@ public final class FlybyRunner {
|
||||
private boolean pauseOnLostFocusSaved;
|
||||
private boolean pauseOnLostFocusOverridden;
|
||||
private boolean vsyncOverridden;
|
||||
private boolean showDebugInfoSaved;
|
||||
private boolean showDebugInfoOverridden;
|
||||
private final FlybyDebugOverrides debugOverrides = new FlybyDebugOverrides();
|
||||
private int cameraSaved;
|
||||
private boolean cameraOverridden;
|
||||
private int shotEvery;
|
||||
@@ -180,8 +181,8 @@ public final class FlybyRunner {
|
||||
this.fixedOrigin = origin;
|
||||
this.waitForTracy = SystemProperties.FLYBY_WAIT_FOR_TRACY;
|
||||
if (this.waitForTracy && !Tracy.ENABLED) {
|
||||
LOGGER.warn("Flyby: waitForTracy requested but Tracy is disabled in this build");
|
||||
this.waitForTracy = false;
|
||||
LOGGER.error("Flyby: a Tracy capture was requested but Tracy is not available (native library missing or init failed, see the Tracy log lines above); exiting instead of running unprofiled");
|
||||
FMLCommonHandler.instance().exitJava(1, false);
|
||||
}
|
||||
this.waitForFocus = SystemProperties.FLYBY_WAIT_FOR_FOCUS;
|
||||
this.exitWhenDone = SystemProperties.FLYBY_EXIT_WHEN_DONE;
|
||||
@@ -339,6 +340,7 @@ public final class FlybyRunner {
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
final IChatComponent result = ScreenShotHelper.saveScreenshot(mc.mcDataDir, "flyby-shot-" + this.shot + ".png", mc.displayWidth, mc.displayHeight, mc.getFramebuffer());
|
||||
LOGGER.info("Flyby screenshot {}", result.getUnformattedText());
|
||||
if (SystemProperties.FLYBY_DEBUG_DUMP) DebugScreenOverlay.INSTANCE.describe(LOGGER, this.shot);
|
||||
this.shot++;
|
||||
this.nextShot += this.shotEvery;
|
||||
}
|
||||
@@ -401,11 +403,7 @@ public final class FlybyRunner {
|
||||
|
||||
this.buildPath();
|
||||
this.suppressPacing();
|
||||
if (SystemProperties.FLYBY_DEBUG_HUD && !this.showDebugInfoOverridden) {
|
||||
this.showDebugInfoSaved = mc.gameSettings.showDebugInfo;
|
||||
this.showDebugInfoOverridden = true;
|
||||
mc.gameSettings.showDebugInfo = true;
|
||||
}
|
||||
this.debugOverrides.apply(mc);
|
||||
if (!this.cameraOverridden) {
|
||||
this.cameraSaved = mc.gameSettings.thirdPersonView;
|
||||
this.cameraOverridden = true;
|
||||
@@ -682,6 +680,7 @@ public final class FlybyRunner {
|
||||
ThreadedFileIOBase.threadedIOInstance.waitForFinish();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOGGER.error("Flyby: interrupted while draining chunk IO before discarding changes", e);
|
||||
}
|
||||
|
||||
worldChangesDiscarded = true;
|
||||
@@ -878,7 +877,7 @@ public final class FlybyRunner {
|
||||
|
||||
this.returnToOrigin(player);
|
||||
this.restorePauseOnLostFocus(mc);
|
||||
this.restoreShowDebugInfo(mc);
|
||||
this.debugOverrides.restore(mc);
|
||||
this.restoreCamera(mc);
|
||||
this.armed = false;
|
||||
sceneGuarded = false;
|
||||
@@ -906,12 +905,6 @@ public final class FlybyRunner {
|
||||
this.pauseOnLostFocusOverridden = false;
|
||||
}
|
||||
|
||||
private void restoreShowDebugInfo(Minecraft mc) {
|
||||
if (!this.showDebugInfoOverridden) return;
|
||||
mc.gameSettings.showDebugInfo = this.showDebugInfoSaved;
|
||||
this.showDebugInfoOverridden = false;
|
||||
}
|
||||
|
||||
private void restoreCamera(Minecraft mc) {
|
||||
if (!this.cameraOverridden) return;
|
||||
mc.gameSettings.thirdPersonView = this.cameraSaved;
|
||||
@@ -1067,7 +1060,7 @@ public final class FlybyRunner {
|
||||
FramePacer.endStats();
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
this.restorePauseOnLostFocus(mc);
|
||||
this.restoreShowDebugInfo(mc);
|
||||
this.debugOverrides.restore(mc);
|
||||
this.restoreCamera(mc);
|
||||
this.armed = false;
|
||||
this.stopRecording(mc);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.StateSet;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
public final class DebugDrawScope {
|
||||
|
||||
private static final StateSet SET = StateSet.forMask(GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_ENABLE_BIT | GL11.GL_POLYGON_BIT | GL11.GL_SCISSOR_BIT | GL11.GL_TEXTURE_BIT | GL11.GL_LINE_BIT);
|
||||
|
||||
private int depth;
|
||||
private int program;
|
||||
private int vao;
|
||||
private int vbo;
|
||||
|
||||
public void beginHud() {
|
||||
capture();
|
||||
GLStateManager.disableDepthTest();
|
||||
common();
|
||||
}
|
||||
|
||||
public void beginWorld() {
|
||||
capture();
|
||||
GLStateManager.enableDepthTest();
|
||||
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
|
||||
GLStateManager.glCullFace(GL11.GL_BACK);
|
||||
GLStateManager.glFrontFace(GL11.GL_CCW);
|
||||
common();
|
||||
}
|
||||
|
||||
private void capture() {
|
||||
depth = GLStateManager.pushState(SET);
|
||||
program = GLStateManager.getActiveProgram();
|
||||
vao = GLStateManager.getBoundVAO();
|
||||
vbo = GLStateManager.getBoundVBO();
|
||||
}
|
||||
|
||||
public void end() {
|
||||
GLStateManager.glUseProgram(program);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.popStateTo(depth);
|
||||
}
|
||||
|
||||
private static void common() {
|
||||
GLStateManager.glDepthMask(false);
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
|
||||
GLStateManager.disableCull();
|
||||
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
|
||||
GLStateManager.disableScissorTest();
|
||||
GLStateManager.glColorMask(true, true, true, true);
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glLineWidth(1.0F);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutByte;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
|
||||
public final class DebugGeometry {
|
||||
|
||||
private DebugGeometry() {}
|
||||
|
||||
public static int createVao(int vbo, VertexFormat format) {
|
||||
final int vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
format.setupBufferState(0L);
|
||||
return vao;
|
||||
}
|
||||
|
||||
public static int offsetOf(VertexFormat format, VertexFormatElement.Usage usage) {
|
||||
int offset = 0;
|
||||
for (final VertexFormatElement element : format.elementsArray) {
|
||||
if (element.getUsage() == usage) return offset;
|
||||
offset += element.getByteSize();
|
||||
}
|
||||
throw new IllegalArgumentException("Vertex format has no " + usage);
|
||||
}
|
||||
|
||||
public static void putColor(long address, int argb) {
|
||||
memPutByte(address, (byte) (argb >> 16));
|
||||
memPutByte(address + 1, (byte) (argb >> 8));
|
||||
memPutByte(address + 2, (byte) argb);
|
||||
memPutByte(address + 3, (byte) (argb >>> 24));
|
||||
}
|
||||
|
||||
public static void putPosition(long address, float x, float y, float z) {
|
||||
memPutFloat(address, x);
|
||||
memPutFloat(address + 4, y);
|
||||
memPutFloat(address + 8, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.lwjgl.BufferUtils;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
public final class DebugProgram {
|
||||
|
||||
private final int handle;
|
||||
private final int mvpLocation;
|
||||
private final int lineWidthLocation;
|
||||
private final int viewportLocation;
|
||||
private final FloatBuffer matrix = BufferUtils.createFloatBuffer(16);
|
||||
|
||||
DebugProgram(int handle) {
|
||||
this.handle = handle;
|
||||
this.mvpLocation = GLStateManager.glGetUniformLocation(handle, "u_MVP");
|
||||
this.lineWidthLocation = GLStateManager.glGetUniformLocation(handle, "u_LineWidth");
|
||||
this.viewportLocation = GLStateManager.glGetUniformLocation(handle, "u_ViewportSize");
|
||||
}
|
||||
|
||||
public void use() {
|
||||
GLStateManager.glUseProgram(handle);
|
||||
}
|
||||
|
||||
public void mvp(Matrix4fc mvp) {
|
||||
mvp.get(matrix);
|
||||
GLStateManager.glUniformMatrix4(mvpLocation, false, matrix);
|
||||
}
|
||||
|
||||
public void lineWidth(float pixels) {
|
||||
GLStateManager.glUniform1f(lineWidthLocation, pixels);
|
||||
}
|
||||
|
||||
public void viewport(int width, int height) {
|
||||
GLStateManager.glUniform2f(viewportLocation, width, height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
|
||||
public final class DebugPrograms {
|
||||
|
||||
private static DebugProgram color;
|
||||
private static DebugProgram textured;
|
||||
private static DebugProgram lines;
|
||||
|
||||
private DebugPrograms() {}
|
||||
|
||||
public static DebugProgram color() {
|
||||
if (color == null) color = link("angelica:debug_color", "angelica:debug.vert", ShaderConstants.EMPTY, false, false);
|
||||
return color;
|
||||
}
|
||||
|
||||
public static DebugProgram textured() {
|
||||
if (textured == null) textured = link("angelica:debug_textured", "angelica:debug.vert", ShaderConstants.builder().add("TEXTURED").build(), true, false);
|
||||
return textured;
|
||||
}
|
||||
|
||||
public static DebugProgram lines() {
|
||||
if (lines == null) lines = link("angelica:debug_lines", "angelica:debug_lines.vert", ShaderConstants.EMPTY, false, true);
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static DebugProgram link(String name, String vertexSource, ShaderConstants constants, boolean texCoord, boolean otherEnd) {
|
||||
final int count = 2 + (texCoord ? 1 : 0) + (otherEnd ? 1 : 0);
|
||||
final String[] attributes = new String[count];
|
||||
final int[] locations = new int[count];
|
||||
attributes[0] = "a_Position";
|
||||
locations[0] = VertexFormatElement.Usage.POSITION.getAttributeLocation();
|
||||
attributes[1] = "a_Color";
|
||||
locations[1] = VertexFormatElement.Usage.COLOR.getAttributeLocation();
|
||||
int next = 2;
|
||||
if (texCoord) {
|
||||
attributes[next] = "a_TexCoord0";
|
||||
locations[next++] = VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation();
|
||||
}
|
||||
if (otherEnd) {
|
||||
attributes[next] = "a_LineOtherEnd";
|
||||
locations[next] = VertexFormatElement.Usage.NORMAL.getAttributeLocation();
|
||||
}
|
||||
return new DebugProgram(link(name, vertexSource, "angelica:debug.frag", constants, attributes, locations));
|
||||
}
|
||||
|
||||
public static int link(String name, String vertexSource, String fragmentSource, ShaderConstants constants, String[] attributes, int[] locations) {
|
||||
final GlShader vert = ShaderLoader.loadShader(ShaderType.VERTEX, vertexSource, constants);
|
||||
final GlShader frag = ShaderLoader.loadShader(ShaderType.FRAGMENT, fragmentSource, constants);
|
||||
try {
|
||||
final GlProgram.Builder builder = GlProgram.builder(name).attachShader(vert).attachShader(frag);
|
||||
for (int i = 0; i < attributes.length; i++) builder.bindAttribute(attributes[i], locations[i]);
|
||||
final GlProgram<Void> program = builder.link(ctx -> null);
|
||||
return program.handle();
|
||||
} finally {
|
||||
vert.delete();
|
||||
frag.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.gtnewhorizons.angelica.debug.render;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
|
||||
|
||||
public final class NativeScratch {
|
||||
|
||||
private final int initial;
|
||||
private ByteBuffer buffer;
|
||||
private long address;
|
||||
private int capacity;
|
||||
|
||||
public NativeScratch(int initial) {
|
||||
this.initial = initial;
|
||||
}
|
||||
|
||||
public long address() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void ensure(int bytes) {
|
||||
if (bytes <= capacity) return;
|
||||
int grown = Math.max(1, Math.max(initial, capacity));
|
||||
while (grown < bytes) grown <<= 1;
|
||||
buffer = memRealloc(buffer, grown);
|
||||
address = memAddress0(buffer);
|
||||
capacity = buffer.capacity();
|
||||
}
|
||||
|
||||
public ByteBuffer window(int bytes) {
|
||||
buffer.clear();
|
||||
buffer.limit(bytes);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
memFree(buffer);
|
||||
buffer = null;
|
||||
address = 0L;
|
||||
capacity = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
public final class DebugContextImpl implements DebugContext {
|
||||
|
||||
private static final long SAMPLE_INTERVAL_NS = 500_000_000L;
|
||||
|
||||
private World world;
|
||||
private EntityClientPlayerMP player;
|
||||
private Chunk chunk;
|
||||
private int blockX;
|
||||
private int blockY;
|
||||
private int blockZ;
|
||||
private long nanoTime;
|
||||
private boolean sampleDue;
|
||||
private boolean sampled;
|
||||
private long sampledVersion;
|
||||
private long sampledNanos;
|
||||
private DebugEntryDef entry;
|
||||
private boolean entryFirstSample;
|
||||
|
||||
public void prepare(Minecraft mc, long nanoTime, long entriesVersion) {
|
||||
final World w = mc.theWorld;
|
||||
final EntityClientPlayerMP p = mc.thePlayer;
|
||||
world = w;
|
||||
player = p;
|
||||
chunk = null;
|
||||
if (w != null && p != null) {
|
||||
blockX = MathHelper.floor_double(p.posX);
|
||||
blockY = MathHelper.floor_double(p.boundingBox.minY);
|
||||
blockZ = MathHelper.floor_double(p.posZ);
|
||||
final Chunk column = w.getChunkFromChunkCoords(blockX >> 4, blockZ >> 4);
|
||||
if (column != null && !column.isEmpty()) chunk = column;
|
||||
}
|
||||
this.nanoTime = nanoTime;
|
||||
sampleDue = !sampled || entriesVersion != sampledVersion || nanoTime - sampledNanos >= SAMPLE_INTERVAL_NS;
|
||||
if (sampleDue) {
|
||||
sampled = true;
|
||||
sampledVersion = entriesVersion;
|
||||
sampledNanos = nanoTime;
|
||||
}
|
||||
}
|
||||
|
||||
void beginEntry(DebugEntryDef def) {
|
||||
entry = def;
|
||||
entryFirstSample = false;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
world = null;
|
||||
player = null;
|
||||
chunk = null;
|
||||
entry = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public World world() {
|
||||
return world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityClientPlayerMP player() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockX() {
|
||||
return blockX;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockY() {
|
||||
return blockY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockZ() {
|
||||
return blockZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Chunk chunk() {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nanoTime() {
|
||||
return nanoTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSampleDue() {
|
||||
final DebugEntryDef def = entry;
|
||||
if (def != null && !def.sampleOffered) {
|
||||
def.sampleOffered = true;
|
||||
entryFirstSample = true;
|
||||
}
|
||||
return sampleDue || entryFirstSample;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugEntryFactory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec.DebugToggleListener;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
|
||||
public final class DebugEntryDef implements DebugEntryHandle {
|
||||
|
||||
public final String id;
|
||||
public final String namespace;
|
||||
public final String path;
|
||||
public final String name;
|
||||
public final DebugEntryCategory category;
|
||||
public final DebugScreenEntryStatus defaultStatus;
|
||||
public final DebugScreenEntryStatus performanceStatus;
|
||||
public final int index;
|
||||
public final int kind;
|
||||
public final int rendererIndex;
|
||||
public final boolean supportsAlways;
|
||||
public final boolean flat;
|
||||
|
||||
final DebugRegistry registry;
|
||||
final DebugEntryFactory textFactory;
|
||||
final DebugToggleListener toggleListener;
|
||||
|
||||
boolean enabled;
|
||||
boolean alwaysOn;
|
||||
boolean failed;
|
||||
boolean sampleOffered;
|
||||
private DebugScreenEntry entry;
|
||||
|
||||
DebugEntryDef(DebugRegistry registry, DebugEntrySpec spec, int index, int kind, int rendererIndex, boolean flat) {
|
||||
this.registry = registry;
|
||||
this.id = spec.getId();
|
||||
final int colon = id.indexOf(':');
|
||||
this.namespace = id.substring(0, colon);
|
||||
this.path = id.substring(colon + 1);
|
||||
this.name = spec.getName();
|
||||
this.category = spec.getCategory();
|
||||
this.defaultStatus = spec.getDefaultStatus();
|
||||
this.performanceStatus = spec.getPerformanceStatus();
|
||||
this.index = index;
|
||||
this.kind = kind;
|
||||
this.rendererIndex = rendererIndex;
|
||||
this.supportsAlways = spec.getSupportsAlways();
|
||||
this.flat = flat;
|
||||
this.textFactory = spec.getTextFactory();
|
||||
this.toggleListener = spec.getToggleListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isAlwaysOn() {
|
||||
return alwaysOn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFailed() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public boolean hasText() {
|
||||
return textFactory != null;
|
||||
}
|
||||
|
||||
public boolean isScreenText() {
|
||||
return category == DebugEntryCategory.SCREEN_TEXT && hasText();
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus profileStatus(DebugScreenProfile profile) {
|
||||
return profile == DebugScreenProfile.PERFORMANCE ? performanceStatus : defaultStatus;
|
||||
}
|
||||
|
||||
public DebugScreenEntry entryOrCreate() {
|
||||
DebugScreenEntry created = entry;
|
||||
if (created == null && textFactory != null) {
|
||||
created = textFactory.create(this);
|
||||
entry = created;
|
||||
}
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
|
||||
public final class DebugFactImpl implements DebugFact {
|
||||
|
||||
private static final String VALUE_ON = "\u00a7f";
|
||||
private static final String VALUE_OFF = "\u00a7r";
|
||||
|
||||
private static final String PLAIN = "";
|
||||
|
||||
private StringBuilder out;
|
||||
private String valueOn = VALUE_ON;
|
||||
private String valueOff = VALUE_OFF;
|
||||
|
||||
public DebugFactImpl into(StringBuilder out) {
|
||||
this.out = out;
|
||||
valueOn = VALUE_ON;
|
||||
valueOff = VALUE_OFF;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DebugFactImpl intoPlain(StringBuilder out) {
|
||||
this.out = out;
|
||||
valueOn = PLAIN;
|
||||
valueOff = PLAIN;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact text(CharSequence text) {
|
||||
out.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact text(char c) {
|
||||
out.append(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(CharSequence value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(int value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(long value) {
|
||||
out.append(valueOn).append(value).append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact value(double value, int decimals) {
|
||||
out.append(valueOn);
|
||||
DebugNumbers.appendFixed(out, value, decimals);
|
||||
out.append(valueOff);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact padded(long value, int width) {
|
||||
out.append(valueOn);
|
||||
DebugNumbers.appendPadded(out, value, width);
|
||||
out.append(valueOff);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupSpec;
|
||||
|
||||
public final class DebugGroup implements DebugGroupHandle {
|
||||
|
||||
public static final String PRIORITY_LEFT_ID = "angelica:priority_left";
|
||||
|
||||
public static final DebugGroup PRIORITY_LEFT = builtin("priority_left", null, -1, LEFT, 0);
|
||||
public static final DebugGroup RENDERING = builtin("rendering", "rendering", 0xF0B0A0, LEFT, 100);
|
||||
public static final DebugGroup POSITION = builtin("position", "position", 0xFFFFFF, LEFT, 200);
|
||||
public static final DebugGroup SHADERS = builtin("shaders", "shaders", 0xFF55FF, LEFT, 300);
|
||||
public static final DebugGroup OTHER_LEFT = builtin("other_left", "other", -1, LEFT, 800);
|
||||
public static final DebugGroup HELP = builtin("help", "help", -1, LEFT, 1000);
|
||||
public static final DebugGroup MEMORY = builtin("memory", "memory", 0xFF9B00, RIGHT, 100);
|
||||
public static final DebugGroup SYSTEM_SPECS = builtin("system_specs", "system_specs", 0xFF0000, RIGHT, 200);
|
||||
public static final DebugGroup FORGE = builtin("forge", "forge", 0x5555FF, RIGHT, 300);
|
||||
public static final DebugGroup ANGELICA = builtin("angelica", "angelica", 0x55FF55, RIGHT, 400);
|
||||
public static final DebugGroup OTHER_RIGHT = builtin("other_right", "other", -1, RIGHT, 800);
|
||||
public static final DebugGroup LOOKING_AT_BLOCK = builtin("looking_at_block", "looking_at_block", 0xCC00FF, RIGHT, 900);
|
||||
|
||||
public final String id;
|
||||
public final String titleKey;
|
||||
public final int accentColor;
|
||||
public final int side;
|
||||
public final int order;
|
||||
int index = -1;
|
||||
|
||||
DebugGroup(String id, String titleKey, int accentColor, int side, int order) {
|
||||
this.id = id;
|
||||
this.titleKey = titleKey;
|
||||
this.accentColor = accentColor;
|
||||
this.side = side;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
private static DebugGroup builtin(String path, String title, int accent, int side, int order) {
|
||||
final DebugGroupSpec spec = new DebugGroupSpec("angelica:" + path).side(side).order(order);
|
||||
if (title != null) spec.title("angelica.debug.group." + title);
|
||||
if (accent >= 0) spec.accent(accent);
|
||||
return DebugRegistry.MAIN.registerGroup(spec);
|
||||
}
|
||||
|
||||
static void bootstrap() {}
|
||||
|
||||
static DebugGroup of(DebugGroupHandle handle) {
|
||||
if (handle instanceof DebugGroup group) return group;
|
||||
throw new IllegalArgumentException("Not a debug group handle from DebugScreenApi: " + handle);
|
||||
}
|
||||
|
||||
static String requireName(String name) {
|
||||
if (name == null) throw new IllegalArgumentException("Debug fact name is null");
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int side() {
|
||||
return side;
|
||||
}
|
||||
|
||||
public int index() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public boolean hasTitle() {
|
||||
return titleKey != null;
|
||||
}
|
||||
|
||||
public boolean hasAccent() {
|
||||
return accentColor >= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class DebugGroupContents {
|
||||
|
||||
public static final int TEXT = 1;
|
||||
public static final int SIZE = 2;
|
||||
|
||||
public static final int ROW_HEIGHT = 9;
|
||||
private static final int SHRINK_HOLD_TICKS = 40;
|
||||
public static final int MARGIN = 3;
|
||||
static final int TITLE_PADDING = 3;
|
||||
static final int FACT_PADDING = 5;
|
||||
static final int FACT_LINE_GAP = 2;
|
||||
static final int MIN_VALUE_BUDGET = 32;
|
||||
|
||||
static final int TITLE_BAR_COLOR = 0x90303030;
|
||||
static final int BODY_COLOR = 0x90505050;
|
||||
static final int TITLE_COLOR = 0xFFFFFFFF;
|
||||
static final int NAME_COLOR = 0xFFE0E0E0;
|
||||
static final int VALUE_COLOR = 0xFFD0D0D0;
|
||||
static final int LINE_COLOR = 0xFFE0E0E0;
|
||||
|
||||
private static final Slot[] NO_SLOTS = new Slot[0];
|
||||
|
||||
public static final class Slot {
|
||||
|
||||
StringBuilder text = new StringBuilder();
|
||||
private StringBuilder previous = new StringBuilder();
|
||||
final StringBuilder wrapped = new StringBuilder();
|
||||
int[] rowStart = new int[1];
|
||||
int[] rowEnd = new int[1];
|
||||
int rowCount;
|
||||
int width;
|
||||
String name;
|
||||
String label;
|
||||
int nameWidth;
|
||||
private int wrapBudget = -1;
|
||||
private boolean isWrapped;
|
||||
private boolean dirty;
|
||||
|
||||
void begin() {
|
||||
final StringBuilder swap = previous;
|
||||
previous = text;
|
||||
text = swap;
|
||||
text.setLength(0);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
private boolean sameAsPrevious() {
|
||||
return CharSequence.compare(text, previous) == 0;
|
||||
}
|
||||
|
||||
boolean measure(DebugText renderer, int budget, boolean force) {
|
||||
boolean textChanged = false;
|
||||
if (dirty) {
|
||||
dirty = false;
|
||||
textChanged = !sameAsPrevious();
|
||||
}
|
||||
if (!force && !textChanged && budget == wrapBudget) return false;
|
||||
wrapBudget = budget;
|
||||
rowCount = 0;
|
||||
width = 0;
|
||||
final int len = text.length();
|
||||
final float full = renderer.measure(text, 0, len);
|
||||
if (full <= budget) {
|
||||
isWrapped = false;
|
||||
addRow(0, len, full);
|
||||
} else {
|
||||
isWrapped = true;
|
||||
renderer.wrap(this, budget);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void addRow(int start, int end, float rowWidth) {
|
||||
if (rowCount == rowStart.length) {
|
||||
final int n = rowCount * 2;
|
||||
rowStart = Arrays.copyOf(rowStart, n);
|
||||
rowEnd = Arrays.copyOf(rowEnd, n);
|
||||
}
|
||||
rowStart[rowCount] = start;
|
||||
rowEnd[rowCount] = end;
|
||||
rowCount++;
|
||||
final int w = (int) Math.ceil(rowWidth);
|
||||
if (w > width) width = w;
|
||||
}
|
||||
|
||||
CharSequence rowText() {
|
||||
return isWrapped ? wrapped : text;
|
||||
}
|
||||
}
|
||||
|
||||
public final DebugGroup group;
|
||||
private Slot[] facts = NO_SLOTS;
|
||||
private int factCount;
|
||||
private Slot[] lines = NO_SLOTS;
|
||||
private int lineCount;
|
||||
private int measuredFactCount;
|
||||
private int measuredLineCount;
|
||||
private String title = "";
|
||||
private int titleWidth;
|
||||
private boolean titleDirty;
|
||||
private int factNameWidth;
|
||||
private int fullWidth;
|
||||
private int fullHeight;
|
||||
private boolean wasEmpty = true;
|
||||
private int widthHeldSince;
|
||||
private int heightHeldSince;
|
||||
private boolean holdReleased;
|
||||
|
||||
public DebugGroupContents(DebugGroup group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
factCount = 0;
|
||||
lineCount = 0;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return factCount == 0 && lineCount == 0;
|
||||
}
|
||||
|
||||
public int lineCount() {
|
||||
return lineCount;
|
||||
}
|
||||
|
||||
public int factCount() {
|
||||
return factCount;
|
||||
}
|
||||
|
||||
public Slot fact(int i) {
|
||||
return facts[i];
|
||||
}
|
||||
|
||||
public Slot line(int i) {
|
||||
return lines[i];
|
||||
}
|
||||
|
||||
public int fullWidth() {
|
||||
return fullWidth;
|
||||
}
|
||||
|
||||
public int fullHeight() {
|
||||
return fullHeight;
|
||||
}
|
||||
|
||||
void releaseSizeHold() {
|
||||
holdReleased = true;
|
||||
}
|
||||
|
||||
void setTitle(String title, int width) {
|
||||
if (this.title.equals(title) && titleWidth == width) return;
|
||||
this.title = title;
|
||||
this.titleWidth = width;
|
||||
titleDirty = true;
|
||||
}
|
||||
|
||||
private static Slot[] grow(Slot[] slots, int count) {
|
||||
if (count < slots.length) return slots;
|
||||
final int n = Math.max(4, count * 2);
|
||||
final Slot[] grown = Arrays.copyOf(slots, n);
|
||||
for (int i = count; i < n; i++) grown[i] = new Slot();
|
||||
return grown;
|
||||
}
|
||||
|
||||
Slot addFact(String name) {
|
||||
facts = grow(facts, factCount);
|
||||
final Slot slot = facts[factCount++];
|
||||
if (slot.name != name) {
|
||||
slot.name = name;
|
||||
slot.label = name.concat(":");
|
||||
slot.nameWidth = -1;
|
||||
}
|
||||
slot.begin();
|
||||
return slot;
|
||||
}
|
||||
|
||||
Slot addLine() {
|
||||
lines = grow(lines, lineCount);
|
||||
final Slot slot = lines[lineCount++];
|
||||
slot.begin();
|
||||
return slot;
|
||||
}
|
||||
|
||||
int measure(DebugText renderer, int lineBudget, boolean force, int tick) {
|
||||
boolean text = force || titleDirty || factCount != measuredFactCount || lineCount != measuredLineCount;
|
||||
titleDirty = false;
|
||||
measuredFactCount = factCount;
|
||||
measuredLineCount = lineCount;
|
||||
int nameWidth = 0;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
if (force || slot.nameWidth < 0) {
|
||||
slot.nameWidth = renderer.width(slot.name, 0, slot.name.length());
|
||||
text = true;
|
||||
}
|
||||
if (slot.nameWidth > nameWidth) nameWidth = slot.nameWidth;
|
||||
}
|
||||
if (nameWidth != factNameWidth) text = true;
|
||||
factNameWidth = nameWidth;
|
||||
final int valueBudget = Math.max(lineBudget - nameWidth - FACT_PADDING, MIN_VALUE_BUDGET);
|
||||
int rows = 0;
|
||||
int width = 0;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
text |= slot.measure(renderer, valueBudget, force);
|
||||
rows += slot.rowCount;
|
||||
final int w = nameWidth + FACT_PADDING + slot.width;
|
||||
if (w > width) width = w;
|
||||
}
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
final Slot slot = lines[i];
|
||||
text |= slot.measure(renderer, lineBudget, force);
|
||||
rows += slot.rowCount;
|
||||
if (slot.width > width) width = slot.width;
|
||||
}
|
||||
if (titleWidth > 0 && titleWidth + TITLE_PADDING > width) width = titleWidth + TITLE_PADDING;
|
||||
int height = rows * ROW_HEIGHT;
|
||||
if (titleWidth > 0) height += ROW_HEIGHT;
|
||||
if (factCount > 0 && lineCount > 0) height += FACT_LINE_GAP;
|
||||
final boolean empty = isEmpty();
|
||||
final boolean free = force || holdReleased;
|
||||
holdReleased = false;
|
||||
if (free || empty || width >= fullWidth || tick - widthHeldSince >= SHRINK_HOLD_TICKS) {
|
||||
widthHeldSince = tick;
|
||||
} else {
|
||||
width = fullWidth;
|
||||
}
|
||||
if (free || empty || height >= fullHeight || tick - heightHeldSince >= SHRINK_HOLD_TICKS) {
|
||||
heightHeldSince = tick;
|
||||
} else {
|
||||
height = fullHeight;
|
||||
}
|
||||
int flags = text ? TEXT : 0;
|
||||
if (width != fullWidth || height != fullHeight || empty != wasEmpty) flags |= SIZE;
|
||||
fullWidth = width;
|
||||
fullHeight = height;
|
||||
wasEmpty = empty;
|
||||
return flags;
|
||||
}
|
||||
|
||||
void emitRects(DebugText renderer, int left, int top, int side, int screenWidth) {
|
||||
int y = top;
|
||||
if (titleWidth > 0) {
|
||||
renderer.rect(left - 1, top - 1, left + fullWidth + 1, top + ROW_HEIGHT - 1, TITLE_BAR_COLOR);
|
||||
y = top + ROW_HEIGHT;
|
||||
}
|
||||
renderer.rect(left - 1, y - 1, left + fullWidth + 1, top + fullHeight + 1, BODY_COLOR);
|
||||
if (group.hasAccent()) {
|
||||
final int x = side == DebugGroup.LEFT ? 0 : screenWidth - 1;
|
||||
renderer.rect(x, top - 1, x + 1, top + fullHeight + 1, 0xFF000000 | group.accentColor);
|
||||
}
|
||||
}
|
||||
|
||||
void drawText(DebugText renderer, int left, int top) {
|
||||
int y = top;
|
||||
if (titleWidth > 0) {
|
||||
renderer.draw(title, 0, title.length(), left + TITLE_PADDING, top, TITLE_COLOR);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
final int valueX = left + factNameWidth + FACT_PADDING;
|
||||
for (int i = 0; i < factCount; i++) {
|
||||
final Slot slot = facts[i];
|
||||
renderer.draw(slot.label, 0, slot.label.length(), left + (factNameWidth - slot.nameWidth), y, NAME_COLOR);
|
||||
y = drawRows(renderer, slot, valueX, y, VALUE_COLOR);
|
||||
}
|
||||
if (factCount > 0 && lineCount > 0) y += FACT_LINE_GAP;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
y = drawRows(renderer, lines[i], left, y, LINE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private static int drawRows(DebugText renderer, Slot slot, int x, int y, int color) {
|
||||
final CharSequence text = slot.rowText();
|
||||
for (int row = 0; row < slot.rowCount; row++) {
|
||||
final int start = slot.rowStart[row];
|
||||
final int end = slot.rowEnd[row];
|
||||
if (end > start) renderer.draw(text, start, end, x, y, color);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
public final class DebugGuiScale {
|
||||
|
||||
public static final int UNCHANGED = -1;
|
||||
public static final int AUTO = 0;
|
||||
|
||||
private DebugGuiScale() {}
|
||||
|
||||
public static boolean evenForUnicode(Minecraft mc) {
|
||||
return mc.func_152349_b() && !AngelicaConfig.removeUnicodeEvenScaling;
|
||||
}
|
||||
|
||||
public static int maxScale(int displayWidth, int displayHeight, boolean evenForUnicode) {
|
||||
int scale = 1;
|
||||
while (displayWidth / (scale + 1) >= 320 && displayHeight / (scale + 1) >= 240) {
|
||||
scale++;
|
||||
}
|
||||
if (evenForUnicode && scale % 2 != 0 && scale != 1) scale--;
|
||||
return scale;
|
||||
}
|
||||
|
||||
public static int offeredMax(int maxScale, int gameScale) {
|
||||
return Math.max(1, Math.min(maxScale, gameScale));
|
||||
}
|
||||
|
||||
public static int resolve(int setting, int gameScale, int displayWidth, int displayHeight, boolean evenForUnicode) {
|
||||
if (setting == UNCHANGED) return gameScale;
|
||||
final int max = maxScale(displayWidth, displayHeight, evenForUnicode);
|
||||
final int wanted = setting == AUTO ? Math.max(1, max / 2) : Math.min(setting, max);
|
||||
return Math.max(1, Math.min(wanted, gameScale));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
public final class DebugLayout {
|
||||
|
||||
public static final int MARGIN_TOP = 2;
|
||||
public static final int GROUP_SPACING = 9;
|
||||
|
||||
public final int[] height;
|
||||
public final int[] side;
|
||||
public final int[] top;
|
||||
public final int[] columnHeight = new int[2];
|
||||
|
||||
private final int[] maxHeight = new int[2];
|
||||
private final int[] homeSide;
|
||||
private final int count;
|
||||
private final int priority;
|
||||
|
||||
public DebugLayout(DebugRegistry registry) {
|
||||
count = registry.groupCount();
|
||||
height = new int[count];
|
||||
side = new int[count];
|
||||
top = new int[count];
|
||||
homeSide = new int[count];
|
||||
for (int g = 0; g < count; g++) {
|
||||
homeSide[g] = registry.groupAt(g).side;
|
||||
}
|
||||
final DebugGroup priorityGroup = registry.groupById(DebugGroup.PRIORITY_LEFT_ID);
|
||||
priority = priorityGroup == null ? -1 : priorityGroup.index;
|
||||
}
|
||||
|
||||
private void place(int group, int column) {
|
||||
top[group] = columnHeight[column];
|
||||
side[group] = column;
|
||||
columnHeight[column] += height[group] + GROUP_SPACING;
|
||||
}
|
||||
|
||||
private boolean isFull(int column) {
|
||||
return columnHeight[column] >= maxHeight[column];
|
||||
}
|
||||
|
||||
private boolean fits(int group, int column) {
|
||||
return columnHeight[column] + height[group] <= maxHeight[column];
|
||||
}
|
||||
|
||||
public void assign(int top, int leftMaxHeight, int rightMaxHeight) {
|
||||
columnHeight[DebugGroup.LEFT] = top;
|
||||
columnHeight[DebugGroup.RIGHT] = top;
|
||||
maxHeight[DebugGroup.LEFT] = leftMaxHeight;
|
||||
maxHeight[DebugGroup.RIGHT] = rightMaxHeight;
|
||||
for (int g = 0; g < count; g++) {
|
||||
side[g] = -1;
|
||||
}
|
||||
if (priority >= 0 && height[priority] > 0) place(priority, homeSide[priority]);
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = homeSide[g];
|
||||
if (fits(g, column)) place(g, column);
|
||||
}
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = 1 - homeSide[g];
|
||||
if (fits(g, column)) place(g, column);
|
||||
}
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
final int column = homeSide[g];
|
||||
if (!isFull(column)) place(g, column);
|
||||
else if (!isFull(1 - column)) place(g, 1 - column);
|
||||
}
|
||||
}
|
||||
|
||||
public void assignSingleColumn(int column, int columnTop, int columnMaxHeight) {
|
||||
columnHeight[column] = columnTop;
|
||||
maxHeight[column] = columnMaxHeight;
|
||||
for (int g = 0; g < count; g++) {
|
||||
side[g] = -1;
|
||||
}
|
||||
if (priority >= 0 && height[priority] > 0) place(priority, column);
|
||||
for (int g = 0; g < count; g++) {
|
||||
if (height[g] <= 0 || side[g] >= 0) continue;
|
||||
if (!isFull(column)) place(g, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class DebugLineRules {
|
||||
|
||||
private static final int MAX_PREFIX = 64;
|
||||
private static final Pattern RULE = Pattern.compile("\\s*(drop|fact)\\s+(left|right)\\s+\"([^\"]*)\"((?:\\s+\\w+=(?:\"[^\"]*\"|[^\\s\"]+))*)\\s*");
|
||||
private static final Pattern OPTION = Pattern.compile("(\\w+)=(?:\"([^\"]*)\"|([^\\s\"]+))");
|
||||
|
||||
static final String[] DEFAULTS = {
|
||||
"drop right \"Allocation rate: \" when=minecraft:memory",
|
||||
"drop right \"Java: \" when=minecraft:system_specs",
|
||||
"drop right \"GPU: \" when=minecraft:system_specs",
|
||||
"drop right \"OpenGL: \" when=minecraft:system_specs",
|
||||
"drop right \"CPU Cores: \" when=minecraft:system_specs",
|
||||
"fact right \"IntCache: \" group=angelica:memory when=minecraft:memory",
|
||||
"fact right \"OS: \" group=angelica:system_specs when=minecraft:system_specs",
|
||||
"fact right \"LWJGL: \" group=angelica:system_specs when=minecraft:system_specs",
|
||||
"fact left \"Cube X: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube Y: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube Z: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"Cube: \" group=angelica:position when=minecraft:player_position",
|
||||
"fact left \"hunger: \" group=angelica:position when=minecraft:player_position name=\"Hunger\"",
|
||||
"fact right \"HydroEnergy GPU RAM: \" group=angelica:memory when=minecraft:memory",
|
||||
"fact right \"renderDebugMode: \" group=angelica:rendering when=minecraft:chunk_render_stats name=\"Render Debug Mode\"",
|
||||
};
|
||||
|
||||
public record Rule(String prefix, DebugEntryDef when, DebugGroup group, String name) {
|
||||
|
||||
int payloadStart(String line) {
|
||||
int i = matchPrefix(line, skipCodes(line, 0), prefix);
|
||||
while (i < line.length() && line.charAt(i) == ' ') i++;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
private final Rule[][] rules;
|
||||
|
||||
private DebugLineRules(Rule[][] rules) {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
static DebugLineRules compile(DebugRegistry registry, String[] configRules) {
|
||||
final ArrayList<Rule> left = new ArrayList<>();
|
||||
final ArrayList<Rule> right = new ArrayList<>();
|
||||
if (configRules != null) {
|
||||
for (String text : configRules) add(registry, text, left, right);
|
||||
}
|
||||
for (String text : DEFAULTS) add(registry, text, left, right);
|
||||
return new DebugLineRules(new Rule[][] { left.toArray(new Rule[0]), right.toArray(new Rule[0]) });
|
||||
}
|
||||
|
||||
private static void add(DebugRegistry registry, String text, ArrayList<Rule> left, ArrayList<Rule> right) {
|
||||
final Matcher matcher = text == null ? null : RULE.matcher(text);
|
||||
final Rule rule = matcher != null && matcher.matches() ? parse(registry, matcher) : null;
|
||||
if (rule == null) {
|
||||
AngelicaMod.LOGGER.warn("invalid debug line rule: {}", text);
|
||||
} else {
|
||||
(matcher.group(2).equals("left") ? left : right).add(rule);
|
||||
}
|
||||
}
|
||||
|
||||
private static Rule parse(DebugRegistry registry, Matcher rule) {
|
||||
final String prefix = stripCodes(rule.group(3));
|
||||
if (prefix.isEmpty() || prefix.length() > MAX_PREFIX) return null;
|
||||
String groupId = null;
|
||||
String name = null;
|
||||
String whenId = null;
|
||||
final Matcher option = OPTION.matcher(rule.group(4));
|
||||
while (option.find()) {
|
||||
final String value = option.group(2) != null ? option.group(2) : option.group(3);
|
||||
switch (option.group(1)) {
|
||||
case "group" -> groupId = value;
|
||||
case "name" -> name = value;
|
||||
case "when" -> whenId = value;
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
final DebugEntryDef when = whenId == null ? null : registry.entryById(whenId);
|
||||
if (whenId != null && when == null) return null;
|
||||
if (rule.group(1).equals("drop")) return groupId == null && name == null ? new Rule(prefix, when, null, null) : null;
|
||||
final DebugGroup group = groupId == null ? null : registry.groupById(groupId);
|
||||
if (name == null) name = defaultName(prefix);
|
||||
return group == null || name.isEmpty() ? null : new Rule(prefix, when, group, name);
|
||||
}
|
||||
|
||||
private static String stripCodes(String s) {
|
||||
if (s.indexOf(DebugText.FORMAT) < 0) return s;
|
||||
final StringBuilder sb = new StringBuilder(s.length());
|
||||
DebugText.appendStripped(sb, s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static String defaultName(String prefix) {
|
||||
int end = prefix.length();
|
||||
while (end > 0 && prefix.charAt(end - 1) == ' ') end--;
|
||||
if (end > 0 && prefix.charAt(end - 1) == ':') end--;
|
||||
return prefix.substring(0, end);
|
||||
}
|
||||
|
||||
static int skipCodes(String s, int from) {
|
||||
final int last = s.length() - 1;
|
||||
int i = from;
|
||||
while (i < last && s.charAt(i) == DebugText.FORMAT) i += 2;
|
||||
return i;
|
||||
}
|
||||
|
||||
static int matchPrefix(String line, int rawStart, String prefix) {
|
||||
final int length = line.length();
|
||||
int i = rawStart;
|
||||
for (int p = 0, n = prefix.length(); p < n; p++) {
|
||||
i = skipCodes(line, i);
|
||||
if (i >= length || line.charAt(i) != prefix.charAt(p)) return -1;
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static boolean isBlank(String line) {
|
||||
final int length = line.length();
|
||||
for (int i = skipCodes(line, 0); i < length; i = skipCodes(line, i + 1)) {
|
||||
if (line.charAt(i) > ' ') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Rule match(int side, String line) {
|
||||
final int start = skipCodes(line, 0);
|
||||
for (Rule rule : rules[side]) {
|
||||
if ((rule.when == null || rule.when.isEnabled()) && matchPrefix(line, start, rule.prefix) >= 0) return rule;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
public final class DebugNumbers {
|
||||
|
||||
private static final long[] POW10 = { 1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L };
|
||||
private static final double MAX_SCALED = 9.0e18;
|
||||
|
||||
private DebugNumbers() {}
|
||||
|
||||
public static void appendFixed(StringBuilder out, double value, int decimals) {
|
||||
final int places = Math.max(0, Math.min(decimals, POW10.length - 1));
|
||||
final long scale = POW10[places];
|
||||
final double magnitude = Math.abs(value) * scale;
|
||||
if (Double.isNaN(value) || Double.isInfinite(value) || magnitude >= MAX_SCALED) {
|
||||
out.append(value);
|
||||
return;
|
||||
}
|
||||
final long scaled = Math.round(magnitude);
|
||||
if (value < 0 && scaled != 0) out.append('-');
|
||||
out.append(scaled / scale);
|
||||
if (places == 0) return;
|
||||
out.append('.');
|
||||
appendPadded(out, scaled % scale, places);
|
||||
}
|
||||
|
||||
public static void appendPadded(StringBuilder out, long value, int width) {
|
||||
if (value >= 0) {
|
||||
int digits = 1;
|
||||
for (long rest = value / 10; rest > 0; rest /= 10) {
|
||||
digits++;
|
||||
}
|
||||
for (int i = digits; i < width; i++) {
|
||||
out.append('0');
|
||||
}
|
||||
}
|
||||
out.append(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.audio.PositionedSoundRecord;
|
||||
import net.minecraft.client.gui.Gui;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.GuiTextField;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.input.Mouse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DebugOptionsScreen extends GuiScreen {
|
||||
|
||||
private static final int ROW_H = 20;
|
||||
private static final int TOOLTIP_WRAP_W = 200;
|
||||
private static final int ROW_BUTTON_W = 60;
|
||||
private static final int ROW_BUTTON_H = 16;
|
||||
private static final int ROW_BUTTON_GAP = 2;
|
||||
private static final int ARROW_W = 20;
|
||||
private static final int LIST_MAX_W = 350;
|
||||
private static final int SCROLLBAR_W = 6;
|
||||
private static final int SCROLLBAR_MIN_THUMB = 10;
|
||||
private static final int HEADER_H = 28;
|
||||
private static final int HEADER_LINE_H = 10;
|
||||
private static final int FOOTER_W = 120;
|
||||
private static final int FOOTER_H = 20;
|
||||
private static final int FOOTER_GAP = 4;
|
||||
private static final int WHEEL_ROWS = 3;
|
||||
private static final int TOOLTIP_OFFSET = 12;
|
||||
|
||||
private static final ResourceLocation BUTTON_PRESS = new ResourceLocation("gui.button.press");
|
||||
private static final Comparator<DebugEntryDef> BY_ID = Comparator.comparing(d -> d.id);
|
||||
private static final Comparator<DebugGroup> GROUPS_BY_ID = Comparator.comparing(g -> g.id);
|
||||
|
||||
private enum RowKind { HEADER, ENTRY, SCALE, PANEL }
|
||||
|
||||
private static final int NORMAL_COLOR = 0xFFFFFF;
|
||||
private static final int WARNING_COLOR = 0xFF5555;
|
||||
private static final int INACTIVE_COLOR = 0x909090;
|
||||
private static final int NOTICE_COLOR = 0xFFFF55;
|
||||
private static final int DISABLED_COLOR = 0x505050;
|
||||
private static final int BUTTON_COLOR = DebugGroupContents.BODY_COLOR;
|
||||
private static final int BUTTON_LIT_COLOR = DebugGroupContents.TITLE_BAR_COLOR;
|
||||
private static final int OUTLINE_COLOR = 0xFFA0A0A0;
|
||||
private static final int PLACEHOLDER_COLOR = 0x707070;
|
||||
private static final int FIELD_BOX_COLOR = 0xFF000000;
|
||||
private static final int FIELD_TEXT_COLOR = 0xE0E0E0;
|
||||
private static final int FIELD_CURSOR_COLOR = 0xFFD0D0D0;
|
||||
private static final int FIELD_SELECTION_COLOR = 0xFF0000FF;
|
||||
private static final int TOOLTIP_BACKGROUND = 0xF0100010;
|
||||
private static final int TOOLTIP_BORDER = 0x505000FF;
|
||||
private static final int TOOLTIP_TEXT_COLOR = 0xFFFFFFFF;
|
||||
|
||||
private record Choice(DebugScreenEntryStatus status, String labelKey, int activeColor) {}
|
||||
|
||||
private static final Choice[] CHOICES = {
|
||||
new Choice(DebugScreenEntryStatus.NEVER, "options.off", 0xFFFFFF),
|
||||
new Choice(DebugScreenEntryStatus.IN_OVERLAY, "angelica.debug.options.status.overlay", 0xFFFF55),
|
||||
new Choice(DebugScreenEntryStatus.ALWAYS_ON, "angelica.debug.options.status.always", 0xDF5050)
|
||||
};
|
||||
private static final int ROW_BUTTONS_W = ROW_BUTTON_W * CHOICES.length + ROW_BUTTON_GAP * (CHOICES.length - 1);
|
||||
|
||||
private final class Button {
|
||||
final int id;
|
||||
final int w;
|
||||
final int h;
|
||||
final String label;
|
||||
final int labelW;
|
||||
int x;
|
||||
int y;
|
||||
boolean enabled = true;
|
||||
boolean selected;
|
||||
boolean pressable = true;
|
||||
int activeColor;
|
||||
|
||||
Button(int id, int w, int h, String label) {
|
||||
this.id = id;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
this.label = label;
|
||||
this.labelW = fontRendererObj.getStringWidth(label);
|
||||
}
|
||||
|
||||
void setSelected(boolean selected, boolean pressable, int activeColor) {
|
||||
this.selected = selected;
|
||||
this.pressable = pressable;
|
||||
this.activeColor = activeColor;
|
||||
}
|
||||
|
||||
boolean contains(int mouseX, int mouseY) {
|
||||
return mouseX >= x && mouseY >= y && mouseX < x + w && mouseY < y + h;
|
||||
}
|
||||
|
||||
boolean pressed(int mouseX, int mouseY) {
|
||||
return enabled && pressable && contains(mouseX, mouseY);
|
||||
}
|
||||
|
||||
void draw(boolean hovered) {
|
||||
rect(x, y, x + w, y + h, selected || hovered && enabled ? BUTTON_LIT_COLOR : BUTTON_COLOR);
|
||||
if (selected) {
|
||||
rect(x, y, x + w, y + 1, OUTLINE_COLOR);
|
||||
rect(x, y + h - 1, x + w, y + h, OUTLINE_COLOR);
|
||||
rect(x, y + 1, x + 1, y + h - 1, OUTLINE_COLOR);
|
||||
rect(x + w - 1, y + 1, x + w, y + h - 1, OUTLINE_COLOR);
|
||||
}
|
||||
drawText(label, x + (w - labelW) / 2, y + (h - 8) / 2, !enabled ? DISABLED_COLOR : selected ? activeColor : INACTIVE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private final class SearchField extends GuiTextField {
|
||||
private int blink;
|
||||
private String measuredText;
|
||||
private int measuredScroll = -1;
|
||||
private int measuredCursor = -1;
|
||||
private int measuredSelection = -1;
|
||||
private String shown;
|
||||
private int cursor;
|
||||
private int selection;
|
||||
private boolean cursorShown;
|
||||
private int prefixW;
|
||||
private int selectionW;
|
||||
private boolean cursorPending;
|
||||
private boolean cursorInsert;
|
||||
private int pendingX;
|
||||
private int pendingY;
|
||||
|
||||
SearchField(int x, int y, int w, int h) {
|
||||
super(fontRendererObj, x, y, w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCursorCounter() {
|
||||
blink++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocused(boolean focused) {
|
||||
if (focused && !isFocused()) blink = 0;
|
||||
super.setFocused(focused);
|
||||
}
|
||||
|
||||
private void measure(String text) {
|
||||
measuredText = text;
|
||||
measuredScroll = lineScrollOffset;
|
||||
measuredCursor = getCursorPosition();
|
||||
measuredSelection = getSelectionEnd();
|
||||
shown = fontRendererObj.trimStringToWidth(text.substring(lineScrollOffset), getWidth());
|
||||
cursor = measuredCursor - lineScrollOffset;
|
||||
selection = Math.min(measuredSelection - lineScrollOffset, shown.length());
|
||||
cursorShown = cursor >= 0 && cursor <= shown.length();
|
||||
prefixW = fontRendererObj.getStringWidth(cursorShown ? shown.substring(0, cursor) : shown);
|
||||
selectionW = fontRendererObj.getStringWidth(shown.substring(0, selection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTextBox() {
|
||||
cursorPending = false;
|
||||
if (!getVisible()) return;
|
||||
rect(xPosition - 1, yPosition - 1, xPosition + width + 1, yPosition + height + 1, OUTLINE_COLOR);
|
||||
rect(xPosition, yPosition, xPosition + width, yPosition + height, FIELD_BOX_COLOR);
|
||||
final String text = getText();
|
||||
if (text != measuredText || lineScrollOffset != measuredScroll || getCursorPosition() != measuredCursor || getSelectionEnd() != measuredSelection) measure(text);
|
||||
|
||||
final int textX = xPosition + 4;
|
||||
final int textY = yPosition + (height - 8) / 2;
|
||||
final int end = shown.isEmpty() ? textX : textX + prefixW + 1;
|
||||
final boolean insert = measuredCursor < text.length() || text.length() >= getMaxStringLength();
|
||||
int cursorX = end;
|
||||
if (!cursorShown) {
|
||||
cursorX = cursor > 0 ? textX + width : textX;
|
||||
} else if (insert) {
|
||||
cursorX = end - 1;
|
||||
}
|
||||
if (selection != cursor) {
|
||||
final int right = xPosition + width;
|
||||
final int selectionX = textX + selectionW - 1;
|
||||
rect(Math.min(Math.min(cursorX, selectionX), right), textY - 1, Math.min(Math.max(cursorX, selectionX), right), textY + 1 + fontRendererObj.FONT_HEIGHT, FIELD_SELECTION_COLOR);
|
||||
}
|
||||
if (!shown.isEmpty()) drawText(shown, textX, textY, FIELD_TEXT_COLOR);
|
||||
cursorPending = isFocused() && blink / 6 % 2 == 0 && cursorShown;
|
||||
cursorInsert = insert;
|
||||
pendingX = cursorX;
|
||||
pendingY = textY;
|
||||
}
|
||||
|
||||
void drawCursor() {
|
||||
if (!cursorPending) return;
|
||||
cursorPending = false;
|
||||
if (cursorInsert) {
|
||||
rect(pendingX, pendingY - 1, pendingX + 1, pendingY + 1 + fontRendererObj.FONT_HEIGHT, FIELD_CURSOR_COLOR);
|
||||
} else {
|
||||
drawText("_", pendingX, pendingY, FIELD_TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Row {
|
||||
final RowKind kind;
|
||||
final DebugEntryDef def;
|
||||
final String plain;
|
||||
final String normalText;
|
||||
final List<String> normalTooltip;
|
||||
final Button[] buttons;
|
||||
String text;
|
||||
int textW;
|
||||
List<String> tooltip;
|
||||
String value;
|
||||
int valueW;
|
||||
int color = NORMAL_COLOR;
|
||||
boolean failed;
|
||||
|
||||
Row(RowKind kind, DebugEntryDef def, String plain, String normalText, List<String> normalTooltip, Button[] buttons) {
|
||||
this.kind = kind;
|
||||
this.def = def;
|
||||
this.plain = plain;
|
||||
this.normalText = normalText;
|
||||
this.normalTooltip = normalTooltip;
|
||||
this.buttons = buttons;
|
||||
this.text = normalText;
|
||||
this.tooltip = normalTooltip;
|
||||
}
|
||||
}
|
||||
|
||||
public static void open(Minecraft mc) {
|
||||
if (DebugScreen.ENTRIES == null) return;
|
||||
mc.displayGuiScreen(new DebugOptionsScreen());
|
||||
}
|
||||
|
||||
private final ArrayList<Row> rows = new ArrayList<>();
|
||||
private final ArrayList<DebugEntryDef> thirdParty = new ArrayList<>();
|
||||
private final ArrayList<DebugGroup> panels = new ArrayList<>();
|
||||
private final Button[] profileButtons = new Button[DebugScreenProfile.VALUES.length];
|
||||
private Button doneButton;
|
||||
|
||||
private FontRendererAccessor fonts;
|
||||
private BatchingFontRenderer batcher;
|
||||
private DebugText.Batched painter;
|
||||
|
||||
private int[] headerLineX;
|
||||
private List<String> measuredTooltip;
|
||||
private int measuredTooltipW;
|
||||
|
||||
private String search = "";
|
||||
private int scrollRow;
|
||||
private int visibleRows;
|
||||
private boolean dragging;
|
||||
private int dragOffset;
|
||||
private boolean grouped;
|
||||
private long lastVersion;
|
||||
private boolean configDirty;
|
||||
private int scaleMax;
|
||||
|
||||
private SearchField searchField;
|
||||
|
||||
private String title;
|
||||
private String searchPlaceholder;
|
||||
private int cursorW;
|
||||
private List<String> failedTooltip;
|
||||
private List<String> headerLines;
|
||||
private int headerColor;
|
||||
|
||||
private int titleX;
|
||||
private int listX;
|
||||
private int listW;
|
||||
private int nameX;
|
||||
private int nameMaxW;
|
||||
private int buttonsX;
|
||||
private int viewTop;
|
||||
private int viewH;
|
||||
private int footerTop;
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
if (DebugScreen.ENTRIES == null) {
|
||||
mc.displayGuiScreen(null);
|
||||
return;
|
||||
}
|
||||
Keyboard.enableRepeatEvents(true);
|
||||
dragging = false;
|
||||
if (fontRendererObj instanceof FontRendererAccessor accessor) {
|
||||
fonts = accessor;
|
||||
batcher = accessor.angelica$getBatcher();
|
||||
painter = new DebugText.Batched(fontRendererObj, batcher);
|
||||
} else {
|
||||
fonts = null;
|
||||
batcher = null;
|
||||
painter = null;
|
||||
}
|
||||
measuredTooltip = null;
|
||||
|
||||
title = translate("angelica.debug.options.title");
|
||||
searchPlaceholder = translate("angelica.debug.options.search");
|
||||
failedTooltip = wrap(translate("angelica.debug.options.failed.tooltip"));
|
||||
|
||||
final boolean even = DebugGuiScale.evenForUnicode(mc);
|
||||
final int gameScale = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight).getScaleFactor();
|
||||
scaleMax = DebugGuiScale.offeredMax(DebugGuiScale.maxScale(mc.displayWidth, mc.displayHeight, even), gameScale);
|
||||
|
||||
listW = Math.min(LIST_MAX_W, width - 20);
|
||||
listX = (width - listW) / 2;
|
||||
nameX = listX + 4;
|
||||
buttonsX = listX + listW - SCROLLBAR_W - 4 - ROW_BUTTONS_W;
|
||||
nameMaxW = buttonsX - 4 - nameX;
|
||||
|
||||
final int titleW = fontRendererObj.getStringWidth(title);
|
||||
titleX = (width - titleW) / 2;
|
||||
final int searchX = titleX + titleW + 8;
|
||||
searchField = new SearchField(searchX, 6, Math.max(40, Math.min(120, width - searchX - 8)), 18);
|
||||
searchField.setMaxStringLength(32);
|
||||
searchField.setText(search);
|
||||
searchField.setFocused(true);
|
||||
cursorW = fontRendererObj.getStringWidth("_");
|
||||
searchPlaceholder = fontRendererObj.trimStringToWidth(searchPlaceholder, searchField.getWidth() - cursorW);
|
||||
|
||||
initFooter();
|
||||
layoutView();
|
||||
buildRows();
|
||||
}
|
||||
|
||||
private void initFooter() {
|
||||
final int count = profileButtons.length + 1;
|
||||
final int buttonW = Math.min(FOOTER_W, (width - 8 - (count - 1) * FOOTER_GAP) / count);
|
||||
final int rowW = count * buttonW + (count - 1) * FOOTER_GAP;
|
||||
final int left = (width - rowW) / 2;
|
||||
footerTop = height - 6 - FOOTER_H;
|
||||
for (int i = 0; i < profileButtons.length; i++) {
|
||||
final Button button = new Button(i, buttonW, FOOTER_H, translate(DebugScreenProfile.VALUES[i].translationKey));
|
||||
button.x = left + i * (buttonW + FOOTER_GAP);
|
||||
button.y = footerTop;
|
||||
profileButtons[i] = button;
|
||||
}
|
||||
doneButton = new Button(profileButtons.length, buttonW, FOOTER_H, translate("gui.done"));
|
||||
doneButton.x = left + profileButtons.length * (buttonW + FOOTER_GAP);
|
||||
doneButton.y = footerTop;
|
||||
}
|
||||
|
||||
private void layoutView() {
|
||||
grouped = DebugScreen.isGroupedActive();
|
||||
final String header = translate(grouped ? "angelica.debug.options.warning" : "angelica.debug.options.flat_notice");
|
||||
headerLines = fontRendererObj.listFormattedStringToWidth(header, Math.min(width - 20, 400));
|
||||
headerLineX = new int[headerLines.size()];
|
||||
for (int i = 0; i < headerLineX.length; i++) {
|
||||
headerLineX[i] = width / 2 - fontRendererObj.getStringWidth(headerLines.get(i)) / 2;
|
||||
}
|
||||
headerColor = grouped ? WARNING_COLOR : NOTICE_COLOR;
|
||||
viewTop = HEADER_H + headerLines.size() * HEADER_LINE_H + 2;
|
||||
visibleRows = Math.max(1, (footerTop - 6 - viewTop) / ROW_H);
|
||||
viewH = visibleRows * ROW_H;
|
||||
}
|
||||
|
||||
private List<String> wrap(String text) {
|
||||
return fontRendererObj.listFormattedStringToWidth(text, TOOLTIP_WRAP_W);
|
||||
}
|
||||
|
||||
private static boolean isBuiltin(String namespace) {
|
||||
return "minecraft".equals(namespace) || "forge".equals(namespace) || "angelica".equals(namespace);
|
||||
}
|
||||
|
||||
private Row headerRow(String label) {
|
||||
final Row row = new Row(RowKind.HEADER, null, null, EnumChatFormatting.BOLD + "" + EnumChatFormatting.UNDERLINE + label, null, null);
|
||||
row.textW = fontRendererObj.getStringWidth(row.text);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void buildRows() {
|
||||
rows.clear();
|
||||
final String query = search.toLowerCase(Locale.ROOT);
|
||||
final DebugRegistry registry = DebugRegistry.MAIN;
|
||||
final DebugEntryCategory[] categories = DebugEntryCategory.values();
|
||||
for (int c = 0; c < categories.length; c++) {
|
||||
final DebugEntryCategory category = categories[c];
|
||||
final int start = rows.size();
|
||||
thirdParty.clear();
|
||||
for (int i = 0, n = registry.entryCount(); i < n; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
if (def.category != category) continue;
|
||||
if (!grouped && category == DebugEntryCategory.SCREEN_TEXT && !def.flat) continue;
|
||||
if (def == DebugScreenEntries.THREE_DIMENSIONAL_CROSSHAIR && !AngelicaConfig.modernizeF3Screen) continue;
|
||||
final boolean builtin = isBuiltin(def.namespace);
|
||||
if (!query.isEmpty() && !(builtin ? def.path : def.id).contains(query) && !(def.name != null && translate(def.name).toLowerCase(Locale.ROOT).contains(query))) continue;
|
||||
if (builtin) {
|
||||
rows.add(entryRow(def, false));
|
||||
} else {
|
||||
thirdParty.add(def);
|
||||
}
|
||||
}
|
||||
thirdParty.sort(BY_ID);
|
||||
for (int i = 0, n = thirdParty.size(); i < n; i++) {
|
||||
rows.add(entryRow(thirdParty.get(i), true));
|
||||
}
|
||||
if (rows.size() > start) rows.add(start, headerRow(translate(category.translationKey)));
|
||||
}
|
||||
if (grouped) {
|
||||
final int start = rows.size();
|
||||
addLayoutRow(RowKind.SCALE, translate("angelica.debug.options.text_size"), wrap(translate("angelica.debug.options.text_size.tooltip")), "<", ">", query);
|
||||
if (rows.size() > start) rows.add(start, headerRow(translate("angelica.debug.options.category.layout")));
|
||||
addPanelRows(registry, query);
|
||||
}
|
||||
thirdParty.clear();
|
||||
refreshStates();
|
||||
scrollTo(scrollRow);
|
||||
}
|
||||
|
||||
private void addLayoutRow(RowKind kind, String label, List<String> tooltip, String decrease, String increase, String query) {
|
||||
if (!query.isEmpty() && !label.toLowerCase(Locale.ROOT).contains(query)) return;
|
||||
final String trimmed = fontRendererObj.trimStringToWidth(label, nameMaxW);
|
||||
final List<String> shownTooltip = tooltip != null ? tooltip : trimmed.equals(label) ? null : wrap(label);
|
||||
final Button[] buttons = {
|
||||
new Button(0, ARROW_W, ROW_BUTTON_H, decrease),
|
||||
new Button(1, ARROW_W, ROW_BUTTON_H, increase)
|
||||
};
|
||||
final Row row = new Row(kind, null, label, trimmed, shownTooltip, buttons);
|
||||
refreshLayoutValue(row);
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
private void addPanelRows(DebugRegistry registry, String query) {
|
||||
for (int i = 0, n = registry.groupCount(); i < n; i++) panels.add(registry.groupAt(i));
|
||||
panels.sort(GROUPS_BY_ID);
|
||||
final int start = rows.size();
|
||||
for (int i = 0, n = panels.size(); i < n; i++) {
|
||||
final DebugGroup group = panels.get(i);
|
||||
final String title = group.hasTitle() ? translate(group.titleKey) : group.id;
|
||||
if (!query.isEmpty() && !group.id.contains(query) && !title.toLowerCase(Locale.ROOT).contains(query)) continue;
|
||||
final List<String> tooltip = wrap(StatCollector.translateToLocalFormatted("angelica.debug.options.panels.tooltip", group.id));
|
||||
final Row row = new Row(RowKind.PANEL, null, title, fontRendererObj.trimStringToWidth(title, nameMaxW), tooltip, null);
|
||||
row.value = fontRendererObj.trimStringToWidth(group.id, ROW_BUTTONS_W);
|
||||
row.valueW = fontRendererObj.getStringWidth(row.value);
|
||||
rows.add(row);
|
||||
}
|
||||
panels.clear();
|
||||
if (rows.size() > start) rows.add(start, headerRow(translate("angelica.debug.options.category.panels")));
|
||||
}
|
||||
|
||||
private Row entryRow(DebugEntryDef def, boolean external) {
|
||||
final boolean named = def.name != null;
|
||||
final String full = named ? translate(def.name) : external ? def.id : def.path;
|
||||
final String trimmed = fontRendererObj.trimStringToWidth(full, nameMaxW);
|
||||
final String normalText;
|
||||
if (external && !named) {
|
||||
final int split = Math.min(trimmed.length(), def.namespace.length() + 1);
|
||||
normalText = EnumChatFormatting.GRAY + trimmed.substring(0, split) + EnumChatFormatting.WHITE + trimmed.substring(split);
|
||||
} else {
|
||||
normalText = trimmed;
|
||||
}
|
||||
final List<String> tooltip;
|
||||
final String helpKey = named ? def.name + ".tooltip" : null;
|
||||
if (helpKey != null && StatCollector.canTranslate(helpKey)) {
|
||||
final List<String> lines = new ArrayList<>(wrap(translate(helpKey)));
|
||||
lines.addAll(wrap(EnumChatFormatting.DARK_GRAY + def.id));
|
||||
tooltip = lines;
|
||||
} else {
|
||||
tooltip = !named && trimmed.equals(full) ? null : wrap(def.id);
|
||||
}
|
||||
final boolean always = def.supportsAlways && (grouped || def.category == DebugEntryCategory.RENDERER);
|
||||
final Button[] buttons = new Button[always ? CHOICES.length : CHOICES.length - 1];
|
||||
for (int i = 0; i < buttons.length; i++) {
|
||||
buttons[i] = new Button(i, ROW_BUTTON_W, ROW_BUTTON_H, translate(CHOICES[i].labelKey));
|
||||
}
|
||||
return new Row(RowKind.ENTRY, def, trimmed, normalText, tooltip, buttons);
|
||||
}
|
||||
|
||||
private void refreshStates() {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
lastVersion = entries.version();
|
||||
for (int i = 0, n = rows.size(); i < n; i++) {
|
||||
final Row row = rows.get(i);
|
||||
if (row.kind != RowKind.ENTRY) continue;
|
||||
final boolean failed = row.def.isFailed();
|
||||
if (failed != row.failed) {
|
||||
row.failed = failed;
|
||||
if (failed) {
|
||||
row.text = EnumChatFormatting.STRIKETHROUGH + row.plain;
|
||||
row.tooltip = failedTooltip;
|
||||
row.color = WARNING_COLOR;
|
||||
} else {
|
||||
row.text = row.normalText;
|
||||
row.tooltip = row.normalTooltip;
|
||||
row.color = NORMAL_COLOR;
|
||||
}
|
||||
}
|
||||
final DebugScreenEntryStatus current = entries.effectiveStatus(row.def);
|
||||
final DebugScreenEntryStatus stored = entries.getStatus(row.def);
|
||||
final DebugScreenEntryStatus shown = current == DebugScreenEntryStatus.ALWAYS_ON && row.buttons.length < CHOICES.length ? DebugScreenEntryStatus.IN_OVERLAY : current;
|
||||
for (final Button button : row.buttons) {
|
||||
final Choice choice = CHOICES[button.id];
|
||||
button.enabled = !failed;
|
||||
button.setSelected(choice.status == shown, choice.status != shown || choice.status != stored, choice.activeColor);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < profileButtons.length; i++) {
|
||||
final boolean using = entries.isUsingProfile(DebugScreenProfile.VALUES[i]);
|
||||
profileButtons[i].setSelected(using, !using, NORMAL_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshLayoutValue(Row row) {
|
||||
switch (row.kind) {
|
||||
case SCALE -> {
|
||||
final int scale = Math.min(AngelicaConfig.debugGuiScale, scaleMax);
|
||||
row.value = scale <= DebugGuiScale.UNCHANGED ? translate("angelica.debug.options.text_size.match")
|
||||
: scale == DebugGuiScale.AUTO ? translate("angelica.debug.options.text_size.auto") : Integer.toString(scale);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
if (row.value != null) row.valueW = fontRendererObj.getStringWidth(row.value);
|
||||
}
|
||||
|
||||
private void stepLayout(Row row, int direction) {
|
||||
switch (row.kind) {
|
||||
case SCALE -> {
|
||||
int next = Math.max(DebugGuiScale.UNCHANGED, Math.min(AngelicaConfig.debugGuiScale, scaleMax)) + direction;
|
||||
if (next > scaleMax) next = DebugGuiScale.UNCHANGED;
|
||||
else if (next < DebugGuiScale.UNCHANGED) next = scaleMax;
|
||||
AngelicaConfig.debugGuiScale = next;
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
configDirty = true;
|
||||
refreshLayoutValue(row);
|
||||
}
|
||||
|
||||
private int maxScroll() {
|
||||
return Math.max(0, rows.size() - visibleRows);
|
||||
}
|
||||
|
||||
private int shownEnd() {
|
||||
return Math.min(rows.size(), scrollRow + visibleRows);
|
||||
}
|
||||
|
||||
private void scrollTo(int row) {
|
||||
scrollRow = Math.max(0, Math.min(row, maxScroll()));
|
||||
for (int i = scrollRow, end = shownEnd(); i < end; i++) {
|
||||
final Row shown = rows.get(i);
|
||||
if (shown.buttons == null) continue;
|
||||
final int y = viewTop + (i - scrollRow) * ROW_H + (ROW_H - ROW_BUTTON_H) / 2;
|
||||
if (shown.kind == RowKind.ENTRY) {
|
||||
for (final Button button : shown.buttons) {
|
||||
button.x = buttonsX + button.id * (ROW_BUTTON_W + ROW_BUTTON_GAP);
|
||||
button.y = y;
|
||||
}
|
||||
} else {
|
||||
shown.buttons[0].x = buttonsX;
|
||||
shown.buttons[0].y = y;
|
||||
shown.buttons[1].x = buttonsX + ROW_BUTTONS_W - ARROW_W;
|
||||
shown.buttons[1].y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int thumbHeight() {
|
||||
return Math.max(SCROLLBAR_MIN_THUMB, viewH * visibleRows / rows.size());
|
||||
}
|
||||
|
||||
private void dragTo(int mouseY) {
|
||||
final int max = maxScroll();
|
||||
if (max == 0) return;
|
||||
final int track = viewH - thumbHeight();
|
||||
if (track <= 0) return;
|
||||
scrollTo(((mouseY - dragOffset - viewTop) * max + track / 2) / track);
|
||||
}
|
||||
|
||||
private static String translate(String key) {
|
||||
return StatCollector.translateToLocal(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void keyTyped(char typedChar, int keyCode) {
|
||||
final int page = Math.max(1, visibleRows - 1);
|
||||
final int step = switch (keyCode) {
|
||||
case Keyboard.KEY_UP -> -1;
|
||||
case Keyboard.KEY_DOWN -> 1;
|
||||
case Keyboard.KEY_PRIOR -> -page;
|
||||
case Keyboard.KEY_NEXT -> page;
|
||||
default -> 0;
|
||||
};
|
||||
if (step != 0) {
|
||||
scrollTo(scrollRow + step);
|
||||
return;
|
||||
}
|
||||
if (searchField.textboxKeyTyped(typedChar, keyCode)) {
|
||||
final String text = searchField.getText();
|
||||
if (!text.equals(search)) {
|
||||
search = text;
|
||||
scrollRow = 0;
|
||||
buildRows();
|
||||
}
|
||||
return;
|
||||
}
|
||||
super.keyTyped(typedChar, keyCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
searchField.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
searchField.setFocused(true);
|
||||
if (mouseButton != 0) return;
|
||||
|
||||
if (doneButton.pressed(mouseX, mouseY)) {
|
||||
playPressSound();
|
||||
mc.displayGuiScreen(null);
|
||||
return;
|
||||
}
|
||||
for (final Button button : profileButtons) {
|
||||
if (!button.pressed(mouseX, mouseY)) continue;
|
||||
playPressSound();
|
||||
DebugScreen.ENTRIES.loadProfile(DebugScreenProfile.VALUES[button.id]);
|
||||
DebugScreen.ENTRIES.save();
|
||||
refreshStates();
|
||||
return;
|
||||
}
|
||||
|
||||
final int barX = listX + listW - SCROLLBAR_W;
|
||||
if (mouseX >= barX && mouseX < barX + SCROLLBAR_W && mouseY >= viewTop && mouseY < viewTop + viewH) {
|
||||
final int max = maxScroll();
|
||||
if (max == 0) return;
|
||||
final int thumbH = thumbHeight();
|
||||
final int thumbY = viewTop + (viewH - thumbH) * scrollRow / max;
|
||||
dragging = true;
|
||||
if (mouseY >= thumbY && mouseY < thumbY + thumbH) {
|
||||
dragOffset = mouseY - thumbY;
|
||||
} else {
|
||||
dragOffset = thumbH / 2;
|
||||
dragTo(mouseY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = scrollRow, end = shownEnd(); i < end; i++) {
|
||||
final Row row = rows.get(i);
|
||||
if (row.buttons == null) continue;
|
||||
for (final Button button : row.buttons) {
|
||||
if (!button.pressed(mouseX, mouseY)) continue;
|
||||
playPressSound();
|
||||
if (row.kind == RowKind.ENTRY) {
|
||||
DebugScreen.ENTRIES.setStatus(row.def, CHOICES[button.id].status);
|
||||
refreshStates();
|
||||
} else {
|
||||
stepLayout(row, button.id == 0 ? -1 : 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {
|
||||
if (dragging && clickedMouseButton == 0) dragTo(mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void mouseMovedOrUp(int mouseX, int mouseY, int state) {
|
||||
if (state == 0) dragging = false;
|
||||
super.mouseMovedOrUp(mouseX, mouseY, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMouseInput() {
|
||||
super.handleMouseInput();
|
||||
final int wheel = Mouse.getEventDWheel();
|
||||
if (wheel != 0) scrollTo(scrollRow - Integer.signum(wheel) * WHEEL_ROWS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateScreen() {
|
||||
searchField.updateCursorCounter();
|
||||
if (DebugScreen.isGroupedActive() != grouped) {
|
||||
layoutView();
|
||||
buildRows();
|
||||
} else if (DebugScreen.ENTRIES.version() != lastVersion) {
|
||||
refreshStates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGuiClosed() {
|
||||
Keyboard.enableRepeatEvents(false);
|
||||
if (configDirty) {
|
||||
configDirty = false;
|
||||
ConfigurationManager.save(AngelicaConfig.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doesGuiPauseGame() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void playPressSound() {
|
||||
mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(BUTTON_PRESS, 1.0F));
|
||||
}
|
||||
|
||||
private void rect(int x1, int y1, int x2, int y2, int argb) {
|
||||
if (painter != null) {
|
||||
painter.rect(x1, y1, x2, y2, argb);
|
||||
} else {
|
||||
Gui.drawRect(x1, y1, x2, y2, argb);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawText(String text, int x, int y, int color) {
|
||||
if (fonts != null) {
|
||||
fonts.angelica$drawStringBatched(text, x, y, color, true);
|
||||
} else {
|
||||
fontRendererObj.drawStringWithShadow(text, x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
private void beginLayer() {
|
||||
if (painter == null) return;
|
||||
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
batcher.beginBatch();
|
||||
painter.beginRects();
|
||||
}
|
||||
|
||||
private void endLayer() {
|
||||
if (painter == null) return;
|
||||
try {
|
||||
painter.endRects();
|
||||
} finally {
|
||||
batcher.endBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
|
||||
final List<String> tooltip;
|
||||
try {
|
||||
beginLayer();
|
||||
tooltip = drawContents(mouseX, mouseY);
|
||||
} finally {
|
||||
endLayer();
|
||||
}
|
||||
if (tooltip == null && !searchField.cursorPending) return;
|
||||
try {
|
||||
beginLayer();
|
||||
searchField.drawCursor();
|
||||
if (tooltip != null) drawTooltip(tooltip, mouseX, mouseY);
|
||||
} finally {
|
||||
endLayer();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> drawContents(int mouseX, int mouseY) {
|
||||
rect(0, 0, width, viewTop, 0x60000000);
|
||||
rect(0, footerTop - 4, width, height, 0x60000000);
|
||||
rect(listX, viewTop, listX + listW, viewTop + viewH, 0x40000000);
|
||||
|
||||
drawText(title, titleX, 11, NORMAL_COLOR);
|
||||
for (int i = 0; i < headerLineX.length; i++) {
|
||||
drawText(headerLines.get(i), headerLineX[i], HEADER_H + i * HEADER_LINE_H, headerColor);
|
||||
}
|
||||
|
||||
searchField.drawTextBox();
|
||||
if (search.isEmpty()) {
|
||||
drawText(searchPlaceholder, searchField.xPosition + 4 + cursorW, searchField.yPosition + 5, PLACEHOLDER_COLOR);
|
||||
}
|
||||
|
||||
List<String> tooltip = null;
|
||||
for (int i = scrollRow, end = shownEnd(); i < end; i++) {
|
||||
final Row row = rows.get(i);
|
||||
final int y = viewTop + (i - scrollRow) * ROW_H;
|
||||
if (row.kind == RowKind.HEADER) {
|
||||
drawText(row.text, listX + listW / 2 - row.textW / 2, y + 6, NORMAL_COLOR);
|
||||
continue;
|
||||
}
|
||||
drawText(row.text, nameX, y + 6, row.color);
|
||||
if (row.kind == RowKind.PANEL) {
|
||||
drawText(row.value, buttonsX + ROW_BUTTONS_W - row.valueW, y + 6, INACTIVE_COLOR);
|
||||
} else {
|
||||
if (row.value != null) drawText(row.value, buttonsX + ROW_BUTTONS_W / 2 - row.valueW / 2, y + 6, NORMAL_COLOR);
|
||||
for (final Button button : row.buttons) {
|
||||
button.draw(button.contains(mouseX, mouseY));
|
||||
}
|
||||
}
|
||||
final int hoverRight = row.buttons == null ? buttonsX + ROW_BUTTONS_W : buttonsX;
|
||||
if (row.tooltip != null && mouseX >= nameX && mouseX < hoverRight && mouseY >= y && mouseY < y + ROW_H) {
|
||||
tooltip = row.tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
final int max = maxScroll();
|
||||
if (max > 0) {
|
||||
final int thumbH = thumbHeight();
|
||||
final int thumbY = viewTop + (viewH - thumbH) * scrollRow / max;
|
||||
final int barX = listX + listW - SCROLLBAR_W;
|
||||
rect(barX, viewTop, barX + SCROLLBAR_W, viewTop + viewH, 0x60000000);
|
||||
rect(barX, thumbY, barX + SCROLLBAR_W, thumbY + thumbH, dragging ? 0xFFFFFFFF : OUTLINE_COLOR);
|
||||
}
|
||||
|
||||
for (final Button button : profileButtons) {
|
||||
button.draw(button.contains(mouseX, mouseY));
|
||||
}
|
||||
doneButton.draw(doneButton.contains(mouseX, mouseY));
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private void drawTooltip(List<String> lines, int mouseX, int mouseY) {
|
||||
if (lines != measuredTooltip) {
|
||||
int widest = 0;
|
||||
for (int i = 0, n = lines.size(); i < n; i++) {
|
||||
widest = Math.max(widest, fontRendererObj.getStringWidth(lines.get(i)));
|
||||
}
|
||||
measuredTooltip = lines;
|
||||
measuredTooltipW = widest;
|
||||
}
|
||||
final int w = measuredTooltipW;
|
||||
final int h = lines.size() > 1 ? 10 + (lines.size() - 1) * 10 : 8;
|
||||
int x = mouseX + TOOLTIP_OFFSET;
|
||||
int y = mouseY - TOOLTIP_OFFSET;
|
||||
if (x + w > width) x -= 28 + w;
|
||||
x = Math.max(4, x);
|
||||
if (y + h + 6 > height) y = height - h - 6;
|
||||
y = Math.max(4, y);
|
||||
|
||||
rect(x - 3, y - 4, x + w + 3, y - 3, TOOLTIP_BACKGROUND);
|
||||
rect(x - 3, y + h + 3, x + w + 3, y + h + 4, TOOLTIP_BACKGROUND);
|
||||
rect(x - 3, y - 3, x + w + 3, y + h + 3, TOOLTIP_BACKGROUND);
|
||||
rect(x - 4, y - 3, x - 3, y + h + 3, TOOLTIP_BACKGROUND);
|
||||
rect(x + w + 3, y - 3, x + w + 4, y + h + 3, TOOLTIP_BACKGROUND);
|
||||
rect(x - 3, y - 2, x - 2, y + h + 2, TOOLTIP_BORDER);
|
||||
rect(x + w + 2, y - 2, x + w + 3, y + h + 2, TOOLTIP_BORDER);
|
||||
rect(x - 3, y - 3, x + w + 3, y - 2, TOOLTIP_BORDER);
|
||||
rect(x - 3, y + h + 2, x + w + 3, y + h + 3, TOOLTIP_BORDER);
|
||||
|
||||
for (int i = 0, n = lines.size(); i < n; i++) {
|
||||
drawText(lines.get(i), x, y, TOOLTIP_TEXT_COLOR);
|
||||
y += i == 0 ? 12 : 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupSpec;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class DebugRegistry {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
private static final DebugGroup[] NO_GROUPS = new DebugGroup[0];
|
||||
private static final DebugEntryDef[] NO_ENTRIES = new DebugEntryDef[0];
|
||||
|
||||
public static final DebugRegistry MAIN = new DebugRegistry();
|
||||
|
||||
private final ArrayList<DebugGroup> openGroups = new ArrayList<>();
|
||||
private final ArrayList<DebugEntryDef> openEntries = new ArrayList<>();
|
||||
private final HashMap<String, DebugGroup> groupsById = new HashMap<>();
|
||||
private final HashMap<String, DebugEntryDef> entriesById = new HashMap<>();
|
||||
private DebugGroup[] groups = NO_GROUPS;
|
||||
private DebugEntryDef[] entries = NO_ENTRIES;
|
||||
private DebugEntryDef[] flat = NO_ENTRIES;
|
||||
private DebugEntryDef[] renderers = NO_ENTRIES;
|
||||
private int rendererSequence;
|
||||
private boolean frozen;
|
||||
private boolean failuresPending;
|
||||
|
||||
static boolean isValidId(String id) {
|
||||
if (id == null) return false;
|
||||
final int colon = id.indexOf(':');
|
||||
final int length = id.length();
|
||||
if (colon <= 0 || colon == length - 1) return false;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i == colon) continue;
|
||||
final char c = id.charAt(i);
|
||||
if (c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_' || c == '.' || c == '-') continue;
|
||||
if (c == '/' && i > colon) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (frozen) throw new IllegalStateException("The debug screen registry is frozen; register before the first client tick");
|
||||
}
|
||||
|
||||
public DebugGroup registerGroup(DebugGroupSpec spec) {
|
||||
requireOpen();
|
||||
final String id = spec.getId();
|
||||
if (!isValidId(id)) throw new IllegalArgumentException("Invalid debug group id: " + id);
|
||||
if (groupsById.containsKey(id)) throw new IllegalArgumentException("Duplicate debug group id: " + id);
|
||||
final int side = spec.getSide();
|
||||
if (side != DebugGroupHandle.LEFT && side != DebugGroupHandle.RIGHT) throw new IllegalArgumentException("Invalid side for debug group " + id + ": " + side);
|
||||
final DebugGroup group = new DebugGroup(id, spec.getTitleKey(), spec.getAccent(), side, spec.getOrder());
|
||||
openGroups.add(group);
|
||||
groupsById.put(id, group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public DebugEntryDef registerEntry(DebugEntrySpec spec) {
|
||||
return register(spec, 0, spec.getCategory() == DebugEntryCategory.SCREEN_TEXT);
|
||||
}
|
||||
|
||||
DebugEntryDef registerBuiltin(DebugEntrySpec spec, int kind, boolean flat) {
|
||||
return register(spec, kind, flat);
|
||||
}
|
||||
|
||||
private DebugEntryDef register(DebugEntrySpec spec, int kind, boolean isFlat) {
|
||||
requireOpen();
|
||||
final String id = spec.getId();
|
||||
if (!isValidId(id)) throw new IllegalArgumentException("Invalid debug entry id: " + id);
|
||||
if (entriesById.containsKey(id)) throw new IllegalArgumentException("Duplicate debug entry id: " + id);
|
||||
final DebugEntryCategory category = spec.getCategory();
|
||||
if (category == null || spec.getDefaultStatus() == null || spec.getPerformanceStatus() == null) {
|
||||
throw new IllegalArgumentException("Debug entry " + id + " needs a category and both profile statuses");
|
||||
}
|
||||
if (category == DebugEntryCategory.RENDERER && spec.getTextFactory() != null) {
|
||||
throw new IllegalArgumentException("Renderer debug entry " + id + " must not have a text factory");
|
||||
}
|
||||
final int rendererIndex = category == DebugEntryCategory.RENDERER ? rendererSequence++ : -1;
|
||||
final DebugEntryDef def = new DebugEntryDef(this, spec, openEntries.size(), kind, rendererIndex, isFlat);
|
||||
openEntries.add(def);
|
||||
entriesById.put(id, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public void freeze() {
|
||||
if (frozen) return;
|
||||
frozen = true;
|
||||
final int groupCount = openGroups.size();
|
||||
final DebugGroup[] sorted = openGroups.toArray(new DebugGroup[groupCount]);
|
||||
Arrays.sort(sorted, Comparator.comparingInt((DebugGroup g) -> g.side).thenComparingInt(g -> g.order));
|
||||
for (int i = 0; i < groupCount; i++) {
|
||||
sorted[i].index = i;
|
||||
}
|
||||
groups = sorted;
|
||||
entries = openEntries.toArray(new DebugEntryDef[openEntries.size()]);
|
||||
int flatCount = 0;
|
||||
for (final DebugEntryDef def : entries) {
|
||||
if (isFlatText(def)) flatCount++;
|
||||
}
|
||||
flat = new DebugEntryDef[flatCount];
|
||||
renderers = new DebugEntryDef[rendererSequence];
|
||||
int f = 0;
|
||||
for (final DebugEntryDef def : entries) {
|
||||
if (isFlatText(def)) flat[f++] = def;
|
||||
if (def.rendererIndex >= 0) renderers[def.rendererIndex] = def;
|
||||
}
|
||||
openGroups.clear();
|
||||
openGroups.trimToSize();
|
||||
openEntries.clear();
|
||||
openEntries.trimToSize();
|
||||
}
|
||||
|
||||
private static boolean isFlatText(DebugEntryDef def) {
|
||||
return def.flat && def.isScreenText();
|
||||
}
|
||||
|
||||
public void failAll() {
|
||||
for (final DebugEntryDef def : entriesById.values()) def.failed = true;
|
||||
}
|
||||
|
||||
public boolean hasNamespace(String namespace) {
|
||||
final String prefix = namespace + ':';
|
||||
for (final String id : groupsById.keySet()) {
|
||||
if (id.startsWith(prefix)) return true;
|
||||
}
|
||||
for (final String id : entriesById.keySet()) {
|
||||
if (id.startsWith(prefix)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isFrozen() {
|
||||
return frozen;
|
||||
}
|
||||
|
||||
public int entryCount() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
public DebugEntryDef entryAt(int index) {
|
||||
return entries[index];
|
||||
}
|
||||
|
||||
public DebugEntryDef entryById(String id) {
|
||||
return entriesById.get(id);
|
||||
}
|
||||
|
||||
public int groupCount() {
|
||||
return groups.length;
|
||||
}
|
||||
|
||||
public DebugGroup groupAt(int index) {
|
||||
return groups[index];
|
||||
}
|
||||
|
||||
public DebugGroup groupById(String id) {
|
||||
return groupsById.get(id);
|
||||
}
|
||||
|
||||
public int flatCount() {
|
||||
return flat.length;
|
||||
}
|
||||
|
||||
public DebugEntryDef flatAt(int index) {
|
||||
return flat[index];
|
||||
}
|
||||
|
||||
public int rendererCount() {
|
||||
return renderers.length;
|
||||
}
|
||||
|
||||
public DebugEntryDef rendererAt(int index) {
|
||||
return renderers[index];
|
||||
}
|
||||
|
||||
public void fail(DebugEntryDef def, Throwable cause) {
|
||||
if (def.failed) return;
|
||||
def.failed = true;
|
||||
failuresPending = true;
|
||||
LOGGER.error("Debug screen entry {} failed and is disabled for the rest of this session", def.id, cause);
|
||||
}
|
||||
|
||||
public boolean consumeFailures() {
|
||||
final boolean pending = failuresPending;
|
||||
failuresPending = false;
|
||||
return pending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.compat.debug.DebugCompat;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.debug.screen.renderers.DebugRenderers;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class DebugScreen {
|
||||
|
||||
public static DebugScreenEntryList ENTRIES;
|
||||
|
||||
private static final DebugContextImpl CONTEXT = new DebugContextImpl();
|
||||
|
||||
private static boolean overlayVisible;
|
||||
private static int clientTick;
|
||||
private static boolean groupedFailed;
|
||||
private static boolean freezeAttempted;
|
||||
|
||||
private DebugScreen() {}
|
||||
|
||||
public static void bootstrap() {
|
||||
DebugGroup.bootstrap();
|
||||
DebugScreenEntries.bootstrap();
|
||||
}
|
||||
|
||||
private static void freeze(File configDir) {
|
||||
bootstrap();
|
||||
DebugCompat.registerMissing();
|
||||
DebugRegistry.MAIN.freeze();
|
||||
final DebugScreenEntryList entries = new DebugScreenEntryList(DebugRegistry.MAIN);
|
||||
entries.setFile(new File(configDir, "angelica-debug-profile.json"));
|
||||
entries.setListener(DebugRenderers::onEnabledChanged);
|
||||
ENTRIES = entries;
|
||||
entries.load();
|
||||
entries.setOverlayVisible(overlayVisible);
|
||||
}
|
||||
|
||||
public static void syncOverlayVisible() {
|
||||
final boolean visible = Minecraft.getMinecraft().gameSettings.showDebugInfo;
|
||||
overlayVisible = visible;
|
||||
final DebugScreenEntryList entries = ENTRIES;
|
||||
if (entries != null && entries.isOverlayVisible() != visible) entries.setOverlayVisible(visible);
|
||||
}
|
||||
|
||||
public static void onClientTickStart() {
|
||||
if (!freezeAttempted) {
|
||||
freezeAttempted = true;
|
||||
try {
|
||||
freeze(Loader.instance().getConfigDir());
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
ENTRIES = null;
|
||||
DebugRegistry.MAIN.failAll();
|
||||
AngelicaMod.LOGGER.error("Failed to set up the debug screen; it stays disabled for this session", e);
|
||||
}
|
||||
}
|
||||
syncOverlayVisible();
|
||||
final DebugScreenEntryList entries = ENTRIES;
|
||||
if (entries != null) entries.refresh();
|
||||
}
|
||||
|
||||
public static void onClientTickEnd() {
|
||||
clientTick++;
|
||||
}
|
||||
|
||||
public static boolean isOverlayVisible() {
|
||||
return overlayVisible;
|
||||
}
|
||||
|
||||
public static int clientTick() {
|
||||
return clientTick;
|
||||
}
|
||||
|
||||
public static boolean isRendererVerbose() {
|
||||
return (SystemProperties.DEBUG_F3_DETAIL && DebugScreenEntries.ANGELICA_RENDERER.isEnabled()) || DebugScreenEntries.ANGELICA_RENDERER_VERBOSE.isEnabled();
|
||||
}
|
||||
|
||||
public static boolean isOverlayIdle() {
|
||||
final DebugScreenEntryList entries = ENTRIES;
|
||||
return !overlayVisible && (entries == null || !entries.anyTextEnabled());
|
||||
}
|
||||
|
||||
public static boolean isGroupedActive() {
|
||||
return AngelicaConfig.modernizeF3Screen && !groupedFailed && AngelicaConfig.enableFontRenderer;
|
||||
}
|
||||
|
||||
public static void disableGrouped() {
|
||||
groupedFailed = true;
|
||||
}
|
||||
|
||||
public static boolean isAlwaysOnTextAtTopLeft() {
|
||||
final DebugScreenEntryList entries = ENTRIES;
|
||||
return entries != null && isGroupedActive() && entries.anyTextAlwaysOn() && Minecraft.getMinecraft().theWorld != null;
|
||||
}
|
||||
|
||||
public static void fail(DebugEntryDef def, Throwable cause) {
|
||||
def.registry.fail(def, cause);
|
||||
}
|
||||
|
||||
static void display(DebugEntryDef def, DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.world() == null || context.player() == null) return;
|
||||
try {
|
||||
def.entryOrCreate().display(displayer, context);
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
fail(def, e);
|
||||
}
|
||||
}
|
||||
|
||||
static void runEntries(DebugScreenDisplayer displayer, Minecraft mc, long version, DebugRegistry flatFrom, boolean alwaysOnOnly) {
|
||||
final DebugContextImpl context = CONTEXT;
|
||||
context.prepare(mc, System.nanoTime(), version);
|
||||
try {
|
||||
if (flatFrom != null) {
|
||||
for (int i = 0, n = flatFrom.flatCount(); i < n; i++) {
|
||||
final DebugEntryDef def = flatFrom.flatAt(i);
|
||||
if (!def.isEnabled()) continue;
|
||||
context.beginEntry(def);
|
||||
display(def, displayer, context);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final DebugScreenEntryList entries = ENTRIES;
|
||||
for (int i = 0, n = entries.enabledCount(); i < n; i++) {
|
||||
final DebugEntryDef def = entries.enabledAt(i);
|
||||
if (!def.isScreenText()) continue;
|
||||
if (alwaysOnOnly && !def.isAlwaysOn()) continue;
|
||||
context.beginEntry(def);
|
||||
display(def, displayer, context);
|
||||
}
|
||||
} finally {
|
||||
context.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntrySpec;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.ChunkDebugMinimap;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.AngelicaEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.AngelicaRendererEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.AngelicaShadersEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.ForgeBrandingsEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.HeaderEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.LookingAtBlockEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.MemoryEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.PositionEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.RenderingEntries;
|
||||
import com.gtnewhorizons.angelica.debug.screen.entries.SystemSpecsEntry;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
|
||||
import static com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus.ALWAYS_ON;
|
||||
import static com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus.IN_OVERLAY;
|
||||
import static com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus.NEVER;
|
||||
|
||||
public final class DebugScreenEntries {
|
||||
|
||||
public static final int KIND_GAME_VERSION = 1;
|
||||
public static final int KIND_FPS = 2;
|
||||
public static final int KIND_TPS = 3;
|
||||
public static final int KIND_PLAYER_POSITION = 4;
|
||||
public static final int KIND_PLAYER_SECTION_POSITION = 5;
|
||||
public static final int KIND_PLAYER_SPEED = 6;
|
||||
public static final int KIND_BIOME = 7;
|
||||
public static final int KIND_LIGHT_LEVELS = 8;
|
||||
public static final int KIND_HEIGHTMAP = 9;
|
||||
public static final int KIND_CHUNK_RENDER_STATS = 10;
|
||||
public static final int KIND_ENTITY_RENDER_STATS = 11;
|
||||
public static final int KIND_PARTICLE_RENDER_STATS = 12;
|
||||
public static final int KIND_CHUNK_SOURCE_STATS = 13;
|
||||
public static final int KIND_MEMORY = 14;
|
||||
public static final int KIND_SYSTEM_SPECS = 15;
|
||||
public static final int KIND_LOOKING_AT_BLOCK = 16;
|
||||
public static final int KIND_FORGE_BRANDINGS = 17;
|
||||
public static final int KIND_ANGELICA_RENDERER = 18;
|
||||
public static final int KIND_ANGELICA_SHADERS = 20;
|
||||
public static final int KIND_ANGELICA_DYNAMIC_LIGHTS = 21;
|
||||
public static final int KIND_ANGELICA_FOG = 22;
|
||||
public static final int KIND_ANGELICA_ANIMATIONS = 23;
|
||||
public static final int KIND_HELP = 26;
|
||||
public static final int KIND_ENTITY_HITBOXES = 28;
|
||||
public static final int KIND_CHUNK_BORDERS = 29;
|
||||
public static final int KIND_CHUNK_SECTION_VISIBILITY = 30;
|
||||
public static final int KIND_VISUALIZE_BLOCK_LIGHT_LEVELS = 31;
|
||||
public static final int KIND_VISUALIZE_SKY_LIGHT_LEVELS = 32;
|
||||
public static final int KIND_VISUALIZE_HEIGHTMAP = 33;
|
||||
public static final int KIND_VISUALIZE_COLLISION_BOXES = 34;
|
||||
public static final int KIND_VISUALIZE_SOLID_FACES = 35;
|
||||
public static final int KIND_VISUALIZE_WATER_LEVELS = 36;
|
||||
public static final int KIND_VISUALIZE_ENTITY_SUPPORTING_BLOCK = 37;
|
||||
public static final int KIND_ANGELICA_CHUNK_MINIMAP = 38;
|
||||
public static final int KIND_POST_EFFECT = 39;
|
||||
|
||||
private static final String NAME_KEY_PREFIX = "angelica.debug.entry.";
|
||||
|
||||
public static final DebugEntryDef GAME_VERSION = add(text("minecraft:game_version", IN_OVERLAY, NEVER), KIND_GAME_VERSION, false);
|
||||
public static final DebugEntryDef FPS = add(text("minecraft:fps", IN_OVERLAY, ALWAYS_ON), KIND_FPS, false);
|
||||
public static final DebugEntryDef TPS = add(text("minecraft:tps", IN_OVERLAY, IN_OVERLAY), KIND_TPS, true);
|
||||
public static final DebugEntryDef PLAYER_POSITION = add(text("minecraft:player_position", IN_OVERLAY, NEVER), KIND_PLAYER_POSITION, false);
|
||||
public static final DebugEntryDef PLAYER_SECTION_POSITION = add(text("minecraft:player_section_position", IN_OVERLAY, NEVER), KIND_PLAYER_SECTION_POSITION, false);
|
||||
public static final DebugEntryDef PLAYER_SPEED = add(text("minecraft:player_speed", IN_OVERLAY, NEVER), KIND_PLAYER_SPEED, false);
|
||||
public static final DebugEntryDef BIOME = add(text("minecraft:biome", IN_OVERLAY, NEVER), KIND_BIOME, false);
|
||||
public static final DebugEntryDef LIGHT_LEVELS = add(text("minecraft:light_levels", IN_OVERLAY, NEVER), KIND_LIGHT_LEVELS, false);
|
||||
public static final DebugEntryDef HEIGHTMAP = add(text("minecraft:heightmap", IN_OVERLAY, NEVER), KIND_HEIGHTMAP, false);
|
||||
public static final DebugEntryDef CHUNK_RENDER_STATS = add(text("minecraft:chunk_render_stats", IN_OVERLAY, NEVER), KIND_CHUNK_RENDER_STATS, false);
|
||||
public static final DebugEntryDef ENTITY_RENDER_STATS = add(text("minecraft:entity_render_stats", IN_OVERLAY, NEVER), KIND_ENTITY_RENDER_STATS, false);
|
||||
public static final DebugEntryDef PARTICLE_RENDER_STATS = add(text("minecraft:particle_render_stats", IN_OVERLAY, NEVER), KIND_PARTICLE_RENDER_STATS, false);
|
||||
public static final DebugEntryDef POST_EFFECT = add(text("minecraft:post_effect", IN_OVERLAY, NEVER), KIND_POST_EFFECT, false);
|
||||
public static final DebugEntryDef CHUNK_SOURCE_STATS = add(text("minecraft:chunk_source_stats", IN_OVERLAY, NEVER), KIND_CHUNK_SOURCE_STATS, false);
|
||||
public static final DebugEntryDef MEMORY = add(text("minecraft:memory", IN_OVERLAY, IN_OVERLAY), KIND_MEMORY, false);
|
||||
public static final DebugEntryDef SYSTEM_SPECS = add(text("minecraft:system_specs", IN_OVERLAY, NEVER), KIND_SYSTEM_SPECS, false);
|
||||
public static final DebugEntryDef LOOKING_AT_BLOCK = add(text("minecraft:looking_at_block", IN_OVERLAY, NEVER), KIND_LOOKING_AT_BLOCK, true);
|
||||
public static final DebugEntryDef FORGE_BRANDINGS = add(text("forge:brandings", IN_OVERLAY, NEVER), KIND_FORGE_BRANDINGS, false);
|
||||
public static final DebugEntryDef ANGELICA_RENDERER = add(text("angelica:renderer", IN_OVERLAY, IN_OVERLAY), KIND_ANGELICA_RENDERER, true);
|
||||
public static final DebugEntryDef ANGELICA_RENDERER_VERBOSE = add(gate("angelica:renderer_verbose", NEVER, NEVER).supportsAlways(false), 0, false);
|
||||
|
||||
static {
|
||||
if (AngelicaConfig.enableIris) {
|
||||
add(text("angelica:shaders", IN_OVERLAY, NEVER), KIND_ANGELICA_SHADERS, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static final DebugEntryDef ANGELICA_DYNAMIC_LIGHTS = add(text("angelica:dynamic_lights", IN_OVERLAY, NEVER), KIND_ANGELICA_DYNAMIC_LIGHTS, true);
|
||||
public static final DebugEntryDef ANGELICA_FOG = add(text("angelica:fog", NEVER, NEVER), KIND_ANGELICA_FOG, true);
|
||||
public static final DebugEntryDef ANGELICA_ANIMATIONS = add(text("angelica:animations", IN_OVERLAY, NEVER), KIND_ANGELICA_ANIMATIONS, true);
|
||||
public static final DebugEntryDef OTHER_LEFT = add(gate("angelica:other_left", IN_OVERLAY, NEVER).supportsAlways(false), 0, false);
|
||||
public static final DebugEntryDef OTHER_RIGHT = add(gate("angelica:other_right", IN_OVERLAY, NEVER).supportsAlways(false), 0, false);
|
||||
public static final DebugEntryDef HELP = add(text("angelica:help", IN_OVERLAY, NEVER).supportsAlways(false), KIND_HELP, false);
|
||||
public static final DebugEntryDef THREE_DIMENSIONAL_CROSSHAIR = add(renderer("minecraft:3d_crosshair", IN_OVERLAY, NEVER), 0, false);
|
||||
public static final DebugEntryDef ENTITY_HITBOXES = add(renderer("minecraft:entity_hitboxes", NEVER, NEVER).toggle(DebugScreenEntries::onToggle), KIND_ENTITY_HITBOXES, false);
|
||||
public static final DebugEntryDef CHUNK_BORDERS = add(renderer("minecraft:chunk_borders", NEVER, NEVER), KIND_CHUNK_BORDERS, false);
|
||||
public static final DebugEntryDef CHUNK_SECTION_VISIBILITY = add(renderer("minecraft:chunk_section_visibility", NEVER, NEVER), KIND_CHUNK_SECTION_VISIBILITY, false);
|
||||
public static final DebugEntryDef VISUALIZE_BLOCK_LIGHT_LEVELS = add(renderer("minecraft:visualize_block_light_levels", NEVER, NEVER), KIND_VISUALIZE_BLOCK_LIGHT_LEVELS, false);
|
||||
public static final DebugEntryDef VISUALIZE_SKY_LIGHT_LEVELS = add(renderer("minecraft:visualize_sky_light_levels", NEVER, NEVER), KIND_VISUALIZE_SKY_LIGHT_LEVELS, false);
|
||||
public static final DebugEntryDef VISUALIZE_HEIGHTMAP = add(renderer("minecraft:visualize_heightmap", NEVER, NEVER), KIND_VISUALIZE_HEIGHTMAP, false);
|
||||
public static final DebugEntryDef VISUALIZE_COLLISION_BOXES = add(renderer("minecraft:visualize_collision_boxes", NEVER, NEVER), KIND_VISUALIZE_COLLISION_BOXES, false);
|
||||
public static final DebugEntryDef VISUALIZE_SOLID_FACES = add(renderer("minecraft:visualize_solid_faces", NEVER, NEVER), KIND_VISUALIZE_SOLID_FACES, false);
|
||||
public static final DebugEntryDef VISUALIZE_WATER_LEVELS = add(renderer("minecraft:visualize_water_levels", NEVER, NEVER), KIND_VISUALIZE_WATER_LEVELS, false);
|
||||
public static final DebugEntryDef VISUALIZE_ENTITY_SUPPORTING_BLOCK = add(renderer("minecraft:visualize_entity_supporting_block", NEVER, NEVER), KIND_VISUALIZE_ENTITY_SUPPORTING_BLOCK, false);
|
||||
public static final DebugEntryDef ANGELICA_CHUNK_MINIMAP = add(renderer("angelica:chunk_minimap", NEVER, NEVER).toggle(DebugScreenEntries::onToggle), KIND_ANGELICA_CHUNK_MINIMAP, false);
|
||||
|
||||
private DebugScreenEntries() {}
|
||||
|
||||
static void bootstrap() {}
|
||||
|
||||
private static DebugScreenEntry create(DebugEntryHandle entry) {
|
||||
final DebugEntryDef def = (DebugEntryDef) entry;
|
||||
return switch (def.kind) {
|
||||
case KIND_GAME_VERSION -> HeaderEntries::gameVersion;
|
||||
case KIND_FPS -> HeaderEntries::fps;
|
||||
case KIND_TPS -> RenderingEntries::tps;
|
||||
case KIND_PLAYER_POSITION -> new PositionEntries.PlayerPosition();
|
||||
case KIND_PLAYER_SECTION_POSITION -> PositionEntries::sectionPosition;
|
||||
case KIND_PLAYER_SPEED -> PositionEntries::speed;
|
||||
case KIND_BIOME -> new PositionEntries.Biome();
|
||||
case KIND_LIGHT_LEVELS -> PositionEntries::lightLevels;
|
||||
case KIND_HEIGHTMAP -> PositionEntries::heightmap;
|
||||
case KIND_CHUNK_RENDER_STATS, KIND_ENTITY_RENDER_STATS, KIND_PARTICLE_RENDER_STATS, KIND_CHUNK_SOURCE_STATS -> new RenderingEntries.RenderStats(def.kind);
|
||||
case KIND_POST_EFFECT -> new RenderingEntries.PostEffect();
|
||||
case KIND_MEMORY -> new MemoryEntry();
|
||||
case KIND_SYSTEM_SPECS -> new SystemSpecsEntry();
|
||||
case KIND_LOOKING_AT_BLOCK -> new LookingAtBlockEntry();
|
||||
case KIND_FORGE_BRANDINGS -> new ForgeBrandingsEntry();
|
||||
case KIND_ANGELICA_RENDERER -> new AngelicaRendererEntry();
|
||||
case KIND_ANGELICA_SHADERS -> new AngelicaShadersEntry();
|
||||
case KIND_ANGELICA_DYNAMIC_LIGHTS -> AngelicaEntries::dynamicLights;
|
||||
case KIND_ANGELICA_FOG -> AngelicaEntries::fog;
|
||||
case KIND_ANGELICA_ANIMATIONS -> AngelicaEntries::animations;
|
||||
case KIND_HELP -> new HeaderEntries.Help();
|
||||
default -> throw new IllegalArgumentException(def.id);
|
||||
};
|
||||
}
|
||||
|
||||
private static void onToggle(DebugEntryHandle entry, boolean enabled) {
|
||||
switch (((DebugEntryDef) entry).kind) {
|
||||
case KIND_ENTITY_HITBOXES -> RenderManager.debugBoundingBox = enabled;
|
||||
case KIND_ANGELICA_CHUNK_MINIMAP -> {
|
||||
if (enabled) {
|
||||
ChunkDebugMinimap.enable();
|
||||
} else {
|
||||
ChunkDebugMinimap.disable();
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private static DebugEntrySpec text(String id, DebugScreenEntryStatus defaultStatus, DebugScreenEntryStatus performanceStatus) {
|
||||
return new DebugEntrySpec(id, DebugEntryCategory.SCREEN_TEXT).defaults(defaultStatus, performanceStatus).text(DebugScreenEntries::create);
|
||||
}
|
||||
|
||||
private static DebugEntrySpec gate(String id, DebugScreenEntryStatus defaultStatus, DebugScreenEntryStatus performanceStatus) {
|
||||
return new DebugEntrySpec(id, DebugEntryCategory.SCREEN_TEXT).defaults(defaultStatus, performanceStatus);
|
||||
}
|
||||
|
||||
private static DebugEntrySpec renderer(String id, DebugScreenEntryStatus defaultStatus, DebugScreenEntryStatus performanceStatus) {
|
||||
return new DebugEntrySpec(id, DebugEntryCategory.RENDERER).defaults(defaultStatus, performanceStatus);
|
||||
}
|
||||
|
||||
private static DebugEntryDef add(DebugEntrySpec spec, int kind, boolean flat) {
|
||||
return DebugRegistry.MAIN.registerBuiltin(spec.name(NAME_KEY_PREFIX + spec.getId().replace(':', '.')), kind, flat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugEntryCategory;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntryStatus;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class DebugScreenEntryList {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
static final int FORMAT_VERSION = 2;
|
||||
static final int MAX_REBUILD_PASSES = 8;
|
||||
private static final String KEY_VERSION = "version";
|
||||
private static final String KEY_PROFILE = "profile";
|
||||
private static final String KEY_CUSTOM = "custom";
|
||||
|
||||
private final DebugRegistry registry;
|
||||
private final int count;
|
||||
private final DebugScreenEntryStatus[] statuses;
|
||||
private final DebugScreenEntryStatus[] overrides;
|
||||
private final DebugEntryDef[] enabledOrder;
|
||||
private final DebugEntryDef[] flipped;
|
||||
private final LinkedHashMap<String, JsonElement> unknownEntries = new LinkedHashMap<>();
|
||||
private final LinkedHashMap<String, JsonElement> unknownMembers = new LinkedHashMap<>();
|
||||
private int enabledCount;
|
||||
private int flipCount;
|
||||
private int overrideCount;
|
||||
private boolean anyTextEnabled;
|
||||
private boolean anyTextAlwaysOn;
|
||||
private boolean overlayVisible;
|
||||
private boolean rebuilding;
|
||||
private boolean rebuildAgain;
|
||||
private DebugScreenProfile profile;
|
||||
private File file;
|
||||
private Runnable listener;
|
||||
private long version;
|
||||
|
||||
public DebugScreenEntryList(DebugRegistry registry) {
|
||||
if (!registry.isFrozen()) throw new IllegalStateException("The debug screen registry must be frozen first");
|
||||
this.registry = registry;
|
||||
this.count = registry.entryCount();
|
||||
this.statuses = new DebugScreenEntryStatus[count];
|
||||
this.overrides = new DebugScreenEntryStatus[count];
|
||||
this.enabledOrder = new DebugEntryDef[count];
|
||||
this.flipped = new DebugEntryDef[count];
|
||||
resetToProfile(DebugScreenProfile.DEFAULT);
|
||||
}
|
||||
|
||||
public void setFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public void setListener(Runnable listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private static DebugScreenEntryStatus clamp(DebugEntryDef def, DebugScreenEntryStatus status) {
|
||||
return status == DebugScreenEntryStatus.ALWAYS_ON && !def.supportsAlways ? DebugScreenEntryStatus.IN_OVERLAY : status;
|
||||
}
|
||||
|
||||
private void resetToDefault() {
|
||||
unknownMembers.clear();
|
||||
resetToProfile(DebugScreenProfile.DEFAULT);
|
||||
rebuildCurrentList();
|
||||
}
|
||||
|
||||
public void load() {
|
||||
unknownEntries.clear();
|
||||
unknownMembers.clear();
|
||||
if (file == null) {
|
||||
resetToDefault();
|
||||
return;
|
||||
}
|
||||
if (!file.exists()) {
|
||||
resetToDefault();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
|
||||
final JsonElement root = new JsonParser().parse(reader);
|
||||
if (root == null || !root.isJsonObject()) throw new IllegalStateException("Expected a JSON object");
|
||||
applyJson(root.getAsJsonObject());
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOGGER.error("Failed to load debug screen profile from {}, resetting to default", file, e);
|
||||
resetToDefault();
|
||||
quarantine();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
rebuildCurrentList();
|
||||
}
|
||||
|
||||
private void quarantine() {
|
||||
final Path source = file.toPath();
|
||||
try {
|
||||
Files.move(source, source.resolveSibling(file.getName() + ".corrupt"), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to set aside the unreadable debug screen profile {}", file, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static DebugScreenEntryStatus statusOf(JsonElement value) {
|
||||
return DebugScreenEntryStatus.bySerializedName(stringOf(value));
|
||||
}
|
||||
|
||||
private static String stringOf(JsonElement value) {
|
||||
return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isString() ? value.getAsString() : null;
|
||||
}
|
||||
|
||||
private void applyJson(JsonObject json) {
|
||||
final JsonElement customElement = json.get(KEY_CUSTOM);
|
||||
if (customElement != null && !customElement.isJsonObject()) throw new IllegalStateException("Expected custom to be an object");
|
||||
final JsonObject custom = customElement == null ? null : customElement.getAsJsonObject();
|
||||
for (final Map.Entry<String, JsonElement> member : json.entrySet()) {
|
||||
final String key = member.getKey();
|
||||
if (KEY_VERSION.equals(key) || KEY_PROFILE.equals(key) || KEY_CUSTOM.equals(key)) continue;
|
||||
unknownMembers.put(key, member.getValue());
|
||||
}
|
||||
final JsonElement profileElement = json.get(KEY_PROFILE);
|
||||
if (profileElement != null || custom == null) {
|
||||
final DebugScreenProfile selected = DebugScreenProfile.bySerializedName(stringOf(profileElement));
|
||||
resetToProfile(selected == null ? DebugScreenProfile.DEFAULT : selected);
|
||||
return;
|
||||
}
|
||||
for (final Map.Entry<String, JsonElement> member : custom.entrySet()) {
|
||||
if (registry.entryById(member.getKey()) == null) unknownEntries.put(member.getKey(), member.getValue());
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
final DebugScreenEntryStatus status = statusOf(custom.get(def.id));
|
||||
statuses[i] = clamp(def, status == null ? def.defaultStatus : status);
|
||||
}
|
||||
reattachProfile();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
if (file == null) return;
|
||||
final JsonObject json = new JsonObject();
|
||||
json.addProperty(KEY_VERSION, FORMAT_VERSION);
|
||||
if (profile != null) {
|
||||
json.addProperty(KEY_PROFILE, profile.serializedName);
|
||||
} else {
|
||||
final JsonObject custom = new JsonObject();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
if (statuses[i] != clamp(def, def.defaultStatus)) custom.addProperty(def.id, statuses[i].serializedName);
|
||||
}
|
||||
for (final Map.Entry<String, JsonElement> member : unknownEntries.entrySet()) {
|
||||
custom.add(member.getKey(), member.getValue());
|
||||
}
|
||||
json.add(KEY_CUSTOM, custom);
|
||||
}
|
||||
for (final Map.Entry<String, JsonElement> member : unknownMembers.entrySet()) {
|
||||
json.add(member.getKey(), member.getValue());
|
||||
}
|
||||
final Path target = file.toPath();
|
||||
final Path temp = target.resolveSibling(file.getName() + ".tmp");
|
||||
try {
|
||||
final File parent = file.getParentFile();
|
||||
if (parent != null) Files.createDirectories(parent.toPath());
|
||||
Files.write(temp, GSON.toJson(json).getBytes(StandardCharsets.UTF_8));
|
||||
try {
|
||||
Files.move(temp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to save debug screen profile to {}", file, e);
|
||||
try {
|
||||
Files.deleteIfExists(temp);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetToProfile(DebugScreenProfile profile) {
|
||||
this.profile = profile;
|
||||
unknownEntries.clear();
|
||||
dropOverrides();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
statuses[i] = clamp(def, def.profileStatus(profile));
|
||||
}
|
||||
}
|
||||
|
||||
public void loadProfile(DebugScreenProfile profile) {
|
||||
resetToProfile(profile);
|
||||
rebuildCurrentList();
|
||||
}
|
||||
|
||||
public boolean isUsingProfile(DebugScreenProfile profile) {
|
||||
return this.profile == profile;
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus getStatus(DebugEntryDef def) {
|
||||
return statuses[def.index];
|
||||
}
|
||||
|
||||
public DebugScreenEntryStatus effectiveStatus(DebugEntryDef def) {
|
||||
final DebugScreenEntryStatus override = overrides[def.index];
|
||||
return override == null ? statuses[def.index] : override;
|
||||
}
|
||||
|
||||
public void setStatus(DebugEntryDef def, DebugScreenEntryStatus status) {
|
||||
final int index = def.index;
|
||||
statuses[index] = clamp(def, status);
|
||||
if (overrides[index] != null) {
|
||||
overrides[index] = null;
|
||||
overrideCount--;
|
||||
}
|
||||
reattachProfile();
|
||||
rebuildCurrentList();
|
||||
save();
|
||||
}
|
||||
|
||||
private void reattachProfile() {
|
||||
profile = matchingProfile();
|
||||
if (profile != null) unknownEntries.clear();
|
||||
}
|
||||
|
||||
private DebugScreenProfile matchingProfile() {
|
||||
for (final DebugScreenProfile candidate : DebugScreenProfile.VALUES) {
|
||||
if (matches(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matches(DebugScreenProfile candidate) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
if (statuses[i] != clamp(def, def.profileStatus(candidate))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean toggleStatus(DebugEntryDef def) {
|
||||
if (def.category == DebugEntryCategory.RENDERER && def.supportsAlways) {
|
||||
overrideForSession(def, def.isEnabled() ? DebugScreenEntryStatus.NEVER : DebugScreenEntryStatus.ALWAYS_ON);
|
||||
return effectiveStatus(def) != DebugScreenEntryStatus.NEVER;
|
||||
}
|
||||
final DebugScreenEntryStatus next = switch (effectiveStatus(def)) {
|
||||
case ALWAYS_ON -> DebugScreenEntryStatus.NEVER;
|
||||
case IN_OVERLAY -> overlayVisible || !def.supportsAlways ? DebugScreenEntryStatus.NEVER : DebugScreenEntryStatus.ALWAYS_ON;
|
||||
default -> overlayVisible ? DebugScreenEntryStatus.IN_OVERLAY : DebugScreenEntryStatus.ALWAYS_ON;
|
||||
};
|
||||
overrideForSession(def, next);
|
||||
return effectiveStatus(def) != DebugScreenEntryStatus.NEVER;
|
||||
}
|
||||
|
||||
public void overrideForSession(DebugEntryDef def, DebugScreenEntryStatus status) {
|
||||
final DebugScreenEntryStatus previous = overrides[def.index];
|
||||
final DebugScreenEntryStatus next = status == null ? null : clamp(def, status);
|
||||
if (previous == next) return;
|
||||
if (previous == null) overrideCount++;
|
||||
if (next == null) overrideCount--;
|
||||
overrides[def.index] = next;
|
||||
rebuildCurrentList();
|
||||
}
|
||||
|
||||
public void clearSessionOverrides() {
|
||||
if (overrideCount == 0) return;
|
||||
dropOverrides();
|
||||
rebuildCurrentList();
|
||||
}
|
||||
|
||||
private void dropOverrides() {
|
||||
if (overrideCount == 0) return;
|
||||
Arrays.fill(overrides, 0, count, null);
|
||||
overrideCount = 0;
|
||||
}
|
||||
|
||||
public int enabledCount() {
|
||||
return enabledCount;
|
||||
}
|
||||
|
||||
public DebugEntryDef enabledAt(int index) {
|
||||
return enabledOrder[index];
|
||||
}
|
||||
|
||||
public boolean anyTextEnabled() {
|
||||
return anyTextEnabled;
|
||||
}
|
||||
|
||||
public boolean anyTextAlwaysOn() {
|
||||
return anyTextAlwaysOn;
|
||||
}
|
||||
|
||||
public boolean isOverlayVisible() {
|
||||
return overlayVisible;
|
||||
}
|
||||
|
||||
public void setOverlayVisible(boolean visible) {
|
||||
if (overlayVisible != visible) {
|
||||
overlayVisible = visible;
|
||||
rebuildCurrentList();
|
||||
}
|
||||
}
|
||||
|
||||
public long version() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (registry.consumeFailures()) rebuildCurrentList();
|
||||
}
|
||||
|
||||
public void rebuildCurrentList() {
|
||||
if (rebuilding) {
|
||||
rebuildAgain = true;
|
||||
return;
|
||||
}
|
||||
rebuilding = true;
|
||||
try {
|
||||
int passes = 0;
|
||||
do {
|
||||
rebuildAgain = false;
|
||||
rebuildPass();
|
||||
} while (rebuildAgain && ++passes < MAX_REBUILD_PASSES);
|
||||
if (rebuildAgain) failUnsettledListeners();
|
||||
} finally {
|
||||
rebuilding = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void failUnsettledListeners() {
|
||||
rebuildAgain = false;
|
||||
LOGGER.error("Debug screen toggle listeners kept changing entry statuses after {} passes; failing the {} entries that flipped last", MAX_REBUILD_PASSES, flipCount);
|
||||
final IllegalStateException cause = new IllegalStateException("Toggle listener did not settle within " + MAX_REBUILD_PASSES + " rebuild passes");
|
||||
for (int i = 0; i < flipCount; i++) {
|
||||
registry.fail(flipped[i], cause);
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildPass() {
|
||||
Arrays.fill(flipped, 0, flipCount, null);
|
||||
enabledCount = 0;
|
||||
anyTextEnabled = false;
|
||||
anyTextAlwaysOn = false;
|
||||
int flips = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
final DebugEntryDef def = registry.entryAt(i);
|
||||
final DebugScreenEntryStatus status = effectiveStatus(def);
|
||||
final boolean always = !def.failed && status == DebugScreenEntryStatus.ALWAYS_ON;
|
||||
final boolean on = always || !def.failed && overlayVisible && status == DebugScreenEntryStatus.IN_OVERLAY;
|
||||
def.alwaysOn = always;
|
||||
if (def.enabled != on) {
|
||||
def.enabled = on;
|
||||
if (def.toggleListener != null) flipped[flips++] = def;
|
||||
}
|
||||
if (on) {
|
||||
enabledOrder[enabledCount++] = def;
|
||||
if (def.isScreenText()) {
|
||||
anyTextEnabled = true;
|
||||
anyTextAlwaysOn |= always;
|
||||
}
|
||||
}
|
||||
}
|
||||
version++;
|
||||
flipCount = flips;
|
||||
for (int i = 0; i < flips; i++) {
|
||||
final DebugEntryDef def = flipped[i];
|
||||
try {
|
||||
def.toggleListener.onEnabledChanged(def, def.enabled);
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
registry.fail(def, e);
|
||||
}
|
||||
}
|
||||
if (listener != null) listener.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
|
||||
import com.gtnewhorizons.angelica.client.font.GlyphSettings;
|
||||
import com.gtnewhorizons.angelica.client.font.TextMesh;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.F3Graph;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class DebugScreenOverlay {
|
||||
|
||||
public static final DebugScreenOverlay INSTANCE = new DebugScreenOverlay();
|
||||
|
||||
public enum Mode { OVERLAY, ALWAYS_ON, FLAT }
|
||||
|
||||
private static final int COLUMN_BUDGET_INSET = 8;
|
||||
private static final int FORGE_ROW_HEIGHT = 10;
|
||||
private static final int CHROME = -1;
|
||||
private static final Tracy.ZoneId Z_ENTRIES = Tracy.zoneId("f3Entries", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_MEASURE = Tracy.zoneId("f3Measure", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_DRAW = Tracy.zoneId("f3Draw", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_CAPTURE = Tracy.zoneId("f3Capture", Tracy.COLOR_CLIENT);
|
||||
|
||||
private final DebugRegistry registry = DebugRegistry.MAIN;
|
||||
private final TextMesh chrome = new TextMesh();
|
||||
private final ForgeLineFilter.Sink externalSink = this::appendExternal;
|
||||
private final StringBuilder dump = new StringBuilder();
|
||||
private GlyphSettings textSettings;
|
||||
private GlyphSettings frameSettings;
|
||||
|
||||
private int groupCount = -1;
|
||||
private ForgeLineFilter filter;
|
||||
private GroupedDebugDisplayer displayer;
|
||||
private FlatDebugDisplayer flat;
|
||||
private DebugLayout layout;
|
||||
private TextMesh[] meshes;
|
||||
private boolean[] groupDirty;
|
||||
private boolean[] placed;
|
||||
private int[] placedX;
|
||||
private int[] placedY;
|
||||
private int placedCount;
|
||||
|
||||
private DebugText.Batched text;
|
||||
private FontRenderer textFont;
|
||||
private boolean textUnicode;
|
||||
|
||||
private long titlesTranslatedAt = -1L;
|
||||
private int lastTick = Integer.MIN_VALUE;
|
||||
private long lastVersion = -1L;
|
||||
private Mode lastMode;
|
||||
private int lastHeight = -1;
|
||||
private boolean chromeDirty = true;
|
||||
|
||||
private boolean scaled;
|
||||
private int scale;
|
||||
private int gameScale;
|
||||
private int screenWidth;
|
||||
|
||||
private boolean ran;
|
||||
private boolean skipped;
|
||||
private Mode lastDraw;
|
||||
|
||||
private DebugScreenOverlay() {}
|
||||
|
||||
public void afterDebugBlock(boolean ran, boolean skipped) {
|
||||
this.ran = ran;
|
||||
this.skipped = skipped;
|
||||
}
|
||||
|
||||
public boolean debugBlockRan() {
|
||||
return ran;
|
||||
}
|
||||
|
||||
private void ensureSized() {
|
||||
if (groupCount >= 0) return;
|
||||
final int n = registry.groupCount();
|
||||
filter = new ForgeLineFilter(registry);
|
||||
displayer = new GroupedDebugDisplayer(registry);
|
||||
flat = new FlatDebugDisplayer(registry);
|
||||
layout = new DebugLayout(registry);
|
||||
meshes = new TextMesh[n];
|
||||
for (int g = 0; g < n; g++) meshes[g] = new TextMesh();
|
||||
groupDirty = new boolean[n];
|
||||
placed = new boolean[n];
|
||||
placedX = new int[n];
|
||||
placedY = new int[n];
|
||||
groupCount = n;
|
||||
}
|
||||
|
||||
public void addFlatLines(Minecraft mc, ArrayList<String> left, ArrayList<String> right) {
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries == null) return;
|
||||
ensureSized();
|
||||
flat.rebuild(mc, DebugScreen.clientTick(), entries.version());
|
||||
flat.addTo(left, right);
|
||||
lastDraw = Mode.FLAT;
|
||||
}
|
||||
|
||||
public boolean onTextEvent(RenderGameOverlayEvent.Text event, FontRenderer font, int width, int height, boolean canceled) {
|
||||
if (canceled) return true;
|
||||
final DebugScreenEntryList entries = DebugScreen.ENTRIES;
|
||||
if (entries == null || !DebugScreen.isGroupedActive()) return canceled;
|
||||
if (!(font instanceof FontRendererAccessor)) {
|
||||
AngelicaMod.LOGGER.error("Grouped debug screen needs the batching font renderer on the HUD font; using the classic layout for the rest of this session");
|
||||
DebugScreen.disableGrouped();
|
||||
return skipped;
|
||||
}
|
||||
final Mode mode = DebugScreen.isOverlayVisible() && ran ? Mode.OVERLAY : Mode.ALWAYS_ON;
|
||||
lastDraw = mode;
|
||||
ensureSized();
|
||||
if (mode == Mode.ALWAYS_ON && !entries.anyTextAlwaysOn()) {
|
||||
clearPlacement();
|
||||
return canceled;
|
||||
}
|
||||
try {
|
||||
prepare(mode, Minecraft.getMinecraft(), event, font, width, height, entries);
|
||||
draw();
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
AngelicaMod.LOGGER.error("Grouped debug screen failed; using the classic layout for the rest of this session", e);
|
||||
DebugScreen.disableGrouped();
|
||||
return skipped;
|
||||
}
|
||||
return mode == Mode.OVERLAY;
|
||||
}
|
||||
|
||||
private void clearPlacement() {
|
||||
if (placedCount == 0) return;
|
||||
placedCount = 0;
|
||||
Arrays.fill(placed, false);
|
||||
}
|
||||
|
||||
private void markAllDirty() {
|
||||
chromeDirty = true;
|
||||
Arrays.fill(groupDirty, true);
|
||||
}
|
||||
|
||||
private void prepare(Mode mode, Minecraft mc, RenderGameOverlayEvent.Text event, FontRenderer font, int width, int height, DebugScreenEntryList entries) {
|
||||
final int gameScale = event.resolution.getScaleFactor();
|
||||
final boolean even = DebugGuiScale.evenForUnicode(mc);
|
||||
final int scale = DebugGuiScale.resolve(AngelicaConfig.debugGuiScale, gameScale, mc.displayWidth, mc.displayHeight, even);
|
||||
final boolean scaled = scale > 0 && scale < gameScale;
|
||||
final int screenWidth = scaled ? width * gameScale / scale : width;
|
||||
final int screenHeight = scaled ? height * gameScale / scale : height;
|
||||
final boolean sizeChanged = screenWidth != this.screenWidth || screenHeight != lastHeight || scale != this.scale || gameScale != this.gameScale;
|
||||
this.scaled = scaled;
|
||||
this.scale = scale;
|
||||
this.gameScale = gameScale;
|
||||
this.screenWidth = screenWidth;
|
||||
if (sizeChanged) {
|
||||
lastHeight = screenHeight;
|
||||
releaseSizeHolds();
|
||||
markAllDirty();
|
||||
}
|
||||
final boolean fontChanged = selectText(font);
|
||||
if (fontChanged) markAllDirty();
|
||||
final boolean titlesChanged = refreshTitles(fontChanged);
|
||||
final boolean refreshed = refreshContent(mc, mode, event, entries);
|
||||
if (refreshed || fontChanged || titlesChanged || sizeChanged) {
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_MEASURE);
|
||||
final int flags;
|
||||
try {
|
||||
flags = measureAll(screenWidth / 2 - COLUMN_BUDGET_INSET, fontChanged);
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
if ((flags & DebugGroupContents.SIZE) != 0) chromeDirty = true;
|
||||
}
|
||||
|
||||
fillHeights();
|
||||
if (mode == Mode.OVERLAY) {
|
||||
int leftMax = screenHeight;
|
||||
int rightMax = screenHeight;
|
||||
if (((IGameSettingsExt) mc.gameSettings).angelica$showFpsGraph()) {
|
||||
int strip = F3Graph.reservedHeight(font);
|
||||
if (scaled) strip = strip * gameScale / scale;
|
||||
leftMax -= strip;
|
||||
if (mc.isSingleplayer()) rightMax -= strip;
|
||||
}
|
||||
layout.assign(DebugLayout.MARGIN_TOP, leftMax, rightMax);
|
||||
} else {
|
||||
final int forgeRows = event.left.size();
|
||||
int forgeBlock = forgeRows > 0 ? forgeRows * FORGE_ROW_HEIGHT + DebugLayout.MARGIN_TOP : 0;
|
||||
if (scaled) forgeBlock = forgeBlock * gameScale / scale;
|
||||
layout.assignSingleColumn(DebugGroup.LEFT, DebugLayout.MARGIN_TOP + forgeBlock, screenHeight);
|
||||
}
|
||||
comparePlacement(screenWidth);
|
||||
}
|
||||
|
||||
private void comparePlacement(int screenWidth) {
|
||||
int count = 0;
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
final boolean p = layout.side[g] >= 0 && layout.height[g] > 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
if (p) {
|
||||
x = panelLeft(displayer.contents(g), layout.side[g], screenWidth);
|
||||
y = layout.top[g];
|
||||
count++;
|
||||
}
|
||||
if (p != placed[g] || x != placedX[g] || y != placedY[g]) {
|
||||
placed[g] = p;
|
||||
placedX[g] = x;
|
||||
placedY[g] = y;
|
||||
chromeDirty = true;
|
||||
groupDirty[g] = true;
|
||||
}
|
||||
}
|
||||
placedCount = count;
|
||||
}
|
||||
|
||||
private boolean selectText(FontRenderer font) {
|
||||
final boolean unicode = font.getUnicodeFlag();
|
||||
final GlyphSettings settings = GlyphSettings.current();
|
||||
frameSettings = settings;
|
||||
if (text != null && font == textFont && unicode == textUnicode && settings.sameMetricsAs(textSettings)) {
|
||||
return false;
|
||||
}
|
||||
if (text == null || font != textFont) {
|
||||
text = new DebugText.Batched(font, ((FontRendererAccessor) font).angelica$getBatcher());
|
||||
textFont = font;
|
||||
}
|
||||
textUnicode = unicode;
|
||||
textSettings = settings;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean refreshTitles(boolean fontChanged) {
|
||||
final long translationUpdate = StatCollector.getLastTranslationUpdateTimeInMilliseconds();
|
||||
if (!fontChanged && translationUpdate == titlesTranslatedAt) return false;
|
||||
titlesTranslatedAt = translationUpdate;
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
final DebugGroup group = registry.groupAt(g);
|
||||
final DebugGroupContents contents = displayer.contents(g);
|
||||
if (!group.hasTitle()) {
|
||||
contents.setTitle("", 0);
|
||||
continue;
|
||||
}
|
||||
final String title = StatCollector.translateToLocal(group.titleKey);
|
||||
contents.setTitle(title, text.width(title, 0, title.length()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean refreshContent(Minecraft mc, Mode mode, RenderGameOverlayEvent.Text event, DebugScreenEntryList entries) {
|
||||
final int tick = DebugScreen.clientTick();
|
||||
final long version = entries.version();
|
||||
if (tick == lastTick && version == lastVersion && mode == lastMode) return false;
|
||||
if (mode != lastMode) {
|
||||
releaseSizeHolds();
|
||||
}
|
||||
lastTick = tick;
|
||||
lastVersion = version;
|
||||
lastMode = mode;
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_ENTRIES);
|
||||
try {
|
||||
displayer.reset();
|
||||
DebugScreen.runEntries(displayer, mc, version, null, mode == Mode.ALWAYS_ON);
|
||||
if (mode == Mode.OVERLAY) filter.sample(event.left, event.right, externalSink);
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void releaseSizeHolds() {
|
||||
for (int g = 0; g < groupCount; g++) displayer.contents(g).releaseSizeHold();
|
||||
}
|
||||
|
||||
private void appendExternal(int group, String source, int start, String name) {
|
||||
final DebugGroupContents contents = displayer.contents(group);
|
||||
final DebugGroupContents.Slot slot = name != null ? contents.addFact(name) : contents.addLine();
|
||||
if (source != null) slot.text.append(source, start, source.length());
|
||||
}
|
||||
|
||||
private int measureAll(int lineBudget, boolean force) {
|
||||
final int tick = DebugScreen.clientTick();
|
||||
int all = 0;
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
final int flags = displayer.contents(g).measure(text, lineBudget, force, tick);
|
||||
if ((flags & DebugGroupContents.TEXT) != 0) groupDirty[g] = true;
|
||||
all |= flags;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
private void fillHeights() {
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
final DebugGroupContents contents = displayer.contents(g);
|
||||
layout.height[g] = contents.isEmpty() ? 0 : contents.fullHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private static int panelLeft(DebugGroupContents contents, int side, int screenWidth) {
|
||||
return side == DebugGroup.LEFT
|
||||
? DebugGroupContents.MARGIN
|
||||
: screenWidth - DebugGroupContents.MARGIN - contents.fullWidth();
|
||||
}
|
||||
|
||||
private void draw() {
|
||||
if (placedCount == 0) return;
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_DRAW);
|
||||
if (scaled) {
|
||||
GLStateManager.glPushMatrix();
|
||||
final float factor = scale / (float) gameScale;
|
||||
GLStateManager.glScalef(factor, factor, 1.0f);
|
||||
}
|
||||
try {
|
||||
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
text.batcher.beginBatch();
|
||||
try {
|
||||
drawChrome();
|
||||
drawGroups();
|
||||
} finally {
|
||||
text.batcher.endBatch();
|
||||
}
|
||||
} finally {
|
||||
if (scaled) GLStateManager.glPopMatrix();
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawChrome() {
|
||||
text.beginRects();
|
||||
try {
|
||||
if (!chromeDirty && text.batcher.replay(chrome, frameSettings)) return;
|
||||
capture(chrome, CHROME);
|
||||
chromeDirty = false;
|
||||
} finally {
|
||||
text.endRects();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGroups() {
|
||||
final BatchingFontRenderer batcher = text.batcher;
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
if (!placed[g]) continue;
|
||||
final TextMesh mesh = meshes[g];
|
||||
if (!groupDirty[g] && batcher.replay(mesh, frameSettings)) continue;
|
||||
capture(mesh, g);
|
||||
groupDirty[g] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void capture(TextMesh mesh, int group) {
|
||||
final BatchingFontRenderer batcher = text.batcher;
|
||||
if (Tracy.ENABLED) Tracy.beginZone(Z_CAPTURE);
|
||||
batcher.beginCapture(mesh);
|
||||
boolean ok = false;
|
||||
try {
|
||||
if (group == CHROME) {
|
||||
emitChrome();
|
||||
} else {
|
||||
displayer.contents(group).drawText(text, placedX[group], placedY[group]);
|
||||
}
|
||||
ok = true;
|
||||
} finally {
|
||||
if (ok) {
|
||||
batcher.endCapture();
|
||||
} else {
|
||||
batcher.abortCapture();
|
||||
}
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
private void emitChrome() {
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
if (!placed[g]) continue;
|
||||
displayer.contents(g).emitRects(text, placedX[g], placedY[g], layout.side[g], screenWidth);
|
||||
}
|
||||
}
|
||||
|
||||
public void describe(Logger log, int shot) {
|
||||
if (DebugScreen.ENTRIES == null) return;
|
||||
ensureSized();
|
||||
final Mode mode = lastDraw != null ? lastDraw : DebugScreen.isGroupedActive() ? Mode.ALWAYS_ON : Mode.FLAT;
|
||||
final int groups = mode != Mode.FLAT ? placedCount
|
||||
: (flat.lineCount(DebugGroup.LEFT) > 0 ? 1 : 0) + (flat.lineCount(DebugGroup.RIGHT) > 0 ? 1 : 0);
|
||||
final StringBuilder out = begin("overlay", shot, null).append(" mode=").append(mode.name()).append(" groups=").append(groups).append(" renderers=");
|
||||
boolean first = true;
|
||||
for (int i = 0, n = registry.rendererCount(); i < n; i++) {
|
||||
final DebugEntryDef def = registry.rendererAt(i);
|
||||
if (!def.isEnabled()) continue;
|
||||
if (!first) out.append(',');
|
||||
out.append(def.id);
|
||||
first = false;
|
||||
}
|
||||
log.info(out.toString());
|
||||
if (mode == Mode.FLAT) {
|
||||
describeFlat(log, shot, DebugGroup.LEFT, "flat_left");
|
||||
describeFlat(log, shot, DebugGroup.RIGHT, "flat_right");
|
||||
return;
|
||||
}
|
||||
for (int g = 0; g < groupCount; g++) {
|
||||
if (!placed[g]) continue;
|
||||
final DebugGroupContents contents = displayer.contents(g);
|
||||
final String id = contents.group.id;
|
||||
log.info(begin("group", shot, id)
|
||||
.append(" side=").append(layout.side[g] == DebugGroup.LEFT ? 'L' : 'R')
|
||||
.append(" x=").append(placedX[g]).append(" y=").append(placedY[g])
|
||||
.append(" w=").append(contents.fullWidth()).append(" h=").append(contents.fullHeight())
|
||||
.append(" facts=").append(contents.factCount()).append(" lines=").append(contents.lineCount()).toString());
|
||||
for (int i = 0, n = contents.factCount(); i < n; i++) {
|
||||
final DebugGroupContents.Slot fact = contents.fact(i);
|
||||
DebugText.appendStripped(begin("line", shot, id).append(" text="), fact.name);
|
||||
dump.append(": ");
|
||||
DebugText.appendStripped(dump, fact.text);
|
||||
log.info(dump.toString());
|
||||
}
|
||||
for (int i = 0, n = contents.lineCount(); i < n; i++) {
|
||||
DebugText.appendStripped(begin("line", shot, id).append(" text="), contents.line(i).text);
|
||||
log.info(dump.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeFlat(Logger log, int shot, int side, String id) {
|
||||
final int n = flat.lineCount(side);
|
||||
if (n == 0) return;
|
||||
log.info(begin("group", shot, id).append(" side=").append(side == DebugGroup.LEFT ? 'L' : 'R').append(" x=0 y=0 w=0 h=0 facts=0 lines=").append(n).toString());
|
||||
for (int i = 0; i < n; i++) {
|
||||
final String line = flat.lineAt(side, i);
|
||||
DebugText.appendStripped(begin("line", shot, id).append(" text="), line == null ? "" : line);
|
||||
log.info(dump.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private StringBuilder begin(String kind, int shot, String id) {
|
||||
dump.setLength(0);
|
||||
dump.append("Flyby debug ").append(kind).append(" shot=").append(shot);
|
||||
return id == null ? dump : dump.append(" id=").append(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
public enum DebugScreenProfile {
|
||||
|
||||
DEFAULT("default", "angelica.debug.options.profile.default"),
|
||||
PERFORMANCE("performance", "angelica.debug.options.profile.performance");
|
||||
|
||||
public static final DebugScreenProfile[] VALUES = values();
|
||||
|
||||
public final String serializedName;
|
||||
public final String translationKey;
|
||||
|
||||
DebugScreenProfile(String serializedName, String translationKey) {
|
||||
this.serializedName = serializedName;
|
||||
this.translationKey = translationKey;
|
||||
}
|
||||
|
||||
public static DebugScreenProfile bySerializedName(String name) {
|
||||
for (final DebugScreenProfile profile : VALUES) {
|
||||
if (profile.serializedName.equals(name)) return profile;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
|
||||
import com.gtnewhorizons.angelica.client.font.FormatCodes;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
|
||||
public abstract class DebugText {
|
||||
|
||||
static final char FORMAT = '\u00a7';
|
||||
|
||||
static final class Format {
|
||||
|
||||
int colorStart;
|
||||
int colorLength;
|
||||
int styles;
|
||||
|
||||
void reset() {
|
||||
colorStart = 0;
|
||||
colorLength = 0;
|
||||
styles = 0;
|
||||
}
|
||||
|
||||
void copyFrom(Format other) {
|
||||
colorStart = other.colorStart;
|
||||
colorLength = other.colorLength;
|
||||
styles = other.styles;
|
||||
}
|
||||
}
|
||||
|
||||
private final Format format = new Format();
|
||||
private final Format atSpace = new Format();
|
||||
|
||||
static int applyFormat(CharSequence text, int index, int end, Format state) {
|
||||
final long fmt = FormatCodes.parse(text, index + 1, end);
|
||||
final int length = 2 + FormatCodes.extraChars(fmt);
|
||||
switch (FormatCodes.kind(fmt)) {
|
||||
case FormatCodes.COLOR, FormatCodes.HEX -> {
|
||||
state.colorStart = index;
|
||||
state.colorLength = length;
|
||||
}
|
||||
case FormatCodes.RESET -> state.colorLength = 0;
|
||||
case FormatCodes.OBFUSCATED, FormatCodes.BOLD, FormatCodes.STRIKETHROUGH, FormatCodes.UNDERLINE, FormatCodes.ITALIC -> state.styles |= 1 << FormatCodes.kind(fmt);
|
||||
default -> {}
|
||||
}
|
||||
if (FormatCodes.endsBold(fmt)) state.styles = 0;
|
||||
return length;
|
||||
}
|
||||
|
||||
protected abstract float advance(char c, boolean bold);
|
||||
|
||||
public abstract float measure(CharSequence text, int start, int end);
|
||||
|
||||
public abstract void draw(CharSequence text, int start, int end, float x, float y, int argb);
|
||||
|
||||
public abstract void rect(int x1, int y1, int x2, int y2, int argb);
|
||||
|
||||
static void appendStripped(StringBuilder out, CharSequence text) {
|
||||
final int last = text.length() - 1;
|
||||
for (int i = 0; i <= last; i++) {
|
||||
final char c = text.charAt(i);
|
||||
if (c == FORMAT && i < last) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
public final int width(CharSequence text, int start, int end) {
|
||||
return (int) Math.ceil(measure(text, start, end));
|
||||
}
|
||||
|
||||
final void wrap(DebugGroupContents.Slot slot, int budget) {
|
||||
final CharSequence src = slot.text;
|
||||
final StringBuilder out = slot.wrapped;
|
||||
final int len = src.length();
|
||||
final Format state = format;
|
||||
final Format spaceState = atSpace;
|
||||
state.reset();
|
||||
out.setLength(0);
|
||||
int rowStart = 0;
|
||||
float pen = 0.0f;
|
||||
boolean rowHasContent = false;
|
||||
int spaceOut = -1;
|
||||
int spaceSrc = -1;
|
||||
float penAtSpace = 0.0f;
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
final char c = src.charAt(i);
|
||||
if (c == FORMAT && i + 1 < len) {
|
||||
final int n = applyFormat(src, i, len, state);
|
||||
out.append(src, i, i + n);
|
||||
i += n;
|
||||
continue;
|
||||
}
|
||||
final float adv = advance(c, (state.styles & 1 << FormatCodes.BOLD) != 0);
|
||||
if (rowHasContent && pen + adv > budget) {
|
||||
if (c == ' ') {
|
||||
slot.addRow(rowStart, out.length(), pen);
|
||||
i++;
|
||||
} else if (spaceOut >= 0) {
|
||||
slot.addRow(rowStart, spaceOut, penAtSpace);
|
||||
out.setLength(spaceOut);
|
||||
state.copyFrom(spaceState);
|
||||
i = spaceSrc + 1;
|
||||
} else {
|
||||
slot.addRow(rowStart, out.length(), pen);
|
||||
}
|
||||
rowStart = out.length();
|
||||
if (state.colorLength > 0) out.append(src, state.colorStart, state.colorStart + state.colorLength);
|
||||
for (int k = FormatCodes.OBFUSCATED; k <= FormatCodes.ITALIC; k++) if ((state.styles & 1 << k) != 0) out.append(FORMAT).append((char) ('k' + k - FormatCodes.OBFUSCATED));
|
||||
pen = 0.0f;
|
||||
rowHasContent = false;
|
||||
spaceOut = -1;
|
||||
continue;
|
||||
}
|
||||
if (c == ' ' && rowHasContent) {
|
||||
spaceOut = out.length();
|
||||
spaceSrc = i;
|
||||
penAtSpace = pen;
|
||||
spaceState.copyFrom(state);
|
||||
}
|
||||
out.append(c);
|
||||
pen += adv;
|
||||
rowHasContent = true;
|
||||
i++;
|
||||
}
|
||||
if (rowHasContent || slot.rowCount == 0) slot.addRow(rowStart, out.length(), pen);
|
||||
}
|
||||
|
||||
public static final class Batched extends DebugText {
|
||||
|
||||
private final FontRenderer font;
|
||||
final BatchingFontRenderer batcher;
|
||||
private boolean depthWasEnabled;
|
||||
|
||||
public Batched(FontRenderer font, BatchingFontRenderer batcher) {
|
||||
this.font = font;
|
||||
this.batcher = batcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float advance(char c, boolean bold) {
|
||||
return batcher.advance(c, bold, font.getUnicodeFlag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float measure(CharSequence text, int start, int end) {
|
||||
return batcher.measure(text, start, end, font.getUnicodeFlag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(CharSequence text, int start, int end, float x, float y, int argb) {
|
||||
batcher.drawString(x, y, argb, false, font.getUnicodeFlag(), text, start, end - start);
|
||||
}
|
||||
|
||||
public void beginRects() {
|
||||
depthWasEnabled = GLStateManager.getDepthTest().isEnabled();
|
||||
GLStateManager.disableDepthTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rect(int x1, int y1, int x2, int y2, int argb) {
|
||||
batcher.fillRect(x1, y1, x2, y2, argb);
|
||||
}
|
||||
|
||||
public void endRects() {
|
||||
if (depthWasEnabled) GLStateManager.enableDepthTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class FlatDebugDisplayer implements DebugScreenDisplayer {
|
||||
|
||||
private static final String SPACER = null;
|
||||
private static final String SEPARATOR = ": ";
|
||||
|
||||
private static final class Side {
|
||||
|
||||
StringBuilder[] builders = new StringBuilder[8];
|
||||
String[] strings = new String[8];
|
||||
int count;
|
||||
DebugGroupHandle lastGroup;
|
||||
|
||||
Side() {
|
||||
for (int i = 0; i < builders.length; i++) builders[i] = new StringBuilder();
|
||||
}
|
||||
|
||||
StringBuilder next() {
|
||||
if (count == builders.length) {
|
||||
final int n = count * 2;
|
||||
builders = Arrays.copyOf(builders, n);
|
||||
for (int i = count; i < n; i++) builders[i] = new StringBuilder();
|
||||
strings = Arrays.copyOf(strings, n);
|
||||
}
|
||||
final StringBuilder builder = builders[count++];
|
||||
builder.setLength(0);
|
||||
return builder;
|
||||
}
|
||||
|
||||
void finish() {
|
||||
for (int i = 0; i < count; i++) {
|
||||
final StringBuilder builder = builders[i];
|
||||
final String current = strings[i];
|
||||
if (builder.length() == 0) {
|
||||
strings[i] = SPACER;
|
||||
} else if (current == null || !current.contentEquals(builder)) {
|
||||
strings[i] = builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addTo(ArrayList<String> target) {
|
||||
if (count == 0) return;
|
||||
target.ensureCapacity(target.size() + count + 1);
|
||||
if (!target.isEmpty()) target.add(SPACER);
|
||||
for (int i = 0; i < count; i++) target.add(strings[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private final DebugRegistry registry;
|
||||
private final DebugFactImpl fact = new DebugFactImpl();
|
||||
private final Side[] sides = { new Side(), new Side() };
|
||||
private int lastTick = Integer.MIN_VALUE;
|
||||
private long lastVersion = -1L;
|
||||
|
||||
public FlatDebugDisplayer(DebugRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
void beginLines() {
|
||||
for (int s = 0; s < 2; s++) {
|
||||
sides[s].count = 0;
|
||||
sides[s].lastGroup = null;
|
||||
}
|
||||
}
|
||||
|
||||
void endLines() {
|
||||
sides[0].finish();
|
||||
sides[1].finish();
|
||||
}
|
||||
|
||||
public void rebuild(Minecraft mc, int tick, long version) {
|
||||
if (tick == lastTick && version == lastVersion) return;
|
||||
lastTick = tick;
|
||||
lastVersion = version;
|
||||
beginLines();
|
||||
try {
|
||||
DebugScreen.runEntries(this, mc, version, registry, false);
|
||||
} finally {
|
||||
endLines();
|
||||
}
|
||||
}
|
||||
|
||||
public void addTo(ArrayList<String> left, ArrayList<String> right) {
|
||||
sides[DebugGroupHandle.LEFT].addTo(left);
|
||||
sides[DebugGroupHandle.RIGHT].addTo(right);
|
||||
}
|
||||
|
||||
public int lineCount(int side) {
|
||||
return sides[side].count;
|
||||
}
|
||||
|
||||
public String lineAt(int side, int i) {
|
||||
return sides[side].strings[i];
|
||||
}
|
||||
|
||||
private DebugFact open(DebugGroupHandle handle) {
|
||||
final DebugGroup group = DebugGroup.of(handle);
|
||||
final Side side = sides[group.side()];
|
||||
if (side.lastGroup != group) {
|
||||
if (side.count > 0) side.next();
|
||||
side.lastGroup = group;
|
||||
}
|
||||
return fact.into(side.next());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact line(DebugGroupHandle group) {
|
||||
return open(group);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact fact(DebugGroupHandle group, String name) {
|
||||
DebugGroup.requireName(name);
|
||||
return open(group).text(name).text(SEPARATOR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class ForgeDebugPlaceholders {
|
||||
|
||||
static final String VERSION = new String("Minecraft 1.7.10 (0 fps)");
|
||||
static final String RENDERS = new String("C: 0/0. F: 0, O: 0, E: 0");
|
||||
static final String ENTITIES = new String("E: 0/0. B: 0, I: 0");
|
||||
static final String PARTICLES = new String("P: 0. T: 0");
|
||||
static final String WORLD = new String("Overworld");
|
||||
static final String USED_MEMORY = new String("Used memory: 0% (0MB) of 0MB");
|
||||
static final String ALLOCATED_MEMORY = new String("Allocated memory: 0% (0MB)");
|
||||
static final String X = new String("x: 0.00000 (0) // c: 0 (0)");
|
||||
static final String Y = new String("y: 0.000 (feet pos, 0.000 eyes pos)");
|
||||
static final String Z = new String("z: 0.00000 (0) // c: 0 (0)");
|
||||
static final String FACING = new String("f: 0 (South) / 0.000000");
|
||||
static final String CHUNK = new String("lc: 0 b: Plains bl: 0 sl: 0 rl: 0");
|
||||
static final String SPEED = new String("ws: 0.000, fs: 0.000, g: false, fl: 0");
|
||||
static final String SHADER = new String("shader: none");
|
||||
|
||||
private static final int LEFT_LINES = 13;
|
||||
private static final int RIGHT_LINES = 3;
|
||||
|
||||
private static final int[] nullsAdded = new int[2];
|
||||
private static List<String> lastBrandings;
|
||||
private static List<String> forgeBrandings;
|
||||
|
||||
private ForgeDebugPlaceholders() {}
|
||||
|
||||
public static boolean isPlaceholder(String line) {
|
||||
if (line == VERSION || line == RENDERS || line == ENTITIES || line == PARTICLES || line == WORLD
|
||||
|| line == USED_MEMORY || line == ALLOCATED_MEMORY || line == X || line == Y || line == Z
|
||||
|| line == FACING || line == CHUNK || line == SPEED || line == SHADER) {
|
||||
return true;
|
||||
}
|
||||
final List<String> brandings = lastBrandings;
|
||||
if (line == null || brandings == null) return false;
|
||||
for (int i = 0, n = brandings.size(); i < n; i++) {
|
||||
if (brandings.get(i) == line) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int nullsAdded(int side) {
|
||||
return nullsAdded[side];
|
||||
}
|
||||
|
||||
public static List<String> brandings() {
|
||||
List<String> brandings = forgeBrandings;
|
||||
if (brandings == null) {
|
||||
brandings = FMLCommonHandler.instance().getBrandings(false);
|
||||
forgeBrandings = brandings;
|
||||
}
|
||||
return brandings;
|
||||
}
|
||||
|
||||
public static void add(ArrayList<String> left, ArrayList<String> right, boolean chunkLoaded, boolean shaderActive) {
|
||||
add(left, right, chunkLoaded, shaderActive, brandings());
|
||||
}
|
||||
|
||||
static void add(ArrayList<String> left, ArrayList<String> right, boolean chunkLoaded, boolean shaderActive, List<String> brandings) {
|
||||
lastBrandings = brandings;
|
||||
nullsAdded[0] = chunkLoaded ? 1 : 2;
|
||||
nullsAdded[1] = 1;
|
||||
final int brandingCount = brandings.size();
|
||||
left.ensureCapacity(left.size() + LEFT_LINES);
|
||||
right.ensureCapacity(right.size() + RIGHT_LINES + brandingCount);
|
||||
left.add(VERSION);
|
||||
left.add(RENDERS);
|
||||
left.add(ENTITIES);
|
||||
left.add(PARTICLES);
|
||||
left.add(WORLD);
|
||||
left.add(null);
|
||||
right.add(USED_MEMORY);
|
||||
right.add(ALLOCATED_MEMORY);
|
||||
left.add(X);
|
||||
left.add(Y);
|
||||
left.add(Z);
|
||||
left.add(FACING);
|
||||
left.add(chunkLoaded ? CHUNK : null);
|
||||
left.add(SPEED);
|
||||
if (shaderActive) left.add(SHADER);
|
||||
right.add(null);
|
||||
for (int i = 0; i < brandingCount; i++) right.add(brandings.get(i));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.config.CompatConfig;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class ForgeLineFilter {
|
||||
|
||||
public interface Sink {
|
||||
|
||||
void add(int group, String source, int start, String name);
|
||||
}
|
||||
|
||||
private final DebugRegistry registry;
|
||||
private final DebugEntryDef[] gates;
|
||||
private final DebugGroup[] others;
|
||||
private DebugLineRules rules;
|
||||
private String[] rulesConfig;
|
||||
|
||||
ForgeLineFilter(DebugRegistry registry) {
|
||||
this.registry = registry;
|
||||
gates = new DebugEntryDef[] { registry.entryById(DebugGroup.OTHER_LEFT.id), registry.entryById(DebugGroup.OTHER_RIGHT.id) };
|
||||
others = new DebugGroup[] { registry.groupById(DebugGroup.OTHER_LEFT.id), registry.groupById(DebugGroup.OTHER_RIGHT.id) };
|
||||
}
|
||||
|
||||
public void sample(ArrayList<String> left, ArrayList<String> right, Sink sink) {
|
||||
final String[] config = CompatConfig.debugLineRules;
|
||||
if (rules == null || config != rulesConfig) {
|
||||
rules = DebugLineRules.compile(registry, config);
|
||||
rulesConfig = config;
|
||||
}
|
||||
sampleSide(DebugGroupHandle.LEFT, left, sink);
|
||||
sampleSide(DebugGroupHandle.RIGHT, right, sink);
|
||||
}
|
||||
|
||||
private void sampleSide(int side, ArrayList<String> list, Sink sink) {
|
||||
final DebugEntryDef gate = gates[side];
|
||||
final DebugGroup other = others[side];
|
||||
if (gate == null || other == null) return;
|
||||
final boolean otherEnabled = gate.isEnabled();
|
||||
final int size = list.size();
|
||||
int nullBudget = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (ForgeDebugPlaceholders.isPlaceholder(list.get(i))) {
|
||||
nullBudget = ForgeDebugPlaceholders.nullsAdded(side);
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean otherEmitted = false;
|
||||
boolean separatorPending = false;
|
||||
for (int i = 0; i < size; i++) {
|
||||
final String s = list.get(i);
|
||||
if (s == null && nullBudget > 0) {
|
||||
nullBudget--;
|
||||
continue;
|
||||
}
|
||||
if (s == null || DebugLineRules.isBlank(s)) {
|
||||
separatorPending |= otherEmitted;
|
||||
continue;
|
||||
}
|
||||
if (ForgeDebugPlaceholders.isPlaceholder(s)) continue;
|
||||
final DebugLineRules.Rule rule = rules.match(side, s);
|
||||
final DebugGroup group = rule == null ? other : rule.group();
|
||||
if (group == null) continue;
|
||||
if (group == other) {
|
||||
if (!otherEnabled) continue;
|
||||
if (separatorPending) sink.add(other.index(), null, 0, null);
|
||||
separatorPending = false;
|
||||
otherEmitted = true;
|
||||
}
|
||||
if (rule == null) {
|
||||
sink.add(group.index(), s, 0, null);
|
||||
} else {
|
||||
sink.add(group.index(), s, rule.payloadStart(s), rule.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugGroupHandle;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
|
||||
public final class GroupedDebugDisplayer implements DebugScreenDisplayer {
|
||||
|
||||
private final DebugGroupContents[] groups;
|
||||
private final DebugFactImpl fact = new DebugFactImpl();
|
||||
|
||||
public GroupedDebugDisplayer(DebugRegistry registry) {
|
||||
groups = new DebugGroupContents[registry.groupCount()];
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
groups[i] = new DebugGroupContents(registry.groupAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
public DebugGroupContents contents(int index) {
|
||||
return groups[index];
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
groups[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact line(DebugGroupHandle group) {
|
||||
return fact.into(groups[DebugGroup.of(group).index].addLine().text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DebugFact fact(DebugGroupHandle group, String name) {
|
||||
return fact.into(groups[DebugGroup.of(group).index].addFact(DebugGroup.requireName(name)).text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.FogState;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import org.joml.Vector3d;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
public final class AngelicaEntries {
|
||||
|
||||
private AngelicaEntries() {}
|
||||
|
||||
public static void animations(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!AngelicaConfig.speedupAnimations) return;
|
||||
displayer.fact(DebugGroup.ANGELICA, "Animations").value(ClientProxy.animationsMode.toString());
|
||||
}
|
||||
|
||||
public static void dynamicLights(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!DynamicLights.configEnabled || !DynamicLights.isEnabled()) return;
|
||||
final DynamicLights lights = DynamicLights.get();
|
||||
final DebugFact fact = displayer.fact(DebugGroup.ANGELICA, "Dynamic Lights");
|
||||
fact.value(lights.getLightSourcesCount()).text(" sources, ").value(lights.getLastUpdateCount()).text(" updated");
|
||||
if (DynamicLights.FrustumCullingEnabled) {
|
||||
fact.text(", ").value(lights.getChunkRebuildManager().getPendingCount()).text(" pending");
|
||||
}
|
||||
}
|
||||
|
||||
public static void fog(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
writeFog(displayer.fact(DebugGroup.ANGELICA, "Fog"));
|
||||
}
|
||||
|
||||
public static void writeFog(DebugFact fact) {
|
||||
final FogState fog = GLStateManager.getFogState();
|
||||
final Vector3d color = fog.getFogColor();
|
||||
fact
|
||||
.value(GLStateManager.getFogMode().isEnabled() ? "ON" : "OFF")
|
||||
.text(' ')
|
||||
.value(fogModeName(fog.getFogMode()))
|
||||
.text(' ')
|
||||
.value(fog.getStart(), 0)
|
||||
.text('-')
|
||||
.value(fog.getEnd(), 0)
|
||||
.text(" d=")
|
||||
.value(fog.getDensity(), 2)
|
||||
.text(" (")
|
||||
.value(color.x, 2)
|
||||
.text(", ")
|
||||
.value(color.y, 2)
|
||||
.text(", ")
|
||||
.value(color.z, 2)
|
||||
.text(')');
|
||||
}
|
||||
|
||||
private static String fogModeName(int mode) {
|
||||
return switch (mode) {
|
||||
case GL11.GL_LINEAR -> "LINEAR";
|
||||
case GL11.GL_EXP -> "EXP";
|
||||
case GL11.GL_EXP2 -> "EXP2";
|
||||
default -> "?";
|
||||
};
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.Tags;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFactLines;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugScreen;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.profiling.BailClassCounts;
|
||||
import com.gtnewhorizons.angelica.profiling.RenderClassTimings;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
|
||||
|
||||
public final class AngelicaRendererEntry implements DebugScreenEntry {
|
||||
|
||||
private static final String BUFFERS_PREFIX = "G: ";
|
||||
private static final String BUFFERS_SPLIT = ", I: ";
|
||||
|
||||
private final DebugFactLines facts = new DebugFactLines();
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final CeleritasWorldRenderer renderer = CeleritasWorldRenderer.getInstanceOrNull();
|
||||
if (renderer == null) return;
|
||||
if (context.isSampleDue()) rebuild(renderer);
|
||||
facts.emit(displayer, DebugGroup.ANGELICA);
|
||||
}
|
||||
|
||||
private void rebuild(CeleritasWorldRenderer renderer) {
|
||||
facts.begin();
|
||||
facts.fact("Version", Tags.VERSION);
|
||||
facts.fact("Renderer", "Celeritas");
|
||||
facts.fact("Backend", RENDER_BACKEND.getName());
|
||||
for (final String line : renderer.getDebugStrings()) {
|
||||
addRendererLine(facts, line);
|
||||
}
|
||||
if (DebugScreen.isRendererVerbose()) rebuildVerbose();
|
||||
facts.end();
|
||||
}
|
||||
|
||||
private void rebuildVerbose() {
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm != null) {
|
||||
addIfPresent(sm.getDebugInfo());
|
||||
}
|
||||
addIfPresent(TessellatorStreamingDrawer.getDebugInfo());
|
||||
addIfPresent(TesrBatchRenderer.INSTANCE.getDebugSummaryLine());
|
||||
addIfPresent(RENDER_BACKEND.getTransferDebugInfo());
|
||||
for (final String line : TesrBatchRenderer.INSTANCE.getDebugDetailStrings()) {
|
||||
addRendererLine(facts, line);
|
||||
}
|
||||
addIfPresent(ModelPartBatcher.INSTANCE.getDebugString());
|
||||
if (Tracy.ENABLED) {
|
||||
addIfPresent(RenderClassTimings.TESR.label(), RenderClassTimings.TESR.debugValue());
|
||||
addIfPresent(RenderClassTimings.ENTITY.label(), RenderClassTimings.ENTITY.debugValue());
|
||||
addIfPresent(RenderClassTimings.SHADOW_ENTITY.label(), RenderClassTimings.SHADOW_ENTITY.debugValue());
|
||||
addIfPresent(BailClassCounts.MATERIAL.label(), BailClassCounts.MATERIAL.debugValue());
|
||||
addIfPresent(BailClassCounts.TEMPLATE.label(), BailClassCounts.TEMPLATE.debugValue());
|
||||
addIfPresent(BailClassCounts.PARTICLE_SPILL.label(), BailClassCounts.PARTICLE_SPILL.debugValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void addIfPresent(String line) {
|
||||
if (line != null && !line.isEmpty()) addRendererLine(facts, line);
|
||||
}
|
||||
|
||||
private void addIfPresent(String label, String value) {
|
||||
if (value != null && !value.isEmpty()) facts.fact(label, value);
|
||||
}
|
||||
|
||||
static void addRendererLine(DebugFactLines out, String line) {
|
||||
if (line.startsWith(BUFFERS_PREFIX)) {
|
||||
final int at = line.indexOf(BUFFERS_SPLIT);
|
||||
if (at > 0) {
|
||||
out.fact("Geometry", line.substring(BUFFERS_PREFIX.length(), at) + " MiB");
|
||||
out.fact("Index", line.substring(at + BUFFERS_SPLIT.length()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
out.split(line);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFactLines;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import net.coderbot.iris.Iris;
|
||||
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
|
||||
import net.coderbot.iris.shaderpack.ShaderPack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class AngelicaShadersEntry implements DebugScreenEntry {
|
||||
|
||||
private static final String TAG = "[" + Iris.MODNAME;
|
||||
private static final String SHADERPACK = "Shaderpack";
|
||||
|
||||
private final DebugFactLines facts = new DebugFactLines();
|
||||
private final List<String> scratch = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) rebuild();
|
||||
facts.emit(displayer, DebugGroup.SHADERS);
|
||||
}
|
||||
|
||||
private void rebuild() {
|
||||
facts.begin();
|
||||
if (Iris.getIrisConfig().areShadersEnabled()) {
|
||||
final String name = Iris.getCurrentPackName();
|
||||
if (name != null) facts.fact(SHADERPACK, Iris.isFallback() ? name + " (fallback)" : name);
|
||||
final Optional<ShaderPack> pack = Iris.getCurrentPack();
|
||||
if (pack.isPresent()) facts.split(pack.get().getProfileInfo());
|
||||
} else {
|
||||
facts.fact(SHADERPACK, "(off)");
|
||||
}
|
||||
final Optional<WorldRenderingPipeline> pipeline = Iris.getPipelineManager().getPipeline();
|
||||
if (pipeline.isPresent()) {
|
||||
scratch.clear();
|
||||
pipeline.get().addDebugText(scratch);
|
||||
for (int i = 0, n = scratch.size(); i < n; i++) {
|
||||
final String line = stripTag(scratch.get(i));
|
||||
if (!line.isEmpty()) facts.split(line);
|
||||
}
|
||||
}
|
||||
facts.end();
|
||||
}
|
||||
|
||||
static String stripTag(String line) {
|
||||
if (line == null) return "";
|
||||
String rest = line;
|
||||
if (line.startsWith(TAG)) {
|
||||
final int close = line.indexOf(']');
|
||||
if (close > 0) rest = line.substring(close + 1);
|
||||
}
|
||||
int start = 0;
|
||||
while (start < rest.length() && rest.charAt(start) == ' ') start++;
|
||||
return start == 0 ? rest : rest.substring(start);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.debug.screen.ForgeDebugPlaceholders;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ForgeBrandingsEntry implements DebugScreenEntry {
|
||||
|
||||
private static final String[] PREFIXES = { "MCP v", "FML v", "Minecraft Forge " };
|
||||
private static final String[] NAMES = { "MCP", "FML", "Forge" };
|
||||
|
||||
private String[] names = new String[0];
|
||||
private String[] values = new String[0];
|
||||
private int modsLoaded;
|
||||
private int modsActive;
|
||||
private boolean resolved;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!resolved) resolve();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (names[i] != null) displayer.fact(DebugGroup.FORGE, names[i]).value(values[i]);
|
||||
}
|
||||
displayer.fact(DebugGroup.FORGE, "Mods").value(modsLoaded).text(" loaded, ").value(modsActive).text(" active");
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
if (names[i] == null) displayer.line(DebugGroup.FORGE).text(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolve() {
|
||||
resolved = true;
|
||||
final List<String> brandings = ForgeDebugPlaceholders.brandings();
|
||||
final int count = Math.max(0, brandings.size() - 1);
|
||||
names = new String[count];
|
||||
values = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
final String line = brandings.get(i);
|
||||
final int kind = prefixIndex(line);
|
||||
if (kind >= 0) {
|
||||
names[i] = NAMES[kind];
|
||||
values[i] = line.substring(PREFIXES[kind].length());
|
||||
} else {
|
||||
values[i] = line;
|
||||
}
|
||||
}
|
||||
modsLoaded = Loader.instance().getModList().size();
|
||||
modsActive = Loader.instance().getActiveModList().size();
|
||||
}
|
||||
|
||||
static int prefixIndex(String line) {
|
||||
for (int i = 0; i < PREFIXES.length; i++) {
|
||||
if (line.startsWith(PREFIXES[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
|
||||
public final class HeaderEntries {
|
||||
|
||||
private static final String GAME_VERSION = "Minecraft " + MinecraftForge.MC_VERSION;
|
||||
|
||||
private HeaderEntries() {}
|
||||
|
||||
public static void gameVersion(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
displayer.line(DebugGroup.PRIORITY_LEFT).text(GAME_VERSION);
|
||||
}
|
||||
|
||||
public static void fps(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
displayer.line(DebugGroup.PRIORITY_LEFT).text(Minecraft.getMinecraft().debug);
|
||||
}
|
||||
|
||||
public static final class Help implements DebugScreenEntry {
|
||||
|
||||
private static final String[] KEYS = {
|
||||
"angelica.debug.help.options",
|
||||
"angelica.debug.help.hitboxes",
|
||||
"angelica.debug.help.chunk_borders",
|
||||
"angelica.debug.help.verbose",
|
||||
"angelica.debug.help.graphs"
|
||||
};
|
||||
|
||||
private String[] translated;
|
||||
private long translatedAt;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
String[] lines = translated;
|
||||
final long translationUpdate = StatCollector.getLastTranslationUpdateTimeInMilliseconds();
|
||||
if (lines == null || translationUpdate != translatedAt) {
|
||||
translatedAt = translationUpdate;
|
||||
lines = new String[KEYS.length];
|
||||
for (int i = 0; i < KEYS.length; i++) {
|
||||
lines[i] = StatCollector.translateToLocal(KEYS[i]);
|
||||
}
|
||||
translated = lines;
|
||||
}
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
displayer.line(DebugGroup.HELP).text(lines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.util.MovingObjectPosition;
|
||||
|
||||
public final class LookingAtBlockEntry implements DebugScreenEntry {
|
||||
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final MovingObjectPosition hit = mc.objectMouseOver;
|
||||
if (hit == null || hit.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK) return;
|
||||
final int x = hit.blockX;
|
||||
final int y = hit.blockY;
|
||||
final int z = hit.blockZ;
|
||||
final Block block = context.world().getBlock(x, y, z);
|
||||
displayer.fact(DebugGroup.LOOKING_AT_BLOCK, "Coordinates").value(x).text(", ").value(y).text(", ").value(z);
|
||||
displayer.fact(DebugGroup.LOOKING_AT_BLOCK, "Type").value(Block.blockRegistry.getNameForObject(block));
|
||||
displayer.fact(DebugGroup.LOOKING_AT_BLOCK, "Meta").value(context.world().getBlockMetadata(x, y, z));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
|
||||
import java.lang.management.BufferPoolMXBean;
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.List;
|
||||
|
||||
public final class MemoryEntry implements DebugScreenEntry {
|
||||
|
||||
private static final String MIB = "MiB";
|
||||
|
||||
static final class AllocationRate {
|
||||
|
||||
private static final long MIN_INTERVAL_MS = 500L;
|
||||
private static final long MAX_INTERVAL_MS = 5000L;
|
||||
|
||||
private long lastTime;
|
||||
private long lastHeapUsage = -1L;
|
||||
private long lastGcCounts = -1L;
|
||||
private long lastRate;
|
||||
|
||||
long rate() {
|
||||
return lastRate;
|
||||
}
|
||||
|
||||
void sample(long timeMs, long heapUsage, long gcCounts) {
|
||||
final long dt = timeMs - lastTime;
|
||||
if (lastTime != 0L && dt < MIN_INTERVAL_MS) return;
|
||||
if (lastTime != 0L && dt <= MAX_INTERVAL_MS && gcCounts == lastGcCounts) {
|
||||
lastRate = Math.round((heapUsage - lastHeapUsage) * (1000.0 / dt));
|
||||
}
|
||||
lastTime = timeMs;
|
||||
lastHeapUsage = heapUsage;
|
||||
lastGcCounts = gcCounts;
|
||||
}
|
||||
}
|
||||
|
||||
private final AllocationRate allocationRate = new AllocationRate();
|
||||
private List<GarbageCollectorMXBean> collectors;
|
||||
private BufferPoolMXBean directPool;
|
||||
private boolean poolResolved;
|
||||
private long directBytes = -1L;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final Runtime runtime = Runtime.getRuntime();
|
||||
final long max = runtime.maxMemory();
|
||||
final long total = runtime.totalMemory();
|
||||
final long used = total - runtime.freeMemory();
|
||||
if (context.isSampleDue()) {
|
||||
allocationRate.sample(context.nanoTime() / 1_000_000L, used, gcCounts());
|
||||
if (!poolResolved) {
|
||||
poolResolved = true;
|
||||
directPool = findDirectPool();
|
||||
}
|
||||
if (directPool != null) directBytes = directPool.getMemoryUsed();
|
||||
}
|
||||
displayer.fact(DebugGroup.MEMORY, "Used").value(used * 100L / max).text("% ").padded(toMib(used), 3).text('/').padded(toMib(max), 3).text(MIB);
|
||||
displayer.fact(DebugGroup.MEMORY, "Allocated").value(total * 100L / max).text("% ").padded(toMib(total), 3).text(MIB);
|
||||
displayer.fact(DebugGroup.MEMORY, "Alloc rate").padded(toMib(allocationRate.rate()), 3).text(MIB).text("/s");
|
||||
if (directBytes >= 0L) {
|
||||
displayer.fact(DebugGroup.MEMORY, "Direct").padded(toMib(directBytes), 3).text(MIB);
|
||||
}
|
||||
}
|
||||
|
||||
private long gcCounts() {
|
||||
if (collectors == null) collectors = ManagementFactory.getGarbageCollectorMXBeans();
|
||||
long sum = 0L;
|
||||
for (int i = 0, n = collectors.size(); i < n; i++) {
|
||||
sum += collectors.get(i).getCollectionCount();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private static BufferPoolMXBean findDirectPool() {
|
||||
final List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);
|
||||
for (int i = 0, n = pools.size(); i < n; i++) {
|
||||
if (pools.get(i).getName().equals("direct")) return pools.get(i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long toMib(long bytes) {
|
||||
return bytes / 1024L / 1024L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugFact;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.compat.mojang.CompatMathHelper;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.helpers.LoadControllerHelper;
|
||||
import cpw.mods.fml.common.ModContainer;
|
||||
import net.minecraft.client.entity.EntityClientPlayerMP;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.world.EnumSkyBlock;
|
||||
import net.minecraft.world.WorldProvider;
|
||||
import net.minecraft.world.biome.BiomeGenBase;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class PositionEntries {
|
||||
|
||||
private PositionEntries() {}
|
||||
|
||||
public static final class PlayerPosition implements DebugScreenEntry {
|
||||
|
||||
private static final String SEPARATOR = " / ";
|
||||
private static final String[] DIRECTION_NAMES = new String[4];
|
||||
private static final String[] TOWARDS = { "Towards positive Z", "Towards negative X", "Towards negative Z", "Towards positive X" };
|
||||
|
||||
static {
|
||||
for (int i = 0; i < DIRECTION_NAMES.length; i++) {
|
||||
DIRECTION_NAMES[i] = Direction.directions[i].toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
private WorldProvider cachedProvider;
|
||||
private String dimensionName = "";
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final EntityClientPlayerMP player = context.player();
|
||||
final double feetY = player.boundingBox.minY;
|
||||
final int bx = context.blockX();
|
||||
final int by = context.blockY();
|
||||
final int bz = context.blockZ();
|
||||
displayer.fact(DebugGroup.POSITION, "XYZ").value(player.posX, 3).text(SEPARATOR).value(feetY, 5).text(SEPARATOR).value(player.posZ, 3);
|
||||
displayer.fact(DebugGroup.POSITION, "Block").value(bx).text(' ').value(by).text(' ').value(bz);
|
||||
displayer.fact(DebugGroup.POSITION, "Chunk").value(bx >> 4).text(' ').value(by >> 4).text(' ').value(bz >> 4);
|
||||
final int heading = MathHelper.floor_double((double) (player.rotationYaw * 4.0F / 360.0F) + 0.5D) & 3;
|
||||
final DebugFact facing = displayer.fact(DebugGroup.POSITION, "Facing");
|
||||
facing.value(DIRECTION_NAMES[heading]).text(" (").text(TOWARDS[heading]).text(") (");
|
||||
facing.value(MathHelper.wrapAngleTo180_float(player.rotationYaw), 1).text(SEPARATOR).value(MathHelper.wrapAngleTo180_float(player.rotationPitch), 1).text(')');
|
||||
final WorldProvider provider = context.world().provider;
|
||||
if (provider != cachedProvider) {
|
||||
cachedProvider = provider;
|
||||
dimensionName = provider.getDimensionName();
|
||||
}
|
||||
displayer.fact(DebugGroup.POSITION, "Dimension").value(dimensionName).text(" (").value(provider.dimensionId).text(')');
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Biome implements DebugScreenEntry {
|
||||
|
||||
private BiomeGenBase biome;
|
||||
private String label;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final Chunk chunk = context.chunk();
|
||||
if (chunk == null) return;
|
||||
final BiomeGenBase current = context.world().getBiomeGenForCoords(context.blockX(), context.blockZ());
|
||||
if (current != biome) {
|
||||
final ModContainer mod = LoadControllerHelper.getOwningMod(current.getClass());
|
||||
label = mod == null ? current.biomeName : current.biomeName + " [" + mod.getName() + "]";
|
||||
biome = current;
|
||||
}
|
||||
displayer.fact(DebugGroup.POSITION, "Biome").value(label);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sectionPosition(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
displayer.fact(DebugGroup.POSITION, "Section-Relative").padded(context.blockX() & 15, 2).text(' ').padded(context.blockY() & 15, 2).text(' ').padded(context.blockZ() & 15, 2);
|
||||
}
|
||||
|
||||
public static void speed(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final EntityClientPlayerMP player = context.player();
|
||||
final double dx = player.posX - player.prevPosX;
|
||||
final double dy = player.posY - player.prevPosY;
|
||||
final double dz = player.posZ - player.prevPosZ;
|
||||
displayer.fact(DebugGroup.POSITION, "Speed").value(Math.sqrt(dx * dx + dy * dy + dz * dz), 3).text(" blocks/tick");
|
||||
displayer.fact(DebugGroup.POSITION, "Walk/Fly").value(player.capabilities.getWalkSpeed(), 3).text(" / ").value(player.capabilities.getFlySpeed(), 3);
|
||||
displayer.fact(DebugGroup.POSITION, "On Ground").value(player.onGround ? "true" : "false");
|
||||
}
|
||||
|
||||
public static void lightLevels(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final Chunk chunk = context.chunk();
|
||||
if (chunk == null) return;
|
||||
final int x = context.blockX() & 15;
|
||||
final int z = context.blockZ() & 15;
|
||||
final int cy = CompatMathHelper.clampHeight(context.blockY());
|
||||
displayer.fact(DebugGroup.POSITION, "Light")
|
||||
.value(chunk.getBlockLightValue(x, cy, z, 0))
|
||||
.text(" (")
|
||||
.value(chunk.getSavedLightValue(EnumSkyBlock.Sky, x, cy, z))
|
||||
.text(" sky, ")
|
||||
.value(chunk.getSavedLightValue(EnumSkyBlock.Block, x, cy, z))
|
||||
.text(" block)");
|
||||
}
|
||||
|
||||
public static void heightmap(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final Chunk chunk = context.chunk();
|
||||
if (chunk == null) return;
|
||||
displayer.fact(DebugGroup.POSITION, "Top Section").value(chunk.getTopFilledSegment() + 15);
|
||||
displayer.fact(DebugGroup.POSITION, "Height").value(context.world().getHeightValue(context.blockX(), context.blockZ()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.AngelicaRenderSectionManager;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.EntityRenderer;
|
||||
import net.minecraft.client.renderer.RenderGlobal;
|
||||
|
||||
import static com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries.KIND_CHUNK_RENDER_STATS;
|
||||
import static com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries.KIND_ENTITY_RENDER_STATS;
|
||||
import static com.gtnewhorizons.angelica.debug.screen.DebugScreenEntries.KIND_PARTICLE_RENDER_STATS;
|
||||
|
||||
public final class RenderingEntries {
|
||||
|
||||
private static final String SPRITES_PREFIX = "A: ";
|
||||
private static final String COLON = ": ";
|
||||
|
||||
private RenderingEntries() {}
|
||||
|
||||
public static void tps(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (Minecraft.getMinecraft().getIntegratedServer() == null) return;
|
||||
displayer.fact(DebugGroup.RENDERING, "Server").value("Integrated").text(" @ ").value(ClientProxy.integratedTickTime(), 0).text(" ms ticks");
|
||||
}
|
||||
|
||||
public static final class RenderStats implements DebugScreenEntry {
|
||||
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
private final int kind;
|
||||
private String sampled;
|
||||
private int visibleChunks;
|
||||
private int totalSections;
|
||||
|
||||
public RenderStats(int kind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (mc.renderGlobal == null) return;
|
||||
switch (kind) {
|
||||
case KIND_CHUNK_RENDER_STATS -> displayChunks(displayer, context);
|
||||
case KIND_ENTITY_RENDER_STATS -> displayEntities(displayer, context);
|
||||
case KIND_PARTICLE_RENDER_STATS -> {
|
||||
if (context.isSampleDue()) sampled = mc.effectRenderer.getStatistics();
|
||||
displayer.fact(DebugGroup.RENDERING, "Particles").value(sampled);
|
||||
}
|
||||
default -> {
|
||||
if (context.isSampleDue()) sampled = chunkCacheValue(mc.getWorldProviderName());
|
||||
displayer.fact(DebugGroup.RENDERING, "Chunk Cache").value(sampled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void displayChunks(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final CeleritasWorldRenderer renderer = CeleritasWorldRenderer.getInstanceOrNull();
|
||||
if (renderer == null) return;
|
||||
if (context.isSampleDue()) {
|
||||
final AngelicaRenderSectionManager sections = renderer.getRenderSectionManager();
|
||||
visibleChunks = renderer.getVisibleChunkCount();
|
||||
totalSections = sections.getTotalSections();
|
||||
sampled = spritesValue(sections.getTickerDebugString());
|
||||
}
|
||||
displayer.fact(DebugGroup.RENDERING, "Chunks").value(visibleChunks).text('/').value(totalSections);
|
||||
displayer.fact(DebugGroup.RENDERING, "Render Distance").value(renderer.getEffectiveRenderDistance());
|
||||
if (sampled != null) displayer.fact(DebugGroup.RENDERING, "Animated Sprites").value(sampled);
|
||||
}
|
||||
|
||||
private void displayEntities(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
final RenderGlobal renderGlobal = mc.renderGlobal;
|
||||
final int total = context.world().loadedEntityList.size();
|
||||
final int rendered = renderGlobal.countEntitiesRendered;
|
||||
final int hidden = renderGlobal.countEntitiesHidden;
|
||||
displayer.fact(DebugGroup.RENDERING, "Entities").value(rendered).text('/').value(total).text(", ").value(hidden).text(" hidden, ").value(Math.max(0, total - hidden - rendered)).text(" culled");
|
||||
displayer.fact(DebugGroup.RENDERING, "Loaded Entities").value(total);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PostEffect implements DebugScreenEntry {
|
||||
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (context.isSampleDue()) {
|
||||
final EntityRenderer renderer = mc.entityRenderer;
|
||||
name = renderer != null && renderer.isShaderActive() ? renderer.getShaderGroup().getShaderGroupName() : null;
|
||||
}
|
||||
if (name != null) displayer.fact(DebugGroup.RENDERING, "Post Effect").value(name);
|
||||
}
|
||||
}
|
||||
|
||||
static String spritesValue(String ticker) {
|
||||
if (ticker == null || !ticker.startsWith(SPRITES_PREFIX)) return null;
|
||||
return ticker.substring(SPRITES_PREFIX.length());
|
||||
}
|
||||
|
||||
static String chunkCacheValue(String provider) {
|
||||
final int at = provider.indexOf(COLON);
|
||||
return at < 0 ? provider : provider.substring(at + COLON.length());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.entries;
|
||||
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugContext;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenDisplayer;
|
||||
import com.gtnewhorizons.angelica.api.debug.DebugScreenEntry;
|
||||
import com.gtnewhorizons.angelica.debug.screen.DebugGroup;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
public final class SystemSpecsEntry implements DebugScreenEntry {
|
||||
|
||||
private static final String UNKNOWN = "Unknown";
|
||||
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
private boolean resolved;
|
||||
private String java;
|
||||
private int cores;
|
||||
private String gpu;
|
||||
private String openGl;
|
||||
|
||||
@Override
|
||||
public void display(DebugScreenDisplayer displayer, DebugContext context) {
|
||||
if (!resolved) resolve();
|
||||
displayer.fact(DebugGroup.SYSTEM_SPECS, "Java").value(java);
|
||||
displayer.fact(DebugGroup.SYSTEM_SPECS, "CPU").value(cores).text('x');
|
||||
displayer.fact(DebugGroup.SYSTEM_SPECS, "Display").value(mc.displayWidth).text('x').value(mc.displayHeight);
|
||||
displayer.fact(DebugGroup.SYSTEM_SPECS, "GPU").value(gpu);
|
||||
displayer.fact(DebugGroup.SYSTEM_SPECS, "OpenGL").value(openGl);
|
||||
}
|
||||
|
||||
private void resolve() {
|
||||
resolved = true;
|
||||
java = System.getProperty("java.version", UNKNOWN) + " (" + System.getProperty("java.vendor", UNKNOWN) + ")";
|
||||
cores = Runtime.getRuntime().availableProcessors();
|
||||
gpu = orUnknown(GLStateManager.glGetString(GL11.GL_RENDERER));
|
||||
openGl = orUnknown(GLStateManager.glGetString(GL11.GL_VERSION));
|
||||
}
|
||||
|
||||
private static String orUnknown(String value) {
|
||||
return value == null ? UNKNOWN : value;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
import com.gtnewhorizons.angelica.debug.screen.renderers.DebugMeshData.LineWidth;
|
||||
|
||||
final class ChunkBordersRenderer implements DebugWorldRenderer {
|
||||
|
||||
private static final int YELLOW = 0xFFFFFF00;
|
||||
private static final int CELL_BORDER = 0xFF009B9B;
|
||||
private static final int MAJOR = 0xFF3F3FFF;
|
||||
private static final int NEIGHBOR = 0x7FFF0000;
|
||||
private static final double MIN_Y = 0.0;
|
||||
private static final double MAX_Y = 256.0;
|
||||
|
||||
@Override
|
||||
public Granularity viewerGranularity() {
|
||||
return Granularity.SECTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minIntervalMillis() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fallbackIntervalMillis() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rebuild(ScanState scan, DebugMeshData mesh) {
|
||||
final double x0 = DebugRenderSlots.anchor(scan.viewerX());
|
||||
final double z0 = DebugRenderSlots.anchor(scan.viewerZ());
|
||||
final double sectionY = DebugRenderSlots.anchor(scan.viewerY());
|
||||
final double x1 = x0 + 16.0;
|
||||
final double z1 = z0 + 16.0;
|
||||
|
||||
for (int x = -16; x <= 32; x += 16) {
|
||||
for (int z = -16; z <= 32; z += 16) {
|
||||
mesh.line(LineWidth.THICK, x0 + x, MIN_Y, z0 + z, x0 + x, MAX_Y, z0 + z, NEIGHBOR);
|
||||
}
|
||||
}
|
||||
for (int i = 2; i < 16; i += 2) {
|
||||
final int color = i % 4 == 0 ? CELL_BORDER : YELLOW;
|
||||
mesh.line(LineWidth.THIN, x0 + i, MIN_Y, z0, x0 + i, MAX_Y, z0, color);
|
||||
mesh.line(LineWidth.THIN, x0 + i, MIN_Y, z1, x0 + i, MAX_Y, z1, color);
|
||||
mesh.line(LineWidth.THIN, x0, MIN_Y, z0 + i, x0, MAX_Y, z0 + i, color);
|
||||
mesh.line(LineWidth.THIN, x1, MIN_Y, z0 + i, x1, MAX_Y, z0 + i, color);
|
||||
}
|
||||
for (int y = 0; y <= 256; y += 2) {
|
||||
mesh.ring(LineWidth.THIN, x0, y, z0, x1, z1, y % 8 == 0 ? CELL_BORDER : YELLOW);
|
||||
}
|
||||
for (int x = 0; x <= 16; x += 16) {
|
||||
for (int z = 0; z <= 16; z += 16) {
|
||||
mesh.line(LineWidth.THICK, x0 + x, MIN_Y, z0 + z, x0 + x, MAX_Y, z0 + z, MAJOR);
|
||||
}
|
||||
}
|
||||
mesh.alwaysOnTop(true);
|
||||
mesh.lineBox(LineWidth.THIN, x0, sectionY, z0, x1, sectionY + 16.0, z1, MAJOR);
|
||||
mesh.alwaysOnTop(false);
|
||||
for (int y = 0; y <= 256; y += 16) {
|
||||
mesh.ring(LineWidth.THICK, x0, y, z0, x1, z1, MAJOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
import com.gtnewhorizons.angelica.debug.screen.renderers.DebugMeshData.LineWidth;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.MathHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class CollisionBoxRenderer implements DebugWorldRenderer {
|
||||
|
||||
private static final int MAX_BOXES = 2048;
|
||||
private static final double RANGE = 6.0;
|
||||
private static final int COLOR = 0xFFFFFFFF;
|
||||
|
||||
@Override
|
||||
public int minIntervalMillis() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fallbackIntervalMillis() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rebuild(ScanState scan, DebugMeshData mesh) {
|
||||
final EntityLivingBase viewer = scan.viewer();
|
||||
final AxisAlignedBB query = viewer.boundingBox.expand(RANGE, RANGE, RANGE);
|
||||
scan.watch(
|
||||
MathHelper.floor_double(query.minX) - 1, MathHelper.floor_double(query.minY) - 1, MathHelper.floor_double(query.minZ) - 1,
|
||||
MathHelper.floor_double(query.maxX) + 1, MathHelper.floor_double(query.maxY) + 1, MathHelper.floor_double(query.maxZ) + 1);
|
||||
final List<AxisAlignedBB> boxes = scan.world().getCollidingBoundingBoxes(viewer, query);
|
||||
final int n = Math.min(boxes.size(), MAX_BOXES);
|
||||
for (int i = 0; i < n; i++) {
|
||||
final AxisAlignedBB box = boxes.get(i);
|
||||
mesh.lineBox(LineWidth.MEDIUM, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, COLOR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
final class CubeWalker {
|
||||
|
||||
interface Sink {
|
||||
|
||||
boolean cell(int x, int y, int z);
|
||||
}
|
||||
|
||||
private static final int MIN_Y = 0;
|
||||
private static final int MAX_Y = 255;
|
||||
|
||||
private CubeWalker() {}
|
||||
|
||||
static void walk(int cx, int cy, int cz, int radius, Sink sink) {
|
||||
for (int r = 0; r <= radius; r++) {
|
||||
final int low = cy - r;
|
||||
final int high = cy + r;
|
||||
final boolean lowIn = low >= MIN_Y && low <= MAX_Y;
|
||||
final boolean highIn = high >= MIN_Y && high <= MAX_Y;
|
||||
final int from = Math.max(low, MIN_Y);
|
||||
final int to = Math.min(high, MAX_Y);
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
if (dx == -r || dx == r || dz == -r || dz == r) {
|
||||
for (int y = from; y <= to; y++) {
|
||||
if (!sink.cell(cx + dx, y, cz + dz)) return;
|
||||
}
|
||||
} else {
|
||||
if (lowIn && !sink.cell(cx + dx, low, cz + dz)) return;
|
||||
if (highIn && !sink.cell(cx + dx, high, cz + dz)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer;
|
||||
import com.gtnewhorizons.angelica.client.font.FontProvider;
|
||||
import com.gtnewhorizons.angelica.client.font.GlyphSettings;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.Color4;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
|
||||
final class DebugLabelBatch {
|
||||
|
||||
static final int MAX_LABELS = 8192;
|
||||
static final int MAX_FALLBACK_LABELS = 256;
|
||||
static final int MAX_VALUE = 99;
|
||||
static final float SCALE = 0.02F;
|
||||
static final float NEAR = 0.05F;
|
||||
static final float MIN_PIXELS = 4.0F;
|
||||
static final float FRUSTUM_MARGIN = 0.1F;
|
||||
|
||||
private static final int DIGITS = 10;
|
||||
private static final int FLAGS = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT;
|
||||
private static final VertexFormat FORMAT = DefaultVertexFormat.ALL_FORMATS[FLAGS];
|
||||
private static final int STRIDE = FORMAT.getVertexSize();
|
||||
private static final int TEXTURE_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.PRIMARY_UV);
|
||||
private static final int COLOR_OFFSET = DebugGeometry.offsetOf(FORMAT, VertexFormatElement.Usage.COLOR);
|
||||
private static final int QUAD_BYTES = 4 * STRIDE;
|
||||
private static final int INITIAL_BYTES = 0x4000;
|
||||
private static final String[] NUMBERS = numbers();
|
||||
|
||||
private final float[] glyphU = new float[DIGITS];
|
||||
private final float[] glyphV = new float[DIGITS];
|
||||
private final float[] glyphUSize = new float[DIGITS];
|
||||
private final float[] glyphVSize = new float[DIGITS];
|
||||
private final float[] glyphWidth = new float[DIGITS];
|
||||
private final float[] glyphTop = new float[DIGITS];
|
||||
private final float[] glyphHeight = new float[DIGITS];
|
||||
private final float[] glyphAdvance = new float[DIGITS];
|
||||
private final int[] glyphBucket = new int[DIGITS];
|
||||
private final int[] bucketTexture = new int[DIGITS];
|
||||
private final NativeScratch[] bucketScratch = new NativeScratch[DIGITS];
|
||||
private final int[] bucketBytes = new int[DIGITS];
|
||||
private final Matrix4f labelMatrix = new Matrix4f();
|
||||
private final Matrix4f projection = new Matrix4f();
|
||||
private final Matrix4f fallbackModelView = new Matrix4f();
|
||||
private final float[] fallbackColor = new float[4];
|
||||
private GlyphSettings metrics;
|
||||
private boolean metricsValid;
|
||||
private boolean metricsUnicode;
|
||||
private float metricsLineHeight;
|
||||
private int bucketCount;
|
||||
private int visible;
|
||||
private int limit;
|
||||
private float projectionX;
|
||||
private float projectionY;
|
||||
private int viewportHeight;
|
||||
private float lineHeight;
|
||||
private FontRenderer font;
|
||||
private BatchingFontRenderer batcher;
|
||||
|
||||
DebugLabelBatch() {
|
||||
for (int i = 0; i < DIGITS; i++) bucketScratch[i] = new NativeScratch(INITIAL_BYTES);
|
||||
}
|
||||
|
||||
private static String[] numbers() {
|
||||
final String[] out = new String[MAX_VALUE + 1];
|
||||
for (int i = 0; i <= MAX_VALUE; i++) {
|
||||
out[i] = Integer.toString(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static int digitCount(int value) {
|
||||
return value >= 10 ? 2 : 1;
|
||||
}
|
||||
|
||||
static int digitAt(int value, int index) {
|
||||
if (value < 10) return value;
|
||||
return index == 0 ? value / 10 : value % 10;
|
||||
}
|
||||
|
||||
static float stringWidth(int value, float[] advance) {
|
||||
return value >= 10 ? advance[value / 10] + advance[value % 10] : advance[value];
|
||||
}
|
||||
|
||||
static float digitLeft(int value, int index, float[] advance) {
|
||||
final float left = -stringWidth(value, advance) * 0.5F;
|
||||
return index == 0 ? left : left + advance[value / 10];
|
||||
}
|
||||
|
||||
static boolean isVisible(float eyeX, float eyeY, float eyeZ, float halfWidth, float height, float projectionX, float projectionY, int viewportHeight) {
|
||||
final float depth = -eyeZ;
|
||||
if (depth < NEAR) return false;
|
||||
if (height * projectionY * viewportHeight * 0.5F < MIN_PIXELS * depth) return false;
|
||||
final float reach = depth * (1.0F + FRUSTUM_MARGIN);
|
||||
final float limitX = reach / projectionX + halfWidth;
|
||||
if (eyeX > limitX || eyeX < -limitX) return false;
|
||||
final float limitY = reach / projectionY + height;
|
||||
return eyeY <= limitY && eyeY >= -limitY;
|
||||
}
|
||||
|
||||
void begin(FontRenderer fontRenderer, Matrix4fc projection, int viewportHeight) {
|
||||
this.font = fontRenderer;
|
||||
this.projection.set(projection);
|
||||
this.projectionX = projection.m00();
|
||||
this.projectionY = projection.m11();
|
||||
this.viewportHeight = viewportHeight;
|
||||
this.lineHeight = fontRenderer.FONT_HEIGHT - 1.0F;
|
||||
this.visible = 0;
|
||||
this.batcher = AngelicaConfig.enableFontRenderer && fontRenderer instanceof FontRendererAccessor accessor ? accessor.angelica$getBatcher() : null;
|
||||
if (batcher == null) {
|
||||
limit = MAX_FALLBACK_LABELS;
|
||||
metricsValid = false;
|
||||
bucketCount = 0;
|
||||
for (int d = 0; d < DIGITS; d++) {
|
||||
glyphAdvance[d] = fontRenderer.getCharWidth((char) ('0' + d));
|
||||
}
|
||||
pinFallback();
|
||||
return;
|
||||
}
|
||||
limit = MAX_LABELS;
|
||||
final boolean unicode = fontRenderer.getUnicodeFlag();
|
||||
final GlyphSettings settings = GlyphSettings.current();
|
||||
if (metricsValid && unicode == metricsUnicode && lineHeight == metricsLineHeight && settings.sameMetricsAs(metrics)) return;
|
||||
metrics = settings;
|
||||
metricsValid = false;
|
||||
bucketCount = 0;
|
||||
final float scaleX = batcher.getGlyphScaleX();
|
||||
final float scaleY = batcher.getGlyphScaleY();
|
||||
for (int d = 0; d < DIGITS; d++) {
|
||||
final char chr = (char) ('0' + d);
|
||||
final FontProvider provider = batcher.glyphProvider(chr, unicode);
|
||||
glyphU[d] = provider.getUStart(chr);
|
||||
glyphV[d] = provider.getVStart(chr);
|
||||
glyphUSize[d] = provider.getUSize(chr);
|
||||
glyphVSize[d] = provider.getVSize(chr);
|
||||
glyphWidth[d] = provider.getGlyphW(chr) * scaleX - 1.0F;
|
||||
glyphTop[d] = BatchingFontRenderer.glyphTop(provider, 0.0F, lineHeight, scaleY);
|
||||
glyphHeight[d] = BatchingFontRenderer.glyphHeight(provider, lineHeight, scaleY);
|
||||
glyphAdvance[d] = batcher.glyphAdvance(provider, chr, false, scaleX);
|
||||
glyphBucket[d] = bucketFor(provider.getTexture(chr));
|
||||
}
|
||||
metricsUnicode = unicode;
|
||||
metricsLineHeight = lineHeight;
|
||||
metricsValid = true;
|
||||
}
|
||||
|
||||
private int bucketFor(int texture) {
|
||||
for (int i = 0; i < bucketCount; i++) {
|
||||
if (bucketTexture[i] == texture) return i;
|
||||
}
|
||||
final int bucket = bucketCount++;
|
||||
bucketTexture[bucket] = texture;
|
||||
bucketBytes[bucket] = 0;
|
||||
return bucket;
|
||||
}
|
||||
|
||||
void add(DebugLabels labels, Matrix4fc modelView) {
|
||||
if (visible >= limit) return;
|
||||
final float[] positions = labels.pos;
|
||||
final int[] values = labels.value;
|
||||
final int[] colors = labels.color;
|
||||
final int count = labels.count;
|
||||
final float m00 = modelView.m00();
|
||||
final float m01 = modelView.m01();
|
||||
final float m02 = modelView.m02();
|
||||
final float m10 = modelView.m10();
|
||||
final float m11 = modelView.m11();
|
||||
final float m12 = modelView.m12();
|
||||
final float m20 = modelView.m20();
|
||||
final float m21 = modelView.m21();
|
||||
final float m22 = modelView.m22();
|
||||
final float m30 = modelView.m30();
|
||||
final float m31 = modelView.m31();
|
||||
final float m32 = modelView.m32();
|
||||
final float[] advance = glyphAdvance;
|
||||
final float height = lineHeight * SCALE;
|
||||
for (int i = 0, p = 0; i < count; i++, p += 3) {
|
||||
final float x = positions[p];
|
||||
final float y = positions[p + 1];
|
||||
final float z = positions[p + 2];
|
||||
final float eyeZ = m02 * x + m12 * y + m22 * z + m32;
|
||||
if (-eyeZ < NEAR) continue;
|
||||
final float eyeX = m00 * x + m10 * y + m20 * z + m30;
|
||||
final float eyeY = m01 * x + m11 * y + m21 * z + m31;
|
||||
final int value = values[i];
|
||||
final float halfWidth = stringWidth(value, advance) * SCALE * 0.5F;
|
||||
if (!isVisible(eyeX, eyeY, eyeZ, halfWidth, height, projectionX, projectionY, viewportHeight)) continue;
|
||||
if (batcher == null) {
|
||||
drawFallback(value, colors[i], eyeX, eyeY, eyeZ);
|
||||
} else {
|
||||
final int digits = digitCount(value);
|
||||
for (int k = 0; k < digits; k++) {
|
||||
glyph(digitAt(value, k), eyeX + digitLeft(value, k, advance) * SCALE, eyeY, eyeZ, colors[i]);
|
||||
}
|
||||
}
|
||||
if (++visible >= limit) return;
|
||||
}
|
||||
}
|
||||
|
||||
private void glyph(int digit, float left, float anchorY, float z, int argb) {
|
||||
final int bucket = glyphBucket[digit];
|
||||
final int start = bucketBytes[bucket];
|
||||
final int end = start + QUAD_BYTES;
|
||||
final NativeScratch scratch = bucketScratch[bucket];
|
||||
scratch.ensure(end);
|
||||
bucketBytes[bucket] = end;
|
||||
final long p = scratch.address() + start;
|
||||
final float right = left + glyphWidth[digit] * SCALE;
|
||||
final float top = anchorY - glyphTop[digit] * SCALE;
|
||||
final float bottom = top - glyphHeight[digit] * SCALE;
|
||||
final float u0 = glyphU[digit];
|
||||
final float v0 = glyphV[digit];
|
||||
final float u1 = u0 + glyphUSize[digit];
|
||||
final float v1 = v0 + glyphVSize[digit];
|
||||
vertex(p, left, top, z, u0, v0, argb);
|
||||
vertex(p + STRIDE, left, bottom, z, u0, v1, argb);
|
||||
vertex(p + 2L * STRIDE, right, bottom, z, u1, v1, argb);
|
||||
vertex(p + 3L * STRIDE, right, top, z, u1, v0, argb);
|
||||
}
|
||||
|
||||
private static void vertex(long p, float x, float y, float z, float u, float v, int argb) {
|
||||
DebugGeometry.putPosition(p, x, y, z);
|
||||
memPutFloat(p + TEXTURE_OFFSET, u);
|
||||
memPutFloat(p + TEXTURE_OFFSET + 4, v);
|
||||
DebugGeometry.putColor(p + COLOR_OFFSET, argb);
|
||||
}
|
||||
|
||||
private void drawFallback(int value, int argb, float eyeX, float eyeY, float eyeZ) {
|
||||
final String text = NUMBERS[value];
|
||||
labelMatrix.translation(eyeX, eyeY, eyeZ).scale(SCALE, -SCALE, SCALE);
|
||||
GLStateManager.setModelViewMatrix(labelMatrix);
|
||||
font.drawString(text, -Math.round(stringWidth(value, glyphAdvance) * 0.5F), 0, argb);
|
||||
}
|
||||
|
||||
private void pinFallback() {
|
||||
fallbackModelView.set(GLStateManager.getModelViewMatrix());
|
||||
final Color4 color = GLStateManager.getColor();
|
||||
fallbackColor[0] = color.getRed();
|
||||
fallbackColor[1] = color.getGreen();
|
||||
fallbackColor[2] = color.getBlue();
|
||||
fallbackColor[3] = color.getAlpha();
|
||||
GLStateManager.glUseProgram(0);
|
||||
GLStateManager.disableLighting();
|
||||
GLStateManager.disableFog();
|
||||
GLStateManager.disableAlphaTest();
|
||||
GLStateManager.glActiveTexture(OpenGlHelper.lightmapTexUnit);
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glActiveTexture(OpenGlHelper.defaultTexUnit);
|
||||
GLStateManager.enableTexture();
|
||||
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
}
|
||||
|
||||
void end() {
|
||||
try {
|
||||
if (batcher != null) {
|
||||
boolean bound = false;
|
||||
for (int i = 0; i < bucketCount; i++) {
|
||||
final int bytes = bucketBytes[i];
|
||||
if (bytes == 0) continue;
|
||||
if (!bound) {
|
||||
final DebugProgram textured = DebugPrograms.textured();
|
||||
textured.use();
|
||||
textured.mvp(projection);
|
||||
bound = true;
|
||||
}
|
||||
batcher.bindGlyphTexture(bucketTexture[i]);
|
||||
TessellatorStreamingDrawer.drawPacked(bucketScratch[i].window(bytes), GL11.GL_QUADS, FLAGS, bytes / STRIDE);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (batcher == null && font != null) {
|
||||
GLStateManager.setModelViewMatrix(fallbackModelView);
|
||||
GLStateManager.glColor4f(fallbackColor[0], fallbackColor[1], fallbackColor[2], fallbackColor[3]);
|
||||
}
|
||||
for (int i = 0; i < bucketCount; i++) bucketBytes[i] = 0;
|
||||
batcher = null;
|
||||
font = null;
|
||||
}
|
||||
}
|
||||
|
||||
void free() {
|
||||
for (int i = 0; i < DIGITS; i++) {
|
||||
bucketScratch[i].free();
|
||||
bucketBytes[i] = 0;
|
||||
}
|
||||
bucketCount = 0;
|
||||
metricsValid = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class DebugLabels {
|
||||
|
||||
private static final int INITIAL = 64;
|
||||
private static final float[] NO_FLOATS = new float[0];
|
||||
private static final int[] NO_INTS = new int[0];
|
||||
|
||||
float[] pos = NO_FLOATS;
|
||||
int[] value = NO_INTS;
|
||||
int[] color = NO_INTS;
|
||||
int count;
|
||||
|
||||
void add(float x, float y, float z, int v, int argb) {
|
||||
final int n = count;
|
||||
if (n == value.length) {
|
||||
final int grown = Math.max(INITIAL, n * 2);
|
||||
pos = Arrays.copyOf(pos, grown * 3);
|
||||
value = Arrays.copyOf(value, grown);
|
||||
color = Arrays.copyOf(color, grown);
|
||||
}
|
||||
pos[n * 3] = x;
|
||||
pos[n * 3 + 1] = y;
|
||||
pos[n * 3 + 2] = z;
|
||||
value[n] = v;
|
||||
color[n] = argb;
|
||||
count = n + 1;
|
||||
}
|
||||
|
||||
void copyFrom(DebugLabels from) {
|
||||
final int n = from.count;
|
||||
if (n > value.length) {
|
||||
pos = new float[from.pos.length];
|
||||
value = new int[from.value.length];
|
||||
color = new int[from.color.length];
|
||||
}
|
||||
System.arraycopy(from.pos, 0, pos, 0, n * 3);
|
||||
System.arraycopy(from.value, 0, value, 0, n);
|
||||
System.arraycopy(from.color, 0, color, 0, n);
|
||||
count = n;
|
||||
}
|
||||
|
||||
void free() {
|
||||
pos = NO_FLOATS;
|
||||
value = NO_INTS;
|
||||
color = NO_INTS;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.gtnewhorizons.angelica.debug.screen.renderers;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugGeometry;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugProgram;
|
||||
import com.gtnewhorizons.angelica.debug.render.DebugPrograms;
|
||||
import com.gtnewhorizons.angelica.debug.render.NativeScratch;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
final class DebugMesh {
|
||||
|
||||
private static final VertexFormat WIDE = LineQuads.quadFormat(DebugMeshData.PLAIN);
|
||||
private static final int WIDE_STRIDE = WIDE.getVertexSize();
|
||||
private static final DebugMeshData.LineWidth[] WIDTHS = DebugMeshData.LineWidth.values();
|
||||
|
||||
private final int[] lineFirst = new int[DebugMeshData.BUCKET_COUNT];
|
||||
private final int[] lineCount = new int[DebugMeshData.BUCKET_COUNT];
|
||||
private final boolean[] lineWide = new boolean[DebugMeshData.BUCKET_COUNT];
|
||||
private final Matrix4f mvp = new Matrix4f();
|
||||
private int plainVao;
|
||||
private int plainVbo;
|
||||
private int wideVao;
|
||||
private int wideVbo;
|
||||
private int fillCount;
|
||||
private int anchorX;
|
||||
private int anchorY;
|
||||
private int anchorZ;
|
||||
final DebugLabels labels = new DebugLabels();
|
||||
|
||||
void upload(DebugMeshData data, NativeScratch scratch) {
|
||||
anchorX = data.anchorX();
|
||||
anchorY = data.anchorY();
|
||||
anchorZ = data.anchorZ();
|
||||
labels.copyFrom(data.labels);
|
||||
|
||||
int plainBytes = data.fillBytes();
|
||||
int wideVertices = 0;
|
||||
fillCount = data.fillVertexCount();
|
||||
for (int b = 0; b < DebugMeshData.BUCKET_COUNT; b++) {
|
||||
final int vertices = data.lineVertexCount(b);
|
||||
final boolean wide = WIDTHS[b % DebugMeshData.WIDTH_COUNT].pixels > 1.0F;
|
||||
lineWide[b] = wide;
|
||||
if (vertices == 0) {
|
||||
lineFirst[b] = 0;
|
||||
lineCount[b] = 0;
|
||||
} else if (wide) {
|
||||
final int corners = LineQuads.segmentCount(GL11.GL_LINES, vertices) * LineQuads.VERTICES_PER_SEGMENT;
|
||||
lineFirst[b] = wideVertices;
|
||||
lineCount[b] = corners;
|
||||
wideVertices += corners;
|
||||
} else {
|
||||
lineFirst[b] = plainBytes / DebugMeshData.STRIDE;
|
||||
lineCount[b] = vertices;
|
||||
plainBytes += data.lineBytes(b);
|
||||
}
|
||||
}
|
||||
|
||||
if (plainBytes > 0) {
|
||||
if (plainVao == 0) {
|
||||
plainVbo = GLStateManager.glGenBuffers();
|
||||
plainVao = DebugGeometry.createVao(plainVbo, DebugMeshData.PLAIN);
|
||||
} else {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, plainVbo);
|
||||
}
|
||||
scratch.ensure(plainBytes);
|
||||
long destination = scratch.address();
|
||||
final int fillBytes = data.fillBytes();
|
||||
if (fillBytes > 0) {
|
||||
data.copySection(DebugMeshData.FILL_SECTION, destination);
|
||||
destination += fillBytes;
|
||||
}
|
||||
for (int b = 0; b < DebugMeshData.BUCKET_COUNT; b++) {
|
||||
if (lineWide[b] || lineCount[b] == 0) continue;
|
||||
data.copySection(DebugMeshData.lineSection(b), destination);
|
||||
destination += data.lineBytes(b);
|
||||
}
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, scratch.window(plainBytes), GL15.GL_DYNAMIC_DRAW);
|
||||
}
|
||||
|
||||
if (wideVertices > 0) {
|
||||
final int wideBytes = wideVertices * WIDE_STRIDE;
|
||||
scratch.ensure(wideBytes);
|
||||
for (int b = 0; b < DebugMeshData.BUCKET_COUNT; b++) {
|
||||
if (!lineWide[b] || lineCount[b] == 0) continue;
|
||||
LineQuads.expandWithOtherEnd(data.lineAddress(b), DebugMeshData.PLAIN, GL11.GL_LINES, data.lineVertexCount(b), scratch.address() + (long) lineFirst[b] * WIDE_STRIDE);
|
||||
}
|
||||
if (wideVao == 0) {
|
||||
wideVbo = GLStateManager.glGenBuffers();
|
||||
wideVao = DebugGeometry.createVao(wideVbo, WIDE);
|
||||
} else {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, wideVbo);
|
||||
}
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, scratch.window(wideBytes), GL15.GL_DYNAMIC_DRAW);
|
||||
}
|
||||
}
|
||||
|
||||
static DebugProgram useColor() {
|
||||
final DebugProgram color = DebugPrograms.color();
|
||||
color.use();
|
||||
return color;
|
||||
}
|
||||
|
||||
static DebugProgram useLines() {
|
||||
final DebugProgram lines = DebugPrograms.lines();
|
||||
lines.use();
|
||||
final ViewportState viewport = GLStateManager.getViewportState();
|
||||
lines.viewport(viewport.width, viewport.height);
|
||||
return lines;
|
||||
}
|
||||
|
||||
void prepare(Matrix4fc projBase, double camX, double camY, double camZ) {
|
||||
mvp.set(projBase).translate((float) (anchorX - camX), (float) (anchorY - camY), (float) (anchorZ - camZ));
|
||||
}
|
||||
|
||||
boolean has(boolean wide, boolean onTop) {
|
||||
if (!wide && !onTop && fillCount > 0) return true;
|
||||
final int from = onTop ? DebugMeshData.WIDTH_COUNT : 0;
|
||||
final int to = from + DebugMeshData.WIDTH_COUNT;
|
||||
for (int b = from; b < to; b++) {
|
||||
if (lineWide[b] == wide && lineCount[b] > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void drawPlain(boolean onTop, DebugProgram color) {
|
||||
if (!has(false, onTop)) return;
|
||||
color.mvp(mvp);
|
||||
GLStateManager.glBindVertexArray(plainVao);
|
||||
if (!onTop && fillCount > 0) {
|
||||
GLStateManager.enableCull();
|
||||
try {
|
||||
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, fillCount);
|
||||
} finally {
|
||||
GLStateManager.disableCull();
|
||||
}
|
||||
}
|
||||
final int from = onTop ? DebugMeshData.WIDTH_COUNT : 0;
|
||||
final int to = from + DebugMeshData.WIDTH_COUNT;
|
||||
for (int b = from; b < to; b++) {
|
||||
if (lineWide[b] || lineCount[b] == 0) continue;
|
||||
GLStateManager.glDrawArrays(GL11.GL_LINES, lineFirst[b], lineCount[b]);
|
||||
}
|
||||
}
|
||||
|
||||
void drawWide(boolean onTop, DebugProgram lines) {
|
||||
if (!has(true, onTop)) return;
|
||||
lines.mvp(mvp);
|
||||
GLStateManager.glBindVertexArray(wideVao);
|
||||
final int from = onTop ? DebugMeshData.WIDTH_COUNT : 0;
|
||||
final int to = from + DebugMeshData.WIDTH_COUNT;
|
||||
for (int b = from; b < to; b++) {
|
||||
if (!lineWide[b] || lineCount[b] == 0) continue;
|
||||
lines.lineWidth(WIDTHS[b % DebugMeshData.WIDTH_COUNT].pixels);
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, lineFirst[b], lineCount[b]);
|
||||
}
|
||||
}
|
||||
|
||||
void modelView(Matrix4f base, double camX, double camY, double camZ, Matrix4f out) {
|
||||
out.set(base).translate((float) (anchorX - camX), (float) (anchorY - camY), (float) (anchorZ - camZ));
|
||||
}
|
||||
|
||||
void delete() {
|
||||
if (plainVbo != 0) GLStateManager.glDeleteBuffers(plainVbo);
|
||||
if (plainVao != 0) GLStateManager.glDeleteVertexArrays(plainVao);
|
||||
if (wideVbo != 0) GLStateManager.glDeleteBuffers(wideVbo);
|
||||
if (wideVao != 0) GLStateManager.glDeleteVertexArrays(wideVao);
|
||||
plainVbo = 0;
|
||||
plainVao = 0;
|
||||
wideVbo = 0;
|
||||
wideVao = 0;
|
||||
fillCount = 0;
|
||||
for (int b = 0; b < DebugMeshData.BUCKET_COUNT; b++) {
|
||||
lineFirst[b] = 0;
|
||||
lineCount[b] = 0;
|
||||
lineWide[b] = false;
|
||||
}
|
||||
labels.free();
|
||||
}
|
||||
}
|
||||
Loaded 100 of 184 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user