Звук в клиенте пропал из-за неверного каталога ассетов

Клиент запрашивал звук, движок его принимал и запускал, а на выходе было
тихо. Причина оказалась не в коде игры.

Minecraft 1.12.2 не умеет читать ассеты по индексу и объектному хранилищу.
Он ждёт обычное дерево: <assetsDir>/minecraft/<путь>. А лаунчер хранит
ассеты по-современному — indexes/1.12.json со ссылками на objects/<aa>/<sha1>.

Мы передавали лаунчеру корень этого хранилища как --assetsDir. Игре это не
выглядит как поломка: язык и текстуры берутся из самого jar, поэтому клиент
выглядит полностью рабочим. Но звуки с 1.7.3 лежат только в ассетах, и их
просто не было на диске. Движок поднимался, play() вызывался, ogg не
находился, а paulscode на это молчит.

- materialize_assets.py разворачивает дерево из индекса и хранилища,
  жёсткими ссылками где возможно, и идемпотентен
- run-game.ps1 материализует ассеты перед запуском и указывает клиенту на
  развёрнутое дерево; --assetIndex больше не нужен
- проверено: 1305 объектов, из них 1085 ogg, sounds.json на месте, ошибок
  загрузки в логе нет

Заодно убрана временная диагностика звука и движения, добавленная ради
поиска причины: она доказала, что запрос и воспроизведение доходят до
OpenAL, и свою задачу выполнила.

Полный prepare-game.ps1 после этого проходит целиком: 112 патчей,
FORK COMPILES CLEAN, 3268 классов, RsxDebug в дереве не осталось.
This commit is contained in:
sasheg dev
2026-10-19 00:04:51 +03:00
parent bdf0ae866a
commit 00671e7c0d
4 changed files with 100 additions and 104 deletions
-30
View File
@@ -1039,15 +1039,6 @@ public void sendChatMessage(String s, boolean flag) {
this.mc.player.sendChatMessage(s);
}
=== FILE net/minecraft/client/entity/EntityPlayerSP.java
--- BEFORE
public void onLivingUpdate() {
++this.sprintingTicksLeft;
--- AFTER
public void onLivingUpdate() {
net.rustnotmc.client.RsxDebug.movement(this);
++this.sprintingTicksLeft;
=== FILE net/minecraft/entity/Entity.java
--- BEFORE
this.collidedHorizontally = d7 != d0 || d9 != d2;
@@ -1098,25 +1089,4 @@ boolean flag = this.onGround || d1 != d1 && d1 < 0.0D;
--- AFTER
boolean flag = this.onGround || d8 != d1 && d8 < 0.0D;
=== FILE net/minecraft/client/audio/SoundHandler.java
--- BEFORE
Map<String, SoundList> map = this.getSoundMap(iresource.getInputStream());
--- AFTER
Map<String, SoundList> map = this.getSoundMap(iresource.getInputStream());
net.rustnotmc.client.RsxDebug.sound("parsed=" + (map == null ? "null" : String.valueOf(map.size())));
=== FILE net/minecraft/client/audio/SoundHandler.java
--- BEFORE
List<IResource> list = iresourcemanager.getAllResources(new ResourceLocation(s, "sounds.json"));
--- AFTER
List<IResource> list = iresourcemanager.getAllResources(new ResourceLocation(s, "sounds.json"));
net.rustnotmc.client.RsxDebug.sound("domain=" + s + " resources=" + list.size());
=== FILE net/minecraft/client/audio/SoundHandler.java
--- BEFORE
this.soundRegistry.clearMap();
Iterator iterator = iresourcemanager.getResourceDomains().iterator();
--- AFTER
this.soundRegistry.clearMap();
net.rustnotmc.client.RsxDebug.sound("domains=" + iresourcemanager.getResourceDomains());
Iterator iterator = iresourcemanager.getResourceDomains().iterator();
@@ -1,72 +0,0 @@
package net.rustnotmc.client;
import net.rustnotmc.loader.Log;
/**
* Temporary diagnostics, added to work out why jumping and flight misbehave.
*
* Static analysis kept producing confident and wrong answers about this path, so
* the state is measured instead. Delete this file and its patch entry once the
* movement bugs are closed.
*/
public final class RsxDebug {
private static int ticks;
private RsxDebug() {
}
/** One-off trace for the empty sound registry. */
public static void sound(String message) {
Log.info("DBG snd " + message);
}
/** Called from EntityPlayerSP.onLivingUpdate, once a second. */
public static void movement(Object self) {
if (++ticks % 20 != 0) {
return;
}
Log.info("DBG sp onGround=" + read(self, "onGround")
+ " inJump=" + read(self, "movementInput")
+ " isJumping=" + read(self, "isJumping")
+ " riding=" + read(self, "isRiding")
+ " srvWorld=" + read(self, "isServerWorld")
+ " allowFly=" + read(self, "allowFlying")
+ " flying=" + read(self, "isFlying")
+ " mY=" + read(self, "motionY")
+ " y=" + read(self, "posY"));
}
/**
* Reads a field by name.
*
* Deliberately reflective and deliberately forgiving: a diagnostic that
* throws when a field is renamed is worse than one that prints null.
*/
private static String read(Object target, String field) {
if (target == null) {
return "?";
}
try {
java.lang.reflect.Field f = findField(target.getClass(), field);
if (f == null) {
return "?";
}
f.setAccessible(true);
return String.valueOf(f.get(target));
} catch (Throwable t) {
return "!";
}
}
private static java.lang.reflect.Field findField(Class<?> type, String name) {
while (type != null) {
try {
return type.getDeclaredField(name);
} catch (NoSuchFieldException e) {
type = type.getSuperclass();
}
}
return null;
}
}
+10 -2
View File
@@ -28,7 +28,16 @@ if (-not $Java) {
}
if (-not $Java) { throw 'no Java 17 found' }
$assets = "$env:APPDATA\AstralRinthApp\meta\assets"
# The launcher's asset store is index-based: indexes/1.12.json pointing at
# objects/<aa>/<sha1>. A 1.12.2 client wants <assetsDir>/minecraft/<path> and
# will not look in either of those. Pointing --assetsDir at the store root
# leaves the game running and looking correct, because language and textures
# come from the jar, while every sound is silently missing.
$store = "$env:APPDATA\AstralRinthApp\meta\assets"
$assets = Join-Path $Run 'assets'
& python (Join-Path $root 'rsx-client\tools\materialize_assets.py') $store $assets --index 1.12
if ($LASTEXITCODE -ne 0) { throw 'asset materialisation failed' }
$cp = (Get-Content (Join-Path $Run 'classpath.txt') -Raw).Trim()
$natives = Join-Path $Run 'natives'
@@ -46,7 +55,6 @@ $javaArgs += '--username'; $javaArgs += 'Player'
$javaArgs += '--version'; $javaArgs += '1.12.2'
$javaArgs += '--gameDir'; $javaArgs += $Run
$javaArgs += '--assetsDir'; $javaArgs += $assets
$javaArgs += '--assetIndex'; $javaArgs += '1.12'
$javaArgs += '--uuid'; $javaArgs += '00000000000000000000000000000000'
$javaArgs += '--accessToken'; $javaArgs += '0'
$javaArgs += '--userType'; $javaArgs += 'legacy'
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Materialise a 1.12 asset tree out of a hash-addressed asset store.
Minecraft 1.12.2 does not read an index plus an object store. It reads
``<assetsDir>/minecraft/<path>`` directly. A launcher such as AstralRinth keeps
assets the modern way, as ``indexes/1.12.json`` pointing at
``objects/<aa>/<sha1>``, and a launcher that wants them for an old client is
responsible for laying them out.
Passing the store root as ``--assetsDir`` does not fail loudly. Texts and
textures still appear, because those live inside the client jar, so the game
looks correct while every sound is silently absent: nothing requested is ever
found on disk.
Usage:
materialize_assets.py <meta-assets-dir> <out-dir> [--index 1.12]
Hard links are used where the filesystem allows it, so the materialised tree
costs almost nothing and repeated runs are cheap.
"""
import argparse
import json
import os
import sys
def human(count):
return "{:,}".format(count).replace(",", " ")
def main(argv):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("meta_assets", help="store root holding indexes/ and objects/")
ap.add_argument("out", help="assets dir to create for the client")
ap.add_argument("--index", default="1.12", help="index file name (default: 1.12)")
args = ap.parse_args(argv)
meta = args.meta_assets
index_path = os.path.join(meta, "indexes", args.index + ".json")
if not os.path.isfile(index_path):
sys.exit("no asset index at " + index_path)
with open(index_path, "r", encoding="utf-8") as fh:
objects = json.load(fh)["objects"]
out = args.out
linked = copied = reused = 0
missing = []
for name, info in sorted(objects.items()):
digest = info["hash"]
src = os.path.join(meta, "objects", digest[:2], digest)
dst = os.path.join(out, *name.split("/"))
if os.path.isfile(src) and os.path.getsize(src) == info["size"]:
if os.path.exists(dst) and os.path.getsize(dst) == info["size"]:
reused += 1
continue
os.makedirs(os.path.dirname(dst), exist_ok=True)
if os.path.exists(dst):
os.remove(dst)
try:
os.link(src, dst)
linked += 1
except OSError:
with open(src, "rb") as r, open(dst, "wb") as w:
w.write(r.read())
copied += 1
else:
missing.append(name)
sounds = sum(1 for n in objects if n.endswith(".ogg"))
print("index : {} ({} objects, {} ogg)".format(
os.path.join("indexes", args.index + ".json"), human(len(objects)), human(sounds)))
print("output : {}".format(out))
print("hardlink: {} copy: {} reused: {}".format(
human(linked), human(copied), human(reused)))
if missing:
print("MISSING : {} object(s) not in the store".format(human(len(missing))))
for name in missing[:10]:
print(" " + name)
return 1
print("ok : every indexed object is present")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))