Add AI-upscaled critter/object art as a new MDL mod type
build / build (push) Successful in 3m36s

Extends the MDL system beyond text overrides: bitmaps/<label>.json
names a model/scale and resource list, and mdlp extracts each
resource's frames, runs them through an ONNX Runtime CPU upscaler
baked into mdlp-image, re-quantizes to the game's palette, and writes
the result back - same build-time pipeline as message overrides.

Adds animgallery, a standalone viewer that renders any data
directory's animated resources as an HTML/GIF gallery with a
checkbox-driven MDL JSON generator, and -only-changed to preview just
what a mod overrides against the base game.

Fixes a real data race in hacked's ChangeNotifier.hashAll() (found via
go build -race causing non-deterministic resource counts), via a new
numbered patch rather than editing the vendored source.

Adds engine-patches/, a general-fixes counterpart to
android/engine-patches/ now staged and applied to the desktop builds
too (previously unpatched): a real GL_UNPACK_ROW_LENGTH stride bug, an
audio error-logging improvement, and two critter-rendering fixes -
mod art was losing to the base game's own re-opened resource files,
and critters rendered at the wrong size/position once that was fixed,
both from how the billboard renderer derives on-screen size and
placement straight from bitmap pixel dimensions.

Updates README.md and AGENTS.md to document engine-patches/.
This commit is contained in:
ml
2026-09-27 20:52:35 +02:00
parent 389abd7de7
commit ab2b778f44
25 changed files with 2009 additions and 35 deletions
+79 -5
View File
@@ -69,7 +69,11 @@ Section "5.1. Installing and playing" has the details.
## 2. Layout
- `engine/` - a vendored snapshot of the Shockolate engine source. Built
via Docker; see "4. Desktop build" below.
via Docker; see "4. Desktop build" below. `engine-patches/`
holds general (not Android-specific) fixes applied to a scratch copy at
build time - same convention as `android/engine-patches/` below, but
for the Linux/Windows desktop builds; `engine/` itself is never
modified. See "4.4. Engine patch reference" below for what each one does.
- `build-image/` - the Dockerfile (and supporting scripts) for the engine
build environment. Every third-party dependency the engine needs to
compile (SDL2, SDL2_mixer, the fluidsynth-lite MIDI synth, a MIDI
@@ -244,10 +248,11 @@ of `build-image/Dockerfile`) - no Windows machine, Wine, or VM involved
in building it. `engine/CMakeLists.txt` already has working `WIN32`/
`MINGW` branches from Shockolate's own upstream Windows build (built
natively via Git Bash/MinGW on a real Windows machine - see
`engine/build_win64.sh`/`engine/appveyor.yml`), so unlike the Quest
build's `android/engine-patches/`, no source patching is needed here -
this cross-compiles those same branches offline and reproducibly, from
Linux, via Docker.
`engine/build_win64.sh`/`engine/appveyor.yml`) - this cross-compiles those
same branches offline and reproducibly, from Linux, via Docker. It applies
the same `engine-patches/` series the Linux build does (section
4.4); unlike the Quest build's `android/engine-patches/`, no
Windows-specific source patching is needed on top of that.
`dist-win/`/the packaged `.zip` ship `systemshock.exe` with its DLLs
(SDL2, SDL2_mixer, GLEW, fluidsynth-lite, plus the MinGW pthread runtime)
@@ -306,6 +311,75 @@ Both CI systems read the key from these secrets: `ANDROID_KEYSTORE_BASE64`,
produces a debug APK and the GitHub build fails on a tag. `*.jks` and
`*.keystore` are git-ignored.
### 4.4. Engine patch reference
`engine-patches/` (applied to a scratch copy of `engine/` by
`build-image/build-engine.sh` and `build-engine-win.sh`) is a plain,
numbered patch series, same convention as the Quest build's
`android/engine-patches/` (section 5.4) - `engine/` itself is never
modified. It only carries fixes that are general (not Android-specific);
anything Quest/OpenXR-only belongs in `android/engine-patches/` instead,
which the desktop build does not apply.
- **`01-desktop-texture-row-length.patch`** - `opengl_cache_texture()`/
`set_texture()` (`OpenGL.cc`) passed the original 8-bit bitmap's own row
stride (`bm->row`) to `GL_UNPACK_ROW_LENGTH`, describing the wrong
layout to OpenGL for the actually-uploaded buffer (a separately
allocated RGBA32 conversion, `converted->pixels`) whenever the two
strides differ - corrupting the texture upload. Uses
`converted->pitch / 4` instead.
- **`02-desktop-audio-error-detail.patch`** - logs `SDL_GetError()`
alongside "Couldn't open audio device" instead of just the bare message.
- **`03-desktop-fix-mod-resource-priority.patch`** - `LoadModFiles()`
(`Modding.c`) registers mod resource files once at startup, but two
places re-open the base game's own object/texture art files afterward -
and opening a resource file re-registers every ID it contains,
unconditionally, with no check for whether a mod already claimed it (see
`ResReadDirEntries()` in `resfile.c`). That silently reverted any mod
override for that art back to the base game's own graphics, twice over:
`obj_set_secondary_properties()` (`objsim.c`) re-opens `objart2.res`/
`objart3.res` transiently, just to read frame-count metadata; later in
the same startup sequence, `load_dynamic_memory()` (`cybmem.c`) re-opens
`texture.res`/`handart.res`/`objart2.res`/`objart3.res` again, this time
persistently - these are the handles actual gameplay renders from, so
this second point is what mattered for the mod override actually
showing up in-game. Fixed by re-running `LoadModFiles()` right after each
place's re-opens, so mods regain priority. Re-registering a mod file's
resource IDs means actually reopening it (there's no "just re-register"
primitive), so `LoadModFiles()` now closes each mod file's previous
handle first - one open handle per mod file at a time, rather than
leaking a fresh file slot on every repeat call (a new game or a loaded
save can both re-run this whole sequence).
- **`04-desktop-fix-critter-sprite-scale.patch`** - critters render as
camera-facing 2D billboards (`gameobj.c`'s `FAUBJ_CRIT` case), not
UV-mapped 3D polygons, and their on-screen size comes directly from the
bitmap's own pixel width/height (`Bitmap.c`'s `g3_bitmap_common()`, via
`_g3d_bitmap_u_anchor`/`_g3d_bitmap_v_anchor` and `bm_w`/`bm_h`) - so a
mod that replaces a critter's art with a higher-resolution bitmap for
more detail made the critter itself render bigger, proportionally to
the resolution increase. Fixed by caching every critter view's base-game
bitmap size (`objsim.c`'s `cache_critter_native_size()`/
`get_critter_native_size()`, keyed by resource ID) while
`objart2.res`/`objart3.res`'s base-game data is still the active
resolver in `obj_set_secondary_properties()` (before patch 03's
`LoadModFiles()` call hands priority to a mod), then using that cached
size instead of the bitmap's own (possibly modded) dimensions when
computing the sprite's on-screen extent - threaded through
`_fr_draw_bitmap()` and `g3_light_anchor_bitmap()`'s new `native_w`/
`native_h` parameters, consumed and reset by `g3_bitmap_common()` so an
unrelated bitmap draw never inherits a stale override. A mod's higher
resolution now only changes visible detail, not the object's size.
This depends on the mod's bitmap keeping a real anchor point in its own
`Header.Area`, in the original, un-upscaled unit space (see
`modding/src/mdlp/upscale.go`'s `imageToBitmap()`) - `_fr_draw_bitmap()`
only forwards `native_w`/`native_h` down the anchor-aware rendering path
when given a positive anchor, so a zeroed-out one silently skips this
patch's override entirely, and an anchor scaled to the mod's higher
resolution instead of left in the original unit space visibly offsets
the sprite from where it should sit, since it shares its coordinate
system with this patch's own size override rather than with the
bitmap's real pixel dimensions.
## 5. Android / Quest build
**Prerequisites:**