Skip to content

feat(objects): route art/skin edits to war3mapSkin.w3* + unit vertex tint - #33

Merged
StephenSHorton merged 4 commits into
mainfrom
feat/skin-edit-routing-tint
Jul 7, 2026
Merged

StephenSHorton merged 4 commits into
mainfrom
feat/skin-edit-routing-tint

Conversation

@StephenSHorton

Copy link
Copy Markdown
Owner

What

Completes the two deferred items from the v1.0.5 Reforged-skin work (the Discord round-trip feedback):

  1. Edit routing to the skin table — Object-Editor field edits now write to the table the vanilla World Editor uses. v1.0.5 taught wc3-forge to read war3mapSkin.w3*; this adds the write half so a saved map round-trips through the vanilla editor with art fields intact.
  2. Unit vertex-tint overrides (uclr/uclg/uclb → Red/Green/Blue) applied in the 3D render merge (commit 5a5c807).

How routing decides

The classifier is the netsafe MetaData.slk column — the exact mechanism the vanilla editor uses — not category (e.g. unam is category text but netsafe → skin; ucbs is category art but netsafe 0 → main). Parsed into a new ObjectFieldMeta.NetSafe.

routeSkinTable (non-allocating):

  1. field already in the primary table for this id → primary
  2. else already in the skin table for this id → skin
  3. else brand-new override → skin iff netsafe, else primary

Presence beats netsafe, so an older all-in-one map that keeps unam in war3map.w3u is edited in place and never migrated between files. A custom object's art edit materializes a matching Custom row (id + baseID) in the skin companion; deleting a custom drops its skin mirror too (restored on undo).

Key changes

  • objects_route.go (new): fileFieldPresent / objectIdentity / routeSkinTable / targetModFile / readBaseOverride / putBaseOverride / putLevelOverride / markObjectDirty.
  • setObjectField / clearObjectField / setObjectFieldLevel / clearObjectFieldLevel return which table they touched; undo commands + public mutators flip the matching dirty flag via markObjectDirty. Replay re-routes deterministically (presence/netsafe are stable) so no per-command skin flag is stored.
  • Session.dirtySkinMods + ensureSkinModsLocked / setSkinDirtyLocked / anySkinDirtyLocked; Save encodes war3mapSkin.w3* per dirtied kind. Clean companions still ride the lossless copy-through, so untouched skin tables round-trip byte-for-byte.

Tests

8 new tests in objects_route_test.go (all green): netsafe split, presence-stays-put, custom skin-row creation, clear-from-skin, leveled routing, delete-mirror cleanup, public Save round-trip (only skin dirtied + reload from skin table), undo/redo table fidelity.

go build ./..., go vet ./..., and the full internal/forge suite pass. Backend-only change (no frontend/binding deltas).

Note: a pre-existing failure in internal/formats/mpq (TestExtractMatchesStormLib_survival/war3map.w3i) is unrelated — it byte-compares two out-of-sync external copies of the wc3-survival-game map (build .w3x name "Just ano…" vs pre-extracted sibling "Co-op Cu…"). That package is untouched here and doesn't import internal/forge.

🤖 Generated with Claude Code

StephenSHorton and others added 3 commits July 6, 2026 19:56
Custom units that set Art - Tinting Color now render with that tint in the
viewport, completing the custom-skin render parity with doodads (which already
carry tint). Maps to red/green/blue and only applies when the override is
present; the renderer already treats an all-zero tint as "no tint" (leaves the
instance default-white), so untinted units are never darkened.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdJMLPgZf25tirfS26CobN
…d-trip)

Object-Editor field edits now land in the table the Reforged World Editor
would use, so a saved map round-trips through the vanilla editor without art
fields collapsing into the plain war3map.w3* table. v1.0.5 taught wc3-forge to
READ the war3mapSkin.w3* companion; this adds the WRITE half.

The classifier is the MetaData.slk `netsafe` column (the exact mechanism the
vanilla editor uses), NOT `category`: netsafe != "" && != "0" => art/skin field
=> war3mapSkin.w3*. Parsed into a new ObjectFieldMeta.NetSafe.

Routing (routeSkinTable): a field already present on disk stays in its table
(presence beats netsafe, so an old all-in-one map that keeps unam in
war3map.w3u is edited in place, never migrated); a brand-new override goes to
the skin table iff the field is netsafe. A custom object's art edit
materializes a matching Custom row (id + baseID) in the skin companion, and
deleting a custom drops its skin mirror too (restored on undo).

- objects_route.go: fileFieldPresent / objectIdentity / routeSkinTable /
  targetModFile / readBaseOverride / putBaseOverride / putLevelOverride /
  markObjectDirty.
- setObjectField / clearObjectField / setObjectFieldLevel /
  clearObjectFieldLevel now return which table they touched; undo commands +
  public mutators flip the matching dirty flag via markObjectDirty. Replay is
  deterministic (presence/netsafe are stable), so no per-command skin flag.
- Session.dirtySkinMods + ensureSkinModsLocked / setSkinDirtyLocked /
  anySkinDirtyLocked; Save encodes war3mapSkin.w3* per dirtied kind (clean
  companions still ride the lossless copy-through).
- 8 routing tests in objects_route_test.go (netsafe split, presence-stays-put,
  custom skin-row creation, clear-from-skin, leveled routing, delete mirror
  cleanup, save round-trip, undo/redo table fidelity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdJMLPgZf25tirfS26CobN
@StephenSHorton
StephenSHorton marked this pull request as ready for review July 7, 2026 03:23
…p read

Two low-severity defects caught by an adversarial pre-ship review of the
edit-routing change:

- clearBaseOverrideFromFile / clearLevelOverrideFromFile dropped an emptied
  OriginalEdit row but never an emptied Custom row. Undoing an art/skin edit on
  a CUSTOM object therefore left a phantom empty {id,baseID} mirror row in the
  war3mapSkin.w3* companion, so edit->undo->save was no longer a disk no-op and
  the skin file fell off the lossless copy-through. Now the skin-table clear
  prunes a now-empty custom mirror row; the PRIMARY custom row (the object's
  definition, legitimately empty for an "inherit everything" custom) is never
  dropped. Threaded via a pruneEmptyCustom flag set only for the skin call.

- SetObjectFieldLevel read the prior value per-(fourCC,level) across both tables
  while routeSkinTable resolves per-fourCC, so on a map that splits one field
  across tables by level the no-op/oldVal read could consult a different table
  than the write, corrupting undo bookkeeping (exotic legacy input; user value
  stayed correct). The leveled read now consults the routed table, matching the
  base path.

Regression tests for both in objects_route_test.go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdJMLPgZf25tirfS26CobN
@StephenSHorton
StephenSHorton merged commit f89ca1e into main Jul 7, 2026
0 of 2 checks passed
@StephenSHorton
StephenSHorton deleted the feat/skin-edit-routing-tint branch July 7, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant