Worker serve in workflow loader-mapview, 2026-09-24 (second run at 3 PM: all remeasured, control samples moved to Verifier, approval check renewed).
Files: wolfsdk/mapserver.py (new), wolfsdk/cli.py (+25 lines, command maps), wolfsdk/gui.py (+26/−2, button in Assets tab), tools/verify_mapview.py (new, with --control-samples). The frontend (wolfsdk/mapview/) originates from the parallel worker; this server serves it exclusively.
Results
python tools/verify_mapview.py→ 97/97 checks OK, rc=0, 36 s (with warm map cache). Checked c2v1, c12p1 (campaign), and the U-Boot.python tools/verify_mapview.py --control-samples→ 9/9, rc=0, 76 s. The control run without mutation remains green (0 red). Each of the 8 built-in bugs colors exactly the check it is supposed to catch red (table below). Control samples run in separate processes each and are repeatable at any time.- The Part-1 GUI is unchanged.
tools/verify_gui.pyran twice: once on a copy of the state before the change (gui.py and cli.py from HEAD, everything else as now, in a temp folder) and once on the current state. Both outputs are byte-identical (diffempty), 31 checks OK each, "All good." and rc=0. - Only
127.0.0.1listens (netstat per PID: 1/1 Socket). LAN addresses refuse connection (4/4),::1as well. - Path breakout: 36/36 attack paths yield 404, the secret next to the folder never emerges. Against the real Viewer folder, 6/6 hold.
- The click in the Loader costs the Tk-Thread 0.001 s (Socket bind). The first import of
mapserverwith backends costs an additional 0.03 s (3/3 measurements).
Structure
Loader (tkinter) --Click--> mapserver.start(tnc_root) -> http://127.0.0.1:<free>/
mapserver.open_app(url) -> msedge --app=<url> --start-fullscreen
--user-data-dir=%LOCALAPPDATA%\wolfsdk\edge-profile
python -m wolfsdk maps [id] [--no-browser] -> same, blocks until Ctrl+C
| Endpoint | Source | Delivery |
|---|---|---|
GET /, /<file> |
wolfsdk/mapview/ |
File, Content-Type from fixed table (not mimetypes: Windows Registry makes .js sometimes text/plain) |
GET /api/maps |
mapcatalog.maps |
JSON [{id,title,group,entities_bytes}]. Maps without .entities are missing (today: c5v1, the Exec carrier from dlc_5). Cached until a Spec or Archive changes (stats via base/*.resources, dlc/dlc_*/base/*, Spec); second fetch takes 0.00 s |
GET /api/map/<id>/scene.json |
mapgeo.scene |
Cache file, streamed. Also opens the map mount for /api/tex |
GET /api/map/<id>/geometry.bin |
mapgeo.scene |
Cache file, streamed in 1-MB blocks |
GET /api/map/<id>/collision.json |
mapcoll.collision |
Cache file, streamed |
GET /api/tex/<id>[?max=N] |
maptex.texture_block |
WTX1 + one Mip, Cache-Control: max-age=86400 (IDs are content-addressed) |
GET /api/tex/<id>.png[?max=N] |
maptex.texture_png |
PNG same Mip: Server gives both calls same max (maptex has 512 or 1024 as default) |
- A missing
maxdefaults to 1024.maxis clamped to the range 16..4096 and rounded down to the nearest power of two so that no single value generates its own cache file in maptex.max=abcyields 400. - Error codes:
KeyError(unknown map or texture) andTncImageError(no Mip) return 404. Everything else returns 500 with the German message astext/plain, e.g.,MapGeoError: game/dlc/c05/c5v1: keine .entities im Container. A request never crashes the server. - The server speaks HTTP/1.1 with Keep-alive; every response includes a Content-Length. Ranges are not supported; responses are always 200 with full content. Only GET and HEAD are allowed; POST returns 501.
- A texture ID does not carry a map.
/api/textherefore queries the last opened mounts (at most 3, newest first). A mount is opened by ascene.jsonrequest. Displaced mounts are not explicitly closed: running requests still hold them, and CPython closes them with the last reference. Verified: after three additional maps, the displaced mount and its archive file are released (weakref), even though maptex had already created state for it. - The race condition in mapcatalog's index is bypassed, not fixed. The server builds a mount's index using
find("", "")in the opening thread before another thread sees the mount.Mount._idxdoes not build the index in a thread-safe manner (see mapcatalog finding). With 8 threads, all 464/187/1328 textures were retrieved without false positives. - Security:
- Binding is restricted to
127.0.0.1, port 0 (the system selects a free one). Windows sockets receiveSO_EXCLUSIVEADDRUSEandallow_reuse_address = False, preventing anyone from binding the same port again. - The Host header must be
127.0.0.1:<port>orlocalhost:<port>; otherwise, a 403 is returned (protection against DNS rebinding). - Static paths:
:,\, NUL, and empty strings, as well as.or..segments, are rejected. The resolved path must then lie undermapview/and be a file. self.pathis split manually, not viaurlsplit: this prevents interpreting a leading//as a hostname.
- Binding is restricted to
- Lifespan: one server per process (
start()returns the same URL on subsequent calls). It runs as a daemon thread, withatexit(stop). Requests are handled by daemon threads so that an ongoing 100-MB download does not block the loader termination.log_messageis silent because stderr isNoneunder pythonw. HTTPServer.server_bindis bypassed. It callssocket.getfqdn, performing a reverse DNS lookup that can take seconds on Windows machines.- Browser launch:
- Edge launches from
%ProgramFiles(x86)%,%ProgramFiles%, and%LOCALAPPDATA%, followed by Chrome at the same locations (with its ownchrome-profile), finally falling back towebbrowser.open. If Popen fails with OSError,webbrowseris still attempted. - By using its own profile, a standalone full-screen app window opens instead of a tab in the user's browser.
- Edge launches from
- GUI:
- The Assets tab receives a header stating "Wolfenstein II: every map with geometry, textures, and collision" and an accent button on the right labeled "Maps in 3D (Full Screen)". The asset browser sits unchanged below it (screenshot verified).
- The button launches the TNC installation even if Part 1 is currently selected. If it is missing, a message appears and no server starts.
- The click binds only the socket (measured at 0.001 s); handling and browser launch occur in background threads; the status line displays the URL.
- CLI:
python -m wolfsdk mapsopens the map selection.maps <id>opens/?map=<id>, the URL parameter of the frontend.--no-browseroutputs only the address.- An unknown ID terminates with "Unknown map: …" and rc=1.
Numbers per Map (Verifier Run, warm cache)
| Map | scene.json | geometry.bin | collision.json | Polygons | Instances | Textures WTX1 | Sample = maptex | PNG = same Mip |
|---|---|---|---|---|---|---|---|---|
| game/dlc/c02/c2v1 | 2.4 MB, 0.3 s* | 16.0 MB, 0.1 s | 5.3 MB | 907/907 | 7741/7741 | 464/464 in 0.5 s | 20/20 | 2/2 (16², 1024²) |
| game/wolf/c12/c12p1 | 0.5 MB, 0.2 s* | 7.8 MB | 1.1 MB | 294/294 | 1384/1384 | 187/187 in 0.4 s | 20/20 | 2/2 |
| game/wolf/submarine/submarine | 13.6 MB, 0.4 s* | 87.9 MB, 0.3 s | 37.1 MB | 5533/5533 | 46736/46736 | 1328/1328 in 1.5 s | 20/20 | 2/2 |
* Includes opening the Mount.
Additionally, for each of the three maps:
- The delivered bytes are identical to the cache files (comparison and sha1), and the Content-Length matches.
- Each polygon resides in
geometry.bin(UV data directly follows position data; indices directly follow UV data). The file is contiguous and ends exactly at the last section. Every index is smaller than vcount. - Each collision hull points to an entity in
scene.json(7760/7760, 1549/1549, 46041/46041). - Each Albedo is located in
textures[](473/473, 190/190, 1345/1345). ?max=256yields a maximum of 256 px (5/5).
Counter-tests (--gegenproben)
Each line represents its own process: verify_mapview.py --mutation <name> builds the error via Monkeypatch only in this specific process and checks only the responsible sections. The file mapserver.py remains untouched.
| Mutation | Built-in Error | Expected (Red) | Result |
|---|---|---|---|
| none | Control, all sections of mutations | nothing | 0 red, rc 0 |
| path | static paths without bounding | "Escape attempts → 404" | red, plus 4 more |
| host | every Host-Header allowed | "foreign Host-Header → 403" | red |
| list | /api/maps unfiltered (c5v1 and entities key present) |
"every entry exactly …" | red, plus count |
| png | PNG with maptex-standard-max instead of equal max | "PNG: same Mip …" | red |
| short | File 1 byte shorter than Content-Length | "check_map without exception" (IncompleteRead) | red |
| fullscreen | Edge without --start-fullscreen |
"Edge found: …" | red |
| blocked | Button waits 1 s in Tk-Thread | "Click does not block Tk" | red |
| open | no Mount is ever evicted (MOUNTS = 99) |
"evicted Mount is freed" | red |
Additional fixed counter-tests:
- The netstat output must recognize
0.0.0.0and::. - Connection must succeed via
127.0.0.1. - The same secret file is delivered as soon as it exists in the directory.
- Without TNC installation, a message appears and no server starts.
Binding to 0.0.0.0 is not run as a mutation: This would briefly expose the server to the LAN and could trigger a firewall dialog. This direction is covered by the netstat counter-test and the LAN-refusal test.
Limits
- c2v1 is today the user's copy. The installation has
dlc/dlc_0, whose spec also refers togame/dlc/c02/c2v1. mapcatalog sorts dlc_0 before dlc_2, so/api/mapsreturns c2v1 in the "Own" group (2 × Own, 25 Campaign, 12 DLC, 1 Other). This is a priority question from mapcatalog; the server only forwards the catalog. The verified c2v1 numbers (464 textures, 907 surfaces) are those of the dlc_0 version. /api/texknows only textures of the last up to 3 maps opened viascene.json. After a server restart, there are 404s until the frontend reloadsscene.json. It does so on map switch and on reload anyway.- Mounts are cached by map ID. If Steam updates the game while the server is running, open mounts remain old. The scene and collision rebuild the map itself (keys via size+mtime); for textures only a loader restart helps.
- On Windows,
os.replacefails in a mapgeo/mapcoll rebuild as long as the server is streaming the same file. A new build only occurs after a game update or with a damaged cache. The request then gets 500; the next fetch succeeds. - There are neither ETag nor Range.
scene.json,geometry.bin, andcollision.jsonare fully transferred on every load. Locally this costs 0.1–0.4 s for 88 MB. - Opening a mount is globally serialized (
ponytail:comment, 1–2 s per map). Texture requests from other already-open maps do not wait for it. - Errors from mapcoll on broken user
.entities(AttributeError instead of CmError, see mapcoll finding) come as 500 with a message and do not bring the server down. - Inherited weaknesses of the backends that the server only forwards:
- Running Loaders and
python -m wolfsdk mapssimultaneously building the same map can cause mapgeo to stumble over fixed.tmpnames (mapgeo finding). The request gets 500; the next fetch hits the cache. - mapcoll holds its lock even on cache lookups. While another map builds,
collision.jsonof a finished map waits up to about 13 s. - A broken user archive in
dlc_*causesmapcatalog.maps()to fail; then/api/mapsreturns 500 and the selection remains empty.
- Running Loaders and
- Browser startup is stubbed in the Verifier; no window opens. That Edge really shows a fullscreen app window with these arguments is documented Chromium behavior and was not machine-tested. The screenshot check of the frontend runs on the frontend-worker and the Verifiers (headless Edge).
- The Verifier reads cache files under
%LOCALAPPDATA%\wolfsdk\mapcache. If they are missing, the first fetch builds them (U-Boot about 6 s geometry and 14 s collision). - The game directory was only read and the game was never started. Not touched: browser.py, tncview.py, tncimage.py, mapcatalog, mapgeo, maptex, mapcoll, and
wolfsdk/mapview/.
Recheck
python tools/verify_mapview.py # 97 checks, rc 0
python tools/verify_mapview.py --gegenproben # Control + 8 mutations, rc 0
python tools/verify_gui.py # rc 0, output as before the change
python -m wolfsdk maps --no-browser # Address, then e.g. /api/maps in browser
python -m wolfsdk maps game/wolf/c12/c12p1