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:
1 parent
86fe26d786
commit
526f6072f6
36 files changed
+1553
-1544
No files matched your search
+129
-126
@@ -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__)
|
||||
|
||||
Reference in new issue
Block a user