translated the remaining german notes, python comments, help and messages to english (in-game strings, parsed report lines and cli values stay german)

This commit is contained in:
MineTec committed 2026-11-07 10:51:14 +01:00
1 parent 86fe26d786
commit 526f6072f6
36 files changed
+1553 -1544

No files matched your search

+129 -126
View File
@@ -1,34 +1,36 @@
#!/usr/bin/env python3
"""lamatest - unbeaufsichtigter Test-Harness fuer "Ein Koenigreich fuer ein Lama".
"""lamatest - unattended test harness for "Ein Koenigreich fuer ein Lama".
Startet groove.exe (Wine) unsichtbar auf Xvfb :5 mit Null-Audio-Sink, laedt den
TestLevel (Welt 9, Kapitel 1) ueber Spielstand-Slot 3 und macht Screenshots.
Starts groove.exe (Wine) invisibly on Xvfb :5 with a null audio sink, loads the
TestLevel (world 9, chapter 1) via save slot 3 and takes screenshots.
lamatest.py start Xvfb + Sink + Spiel, wartet aufs Fenster
lamatest.py shot out.png Screenshot (1920x1080) von :5
lamatest.py key <key> [hold] Taste halten (xdotool-Name, Default 0.12 s)
lamatest.py keys <k1> <k2> ... mehrere Tasten nacheinander (k[:hold])
lamatest.py seq <outdir> "TOKENS" Tastenfolge wie run --seq im laufenden Spiel (shot:/fp:/rj: ...)
lamatest.py tolevel Demo -> Hauptmenue -> Slot 3 -> TestLevel 1
lamatest.py tour <outdir> [n] Rundum-Fotos: drehen/laufen/Kamera
lamatest.py status laeuft was? Fenster? Prozesse?
lamatest.py stop Spiel beenden, Sink entladen, Xvfb killen
lamatest.py run <wad> <outdir> [n] WAD als T1L9M001.wad installieren, starten,
tolevel, tour, Crash-Erkennung, Log sichern
lamatest.py restore-wad Original-T1L9M001.wad (.orig) zurueck
run-Optionen: --seq "Up:1.5 x shot:name sleep=1 ..." statt Tour eine Tastenfolge;
shot:NAME = Foto NAME.jpg, fp:NAME = Foto in Lama-Sicht (Return gehalten),
dialog = Dialog wegklicken, keys wie bei 'keys' (Taste[:Haltezeit]),
Up+x:0.8 = Tasten gleichzeitig, rj:T1:T2 = Anlauf-Sprung (Up halten,
nach T1 s X, dann noch T2 s Up),
run:T:x@1.3,shot@5,Left@3-3.4 = Up T s halten, X/Foto/Taste nach Zeitplan
(run/Left:T:... haelt eine andere Taste)
--tour zusaetzlich zur --seq auch die Standard-Tour
--keep getestete WAD installiert lassen (sonst wird die vorher
installierte T1L9M001.wad wiederhergestellt - nicht .orig!)
lamatest.py start Xvfb + sink + game, waits for the window
lamatest.py shot out.png screenshot (1920x1080) of :5
lamatest.py key <key> [hold] hold a key (xdotool name, default 0.12 s)
lamatest.py keys <k1> <k2> ... several keys one after another (k[:hold])
lamatest.py seq <outdir> "TOKENS" key sequence like run --seq in the running game (shot:/fp:/rj: ...)
lamatest.py tolevel demo -> main menu -> slot 3 -> TestLevel 1
lamatest.py tour <outdir> [n] all-round photos: turn/walk/camera
lamatest.py status anything running? window? processes?
lamatest.py stop quit the game, unload the sink, kill Xvfb
lamatest.py run <wad> <outdir> [n] install WAD as T1L9M001.wad, start,
tolevel, tour, crash detection, save the log
lamatest.py restore-wad restore the original T1L9M001.wad (.orig)
run options: --seq "Up:1.5 x shot:name sleep=1 ..." a key sequence instead of the tour;
shot:NAME = photo NAME.jpg, fp:NAME = photo in llama view (Return held),
dialog = click dialog away, keys as with 'keys' (key[:hold time]),
Up+x:0.8 = keys simultaneously, rj:T1:T2 = running jump (hold Up,
X after T1 s, then Up for another T2 s),
run:T:x@1.3,shot@5,Left@3-3.4 = hold Up for T s, X/photo/key on a schedule
(run/Left:T:... holds a different key)
--tour run the standard tour in addition to --seq
--keep leave the tested WAD installed (otherwise the previously
installed T1L9M001.wad is restored - not .orig!)
Zustand/Logs: $LAMATEST_DIR (Default /tmp/lamatest-$USER).
Doku: ../notes/testharness.md
State/logs: $LAMATEST_DIR (default /tmp/lamatest-$USER).
Report lines (report.txt, "ergebnis: ...") and `status` output stay German: helper
scripts grep for them. OCR expectations are German game text.
Docs: ../notes/testharness.md
"""
import hashlib
import os
@@ -62,7 +64,7 @@ def log(*a):
def die(msg, code=1):
log("FEHLER:", msg)
log("ERROR:", msg)
sys.exit(code)
@@ -85,9 +87,9 @@ def read_pid(name):
return None
# ---------------------------------------------------------------- Prozesse
# ---------------------------------------------------------------- processes
def groove_procs():
"""Alle laufenden groove.exe-Prozesse: [(pid, environ-DISPLAY)]."""
"""All running groove.exe processes: [(pid, environ DISPLAY)]."""
out = []
for p in os.listdir("/proc"):
if not p.isdigit():
@@ -114,7 +116,7 @@ def foreign_groove():
def xdo(*args):
# Sicherheitsnetz: xdotool immer nur gegen :5
# safety net: xdotool only ever against :5
assert ENV["DISPLAY"] == DISPLAY
return sh(["xdotool"] + list(args))
@@ -130,7 +132,8 @@ def game_alive():
def error_dialog():
"""Wine-Fehlerdialog / Absturzfenster auf :5? Liefert Fenstertitel oder None."""
"""Wine error dialog / crash window on :5? Returns the window title or None
(matches English and German Wine dialog titles)."""
r = xdo("search", "--name", ".")
titles = []
for wid in r.stdout.split():
@@ -145,7 +148,7 @@ def error_dialog():
return None
# ---------------------------------------------------------------- Spielstaende
# ---------------------------------------------------------------- save games
def ksv_files():
return sorted(f for f in os.listdir(GAMEDIR) if f.lower().endswith(".ksv"))
@@ -162,11 +165,11 @@ def restore_saves():
for f in os.listdir(SAVEBAK):
src, dst = os.path.join(SAVEBAK, f), os.path.join(GAMEDIR, f)
if not os.path.exists(dst) or md5(src) != md5(dst):
log("Spielstand %s wurde veraendert -> wiederhergestellt" % f)
log("save game %s was modified -> restored" % f)
shutil.copy2(src, dst)
for f in ksv_files():
if not os.path.exists(os.path.join(SAVEBAK, f)):
log("neuer Spielstand %s vom Test -> entfernt" % f)
log("new save game %s from the test -> removed" % f)
os.remove(os.path.join(GAMEDIR, f))
@@ -177,35 +180,35 @@ def md5(p):
def check_slot3():
p = os.path.join(GAMEDIR, "(null)king2.ksv")
if not os.path.exists(p):
die("Slot 3 (%s) fehlt" % p)
die("slot 3 (%s) missing" % p)
b = open(p, "rb").read()
# Kartencursor (+4/+5) bestimmt, was auf der Auswahlkarte vorgewaehlt ist
# map cursor (+4/+5) decides what is preselected on the selection map
if len(b) != 256 or b[4] != 9 or b[5] != 1:
log("WARNUNG: Slot 3: Kartencursor nicht auf Welt 9/Kapitel 1 (Cursor %d/%d, "
"aktuell %d/%d)" % (b[4], b[5], b[0x3f], b[0x40]))
log("WARNING: slot 3: map cursor not on world 9/chapter 1 (cursor %d/%d, "
"current %d/%d)" % (b[4], b[5], b[0x3f], b[0x40]))
# ---------------------------------------------------------------- Kommandos
# ---------------------------------------------------------------- commands
def cmd_start(timeout=60):
os.makedirs(STATE, exist_ok=True)
f = foreign_groove()
if f:
die("Elias' eigene Spielinstanz laeuft (pid %s) - Test abgebrochen" % f)
die("Elias' own game instance is running (pid %s) - test aborted" % f)
if game_alive():
log("Spiel laeuft schon auf %s" % DISPLAY)
log("game already running on %s" % DISPLAY)
return
if not os.path.isdir(OVERLAY) or not os.path.realpath(
os.path.join(PREFIX, "dosdevices", "l:")) == os.path.realpath(OVERLAY):
die("Mod ist nicht aktiv (lama-mod an)")
die("mod is not active (lama-mod an)")
check_slot3()
backup_saves()
# Xvfb
if not read_pid("xvfb"):
if os.path.exists("/tmp/.X11-unix/X5") or os.path.exists("/tmp/.X5-lock"):
if sh(["xdpyinfo"]).returncode == 0:
log("fremder X-Server auf :5 laeuft bereits - wird mitbenutzt")
log("foreign X server already running on :5 - sharing it")
else:
die("/tmp/.X5-lock existiert, aber :5 antwortet nicht")
die("/tmp/.X5-lock exists, but :5 does not respond")
else:
xl = open(XVFBLOG, "w")
p = subprocess.Popen(["Xvfb", DISPLAY, "-screen", "0", "1920x1200x24",
@@ -217,16 +220,16 @@ def cmd_start(timeout=60):
break
time.sleep(0.1)
else:
die("Xvfb startet nicht (%s)" % XVFBLOG)
# Null-Sink (nie die App stummschalten!)
die("Xvfb does not start (%s)" % XVFBLOG)
# null sink (never mute the app!)
if not os.path.exists(pidfile("sink")):
r = sh(["pactl", "load-module", "module-null-sink", "sink_name=" + SINK,
"sink_properties=device.description=lama_stumm"])
if r.returncode == 0:
open(pidfile("sink"), "w").write(r.stdout.strip())
else:
log("WARNUNG: Null-Sink nicht ladbar:", r.stderr.strip())
# Spiel
log("WARNING: cannot load null sink:", r.stderr.strip())
# game
wl = open(WINELOG, "w")
p = subprocess.Popen(["wine", GAMEEXE], cwd=GAMEDIR, env=ENV, stdout=wl,
stderr=subprocess.STDOUT, start_new_session=True)
@@ -234,12 +237,12 @@ def cmd_start(timeout=60):
t0 = time.time()
while time.time() - t0 < timeout:
if find_window():
log("Fenster da nach %.1f s" % (time.time() - t0))
log("window up after %.1f s" % (time.time() - t0))
return
if p.poll() is not None and not game_alive():
die("wine beendet sich sofort (Log: %s)" % WINELOG)
die("wine exits immediately (log: %s)" % WINELOG)
time.sleep(0.5)
die("kein Groove-Fenster nach %d s" % timeout)
die("no Groove window after %d s" % timeout)
def shot(path):
@@ -253,8 +256,8 @@ def shot(path):
def small_shot(path):
"""Kleiner Screenshot (960x540 JPEG) fuer Serien - Platte ist knapp.
Mit LAMATEST_SHOT=1280 (Umgebung) 1280x720 / Qualitaet 85 (Vorschaubilder)."""
"""Small screenshot (960x540 JPEG) for series - disk space is tight.
With LAMATEST_SHOT=1280 (environment) 1280x720 / quality 85 (preview images)."""
w = os.environ.get("LAMATEST_SHOT")
scale, q = ("%sx" % w, "85") if w else ("50%", "80")
r = sh(["import", "-display", DISPLAY, "-window", "root", "-crop",
@@ -267,7 +270,7 @@ def small_shot(path):
def key(k, hold=0.12):
wid = find_window()
if not wid:
die("kein Groove-Fenster")
die("no Groove window")
xdo("windowfocus", "--sync", wid)
xdo("keydown", k)
time.sleep(float(hold))
@@ -284,7 +287,7 @@ def keys(seq, gap=0.35):
time.sleep(gap)
# Bildvergleich ueber kleine Graustufen-Thumbnails (nur ImageMagick noetig)
# image comparison via small greyscale thumbnails (only ImageMagick needed)
def thumb(path=None):
src = ["import", "-display", DISPLAY, "-window", "root", "-crop", "1920x1080+0+0",
"+repage"] if path is None else ["convert", path]
@@ -304,7 +307,7 @@ def mean(t):
def wait_stable(min_time=0.0, timeout=30, still=1.5, thresh=1.0, dark_ok=False):
"""Warten bis sich das Bild `still` Sekunden kaum aendert."""
"""Wait until the image barely changes for `still` seconds."""
t0 = time.time()
time.sleep(min_time)
last, since = thumb(), time.time()
@@ -320,9 +323,9 @@ def wait_stable(min_time=0.0, timeout=30, still=1.5, thresh=1.0, dark_ok=False):
# ---------------------------------------------------------------- Bilderkennung
# ---------------------------------------------------------------- image recognition
def rgb_thumb():
"""48x27 RGB-Thumbnail des aktuellen Bildes (Liste von (r,g,b)-Zeilen)."""
"""48x27 RGB thumbnail of the current image (list of rows of (r,g,b))."""
r = subprocess.run(["import", "-display", DISPLAY, "-window", "root", "-crop",
"1920x1080+0+0", "+repage", "-resize", "48x27!", "-depth", "8",
"rgb:-"], stdout=subprocess.PIPE, env=ENV)
@@ -339,18 +342,18 @@ def _rows_mean(t, ys):
def classify(t=None):
"""Grobe Bildklasse: black / letterbox / map / bright (Menue oder Spiel)."""
"""Rough image class: black / letterbox / map / bright (menu or game)."""
t = t or rgb_thumb()
if t is None:
return "none"
allm = _rows_mean(t, range(27))
if allm < 6:
return "black"
# nur oberste/unterste Thumbnail-Zeile pruefen: Untertitel liegen im unteren Balken
# only check the top/bottom thumbnail row: subtitles sit in the lower bar
top, bot, mid = _rows_mean(t, (0,)), _rows_mean(t, (26,)), _rows_mean(t, range(6, 18))
if top < 4 and bot < 4 and mid > 15:
return "letterbox" # Kamera-Flug / Dialog-Zwischensequenz
# Auswahlkarte Welt 9: fast einfarbig beige (~205,175,110)
return "letterbox" # camera fly-by / dialogue cutscene
# selection map world 9: almost uniformly beige (~205,175,110)
beige = sum(1 for row in t for (r, g, b) in row
if 180 < r < 230 and 150 < g < 200 and 85 < b < 140 and r > g > b)
if beige > 0.75 * 48 * 27:
@@ -359,7 +362,7 @@ def classify(t=None):
def hud_visible():
"""Goldene Energieleiste/Sonnenkrone oben links (Gameplay-HUD)?"""
"""Golden energy bar/sun crown at the top left (gameplay HUD)?"""
r = subprocess.run(["import", "-display", DISPLAY, "-window", "root", "-crop",
"900x110+100+10", "+repage", "-resize", "25%", "-depth", "8",
"rgb:-"], stdout=subprocess.PIPE, env=ENV)
@@ -370,8 +373,8 @@ def hud_visible():
def ocr(crop="1920x1080+0+0", threshold=None, psm=11, scale="50%"):
"""Text auf dem Bildschirm (tesseract, deu), klein geschrieben.
threshold (Prozent): nur sehr helle Schrift behalten (z. B. markierter Menuepunkt)."""
"""Text on screen (tesseract, deu), lower-cased.
threshold (percent): keep only very bright text (e.g. the highlighted menu item)."""
filt = ["-threshold", "%d%%" % threshold] if threshold else []
raw = subprocess.run(["import", "-display", DISPLAY, "-window", "root", "-crop",
crop, "+repage", "ppm:-"], stdout=subprocess.PIPE, env=ENV).stdout
@@ -384,7 +387,7 @@ def ocr(crop="1920x1080+0+0", threshold=None, psm=11, scale="50%"):
def crashed():
"""None oder Grund-String, wenn das Spiel abgestuerzt ist."""
"""None, or a reason string if the game has crashed (goes into report.txt)."""
if not game_alive():
return "groove.exe-Prozess weg"
e = error_dialog()
@@ -416,16 +419,16 @@ class Timeout(Exception):
pass
SLOT_CROP = "1600x280+160+740" # Textfeld "Gespeichertes Spiel N / Level Kapitel"
SLOT_CROP = "1600x280+160+740" # text field "Gespeichertes Spiel N / Level Kapitel" (German game text)
def slot_text():
# zwei Schwellen kombinieren - der Hintergrund flimmert, OCR schwankt
# combine two thresholds - the background flickers, OCR fluctuates
return ocr(SLOT_CROP, threshold=70, psm=6) + " | " + ocr(SLOT_CROP, threshold=82, psm=6)
def fuzzy_in(word, text, ratio=0.75):
"""word (evtl. mehrere Woerter) unscharf in text enthalten?"""
"""Is word (possibly several words) fuzzily contained in text?"""
import difflib
if word in text:
return True
@@ -446,63 +449,63 @@ def _has(*words):
def tolevel(skip_flyby=True, log_times=None):
"""Hauptmenue -> Spiel starten -> Slot mit TestLevel -> Kapitel 1 -> Gameplay."""
"""Main menu -> "Spiel starten" -> slot with TestLevel -> chapter 1 -> gameplay."""
T = log_times if log_times is not None else {}
t0 = time.time()
# 1) Hauptmenue (Logo ~2 s, Menue nach ~5-6 s). Kommt stattdessen die Demo,
# bringt Return zurueck ins Menue.
# 1) main menu (logo ~2 s, menu after ~5-6 s). If the demo comes instead,
# Return brings you back to the menu.
tx = time.time()
while True:
c = crashed()
if c:
raise Crash(c)
# Hauptmenue: Menuepunkte rechts unten; bei 75 % Schwelle bleibt nur der
# markierte (helle) Punkt lesbar -> prueft zugleich die Cursorposition.
# main menu: menu items at the bottom right; at a 75 % threshold only the
# highlighted (bright) item stays readable -> also checks the cursor position.
txt = ocr("700x300+1150+650", threshold=75, psm=6, scale="100%")
if fuzzy_in("spiel starten", txt):
log("Hauptmenue erkannt")
log("main menu detected")
time.sleep(0.5)
key("Return")
break
if fuzzy_in("gespeichertes", slot_text()):
break # schon im Slot-Bildschirm
break # already on the slot screen
if time.time() - tx > 40:
log("kein Menue erkannt -> Return (Demo/Logo ueberspringen)")
log("no menu detected -> Return (skip demo/logo)")
key("Return")
tx = time.time()
if time.time() - t0 > 90:
raise Timeout("Hauptmenue nicht erreicht")
time.sleep(1)
T["menu"] = round(time.time() - t0, 1)
# 2) Spielstandauswahl: ganz nach links (kein Wrap), dann den Slot mit TestLevel suchen
# 2) save game selection: all the way left (no wrap), then find the slot with TestLevel
wait_for(_has("gespeichertes"), 20, "Spielstandauswahl")
time.sleep(0.4)
# Dreier-Slider ohne Wrap: Slot 3 ist ganz rechts, weiteres Right bleibt dort.
# Also einfach 2x Right (egal wo man startet) und einmal per OCR pruefen.
# three-slot slider without wrap: slot 3 is at the far right, further Right stays there.
# So just press Right twice (wherever you start) and check once via OCR.
def _is_test():
return (fuzzy_in("testlevel", ocr(SLOT_CROP, threshold=70, psm=6))
or fuzzy_in("testlevel", ocr(SLOT_CROP, threshold=82, psm=6)))
for _ in range(2):
key("Right", 0.3) # 0.3 s halten: bei niedriger Bildrate gehen kurze Druecke verloren
key("Right", 0.3) # hold 0.3 s: short presses get lost at low frame rates
time.sleep(0.3)
for i in range(3): # unter Last kann ein Druck verloren gehen -> noch einmal Right
for i in range(3): # under load a press can get lost -> Right once more
time.sleep(0.6)
if _is_test():
log("Slot 3: TestLevel gefunden (Versuch %d)" % (i + 1))
log("slot 3: TestLevel found (attempt %d)" % (i + 1))
break
key("Right", 0.3)
else:
raise Timeout("kein Spielstand mit TestLevel gefunden (Slot 3 kaputt?)")
key("Return")
T["slot"] = round(time.time() - t0, 1)
# 3) Auswahlkarte
# 3) selection map
wait_for(lambda: classify() == "map", 30, "Auswahlkarte")
time.sleep(1.0)
# Vorauswahl auf der Karte = Kartencursor im KSV (+4/+5), geprueft in check_slot3()
# preselection on the map = map cursor in the KSV (+4/+5), checked in check_slot3()
key("Return")
t_load = time.time()
T["map"] = round(t_load - t0, 1)
# 4) Laden -> (Kamera-Flug im Letterbox) -> Gameplay
# 4) loading -> (camera fly-by in letterbox) -> gameplay
skipped = False
good = 0
states = []
@@ -513,16 +516,16 @@ def tolevel(skip_flyby=True, log_times=None):
st = classify()
if not states or states[-1] != st:
states.append(st)
log("Zustand: %s (%.1f s nach Level-Start)" % (st, time.time() - t_load))
log("state: %s (%.1f s after level start)" % (st, time.time() - t_load))
if st == "letterbox" and skip_flyby and not skipped:
time.sleep(0.5)
key("Return") # Kamera-Flug ueberspringen
key("Return") # skip the camera fly-by
skipped = True
T["flyby"] = round(time.time() - t_load, 1)
elif st == "letterbox" and skipped and time.time() - t_load > 15:
key("x") # Dialog weiterklicken
# Gameplay = helles Bild, kein Letterbox, und HUD sichtbar (oder nach
# Kamera-Flug / spaetestens 25 s - falls das HUD schon ausgeblendet ist)
key("x") # click through the dialogue
# gameplay = bright image, no letterbox, and HUD visible (or after the
# camera fly-by / after 25 s at the latest - in case the HUD has already faded out)
el = time.time() - t_load
if st == "bright" and el > 2 and (hud_visible() or "letterbox" in states or el > 25):
good += 1
@@ -536,7 +539,7 @@ def tolevel(skip_flyby=True, log_times=None):
T["gameplay"] = round(time.time() - t0, 1)
T["load"] = round(time.time() - t_load, 1)
T["states"] = states
log("Gameplay nach %.1f s (Laden %.1f s), Zustaende %s" % (T["gameplay"], T["load"], states))
log("gameplay after %.1f s (loading %.1f s), states %s" % (T["gameplay"], T["load"], states))
return T
@@ -549,7 +552,7 @@ def cmd_tolevel():
def settle_dialog(maxpress=12):
"""Dialog-/Zwischensequenz (Letterbox) wegklicken."""
"""Click away a dialogue/cutscene (letterbox)."""
n = 0
while classify() == "letterbox" and n < maxpress:
key("x")
@@ -559,8 +562,8 @@ def settle_dialog(maxpress=12):
def tour(outdir, n=8, prefix="tour"):
"""Fotoserie: n Bilder Kamera-Rundumblick (A), dann n Bilder Laufen+Drehen.
Liefert Liste der Dateien. Bilder als 960x540-JPEG (Platz sparen)."""
"""Photo series: n images of a camera pan (A), then n images walking+turning.
Returns the list of files. Images as 960x540 JPEG (saves space)."""
os.makedirs(outdir, exist_ok=True)
files = []
@@ -573,13 +576,13 @@ def tour(outdir, n=8, prefix="tour"):
files.append(f)
snap("start")
# Kamera einmal rundum: 0.5 s A-Taste ~ 90 Grad
# camera once all round: 0.5 s of the A key ~ 90 degrees
hold = 2.0 / max(n, 1)
for i in range(n):
key("a", hold)
time.sleep(0.7)
snap("cam%d" % i)
# Laufen: vorwaerts, zwischendurch drehen; Dialoge wegklicken
# walking: forwards, turning in between; click dialogues away
for i in range(n):
key("Up", 1.2)
time.sleep(0.4)
@@ -589,7 +592,7 @@ def tour(outdir, n=8, prefix="tour"):
snap("walk%d" % i)
key("Left" if i % 3 != 2 else "Right", 0.45)
time.sleep(0.3)
# Lama-Sicht (Return gehalten) einmal pro Richtung
# llama view (Return held) once per direction
key("Return", 1.5)
time.sleep(0.2)
snap("lamasicht")
@@ -600,7 +603,7 @@ def cmd_tour(outdir, n):
try:
fs = tour(outdir, n)
except Crash as e:
die("ABSTURZ: %s" % e, 3)
die("CRASH: %s" % e, 3)
print("\n".join(fs))
@@ -610,31 +613,31 @@ def install_wad(wad):
orig = MODWAD + ".orig"
if not os.path.exists(orig):
shutil.copy2(MODWAD, orig)
log("Original gesichert: %s" % orig)
log("original backed up: %s" % orig)
if os.path.abspath(MODWAD) != wad:
shutil.copyfile(wad, MODWAD)
# Overlay zeigt per Symlink auf MODWAD -> neu bauen nur wenn Link fehlt/falsch
# overlay points to MODWAD via symlink -> rebuild only if the link is missing/wrong
link = os.path.join(OVERLAY, "wads", "T1L9M001.wad")
if not (os.path.islink(link) and os.path.realpath(link) == os.path.realpath(MODWAD)):
log("Overlay-Link fehlt -> lama-mod an")
log("overlay link missing -> lama-mod an")
sh([LAMA_MOD, "an"], check=True)
others = [f for f in os.listdir(os.path.dirname(link))
if f.lower() == "t1l9m001.wad" and f != "T1L9M001.wad"]
if others:
log("WARNUNG: weitere Schreibweise im Overlay: %s" % others)
log("WAD installiert: %s (%d Bytes, md5 %s)" % (wad, os.path.getsize(MODWAD), md5(MODWAD)[:12]))
log("WARNING: other spelling in the overlay: %s" % others)
log("WAD installed: %s (%d bytes, md5 %s)" % (wad, os.path.getsize(MODWAD), md5(MODWAD)[:12]))
def cmd_restore_wad():
orig = MODWAD + ".orig"
if not os.path.exists(orig):
die("kein %s" % orig)
die("no %s" % orig)
shutil.copy2(orig, MODWAD)
log("Original-T1L9M001.wad wiederhergestellt")
log("original T1L9M001.wad restored")
def run_seq(outdir, seq):
"""Tastenfolge abarbeiten; 'shot:NAME' fotografiert. Liefert Bilddateien."""
"""Process a key sequence; 'shot:NAME' takes a photo. Returns the image files."""
files = []
for item in seq.split():
c = crashed()
@@ -645,7 +648,7 @@ def run_seq(outdir, seq):
small_shot(f)
files.append(f)
elif item.startswith("fp:"):
# Lama-Sicht: Return halten, waehrenddessen fotografieren
# llama view: hold Return, take the photo meanwhile
wid = find_window()
xdo("windowfocus", "--sync", wid)
xdo("keydown", "Return")
@@ -656,7 +659,7 @@ def run_seq(outdir, seq):
xdo("keyup", "Return")
time.sleep(0.4)
elif item.startswith("rj:"):
# Anlauf-Sprung: Up halten, nach T1 s X tippen, Up noch T2 s halten
# running jump: hold Up, tap X after T1 s, keep holding Up for T2 s
_, t1, t2 = item.split(":")
wid = find_window()
xdo("windowfocus", "--sync", wid)
@@ -669,9 +672,9 @@ def run_seq(outdir, seq):
xdo("keyup", "Up")
time.sleep(0.3)
elif item.startswith("run:") or item.startswith("run/"):
# Durchlaufen mit Zeitplan: run:T:x@2.1,x@3.4,shot@4.0,Left@5.0-5.3
# Up (bzw. bei run/Left:... die angegebene Taste) wird T s gehalten; zu den Zeiten (s ab Beginn) X tippen / Foto (seq_NN_runK) /
# Taste von-bis halten. Fotos dauern ~0.2 s, der Zeitplan laeuft weiter (Echtzeit).
# run through with a schedule: run:T:x@2.1,x@3.4,shot@4.0,Left@5.0-5.3
# Up (or the given key for run/Left:...) is held for T s; at the given times (s from start) tap X / photo (seq_NN_runK) /
# hold a key from-to. Photos take ~0.2 s, the schedule keeps running (real time).
parts = item.split(":", 2)
mk = parts[0][4:] or "Up"
total, ev = float(parts[1]), []
@@ -709,7 +712,7 @@ def run_seq(outdir, seq):
xdo("keyup", arg)
time.sleep(0.3)
elif item.startswith("hop:"):
# Up halten und N-mal alle T s springen (Fotos dazwischen: shot nach dem Token)
# hold Up and jump N times every T s (photos in between: shot after the token)
_, n, t = item.split(":")
wid = find_window()
xdo("windowfocus", "--sync", wid)
@@ -719,7 +722,7 @@ def run_seq(outdir, seq):
xdo("keyup", "Up")
time.sleep(0.3)
elif item.startswith("dj:"):
# Doppelsprung mit Anlauf: Up halten, X, nach 0.3 s nochmal X, Up noch T s
# double jump with run-up: hold Up, X, X again after 0.3 s, Up for another T s
t = float(item.split(":")[1])
wid = find_window()
xdo("windowfocus", "--sync", wid)
@@ -730,7 +733,7 @@ def run_seq(outdir, seq):
xdo("keyup", "Up")
time.sleep(0.3)
elif "+" in item.split(":")[0]:
# mehrere Tasten gleichzeitig: Up+x:0.8
# several keys simultaneously: Up+x:0.8
ks, _, h = item.partition(":")
wid = find_window()
xdo("windowfocus", "--sync", wid)
@@ -753,8 +756,8 @@ def cmd_run(wad, outdir, n=8, seq=None, do_tour=None, keep=False):
rep = ["wad: %s" % os.path.abspath(wad)]
status = 0
if foreign_groove():
die("Elias' eigene Spielinstanz laeuft - abgebrochen")
# vorher installierte WAD merken -> danach zurueck (nicht .orig!)
die("Elias' own game instance is running - aborted")
# remember the previously installed WAD -> put it back afterwards (not .orig!)
prev = os.path.join(STATE, "vorher_T1L9M001.wad")
os.makedirs(STATE, exist_ok=True)
if not keep and os.path.exists(MODWAD):
@@ -775,7 +778,7 @@ def cmd_run(wad, outdir, n=8, seq=None, do_tour=None, keep=False):
if do_tour or (do_tour is None and not seq):
fs += tour(outdir, n)
rep.append("bilder: %d" % (len(fs) + 2))
# eingefroren? (Kamera dreht sich -> aufeinanderfolgende Bilder muessen sich aendern)
# frozen? (the camera turns -> consecutive images must differ)
th = [thumb(f) for f in fs]
dmax = max([diff(a, b) for a, b in zip(th, th[1:])] or [0])
rep.append("bildaenderung max: %.1f" % dmax)
@@ -808,12 +811,12 @@ def cmd_run(wad, outdir, n=8, seq=None, do_tour=None, keep=False):
cmd_stop()
finally:
if os.path.exists(WINELOG):
# Log kuerzen: max. 2 MB behalten (Anfang + Ende)
# truncate the log: keep at most 2 MB (start + end)
data = open(WINELOG, "rb").read()
if len(data) > 2_000_000:
data = data[:1_000_000] + b"\n...[gekuerzt]...\n" + data[-1_000_000:]
open(os.path.join(outdir, "wine.log"), "wb").write(data)
# bekannte harmlose Meldungen (laufen auch bei funktionierendem Spiel)
# known harmless messages (also appear when the game works fine)
noise = ("com_get_class_object", "ChangeDisplaySettingsExW",
"wined3d_restore_display_modes", "gstreamer", "MIDIMAP",
"RestoreDisplayMode")
@@ -844,7 +847,7 @@ def cmd_stop():
pass
time.sleep(1)
if f:
log("fremde Instanz laeuft (%s) - kein wineserver -k, nur eigene Prozesse" % f)
log("foreign instance running (%s) - no wineserver -k, only own processes" % f)
for p, d in groove_procs():
if d == DISPLAY:
os.kill(p, signal.SIGKILL)
@@ -875,13 +878,13 @@ def cmd_stop():
os.remove(pidfile("sink"))
except OSError:
pass
# Sicherheitsnetz: Sink per Name entladen, falls Modulnummer verloren
# safety net: unload the sink by name in case the module number got lost
r = sh(["pactl", "list", "short", "modules"])
for line in r.stdout.splitlines():
if "sink_name=" + SINK in line:
sh(["pactl", "unload-module", line.split()[0]])
restore_saves()
log("gestoppt")
log("stopped")
def cmd_status():
@@ -911,7 +914,7 @@ def main(argv):
keys(a)
elif c == "tolevel":
cmd_tolevel()
elif c == "seq": # Tastenfolge wie run --seq, aber im laufenden Spiel: seq <outdir> "TOKENS"
elif c == "seq": # key sequence like run --seq, but in the running game: seq <outdir> "TOKENS"
os.makedirs(a[0], exist_ok=True)
for f in run_seq(a[0], a[1]):
print(f)
@@ -933,7 +936,7 @@ def main(argv):
sys.exit(cmd_run(pos[0], pos[1], int(pos[2]) if len(pos) > 2 else 8, seq, do_tour, keep))
elif c == "restore-wad":
cmd_restore_wad()
elif c == "mean": # Debug: Helligkeit/Diff
elif c == "mean": # debug: brightness/diff
print(mean(thumb(a[0] if a else None)))
else:
print(__doc__)