Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c8854a9
preview: replace the render-capture route with a route-independent tr…
mefisme Aug 3, 2026
ef7f10d
assets: ship the browser -- full catalog, apply, new entity, sound pr…
mefisme Aug 5, 2026
7c60e43
assets: brush models, clip models and placeable SnapMap modules
mefisme Aug 5, 2026
12c868a
sigs: resolve the last hardcoded engine RVAs, and fix the [64] in the…
mefisme Aug 5, 2026
f105d72
docs: the asset browser, and the signature/test-array entries
mefisme Aug 5, 2026
3e2bcb5
sound: run every audition on the main thread -- the emitter list is r…
mefisme Aug 5, 2026
49c85b7
swf-textedit: stop latching Ctrl -- a bare `c` was copying and `v` pa…
mefisme Aug 5, 2026
66adc15
backend: expand the built-in Unknown tile's property sheet to size an…
mefisme Aug 10, 2026
d256544
fix: make every material searchable, not just decl-backed ones
mefisme Aug 10, 2026
e2200e6
ui: asset browser polish and materials-union wiring
mefisme Aug 10, 2026
581799a
ui: add 29 new cross-platform textures from an updated rawmap
mefisme Aug 10, 2026
0d287c1
fix: paging with arrow keys auto-selects assets, not folders
mefisme Aug 10, 2026
8e404d2
ui: fix a missed 195->224 count reference from the cross-platform tex…
mefisme Aug 10, 2026
53f0600
docs: record the rawmap save-shadow gate as an open decision
mefisme Aug 10, 2026
183e48c
fix: collapse the sound rows that list one sound twice
mefisme Aug 10, 2026
6211632
assets: filter sounds by soundbank
mefisme Aug 10, 2026
cbd5263
ui: give the browser-preview side a real sound + soundbank sample
mefisme Aug 10, 2026
ee2123c
assets: let the mapper pin a shortlist of assets
mefisme Aug 10, 2026
ed24163
assets: file sounds by soundbank as a folder tree
mefisme Aug 10, 2026
f98799c
assets: make the Pinned list flat
mefisme Aug 10, 2026
89f4ce9
assets: collapse records that repeat a name in the same box
mefisme Aug 10, 2026
863c8b3
assets: make "No ramp" actually remove the ramp
mefisme Aug 10, 2026
454dc66
assets: drop the catalog Refresh button
mefisme Aug 10, 2026
eb595aa
assets: index the breakable prop models
mefisme Aug 11, 2026
b476e44
assets: add Lights, Perks and SWFs
mefisme Aug 11, 2026
fe61d3c
docs: sync the docs to twenty commits of asset-browser work
mefisme Aug 11, 2026
89f6e00
assets: gate each single-purpose carrier to the class that owns its f…
mefisme Aug 11, 2026
aca1ef4
docs: record the per-carrier Apply allowlists
mefisme Aug 11, 2026
6f40391
fix: keep the new source files pure ASCII
mefisme Aug 11, 2026
af68a4b
Merge branch 'main' into experimental/asset-preview-codec
mefisme Aug 11, 2026
c09d942
fix: let a timeline larger than 1 MB open
mefisme Aug 13, 2026
20bb8bf
Merge branch 'main' into experimental/asset-preview-codec
mefisme Aug 13, 2026
203cd36
Merge branch 'experimental/asset-preview-codec' of https://github.com…
mefisme Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,13 @@ include that header** — it is a matched pair. The backend writes the vtable an
frontend reads them at the same offsets.

- The backend builds it (`operator_new(0x60)`), installs the vtable — the **77 original-faithful
slots** (`+0x00..+0x260`) plus the **clone-extension slots** appended after them (`+0x268..+0x2B8`
today: the atomic class+inherit apply, the class/inherit enumerators, the dev-layer query, the
wire-edit generation counter, the synchronous `apply_sync`, the timeline inherit-normalize,
push/clear-stack, and the generic configuration getter/setter) — initializes the mutex at `+0x08`,
and hangs a sub-object off `+0x58` that holds the SnapStack subcommand map and the main-thread
work-queue.
slots** (`+0x00..+0x260`) plus the **clone-extension slots** appended after them (`+0x268..+0x300`
today, `sizeof(sh_iface_vtbl) == 0x308`: the atomic class+inherit apply, the class/inherit
enumerators, the dev-layer query, the wire-edit generation counter, the synchronous `apply_sync`,
the timeline inherit-normalize, push/clear-stack, the generic configuration getter/setter, and the
asset-browser group — preview request/publish, request-by-name, the material atlas rect, the
catalog pager, and sound preview/session) — initializes the mutex at `+0x08`, and hangs a
sub-object off `+0x58` that holds the SnapStack subcommand map and the main-thread work-queue.
- **Extension slots are append-only**: a new capability gets the next slot after the current end;
original-block offsets never move. This is also a real failure mode, not a formality — a frontend
calling an extension slot that an older backend never installed would call through garbage. That is
Expand All @@ -120,6 +121,14 @@ the backend, where they are re-derived per build.

## Persistent configuration

Two files, owned by different sides on purpose. The backend owns `config.json` — the registered
settings, validated and versioned. The **frontend host** owns `pinned.json`, the asset browser's
shortlist, and deliberately keeps it out of the settings registry: `config.json` is all-or-nothing,
so any parse failure resets the whole document to defaults, which is an acceptable trade for a
handful of validated scalars and not for unbounded data a user grows themselves. The host moves those
bytes and parses none of them; shape and validation live in the UI, the only side that knows what a
pin means. See [`capabilities.md`](capabilities.md#persistent-settings).

The backend is the sole owner of `%LOCALAPPDATA%\snapmap-plus\config.json`; the installer does not
generate, parse, or replace it. `sh_config_init` runs after the common per-user directories are available
and creates this version-1 document when the file is absent:
Expand Down
106 changes: 106 additions & 0 deletions docs/backend-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,112 @@ where our own reimplementation was wrong, not the original SnapHak's behavior; a
(or faithful reproduction of) the *original's* behavior belongs in [`fidelity.md`](fidelity.md)
instead. Entries are chronological, newest first.

## 2026-08-10 — the asset catalog listed the same asset twice, because the game index is not a catalog

**What changed.** `imgpreview.c` now collapses records that repeat a name within one resource box,
before anything else reads the record list.

**The bug.** Decal atlases appeared twice per asset in the browser. Clicking one row selected both and
starring one starred both — which looked like a selection bug and was not: the UI keys off the asset
name, and the catalog genuinely held two rows carrying the same one.

**Why.** We had been treating `snap_gameresources.index` as a catalog of distinct assets. It is a
record-per-blob table: the same decl can be baked into the `.resources` file more than once, at
different offsets, and the index lists each copy. Measured across the box — `decalatlas` 1,673 records
for 1,024 distinct names, `image` 3,423 for 3,422, and exactly zero repeats for `material`, `model`,
`md6Def`, `sound`, `fx`, `particle`, `entityDef`, `snapEditorEntityDef` and `cm`. So the collapse is
written generally but only ever fires where the data actually repeats.

The first record wins, which is what `find_rec` would have resolved to anyway, so nothing that already
previewed changes which blob it reads.

**Adjacent, unresolved.** The sound catalog files 8,028 rows against 7,649 distinct Wwise events with
378 unbanked, which leaves one more row carrying a bank than there are events to carry. Sound names are
unique in both boxes, so the obvious explanation — two names differing only by case — is ruled out. One
row in 8,028, no known consequence, recorded here rather than chased.

## 2026-08-05 — a sound preview could take the whole sound engine down: the emitter list is published before it is initialised

**What changed.** Every audition now runs on the main thread instead of the caller's.

**The bug.** Previewing a sound could fault and then freeze the game (owner-reported: an access
violation followed by a hang).

**Why.** The race is in the engine, not in our code — but we were the ones exercising it. DOOM's
`StartSound_wwise` publishes a brand-new emitter into the sound world's **live list** before
initialising it: the emitter is allocated and base-constructed, written into the world's array and the
count bumped, and only *then* is its sound-shader pointer set. `idSoundWorld::Update` walks that same
list on an audio worker thread, and `idSound::Update` opens by dereferencing exactly the pointer that
is still null inside that window.

Calling the preview from any thread but the main one widened a window the engine gets away with
internally. Serialising our calls onto the main thread closes it from our side without patching engine
code.

## 2026-08-05 — the SWF text editor latched Ctrl: a bare `c` copied and a bare `v` pasted

**What changed.** `swf_textedit.c` no longer tracks Ctrl in a static flag consulted on later
keystrokes.

**The bug.** After one Ctrl+C or Ctrl+V, every subsequent bare `c` or `v` typed into any editor text
field copied or pasted — typing "variable" pasted the clipboard once per `v`. Owner-reported, confirmed
fixed live.

**Why, and the lesson.** The flag was set on Ctrl key-down and cleared on key-up. Modifier key-ups are
not reliably delivered to a focused SWF script object — the field or the window can lose focus, or the
engine may simply not dispatch them — and one missed key-up left the flag stuck on for the session.

The design had been justified in a comment as tracking Ctrl "exactly the way the stock handler tracks
Shift", and that is precisely where the reasoning failed: the stock handler reads Shift only while
processing the keystroke it was handed, so a release it never sees costs nothing. Ours was consulted on
*later* keystrokes, which turns the same pattern into a permanent latch. The mechanism was copied
without its lifetime assumption.

## 2026-08-05 — the last hardcoded engine RVAs are gone, and the `[64]` that hid a truncated signature DB bit again

**What changed.** Three engine functions were still reached as a raw `module_base + RVA`, which is
build-locked in exactly the way the signature resolver exists to avoid. All three are now ordinary
signature entries, verified unique across the whole executable image:

| Signature | Pinned RVA | What it is |
|---|---|---|
| `Mega2PageDecode` | `0x196E140` | the megatexture page decoder the asset browser calls |
| `PrefabDtor` | `0x51D870` | `idSnapEntityPrefab::~idSnapEntityPrefab` |
| `EntityDeshare` | `0x52C920` | COW make-unique, before an in-place entity edit |

The two prefab functions join the ctor/populate/`MemLocal` group on `ae_pick_engine_fn` — signature
wins, `known_rva` cross-checks and logs a mismatch loudly rather than silently preferring either.

The decoder previously carried a **local `memcmp` of its own prologue** at the hardcoded address.
That is strictly weaker than a signature and the difference is not academic: a byte check can detect
that a function moved but can never *find* it, so any build with a shifted `.text` lost previews
outright. It now resolves wherever the loader put it, and a build whose bytes genuinely differ fails
to resolve — which still disables previews, but by refusing rather than by calling into whatever now
occupies that address.

**One function could not be signatured, and is not pretending to be.** idList-grow (pinned
`0x699A60`) is **one of 1,560 byte-identical instantiations** of the same idList template in the
image, separable only by their displacements. No lengthening of a prologue pattern gets past that —
the same wall `StopSound`'s clone hit. It is resolved *relationally* instead: `AddCommand` (which is
signatured) calls it on `cmdSys+0x08` via `LEA RCX,[RSI+8]` / `CALL rel32`, so the callee is decoded
out of that call site, range-checked against the module, and logged with the pinned RVA as a
cross-check. The decode runs before our own `AddCommand` detour is installed, and the call site sits
well past the 15 stolen prologue bytes either way.

**The bug this uncovered.** `tests/sig_test.c` and `tests/hooktol_test.c` both still declared
`sig_result results[64]` — the same fixed array that silently truncated the database at entry 65 and
prompted `SIG_RESULTS_MAX` in the first place. The constant was applied to the shipping callers and
missed in the tests. At 67 entries it stopped being silent and became a stack overwrite:
`sig_resolve_all` wrote past the end of the array, producing garbage status values and an access
violation **inside the test process**. Both now size with `SIG_RESULTS_MAX` and print a loud
`SIGNATURE DB OVERFLOW` and fail if the database ever outgrows it.

The general lesson is worth keeping: a constant introduced to fix a class of bug only fixes the call
sites it is actually applied to. Grep for the literal, not just for the symptom.

**Verified.** `sig_test` 67/67 signatures resolve to their known RVAs; `hooktol_test` 3/3
hook-tolerant fallbacks; 16/16 native tests; `go test ./...` clean.

## 2026-07-28 — Load/Place now picks the prefab up (auto-grab enabled): the corruption was the heap bug

**What changed.** `kind=2` (stage → pick up) is enabled. Load/Place no longer asks the user to press Ctrl+V.
Expand Down
Loading
Loading