Инструменты для сверки с оригинальным байткодом
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:]))
|
||||
@@ -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:]))
|
||||
Reference in New Issue
Block a user