Prevent Naldo shader startup crash
Browse filesUse vanilla autojoin after resource loading, gate manual reconnect by mapped overlay and safe screens, skip first-run onboarding. Rebuilt Java17 bridge 1.0.3 and 61 targeted tests pass.
- altoclef/SpaceBridge.java +53 -4
- altoclef/build.py +1 -1
- altoclef/space-altoclef-bridge.jar +0 -0
- altoclef_stack.py +16 -3
- tests/test_altoclef_stack.py +58 -0
altoclef/SpaceBridge.java
CHANGED
|
@@ -27,6 +27,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|
| 27 |
* scheduler tick itself (bridge alive), while {@code observed_at} is refreshed
|
| 28 |
* only by a snapshot that actually ran on the client thread. At most one
|
| 29 |
* snapshot is ever queued, so rendering load cannot build a backlog.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
*/
|
| 31 |
public final class SpaceBridge implements ClientModInitializer {
|
| 32 |
private static final MappingResolver MAP = FabricLoader.getInstance().getMappingResolver();
|
|
@@ -35,6 +41,9 @@ public final class SpaceBridge implements ClientModInitializer {
|
|
| 35 |
private volatile String lastCommand = "";
|
| 36 |
private final AtomicBoolean snapshotPending = new AtomicBoolean(false);
|
| 37 |
private long connectAttempt;
|
|
|
|
|
|
|
|
|
|
| 38 |
private long creditsSince;
|
| 39 |
private boolean creditsObserved;
|
| 40 |
private boolean enteredEnd;
|
|
@@ -179,9 +188,38 @@ public final class SpaceBridge implements ClientModInitializer {
|
|
| 179 |
System.out.println("[space-altoclef] local bridge ready on 127.0.0.1:" + port);
|
| 180 |
} catch (Exception err) { throw new RuntimeException("Space bridge failed", err); }
|
| 181 |
}
|
| 182 |
-
private void connect(Object client) throws Exception {
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
connectAttempt = System.currentTimeMillis();
|
|
|
|
| 185 |
String address = System.getenv().getOrDefault("ALTOCLEF_SERVER", "127.0.0.1:25568");
|
| 186 |
Object parsed = method(null, "class_639", "method_2950", "(Ljava/lang/String;)Lnet/minecraft/class_639;",
|
| 187 |
new Class<?>[]{String.class}, address);
|
|
@@ -191,7 +229,16 @@ public final class SpaceBridge implements ClientModInitializer {
|
|
| 191 |
"(Lnet/minecraft/class_437;Lnet/minecraft/class_310;Lnet/minecraft/class_639;Lnet/minecraft/class_642;)V",
|
| 192 |
new Class<?>[]{mc("class_437"), mc("class_310"), mc("class_639"), mc("class_642")},
|
| 193 |
screen(client), client, parsed, info);
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
}
|
| 196 |
private String currentDimension() throws Exception {
|
| 197 |
// Upstream WorldHelper.getCurrentDimension is package-private static;
|
|
@@ -224,8 +271,10 @@ public final class SpaceBridge implements ClientModInitializer {
|
|
| 224 |
data.put("credits_screen", credits);
|
| 225 |
data.put("credits_observed", creditsObserved);
|
| 226 |
data.put("entered_end", enteredEnd);
|
|
|
|
|
|
|
| 227 |
if (player == null) {
|
| 228 |
-
if (executor != null && !credits) connect(client);
|
| 229 |
} else {
|
| 230 |
String dimension = currentDimension();
|
| 231 |
if ("END".equals(dimension)) enteredEnd = true;
|
|
|
|
| 27 |
* scheduler tick itself (bridge alive), while {@code observed_at} is refreshed
|
| 28 |
* only by a snapshot that actually ran on the client thread. At most one
|
| 29 |
* snapshot is ever queued, so rendering load cannot build a backlog.
|
| 30 |
+
*
|
| 31 |
+
* <p>Connect contract: primary autojoin is vanilla {@code --server/--port},
|
| 32 |
+
* which sequences itself after resource/shader load. The manual fallback in
|
| 33 |
+
* this bridge only fires on a Title/Disconnected screen with no resource
|
| 34 |
+
* reload overlay (connecting mid-reload NPEs WorldRenderer via
|
| 35 |
+
* ShaderProgram.addSampler), on the client thread only.
|
| 36 |
*/
|
| 37 |
public final class SpaceBridge implements ClientModInitializer {
|
| 38 |
private static final MappingResolver MAP = FabricLoader.getInstance().getMappingResolver();
|
|
|
|
| 41 |
private volatile String lastCommand = "";
|
| 42 |
private final AtomicBoolean snapshotPending = new AtomicBoolean(false);
|
| 43 |
private long connectAttempt;
|
| 44 |
+
private long connectAttempts;
|
| 45 |
+
private volatile String connectState = "startup";
|
| 46 |
+
private String lastGate = "";
|
| 47 |
private long creditsSince;
|
| 48 |
private boolean creditsObserved;
|
| 49 |
private boolean enteredEnd;
|
|
|
|
| 188 |
System.out.println("[space-altoclef] local bridge ready on 127.0.0.1:" + port);
|
| 189 |
} catch (Exception err) { throw new RuntimeException("Space bridge failed", err); }
|
| 190 |
}
|
| 191 |
+
private void connect(Object client, Object currentScreen) throws Exception {
|
| 192 |
+
// Gated manual connect: NEVER while a resource/shader reload overlay
|
| 193 |
+
// is up (that NPEs WorldRenderer via ShaderProgram.addSampler on a
|
| 194 |
+
// half-built reload), and never while vanilla is already connecting.
|
| 195 |
+
// Primary autojoin is vanilla --server/--port; this is only the
|
| 196 |
+
// safe-reconnect fallback for Title/Disconnected screens.
|
| 197 |
+
// Pinned Yarn 1.19.4+build.2: METHOD emh ()Lesx; aJ method_18506 getOverlay
|
| 198 |
+
// (owner class_310 MinecraftClient, returns class_4071 Overlay).
|
| 199 |
+
Object overlay = method(client, "class_310", "method_18506", "()Lnet/minecraft/class_4071;",
|
| 200 |
+
new Class<?>[0]);
|
| 201 |
+
boolean title = currentScreen != null
|
| 202 |
+
&& (mc("class_442").isInstance(currentScreen) || mc("class_419").isInstance(currentScreen));
|
| 203 |
+
boolean connecting = currentScreen != null && mc("class_412").isInstance(currentScreen);
|
| 204 |
+
String screenName = currentScreen == null ? "<none>" : currentScreen.getClass().getName();
|
| 205 |
+
if (overlay != null) {
|
| 206 |
+
gate("waiting resource reload (overlay=" + overlay.getClass().getName() + ")");
|
| 207 |
+
return;
|
| 208 |
+
}
|
| 209 |
+
if (connecting) {
|
| 210 |
+
connectState = "connecting (vanilla ConnectScreen)";
|
| 211 |
+
return;
|
| 212 |
+
}
|
| 213 |
+
if (!title) {
|
| 214 |
+
gate("waiting title screen (screen=" + screenName + ")");
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
if (System.currentTimeMillis() - connectAttempt < 30000) {
|
| 218 |
+
connectState = "retry waits 30s throttle (screen=" + screenName + ")";
|
| 219 |
+
return;
|
| 220 |
+
}
|
| 221 |
connectAttempt = System.currentTimeMillis();
|
| 222 |
+
connectAttempts++;
|
| 223 |
String address = System.getenv().getOrDefault("ALTOCLEF_SERVER", "127.0.0.1:25568");
|
| 224 |
Object parsed = method(null, "class_639", "method_2950", "(Ljava/lang/String;)Lnet/minecraft/class_639;",
|
| 225 |
new Class<?>[]{String.class}, address);
|
|
|
|
| 229 |
"(Lnet/minecraft/class_437;Lnet/minecraft/class_310;Lnet/minecraft/class_639;Lnet/minecraft/class_642;)V",
|
| 230 |
new Class<?>[]{mc("class_437"), mc("class_310"), mc("class_639"), mc("class_642")},
|
| 231 |
screen(client), client, parsed, info);
|
| 232 |
+
connectState = "connecting to " + address + " (attempt " + connectAttempts + ")";
|
| 233 |
+
System.out.println("[space-altoclef] connecting to " + address + " (attempt " + connectAttempts + ")");
|
| 234 |
+
}
|
| 235 |
+
/** Throttled gate diagnostic: log only when the reason changes. */
|
| 236 |
+
private void gate(String reason) {
|
| 237 |
+
connectState = reason;
|
| 238 |
+
if (!reason.equals(lastGate)) {
|
| 239 |
+
lastGate = reason;
|
| 240 |
+
System.out.println("[space-altoclef] connect gated: " + reason);
|
| 241 |
+
}
|
| 242 |
}
|
| 243 |
private String currentDimension() throws Exception {
|
| 244 |
// Upstream WorldHelper.getCurrentDimension is package-private static;
|
|
|
|
| 271 |
data.put("credits_screen", credits);
|
| 272 |
data.put("credits_observed", creditsObserved);
|
| 273 |
data.put("entered_end", enteredEnd);
|
| 274 |
+
data.put("connect_state", connectState);
|
| 275 |
+
data.put("connect_attempts", connectAttempts);
|
| 276 |
if (player == null) {
|
| 277 |
+
if (executor != null && !credits) connect(client, currentScreen);
|
| 278 |
} else {
|
| 279 |
String dimension = currentDimension();
|
| 280 |
if ("END".equals(dimension)) enteredEnd = true;
|
altoclef/build.py
CHANGED
|
@@ -52,7 +52,7 @@ def build():
|
|
| 52 |
],
|
| 53 |
check=True,
|
| 54 |
)
|
| 55 |
-
metadata = {"schemaVersion": 1, "id": "space_altoclef_bridge", "version": "1.0.
|
| 56 |
"name": "Space AltoClef Bridge", "environment": "client",
|
| 57 |
"entrypoints": {"client": ["space.minecraft.SpaceBridge"]},
|
| 58 |
"depends": {"fabricloader": ">=0.16.2", "minecraft": "1.19.4",
|
|
|
|
| 52 |
],
|
| 53 |
check=True,
|
| 54 |
)
|
| 55 |
+
metadata = {"schemaVersion": 1, "id": "space_altoclef_bridge", "version": "1.0.3",
|
| 56 |
"name": "Space AltoClef Bridge", "environment": "client",
|
| 57 |
"entrypoints": {"client": ["space.minecraft.SpaceBridge"]},
|
| 58 |
"depends": {"fabricloader": ">=0.16.2", "minecraft": "1.19.4",
|
altoclef/space-altoclef-bridge.jar
CHANGED
|
Binary files a/altoclef/space-altoclef-bridge.jar and b/altoclef/space-altoclef-bridge.jar differ
|
|
|
altoclef_stack.py
CHANGED
|
@@ -152,10 +152,17 @@ class AltoClefStack:
|
|
| 152 |
shutil.copyfile(MOD_CACHE / "fabric-api-0.87.2+1.19.4.jar", mods / "fabric-api-0.87.2+1.19.4.jar")
|
| 153 |
shutil.copyfile(SOURCE / "altoclef/space-altoclef-bridge.jar", mods / "space-altoclef-bridge.jar")
|
| 154 |
settings = CLIENT / "options.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
if not settings.exists():
|
| 156 |
-
settings.write_text(
|
| 157 |
-
|
| 158 |
-
|
|
|
|
| 159 |
config_dir = CLIENT / "altoclef/configs"
|
| 160 |
config_dir.mkdir(parents=True, exist_ok=True)
|
| 161 |
config = config_dir / "beat_minecraft.json"
|
|
@@ -164,10 +171,16 @@ class AltoClefStack:
|
|
| 164 |
"placeSpawnNearEndPortal":False,"requiredBeds":10}))
|
| 165 |
# Match Java's offline UUID algorithm without changing server auth.
|
| 166 |
player_uuid = str(uuid.UUID(bytes=hashlib.md5(("OfflinePlayer:" + USERNAME).encode()).digest(), version=3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
return launcher.command.get_minecraft_command(version, str(CLIENT), {
|
| 168 |
"username": USERNAME, "uuid": player_uuid, "token": "0",
|
| 169 |
"executablePath": str(java), "jvmArguments": ["-Xms256M", "-Xmx2G", "-XX:ActiveProcessorCount=2"],
|
| 170 |
"customResolution":True, "resolutionWidth":"640", "resolutionHeight":"360",
|
|
|
|
| 171 |
})
|
| 172 |
|
| 173 |
def _loading_grace(self) -> bool:
|
|
|
|
| 152 |
shutil.copyfile(MOD_CACHE / "fabric-api-0.87.2+1.19.4.jar", mods / "fabric-api-0.87.2+1.19.4.jar")
|
| 153 |
shutil.copyfile(SOURCE / "altoclef/space-altoclef-bridge.jar", mods / "space-altoclef-bridge.jar")
|
| 154 |
settings = CLIENT / "options.txt"
|
| 155 |
+
defaults = ("maxFps:20\nrenderDistance:4\nsimulationDistance:4\nentityDistanceScaling:0.5\n"
|
| 156 |
+
"graphicsMode:0\nparticles:2\nsoundCategory_master:0.0\nuseVbo:true\n"
|
| 157 |
+
"pauseOnLostFocus:false\nfullscreen:false\nshowSubtitles:false\n"
|
| 158 |
+
# 1.19.4 first-run accessibility onboarding would park the
|
| 159 |
+
# headless client on a setup screen; skip it when present.
|
| 160 |
+
"onboardAccessibility:false\n")
|
| 161 |
if not settings.exists():
|
| 162 |
+
settings.write_text(defaults)
|
| 163 |
+
elif "onboardAccessibility" not in settings.read_text(errors="replace"):
|
| 164 |
+
with settings.open("a", encoding="utf-8") as out:
|
| 165 |
+
out.write("onboardAccessibility:false\n")
|
| 166 |
config_dir = CLIENT / "altoclef/configs"
|
| 167 |
config_dir.mkdir(parents=True, exist_ok=True)
|
| 168 |
config = config_dir / "beat_minecraft.json"
|
|
|
|
| 171 |
"placeSpawnNearEndPortal":False,"requiredBeds":10}))
|
| 172 |
# Match Java's offline UUID algorithm without changing server auth.
|
| 173 |
player_uuid = str(uuid.UUID(bytes=hashlib.md5(("OfflinePlayer:" + USERNAME).encode()).digest(), version=3))
|
| 174 |
+
# Primary autojoin is vanilla --server/--port (supported by official
|
| 175 |
+
# 1.19.4 and minecraft_launcher_lib 7.1): vanilla sequences the connect
|
| 176 |
+
# itself after resource/shader load, so the bridge never has to force
|
| 177 |
+
# a connect mid-reload. The bridge keeps only a readiness-gated
|
| 178 |
+
# safe-reconnect fallback for Title/Disconnected screens.
|
| 179 |
return launcher.command.get_minecraft_command(version, str(CLIENT), {
|
| 180 |
"username": USERNAME, "uuid": player_uuid, "token": "0",
|
| 181 |
"executablePath": str(java), "jvmArguments": ["-Xms256M", "-Xmx2G", "-XX:ActiveProcessorCount=2"],
|
| 182 |
"customResolution":True, "resolutionWidth":"640", "resolutionHeight":"360",
|
| 183 |
+
"server": "127.0.0.1", "port": "25568",
|
| 184 |
})
|
| 185 |
|
| 186 |
def _loading_grace(self) -> bool:
|
tests/test_altoclef_stack.py
CHANGED
|
@@ -88,6 +88,64 @@ def test_boot_resets_launch_clocks_before_startup():
|
|
| 88 |
assert "_reset_launch_clocks()" in source
|
| 89 |
assert source.index("_reset_launch_clocks()") < source.index("minecraft_ready.wait")
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def test_survival_verified_requires_explicit_survival():
|
| 92 |
good = {"connected": True, "error": "", "creative": False}
|
| 93 |
assert module.AltoClefStack._survival_verified(good) is True
|
|
|
|
| 88 |
assert "_reset_launch_clocks()" in source
|
| 89 |
assert source.index("_reset_launch_clocks()") < source.index("minecraft_ready.wait")
|
| 90 |
|
| 91 |
+
def _bridge_source() -> str:
|
| 92 |
+
return (Path(module.__file__).parents[0] / "altoclef" / "SpaceBridge.java").read_text(encoding="utf-8")
|
| 93 |
+
|
| 94 |
+
def test_bridge_gates_manual_connect_on_readiness():
|
| 95 |
+
# Pinned Yarn 1.19.4+build.2: TitleScreen class_442, DisconnectedScreen
|
| 96 |
+
# class_419, ConnectScreen class_412, overlay via getOverlay. The manual
|
| 97 |
+
# connect must take the current screen and refuse while a reload overlay
|
| 98 |
+
# is up or vanilla is already connecting.
|
| 99 |
+
source = _bridge_source()
|
| 100 |
+
assert "class_442" in source and "class_419" in source
|
| 101 |
+
# Pinned Yarn 1.19.4+build.2 row: METHOD emh ()Lesx; aJ method_18506
|
| 102 |
+
# getOverlay. The live runtime is intermediary, so the mapped ID (not the
|
| 103 |
+
# Yarn name) must be used.
|
| 104 |
+
assert "method_18506" in source
|
| 105 |
+
assert '"getOverlay"' not in source
|
| 106 |
+
assert "connect(client, currentScreen)" in source
|
| 107 |
+
assert "mc(\"class_412\").isInstance(currentScreen)" in source
|
| 108 |
+
import re
|
| 109 |
+
assert not re.search(r"(?<!, )connect\(client\)", source), \
|
| 110 |
+
"ungated single-arg connect call remains"
|
| 111 |
+
|
| 112 |
+
def test_bridge_reports_connect_diagnostics():
|
| 113 |
+
source = _bridge_source()
|
| 114 |
+
assert '"connect_state"' in source and '"connect_attempts"' in source
|
| 115 |
+
|
| 116 |
+
def test_built_bridge_jar_matches_source_version():
|
| 117 |
+
import json as _json
|
| 118 |
+
import zipfile as _zip
|
| 119 |
+
jar = Path(module.__file__).parents[0] / "altoclef" / "space-altoclef-bridge.jar"
|
| 120 |
+
meta = _json.loads(_zip.ZipFile(jar).read("fabric.mod.json").decode())
|
| 121 |
+
assert meta["version"] == "1.0.3"
|
| 122 |
+
assert "space/minecraft/SpaceBridge.class" in _zip.ZipFile(jar).namelist()
|
| 123 |
+
|
| 124 |
+
def test_client_uses_vanilla_autojoin_and_skips_onboarding(runner, monkeypatch, tmp_path):
|
| 125 |
+
import sys as _sys
|
| 126 |
+
seen: dict = {}
|
| 127 |
+
fake_launcher = SimpleNamespace(
|
| 128 |
+
fabric=SimpleNamespace(install_fabric=lambda *a, **k: None),
|
| 129 |
+
command=SimpleNamespace(
|
| 130 |
+
get_minecraft_command=lambda *a, **k: (seen.update(a[2]), ["java"])[1]),
|
| 131 |
+
)
|
| 132 |
+
monkeypatch.setitem(_sys.modules, "minecraft_launcher_lib", fake_launcher)
|
| 133 |
+
monkeypatch.setattr(module, "CLIENT", tmp_path / "client")
|
| 134 |
+
monkeypatch.setattr(module, "MOD_CACHE", tmp_path / "cache")
|
| 135 |
+
monkeypatch.setattr(module, "_download",
|
| 136 |
+
lambda url, path, sha=None: (path.parent.mkdir(parents=True, exist_ok=True),
|
| 137 |
+
path.write_bytes(b"x"))[1])
|
| 138 |
+
assert runner._ensure_client(Path("java")) == ["java"]
|
| 139 |
+
# Vanilla --server/--port autojoin via ViaProxy; bridge keeps only fallback.
|
| 140 |
+
assert seen.get("server") == "127.0.0.1"
|
| 141 |
+
assert seen.get("port") == "25568"
|
| 142 |
+
options = (tmp_path / "client" / "options.txt").read_text(encoding="utf-8")
|
| 143 |
+
assert "onboardAccessibility:false" in options
|
| 144 |
+
# Second ensure must not duplicate the key.
|
| 145 |
+
runner._ensure_client(Path("java"))
|
| 146 |
+
options = (tmp_path / "client" / "options.txt").read_text(encoding="utf-8")
|
| 147 |
+
assert options.count("onboardAccessibility:false") == 1
|
| 148 |
+
|
| 149 |
def test_survival_verified_requires_explicit_survival():
|
| 150 |
good = {"connected": True, "error": "", "creative": False}
|
| 151 |
assert module.AltoClefStack._survival_verified(good) is True
|