Full correctness check against the docs: safer writes, reverts and texts
Safety - Writes are serialized (Workspace.lock): two background jobs rebuilding one package (a sound and its subtitle applied side by side) lost an edit. - A store copies a bank file or package as "original" only if it matches the shipped catalog (Workspace.unoriginal_files: in-memory sounds, bank copies of voice lines, subtitles); the editor refuses to switch stores while changes are applied. A second store could otherwise resurrect a reverted sound. - Mod import in the editor asks before overwriting files changed outside the tool (it forced silently). - Revert all in the editor reverts texts too; the uninstaller counts text edits before offering to delete the store. - ackit restore-backup: the full-backup restore decisions.md described; it keeps the sounds of WADs without a full copy recorded. - build_catalog also stops when a subtitle differs from the catalog. Languages and texts - Game.language() returns languages without voices (JPN, RUS, ...) and Game.voice_archive() their INT voices; ackit text --lang JPN works. - Menu lookups parse one cached file (find & replace: minutes -> ms). - Subtitles decode cp1252 as Windows does: the UTF-8-in-cp1252 line Speech_TAPE_J_Answer_1_00 (KOR/POL/POR/RUS) is written back byte-for-byte; SUB_KOR/SUB_RUS rebuilt from the original game (only those rows changed). - Mods exported while another language is loaded keep name and line. Other fixes - Down-mix folds extra channels in at -3 dB (a 5.1 centre voice was dropped). - e2e_editor no longer overwrites the user's settings.json; kill_game waits for the game to exit before reverts. - Recorder handles Int32/UInt8 input and split frames; package block cache locked; conflict messages, CLI help examples, stale docstrings and doc references fixed; tests no longer read this PC's game language. Docs (decisions, user guide, README, subtitles.md, banks.md, catalog README, REPORT, NOTES, CLAUDE.md) updated; check_docs: 102 claims match. 148 tests; e2e_features 9/9, e2e_editor and verify PASS; game original afterwards.
This commit is contained in:
@@ -223,6 +223,60 @@ def test_mod_with_texts(win, tmp_path):
|
||||
tx.revert_all()
|
||||
|
||||
|
||||
def test_revert_all_reverts_sounds_and_texts(win):
|
||||
w, root, _ = win
|
||||
before = files(root)
|
||||
tx = w.ws.texts
|
||||
tx.apply({"SUB:INT:%s:0" % synth.SPEECH_EVENT: "Undo me.", "MENU:INT:FRONTEND.int:Menu:Quit": "Leave"})
|
||||
s = w.ws.sound("CHR_RadioScanner_1")
|
||||
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
||||
w.revert_all()
|
||||
assert files(root) == before and not w.ws.store.entries and not tx.records()
|
||||
|
||||
|
||||
def test_mod_import_asks_before_overwriting(win, tmp_path):
|
||||
"""Files changed outside the editor are overwritten by a mod only when the user confirms."""
|
||||
w, root, originals = win
|
||||
s = w.ws.sound("CHR_RadioScanner_1")
|
||||
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
||||
pkg = str(tmp_path / "m.acmod")
|
||||
w.export_mod_dialog(path=pkg, keys=[s.key], title="m")
|
||||
other = synth.wem(0.4, 150)
|
||||
inject(w.ws.wad_path(s), {s.target: other}, {s.target: w.ws.limit(s)}) # another tool writes the slot
|
||||
asked = []
|
||||
w.confirm = lambda *a: asked.append(a) or False
|
||||
w.import_mod_dialog(path=pkg, confirm=False)
|
||||
assert asked and w.ws.current_bytes(s) == other # declined: left alone
|
||||
w.confirm = lambda *a: True
|
||||
w.import_mod_dialog(path=pkg, confirm=False)
|
||||
assert w.ws.state(s) == W.MODIFIED
|
||||
w.revert_all()
|
||||
|
||||
|
||||
def test_store_cannot_change_while_changes_are_applied(win, tmp_path, monkeypatch):
|
||||
from app import mainwindow
|
||||
w, root, _ = win
|
||||
s = w.ws.sound("CHR_RadioScanner_1")
|
||||
w.ws.apply([(s, synth.wem(0.5, 1200), {})])
|
||||
store = w.ws.store.root
|
||||
|
||||
class Dlg:
|
||||
def __init__(self, settings, parent):
|
||||
pass
|
||||
|
||||
def exec(self):
|
||||
return mainwindow.QDialog.DialogCode.Accepted
|
||||
|
||||
def values(self):
|
||||
return dict(w.settings, store_root=str(tmp_path / "elsewhere"))
|
||||
monkeypatch.setattr(mainwindow, "SettingsDialog", Dlg)
|
||||
told = []
|
||||
w.inform = lambda *a: told.append(a)
|
||||
w.settings_dialog()
|
||||
assert told and w.settings.get("store_root") != str(tmp_path / "elsewhere") and w.ws.store.root == store
|
||||
w.revert_all()
|
||||
|
||||
|
||||
def test_text_undone_outside_shows_banner(win):
|
||||
w, root, _ = win
|
||||
tx = w.ws.texts
|
||||
@@ -387,3 +441,18 @@ def test_editor_panel_fits_its_contents(win):
|
||||
need = w.editor.sizeHint().height()
|
||||
assert w.scroll.viewport().height() >= need - 2, (w.scroll.viewport().height(), need)
|
||||
assert w.vsplit.sizes()[0] >= 160
|
||||
|
||||
|
||||
def test_recorder_sample_formats(qapp):
|
||||
"""Input devices may deliver 16/32-bit integer, 8-bit or float samples, and reads can split a frame."""
|
||||
import numpy as np
|
||||
from app.audioio import Recorder
|
||||
r = Recorder()
|
||||
x = np.array([[0.5, -0.25], [0.125, -1.0]], np.float32)
|
||||
for sample, raw in (("<i2", (x * 32768).astype("<i2").tobytes()), ("<i4", (x * 2147483648.0).astype("<i4").tobytes()),
|
||||
("u1", (x * 128 + 128).astype("u1").tobytes()), ("<f4", x.astype("<f4").tobytes())):
|
||||
r.chunks, r._rest, r.channels, r.sample = [], b"", 2, sample
|
||||
r._ingest(raw[:3])
|
||||
r._ingest(raw[3:])
|
||||
got, _ = r.stop()
|
||||
assert np.allclose(got, x, atol=1 / 128), sample
|
||||
|
||||
Reference in New Issue
Block a user