freedoom-licensing #1

Merged
thunerbl merged 15 commits from freedoom-licensing into main 2026-10-08 20:06:19 +00:00
Owner
No description provided.
Compiles DoomGeneric to WebAssembly with an SDL-free Emscripten backend and
serves it from the doom_manager Frappe app.

- doomgeneric_emscripten.c: platform backend (main loop, framebuffer hand-off,
  key queue)
- patch_wi_stuff.py: injects the level-complete bridge into wi_stuff.c by
  string search/insert, so it survives upstream line-number drift
- build.sh: manual emcc invocation (the Makefile.emscripten route linked SDL2
  and dropped our flags)
- docker/compose.yaml: Frappe v15 + MariaDB + Redis with doom_manager installed

doomgeneric/ is a submodule pinned to upstream dcb7a8d, declared ignore = dirty:
build.sh copies the backend into it and patches wi_stuff.c on every build, so
the checkout is a build directory whose edits are regenerated, not authored.
This is why no fork is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbn3DfU2ARbCgJ19kPjaoH
A run is now opened when a level starts and closed when it ends, either by
reaching the exit or by dying, and it carries the stats that were previously
computed and then dropped.

Engine (patch_doom_bridge.py, renamed from patch_wi_stuff.py since it now
patches two files):

- onDoomLevelStart, injected into g_game.c G_Ticker. Not into the obvious
  G_DoLoadLevel: G_DoPlayDemo calls G_InitNew -- which loads the level and
  clears demoplayback -- and only sets demoplayback = true after it returns, so
  inside G_DoLoadLevel the flag reads false even for attract-mode demos and
  every idle title screen would open a run. Testing (levelstarttic, gamemap)
  once per tic sees the flag settled, one tic later.
- onDoomGameOver, injected into g_game.c G_DoReborn. Doom 1 has no GAME OVER
  screen; in single player dying routes through G_DoReborn, which reloads the
  level -- and that reload opens the next run on its own.
- onDoomLevelComplete now reports raw counts with their level totals (6 kills
  out of 9) instead of percentages, and 1-based episode/map like the other two.

App (doom_manager, not a git repo -- listed here for the record):

- Doom Run gains items, secrets, total_kills, total_items, total_secrets and an
  outcome Select (In Progress / Completed / Died). record_run accepted items and
  secrets but wrote them nowhere, and the DocType had no field to hold them.
- api.start_run inserts the draft, api.finish_run fills it in and submits it;
  record_run stays as a one-shot wrapper. finish_run checks the run belongs to
  the session user and is still a draft.
- Play.vue holds the open run as a promise, not an id, so a death in the first
  seconds still finishes the right document.

Closing the tab mid-level leaves a draft behind; get_runs and get_leaderboard
filter on docstatus 1, so drafts stay out of the feed and the leaderboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbn3DfU2ARbCgJ19kPjaoH
Running `bench migrate` without `docker compose build && up -d` migrates the
old app baked into the image: it reports success and changes nothing, because
the app is COPYed in at build time and only `sites` and `logs` are volumes.
The README said to rebuild after changing app code but never mentioned migrate
for DocType changes, nor that the order is load-bearing.

Also notes that "Queued rebuilding of search index for frontend" is the normal
last line of a successful migrate -- the index rebuild is enqueued on the long
queue by Migrate.tearDown(), not run inline -- since it reads like the command
is still working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbn3DfU2ARbCgJ19kPjaoH
Adds a fourth bridge hook, window.onDoomStats(k, tk, i, ti, s, ts, secs),
injected into the G_Ticker block that already detects the start of a level. It
fires on every 7th tic -- 5 Hz, which lands a kill on screen at once while the
clock only shows whole seconds.

leveltime freezes while the menu is up, so the HUD freezes with the game rather
than drifting. A freeze on a multiple of 7 would otherwise re-emit the same
values every tic, hence the fdb_last_stats guard.

Co-authored-by: AI
Swaps the base image for registry.gitlab.com/dokos/dokos:latest so the stack
runs on Dodock, the Frappe fork, rather than upstream Frappe.

Nothing else had to change. The Dokos image keeps the frappe-bench layout and
still names the framework directory `apps/frappe` -- it reports version 5.16.5,
Dokos's own scheme, not Frappe 15 -- so the pip install and the asset symlink
below work unmodified. wait-for-it, jq and nginx-entrypoint.sh, which the
compose file relies on, are all present too.

The site installs doom_manager only, never erpnext/dokos, so this exercises the
framework fork on its own.

Co-authored-by: AI
Separate compose project (doomdokos), image tag (doom-dokos:local) and port
(8098), so the Frappe stack on 8099 keeps its containers and its named volumes
untouched and both can run at once.

Pairs with the dokos-base branch of the doom_manager repo, which switches the
base image; compose builds from that working tree, so the two branches have to
be checked out together.

Co-authored-by: AI
The shareware licence covers redistributing the complete, unmodified shareware
package -- not a lone IWAD extracted from it and served over HTTP, which is what
this was doing. The WAD was committed twice (same blob in both repos) and, more
to the point, handed to every visitor of /doom as doom.data. The README and the
write-ups all asserted it was "freely redistributable"; that claim is gone.

freedoom1.wad (BSD 3-clause, 27.5 MB) replaces it, with its COPYING and CREDITS
alongside as the licence requires. No engine change was needed: d_iwad.c already
lists freedoom1.wad, and the search runs over FILES_DIR "." which is / under
Emscripten, so preloading it under its own name is enough.

build.sh defaults to Freedoom rather than picking up whatever IWAD is lying
around -- a directory scan would silently bake a non-redistributable WAD into a
published doom.data. Using your own is opt-in and says so:

    IWAD=doom1.wad ./build.sh

LICENSES.md carries the GPL-2.0 written offer of source for the compiled engine,
which publishing doom.wasm obliges, and separates it from the app's MIT licence.

Still open: the shareware WAD remains in both repositories' history (13d7bff).
Making these repos public needs either a history rewrite or a fresh mirror.

Co-authored-by: AI
doom.data packs freedoom1.wad (BSD 3-clause), byte-identical to the Freedoom
release: the swap happens in the engine build, nothing here changed but
the artefacts.

App.vue gains a footer naming the engine's GPL-2.0 licence and pointing at the
source, plus Freedoom's. The binary is distributed from this page, so the notice
has to live here and not only in the repository.

Co-authored-by: AI
useDoomEngine.js passed arguments: ['-iwad', '/doom1.wad'] when creating the
module. That duplicated a decision owned by build.sh, and the two drifted apart
the moment the shipped WAD became freedoom1.wad: the engine took the -iwad
branch of D_FindIWAD, looked for a file that is no longer packaged, and aborted
with "IWAD file '/doom1.wad' not found!" before the first frame.

Dropping the argument lets D_FindIWAD fall through to its search, which walks
the iwads[] table over FILES_DIR "." -- / under Emscripten, where build.sh
preloads whichever WAD it used. Both the shipped Freedoom and a personal
doom1.wad are in that table, so the two sides no longer have to agree on a name.

Co-authored-by: AI
Two changes that go together, because both are about the engine binary and its
game data no longer being one artefact.

The IWAD leaves the build. --preload-file put it inside doom.data, which meant
the only way to play a different one was to rebuild, and a 27 MB WAD was
re-downloaded whenever the engine changed. build.sh now emits doom.js and
doom.wasm alone -- 380 KB together -- and publishes the shipped Freedoom as a
plain static asset next to them. The page fetches whichever IWAD the session may
play and writes it into MEMFS before main() runs.

That is also what makes uploads possible: a player's file keeps whatever name
they gave it, which the engine will not recognise, so the frontend passes -iwad
with the mounted path and D_IdentifyVersion works the game out from the lumps
instead. Verified both ways against a Freedoom and a shareware WAD mounted under
arbitrary names.

build.sh also writes doom.build.json with a hash of doom.wasm. /doom is
no_cache, so it can hand the SPA that build id to hang on the engine URLs as a
query string -- /assets carries no Cache-Control and the filenames are not
content-hashed, so a browser could otherwise keep a stale engine indefinitely.
That is the bug that made the shareware-to-Freedoom switch look like a failure.

Co-authored-by: AI
Doom Iwad stores an uploaded WAD; Doom Player Setting remembers which one a
player picked. The API lists the shipped Freedoom plus the caller's own and any
global ones, resolves the active IWAD for a session, and streams a stored one
through a permission check. A new Game data page drives all of it.

Two rules are enforced in the controller rather than in the UI, because a
personal IWAD is somebody's copy of a commercial game:

- It can never become the default. The default is what guests are served, so
  only a WAD whose hash is on the free list qualifies -- the shipped Freedoom
  hash comes from the build manifest, and site config can add others. There is
  no way to tell a free WAD from a commercial one by inspecting it, so this is
  an allowlist on purpose.
- It is refused to every session but its owner's. download_iwad checks
  readable_by() before opening the file, and a guest asking for one gets a
  PermissionError rather than the bytes.

The controller also rejects anything that is not an IWAD, PWADs included, by
reading the four-byte header before hashing.

Verified end to end: guest gets Freedoom, an upload registers as not-free,
selection takes effect, making it the default is refused, and a guest is refused
the download while still being served Freedoom.

www/doom.py now also passes the engine build id into boot data for the asset
cache busting that lives in the engine repo.

Co-authored-by: AI
Two defects in the previous commit, both from checking a diagnostic build
instead of the one that ships.

FS was never exported. The runtime WAD mount calls Module.FS.writeFile in
preRun, and EXPORTED_RUNTIME_METHODS listed only ccall, cwrap and HEAPU32 -- the
node build I verified against had FS added by hand, the real one did not. In the
browser that surfaced as "can't access property writeFile, r.FS is undefined".
The frontend now throws a sentence that names the cause instead.

The build id hashed doom.wasm alone. Changing EXPORTED_RUNTIME_METHODS rewrites
doom.js without touching the wasm, so this very fix would have shipped under an
unchanged version and stayed cached. It now hashes both files together.

Co-authored-by: AI
The preRun hook that mounts the IWAD assumed Module.FS exists, which it only
does when FS is in the engine's EXPORTED_RUNTIME_METHODS. When it was not, the
page died on "cannot read property writeFile of undefined", which says nothing
about the actual cause.

Co-authored-by: AI
History filtered down to the build pipeline, the docker stack and the
licence files. The social posts and id's shareware doom1.wad are left out
of every commit, and Freedoom's own copy is dropped as a duplicate of the
one already shipped in public/js.

Co-authored-by: AI
engine/build.sh writes into ../doom_manager/public/js and takes the shipped
freedoom1.wad from there, so the repo holds a single copy; a personal IWAD
still goes next to the script. The submodule moves to engine/doomgeneric,
compose builds from the repo root, and .dockerignore keeps engine/ and
docker/ out of the image.

The two READMEs split along the new layout: the root one covers running
and installing, engine/README.md the build and the bridge internals.
Paths pointing at GenericDoom_Frappe are updated, and the SPA rebuilt.

Co-authored-by: AI
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thunerbl/frappe-doom-manager!1
No description provided.