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:
@@ -0,0 +1,30 @@
|
||||
--- a/src/MacSrc/OpenGL.cc 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/MacSrc/OpenGL.cc 2026-09-27 11:42:22.497868697 +0200
|
||||
@@ -562,7 +562,14 @@
|
||||
static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) {
|
||||
SDL_GL_MakeCurrent(window, context);
|
||||
|
||||
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||
+ // GL_UNPACK_ROW_LENGTH must describe the buffer actually passed to
|
||||
+ // glTexImage2D() below (toCache.converted->pixels, a separately
|
||||
+ // allocated SDL_PIXELFORMAT_RGBA32 surface from
|
||||
+ // SDL_ConvertSurfaceFormat()) - not bm->row, the original 8-bit source
|
||||
+ // bitmap's own unrelated row stride. Using bm->row here describes the
|
||||
+ // wrong stride to OpenGL whenever the two differ, corrupting the
|
||||
+ // texture upload.
|
||||
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, toCache.converted->pitch / 4);
|
||||
|
||||
if (texturesByBitsPtr.size() < MAX_CACHED_TEXTURES) {
|
||||
// We have enough room, generate the new texture
|
||||
@@ -696,7 +703,10 @@
|
||||
t->lastDrawTime = *tmd_ticks;
|
||||
|
||||
if (isDirty) {
|
||||
- glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
|
||||
+ // Same fix as opengl_cache_texture() above - t->converted->pixels is
|
||||
+ // what's actually uploaded, so its own pitch is what
|
||||
+ // GL_UNPACK_ROW_LENGTH needs to describe, not bm->row.
|
||||
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, t->converted->pitch / 4);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, t->converted->pixels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- a/src/MacSrc/SDLSound.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/MacSrc/SDLSound.c 2026-09-27 11:42:37.969726579 +0200
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 2048) < 0) {
|
||||
- ERROR("%s: Couldn't open audio device", __FUNCTION__);
|
||||
+ ERROR("%s: Couldn't open audio device: %s", __FUNCTION__, SDL_GetError());
|
||||
}
|
||||
|
||||
Mix_AllocateChannels(SND_MAX_SAMPLES);
|
||||
@@ -0,0 +1,98 @@
|
||||
--- a/src/MacSrc/Modding.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/MacSrc/Modding.c 2026-09-27 19:44:24.718059783 +0200
|
||||
@@ -27,6 +27,11 @@
|
||||
char *modding_additional_files[MAX_MOD_FILES];
|
||||
int num_mod_files;
|
||||
|
||||
+// One filenum per modding_additional_files[] entry, or -1 if not open yet -
|
||||
+// lets LoadModFiles() close a mod file's previous handle before reopening it
|
||||
+// (see LoadModFiles() below for why it needs to be called more than once).
|
||||
+static int32_t modding_loaded_filenum[MAX_MOD_FILES];
|
||||
+
|
||||
int StringEndsWith(char *src, char *dst) {
|
||||
char *s = strrchr(src, '.');
|
||||
|
||||
@@ -86,6 +91,7 @@
|
||||
modding_archive_override = NULL;
|
||||
for (int i = 0; i < MAX_MOD_FILES; i++) {
|
||||
modding_additional_files[i] = NULL;
|
||||
+ modding_loaded_filenum[i] = -1;
|
||||
}
|
||||
|
||||
// Now go process args
|
||||
@@ -135,10 +141,24 @@
|
||||
}
|
||||
|
||||
int LoadModFiles() {
|
||||
+ // Called again by obj_set_secondary_properties() (objsim.c) after it
|
||||
+ // re-opens the base game's own object art files, which would otherwise
|
||||
+ // silently reclaim any resource ID a mod file already overrode here (see
|
||||
+ // ResReadDirEntries() in resfile.c - opening a file re-registers every
|
||||
+ // resource ID it contains, last one in wins, with no check for whether
|
||||
+ // an ID is already claimed). Re-registering means actually re-opening
|
||||
+ // (there is no "just re-register" primitive), so a mod file already open
|
||||
+ // from an earlier call is closed first - one open handle per mod file at
|
||||
+ // a time, rather than leaking a fresh resFile[] slot on every repeat call
|
||||
+ // (new game, load game - object_data_load() can run more than once per
|
||||
+ // process).
|
||||
for (int i = 0; i < num_mod_files; i++) {
|
||||
if (modding_additional_files[i] != NULL) {
|
||||
+ if (modding_loaded_filenum[i] >= 0) {
|
||||
+ ResCloseFile(modding_loaded_filenum[i]);
|
||||
+ }
|
||||
printf("Loading mod file %s\n", modding_additional_files[i]);
|
||||
- ResOpenFile(modding_additional_files[i]);
|
||||
+ modding_loaded_filenum[i] = ResOpenFile(modding_additional_files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
--- a/src/GameSrc/objsim.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/GameSrc/objsim.c 2026-09-27 19:44:17.014441306 +0200
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "gettmaps.h"
|
||||
#include "lvldata.h"
|
||||
#include "mapflags.h"
|
||||
+#include "Modding.h"
|
||||
#include "objart2.h"
|
||||
#include "objart3.h"
|
||||
#include "objects.h"
|
||||
@@ -1877,6 +1878,13 @@
|
||||
// Warning(("Problem opening object art cache!\n"));
|
||||
return (ERR_NOEFFECT);
|
||||
}
|
||||
+ // The two opens above re-register every resource ID objart2.res/
|
||||
+ // objart3.res contain, reclaiming any of them a mod file already
|
||||
+ // overrode (see LoadModFiles() in Modding.c) - restore mod priority
|
||||
+ // for the reftable reads below. load_dynamic_memory() (cybmem.c) reopens
|
||||
+ // these same two files again later, persistently, for actual rendering -
|
||||
+ // restoring priority there too.
|
||||
+ LoadModFiles();
|
||||
|
||||
// Now go and set specific computed data, like frame counts
|
||||
for (i = 0; i < NUM_CRITTER; i++) {
|
||||
--- a/src/GameSrc/cybmem.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/GameSrc/cybmem.c 2026-09-27 19:44:24.718059783 +0200
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "textmaps.h"
|
||||
#include "objcrit.h"
|
||||
#include "dynmem.h"
|
||||
+#include "Modding.h"
|
||||
#include "Shock.h"
|
||||
#include "sideicon.h"
|
||||
#include "criterr.h"
|
||||
@@ -120,6 +121,15 @@
|
||||
critical_error(CRITERR_RES | 8);
|
||||
}
|
||||
|
||||
+ // The four opens above re-register every resource ID their files
|
||||
+ // contain, reclaiming any of them a mod file already overrode (see
|
||||
+ // LoadModFiles() in Modding.c) - these are the persistent handles
|
||||
+ // actual gameplay renders from (unlike obj_set_secondary_properties()'s
|
||||
+ // own transient open/close of objart2.res/objart3.res for reading
|
||||
+ // frame-count metadata), so this is the last point mod priority needs
|
||||
+ // restoring before rendering.
|
||||
+ LoadModFiles();
|
||||
+
|
||||
loadcount |= mask;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
--- a/src/GameSrc/objsim.c 2026-09-27 19:44:17.014441306 +0200
|
||||
+++ b/src/GameSrc/objsim.c 2026-09-27 20:44:23.147083066 +0200
|
||||
@@ -1867,6 +1867,40 @@
|
||||
return (OK);
|
||||
}
|
||||
|
||||
+// Every critter view's base-game bitmap size, keyed by resource ID - filled
|
||||
+// in by cache_critter_native_size() below while objart2.res/objart3.res's
|
||||
+// base-game data is still the active resolver for these IDs (before
|
||||
+// LoadModFiles() hands priority to a mod again), read back by
|
||||
+// get_critter_native_size() in gameobj.c. A mod that replaces a critter's
|
||||
+// art with a different-resolution bitmap changes how much detail is visible,
|
||||
+// not the object's on-screen size: g3_bitmap_common() (Bitmap.c) uses this
|
||||
+// cached size instead of the (possibly modded, possibly larger or smaller)
|
||||
+// bitmap's own width/height when computing the sprite's world-space extent.
|
||||
+// Id is uint16_t, so a flat array indexed by it needs no bounds check.
|
||||
+static short s_critterNativeW[65536];
|
||||
+static short s_critterNativeH[65536];
|
||||
+
|
||||
+static void cache_critter_native_size(Id id) {
|
||||
+ FrameDesc *f;
|
||||
+ if (s_critterNativeW[id] != 0)
|
||||
+ return;
|
||||
+ f = (FrameDesc *)RefLock(MKREF(id, 0));
|
||||
+ if (f == NULL)
|
||||
+ return;
|
||||
+ s_critterNativeW[id] = f->bm.w;
|
||||
+ s_critterNativeH[id] = f->bm.h;
|
||||
+ RefUnlock(MKREF(id, 0));
|
||||
+}
|
||||
+
|
||||
+// w/h are left untouched (so a caller can default to 0, meaning "no cached
|
||||
+// native size, use the bitmap's own dimensions") if id was never cached.
|
||||
+void get_critter_native_size(Id id, short *w, short *h) {
|
||||
+ if (s_critterNativeW[id] == 0)
|
||||
+ return;
|
||||
+ *w = s_critterNativeW[id];
|
||||
+ *h = s_critterNativeH[id];
|
||||
+}
|
||||
+
|
||||
errtype obj_set_secondary_properties() {
|
||||
char i, j;
|
||||
RefTable *prt;
|
||||
@@ -1878,6 +1912,28 @@
|
||||
// Warning(("Problem opening object art cache!\n"));
|
||||
return (ERR_NOEFFECT);
|
||||
}
|
||||
+
|
||||
+ // Snapshot every critter view's base-game bitmap size while it's still
|
||||
+ // the active resolver for these IDs, before LoadModFiles() below hands
|
||||
+ // priority back to a mod - see cache_critter_native_size() and its own
|
||||
+ // comment. Full 8-view postures need every view individually (their IDs
|
||||
+ // are 8 consecutive resource IDs, one per view); front-only postures are
|
||||
+ // a single ID (matching the frame-count loop below).
|
||||
+ for (i = 0; i < NUM_CRITTER; i++) {
|
||||
+ if (CritterProps[i].views > 1) {
|
||||
+ for (j = 0; j < NUM_CRITTER_POSTURES; j++) {
|
||||
+ if (j >= FIRST_FRONT_POSTURE) {
|
||||
+ cache_critter_native_size(posture_bases[j] + i);
|
||||
+ } else {
|
||||
+ int v;
|
||||
+ for (v = 0; v < 8; v++) {
|
||||
+ cache_critter_native_size(critter_id_table[i] + posture_bases[j] + v);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
// The two opens above re-register every resource ID objart2.res/
|
||||
// objart3.res contain, reclaiming any of them a mod file already
|
||||
// overrode (see LoadModFiles() in Modding.c) - restore mod priority
|
||||
--- a/src/GameSrc/Headers/objsim.h 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/GameSrc/Headers/objsim.h 2026-09-27 19:39:39.585009491 +0200
|
||||
@@ -108,6 +108,7 @@
|
||||
errtype obj_load_properties();
|
||||
errtype obj_create_player(ObjLoc *plr_loc);
|
||||
Ref ref_from_critter_data(ObjID oid, int triple, byte posture, short frame, short view);
|
||||
+void get_critter_native_size(Id id, short *w, short *h);
|
||||
errtype obj_zero_unused();
|
||||
grs_bitmap *obj_get_model_data(ObjID id, fix *x, fix *y, fix *z, grs_bitmap *bm2, Ref *ref1, Ref *ref2);
|
||||
errtype obj_model_hack(ObjID id, uchar *hack_x, uchar *hack_y, uchar *hack_z, uchar *hack_type);
|
||||
--- a/src/GameSrc/gameobj.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/GameSrc/gameobj.c 2026-09-27 20:44:35.195019539 +0200
|
||||
@@ -104,7 +104,7 @@
|
||||
void draw_ice(void);
|
||||
void draw_ice_wall(void);
|
||||
void _fr_draw_tmtile(grs_bitmap *draw_bm, int col_val, g3s_phandle *plst, uchar dblface, uchar use_lighting);
|
||||
-void _fr_draw_bitmap(grs_bitmap *draw_bm, int dist, int sc, int anch_x, int anch_y);
|
||||
+void _fr_draw_bitmap(grs_bitmap *draw_bm, int dist, int sc, int anch_x, int anch_y, short native_w, short native_h);
|
||||
|
||||
int munge_val(int val, int range, int delta) {
|
||||
int base = range - delta;
|
||||
@@ -575,7 +575,7 @@
|
||||
|
||||
// note this always has show_obj's p for p and _fdt_dist for dist.. perhaps shouldnt pass them
|
||||
//#pragma disable_message(202)
|
||||
-void _fr_draw_bitmap(grs_bitmap *draw_bm, int dist, int sc, int anch_x, int anch_y) {
|
||||
+void _fr_draw_bitmap(grs_bitmap *draw_bm, int dist, int sc, int anch_x, int anch_y, short native_w, short native_h) {
|
||||
#ifdef SMOOTH_BITMAPS
|
||||
grs_canvas tmp_can;
|
||||
grs_bitmap tmp_bm;
|
||||
@@ -597,7 +597,7 @@
|
||||
if ((anch_x <= 0) && (anch_y <= 0))
|
||||
bitmap_verts = g3_light_bitmap(draw_bm, anchor);
|
||||
else
|
||||
- bitmap_verts = g3_light_anchor_bitmap(draw_bm, anchor, anch_x, anch_y);
|
||||
+ bitmap_verts = g3_light_anchor_bitmap(draw_bm, anchor, anch_x, anch_y, native_w, native_h);
|
||||
if (sc)
|
||||
g3_set_bitmap_scale(fix_make(0, (int)(2048 / 3)), fix_make(0, (int)(2048 / 3)));
|
||||
|
||||
@@ -855,7 +855,7 @@
|
||||
if ((anchors_3d[o3drep].x > 0) || (anchors_3d[o3drep].y > 0))
|
||||
_o_rad = 0;
|
||||
_fr_draw_bitmap(bitmaps_3d[o3drep], _fdt_dist, ObjProps[objtrip].flags & MY_IM_LARGE, anchors_3d[o3drep].x,
|
||||
- anchors_3d[o3drep].y);
|
||||
+ anchors_3d[o3drep].y, 0, 0);
|
||||
if (global_fullmap->cyber)
|
||||
if (obj_ICE_ICE_BABY(_fr_cobj))
|
||||
draw_ice();
|
||||
@@ -995,7 +995,7 @@
|
||||
trgp = tpdata->bits + tele_bm.h * tele_bm.w;
|
||||
LG_memcpy(dstp, srcp, trgp - srcp);
|
||||
|
||||
- _fr_draw_bitmap(&tele_bm, _fdt_dist, FALSE, anch.ul.x, anch.ul.y);
|
||||
+ _fr_draw_bitmap(&tele_bm, _fdt_dist, FALSE, anch.ul.x, anch.ul.y, 0, 0);
|
||||
release_critter_bitmap_fast(ref);
|
||||
return;
|
||||
}
|
||||
@@ -1016,11 +1016,15 @@
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
- _fr_draw_bitmap(tpdata, _fdt_dist, 0, anch.ul.x, anch.ul.y);
|
||||
+ {
|
||||
+ short native_w = 0, native_h = 0;
|
||||
+ get_critter_native_size(REFID(ref), &native_w, &native_h);
|
||||
+ _fr_draw_bitmap(tpdata, _fdt_dist, 0, anch.ul.x, anch.ul.y, native_w, native_h);
|
||||
+ }
|
||||
release_critter_bitmap_fast(ref);
|
||||
} else {
|
||||
tpdata = bitmaps_3d[o3drep + view];
|
||||
- _fr_draw_bitmap(tpdata, _fdt_dist, 0, -1, -1);
|
||||
+ _fr_draw_bitmap(tpdata, _fdt_dist, 0, -1, -1, 0, 0);
|
||||
}
|
||||
} break;
|
||||
|
||||
--- a/src/Libraries/3D/Source/Bitmap.c 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/Libraries/3D/Source/Bitmap.c 2026-09-27 20:44:47.378954871 +0200
|
||||
@@ -88,6 +88,14 @@
|
||||
// fix _g3d_bitmap_y_iscale = 0x010000;
|
||||
long _g3d_bitmap_u_anchor = 0;
|
||||
long _g3d_bitmap_v_anchor = 0;
|
||||
+// Non-zero overrides the bitmap's own w/h in g3_bitmap_common()'s world-space
|
||||
+// size computation below, decoupling a bitmap's on-screen size from its
|
||||
+// actual pixel dimensions - set by g3_light_anchor_bitmap(), consumed and
|
||||
+// reset back to 0 by g3_bitmap_common() so it never leaks into an unrelated
|
||||
+// bitmap drawn via a different entry point (g3_bitmap()/g3_light_bitmap()/
|
||||
+// do_bitmap() never set these).
|
||||
+short _g3d_bitmap_w_override = 0;
|
||||
+short _g3d_bitmap_h_override = 0;
|
||||
fix _g3d_roll_matrix[6];
|
||||
uchar *_g3d_bitmap_clut;
|
||||
int _g3d_light_flag;
|
||||
@@ -132,10 +140,13 @@
|
||||
return (g3_bitmap_common(bm, (g3s_phandle)p));
|
||||
}
|
||||
|
||||
-grs_vertex **g3_light_anchor_bitmap(grs_bitmap *bm, g3s_phandle p, short u_anchor, short v_anchor) {
|
||||
+grs_vertex **g3_light_anchor_bitmap(grs_bitmap *bm, g3s_phandle p, short u_anchor, short v_anchor, short w_override,
|
||||
+ short h_override) {
|
||||
_g3d_light_flag = 2;
|
||||
_g3d_bitmap_u_anchor = u_anchor;
|
||||
_g3d_bitmap_v_anchor = v_anchor;
|
||||
+ _g3d_bitmap_w_override = w_override;
|
||||
+ _g3d_bitmap_h_override = h_override;
|
||||
_g3d_bitmap_clut = (p->i & 0x00ff00) + grd_screen->ltab;
|
||||
_g3d_bitmap_poly = vpl;
|
||||
return (g3_bitmap_common(bm, p));
|
||||
@@ -185,6 +196,15 @@
|
||||
long rm2;
|
||||
long rm3;
|
||||
|
||||
+ // Read and reset right at entry, before any early return below - a
|
||||
+ // pending override must never survive to affect a later, unrelated
|
||||
+ // bitmap draw that doesn't set it itself (see _g3d_bitmap_w_override's
|
||||
+ // own comment).
|
||||
+ short w_override = _g3d_bitmap_w_override;
|
||||
+ short h_override = _g3d_bitmap_h_override;
|
||||
+ _g3d_bitmap_w_override = 0;
|
||||
+ _g3d_bitmap_h_override = 0;
|
||||
+
|
||||
#ifdef stereo_on
|
||||
if (_g3d_stereo & 1) {
|
||||
|
||||
@@ -209,9 +229,11 @@
|
||||
if ((p->codes & CC_CLIP_OVERFLOW) != 0)
|
||||
return (0L);
|
||||
|
||||
- // copy a few things into locals
|
||||
- bm_w = bm->w;
|
||||
- bm_h = bm->h;
|
||||
+ // copy a few things into locals - w_override/h_override, if set (see
|
||||
+ // _g3d_bitmap_w_override's own comment), take the place of the bitmap's
|
||||
+ // own pixel dimensions here.
|
||||
+ bm_w = w_override ? w_override : bm->w;
|
||||
+ bm_h = h_override ? h_override : bm->h;
|
||||
|
||||
tempL = p->gZ; // mov eax,[edi].z
|
||||
tempL <<= 8; // sal eax,8 ;should be
|
||||
--- a/src/Libraries/3D/Source/3d.h 2026-07-19 13:11:51.000000000 +0200
|
||||
+++ b/src/Libraries/3D/Source/3d.h 2026-09-27 19:42:26.528424908 +0200
|
||||
@@ -720,7 +720,8 @@
|
||||
grs_vertex **g3_light_bitmap(grs_bitmap *bm, g3s_phandle p);
|
||||
// MLA #pragma aux g3_light_bitmap "*" parm [esi] [edi] modify [eax ebx ecx
|
||||
// edx];
|
||||
-grs_vertex **g3_light_anchor_bitmap(grs_bitmap *bm, g3s_phandle p, short u_anchor, short v_anchor);
|
||||
+grs_vertex **g3_light_anchor_bitmap(grs_bitmap *bm, g3s_phandle p, short u_anchor, short v_anchor, short w_override,
|
||||
+ short h_override);
|
||||
// MLA #pragma aux g3_light_anchor_bitmap "*" parm [esi] [edi] [eax] [edx]
|
||||
// modify [eax ebx ecx edx];
|
||||
grs_vertex **g3_full_light_bitmap(grs_bitmap *bm, grs_vertex **p);
|
||||
Reference in New Issue
Block a user