Инструменты для сверки с оригинальным байткодом

struct_diff сравнивает классы по составу и показывает, каких не хватает.
Но он отвечает на вопрос «того же ли набора методов», а не «тот ли код».
Оба вопроса приходится задавать: прыжки и звуки шагов были сломаны именно
кодом, который компилировался без ошибок и состоял из правильных методов.

javap_diff сравнивает инструкции построчно, вызывая javap на обоих деревьях
классификации. Наивное сравнение текста здесь бесполезно: перекомпиляция
перенумеровывает constant pool, поэтому почти каждая строка отличается
номером #123, хотя код совпадает. Инструмент убирает номера пула, сдвиги
смещений от ldc/ldc_w и форму инструкций, после чего остаётся реальная
разница.

На звуке инструмент показал, что все аудио-классы совпадают с оригиналом.
Остатки объясняются версией компилятора, а не правками: javac 8 собирает
конкатенацию строк через StringBuilder, javac 17 через invokedynamic, а
$values() у enum появился в более новых версиях.

ogg_duration читает частоту дискретизации из заголовка Vorbis и число
отсчётов из granule position последней страницы OggS. Отсюда видно, что файл
сам по себе короче или что его просто обрывают при воспроизведении, —
вопрос, который иначе приходится решать на слух.

Проверено, что все 1085 ogg совпадают с индексом по SHA-1.
This commit is contained in:
sasheg dev
2026-10-19 00:25:35 +03:00
parent 00671e7c0d
commit b6e847e981
4 changed files with 161 additions and 4 deletions
+2
View File
@@ -1090,3 +1090,5 @@ boolean flag = this.onGround || d1 != d1 && d1 < 0.0D;
boolean flag = this.onGround || d8 != d1 && d8 < 0.0D;
@@ -140,10 +140,10 @@ public final class RsxBootstrap {
* matrices already unwound, so the overlay sets up its own projection and
* leaves the state it did not set alone.
*/
public static void renderFrame(Minecraft minecraft) {
if (loader == null || surface == null || !surface.available()) {
return;
}
public static void renderFrame(Minecraft minecraft) {
if (loader == null || surface == null || !surface.available()) {
return;
}
if (frameFailed) {
return;
}
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Compare recompiled class files against the reference, instruction by instruction.
A naive text diff of two javap dumps is useless here: recompiling renumbers the
constant pool, so nearly every line differs by its ``#123`` reference even when
the code is identical. Normalising those references away turns the diff into
something that only reports real behavioural differences.
Usage:
javap_diff.py <javap-exe> <reference-classes-dir> <rebuilt-classes-dir> <class> [class ...]
"""
import re
import subprocess
import sys
# "12: invokevirtual #123 // Method foo" -> "invokevirtual // Method foo"
POOL = re.compile(r"#\d+(?:,\s*\d+)*\b")
# jump targets are printed as "42" / "123" after a branch opcode
OFFSET = re.compile(r"^(\s*\d+:\s*\w+)\s+\d+\s*$")
# ldc_w is only a wider index form of ldc; javap picks whichever fits
LDC_W = re.compile(r"\bldc_w\b")
# instruction offsets shift by one byte per ldc_w/ldc difference, so they carry
# no meaning here; the line order already encodes control flow
OFF = re.compile(r"^\s*\d+:\s*")
def dump(javap, classpath, name):
out = subprocess.run(
[javap, "-p", "-c", "-cp", classpath, name],
capture_output=True, text=True, errors="replace")
return out.stdout
def normalise(text):
lines = []
for raw in text.splitlines():
line = raw.rstrip()
if not line.strip():
continue
if line.startswith("Compiled from"):
continue
if "major version" in line:
continue
line = POOL.sub("#", line)
line = re.sub(r"#\s+", "# ", line)
line = LDC_W.sub("ldc", line)
m = OFFSET.match(line)
if m:
line = m.group(1) + " <target>"
line = OFF.sub("", line)
lines.append(line.strip())
return lines
def main(argv):
if len(argv) < 4:
sys.exit(__doc__)
javap, ref, new, classes = argv[0], argv[1], argv[2], argv[3:]
bad = 0
for cls in classes:
a = normalise(dump(javap, ref, cls))
b = normalise(dump(javap, new, cls))
if not a or not b:
print("=== {} : javap produced nothing (ref={} new={})".format(
cls, len(a), len(b)))
bad += 1
continue
if a == b:
print("=== {} : IDENTICAL ({})".format(cls, len(a)))
continue
sa, sb = set(a), set(b)
only_ref = [x for x in a if x not in sb]
only_new = [x for x in b if x not in sa]
print("=== {} : {} ref / {} new -> {} ref-only, {} new-only".format(
cls, len(a), len(b), len(only_ref), len(only_new)))
bad += 1
import difflib
for line in list(difflib.unified_diff(a, b, "reference", "rebuilt", lineterm="", n=2))[:80]:
print(" " + line)
return 1 if bad else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Report the real playback duration of Ogg Vorbis files, without decoding audio.
Reads the sample rate out of the Vorbis identification header and the total
sample count from the granule position of the final OggS page, then divides.
This is how you tell a sound file that is genuinely shorter from a sound that
merely stops early: the file either contains fewer samples or it does not.
Usage:
ogg_duration.py <file.ogg> [file.ogg ...]
"""
import struct
import sys
def info(path):
with open(path, "rb") as fh:
data = fh.read()
mark = data.find(b"\x01vorbis")
if mark < 0:
return None
# packet type(1) + 'vorbis'(6) + version(4) + channels(1) -> rate(4)
if len(data) < mark + 16:
return None
rate = struct.unpack_from("<I", data, mark + 12)[0]
channels = data[mark + 11]
granule = 0
pos = 0
while True:
pos = data.find(b"OggS", pos)
if pos < 0:
break
if pos + 27 <= len(data):
granule = struct.unpack_from("<q", data, pos + 6)[0]
seg_count = data[pos + 26]
table_end = pos + 27 + seg_count
body = sum(data[pos + 27:table_end])
pos = table_end + body
else:
pos += 4
seconds = granule / float(rate) if rate else 0.0
return {
"bytes": len(data),
"rate": rate,
"channels": channels,
"samples": granule,
"seconds": seconds,
}
def main(argv):
for path in argv:
got = info(path)
if got is None:
print("{:<52} not a Vorbis stream".format(path[-52:]))
continue
print("{:<52} {:>7.3f}s {:>6} Hz {:>2}ch {:>8} samples {:>6} B".format(
path[-52:], got["seconds"], got["rate"], got["channels"],
got["samples"], got["bytes"]))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))