Skip to content

Foscail milestone-2 + dalib-ts format fixes + independent tool release wiring - #19

Merged
baughj merged 5 commits into
mainfrom
foscail
Jul 25, 2026
Merged

Foscail milestone-2 + dalib-ts format fixes + independent tool release wiring#19
baughj merged 5 commits into
mainfrom
foscail

Conversation

@baughj

@baughj baughj commented Jul 25, 2026

Copy link
Copy Markdown
Member

Continues the foscail branch past the milestone-1 merge (#18). Five commits: foscail milestone-2, the dalib-ts reconciliation fixes, a follow-up SPF fix, version bumps, and an independent Foscail publish workflow. Full suite green (947 tests).

Foscail milestone-2 (86c6642)

Palette-dependent asset conversion for every family (stc/mpf/epf/khan/tiles), APNG + GIF animation export, the 3.61 single-palette legend.pal fallback. 7.41 corpus → ~531k images, 3.61 → ~52k, zero failures.

Six DALib file-format fixes (523b149)

Bugs surfaced by the dalib-ts TypeScript port reconciling against a real 7.41 client + the darkages-741 RE docs; each re-verified here against ground truth before fixing. Ranked by impact:

  1. ControlFile <IMAGE> — kept as an explicit ordered frame list instead of range-filling. Retail-verified: shipped field001.txt has emot000.epf {0,1,3} and emot001.epf {0,1,7}, which the old range-fill expanded to {0,1,2,3} / {0..7}, fabricating frames and shifting emote button states.
  2. Ground tilesRenderTile now draws the isometric diamond (index-0 opaque inside, masked outside) instead of routing through the sprite SimpleRender, which dropped index-0 ground pixels as holes.
  3. MapFile — cells read as u16le (a foreground id ≥ 32768 read negative and was then skipped as non-renderable); length check accepts trailing bytes. MapTile id fields short → ushort (breaking, wire-faithful).
  4. HeaFile — run intensity masked with & 0x3F (top two bits are flags).
  5. SPF — decode uses the visible rectangle (Right-Left × Bottom-Top) and honors ByteWidth pitch.
  6. PaletteTable — strips // comments and tolerates whitespace runs, matching the client's parser.

Colorized-SPF rework (ee57157)

A two-agent code review of 523b149 confirmed (via a parent-commit worktree probe) a pre-existing bug: multi-frame colorized SPFs decoded garbage because frames were read sequentially while the format stores two pixel copies (565 + 555) each, and the per-frame StartAddress seek was dead code. ReadColorized now seeks per-frame; SaveColorized moved onto relative dims so decode/save agree. No real 7.41 asset is multi-frame colorized, so this is covered by synthetic round-trip tests.

Versions + release wiring (38a6a7e, 1a342a2)

  • DALib 1.0.0-beta2 → beta3, Foscail 1.0.0-beta1 → beta2 (more testing wanted before 1.0).
  • New foscail-package.yml publishes the tool on its own foscail-v* tags (mirrors dalib-package.yml; packs only Foscail, with DALib.dll bundled). Foscail stays in this monorepo and is versioned independently — rationale and split criteria in the imbas foscail-packaging-decision note.

Publishing note

Merging lands the code on main but ships nothing — publishing is tag-triggered. After merge, push v1.0.0-beta3 (→ DALib) and foscail-v1.0.0-beta2 (→ Foscail) to publish each to nuget.org.

Testing

  • 947 unit tests pass (net9.0 + net10.0).
  • Clean asset re-export of both eras; SPF-heavy archives (setoa, national) convert with zero failures.

Generated with Imbas imbas@eris.co

baughj and others added 5 commits July 23, 2026 21:16
…llback

Conversion now covers the palette-dependent tier:

- stc*.hpf statics via the archive's stc TBL lookup (RenderMap's +1
  keying)
- MPF monsters via the embedded palette number into mns*.pal (sibling
  hades.dat for strays)
- efct*/mefc* effect EPFs via effpal.tbl / mefcpal.tbl
- item*.epf icon sheets via itempal.tbl at a fixed 266-icon stride
  (sheets may carry fewer frames than capacity; every range boundary
  in the retail table is congruent to 1 mod 266)
- khan part EPFs: <gender m|w><part letter><sprite:D3><anim> naming;
  the part letter selects the khanpal.dat palette family, letters
  sharing families (a/b/n -> palb, c/g/j -> palc, p/s -> palp) and the
  body/face letters (m/o) using the palm0-9 skin-tone ramp; male and
  female palette overrides honored
- ground tiles rendered from the tilea bank via the mpt lookup (+2
  keying), into a tiles/ subdirectory
- single-palette-era archives (3.61's merged DarkAges.dat carries no
  family palette tables): legend.pal renders everything, gated on the
  archive having no *pal.tbl so modern archives never fall back

Animations: --animate emits looping APNG (in-repo chunk splicer over
Skia per-frame encodes; no new dependency), --gif emits animated GIF
via KGySoft's encoder (already in the dependency closure). EFA files
play at their embedded FrameIntervalMs; MPF named sequences (standing,
walk, attack, attack2, attack3) play at AnimationIntervalMs, treating
the >= 10000 values carried by 294 of 581 retail monsters as a
no-authored-interval sentinel (200ms fallback); EPF effects play in
file order at 100ms since Effect.tbl sequences carry frame selectors
but no timing (the interval is server-supplied at runtime).

Cross-archive palettes (khanpal.dat, hades.dat) resolve lazily from
siblings of the input via a per-run ConversionContext that also caches
palette lookups across entries.

Verified against the 7.41 corpus: 55,518 entries -> 536,125 images,
zero failures, 99%+ of image-format entries converted (the remainder
is empty placeholder EPFs and UI families with client-hardcoded
palettes). 3.61: 52,693 images via the legend.pal fallback. 17 tests
pin the conventions at pixel level, including two review-caught fixes:
the item stride must be the 266 constant (item041 has 247 frames), and
the khan sprite id is the 3-digit field, not the whole digit run
(mu05501 is sprite 55 anim 01 - the old parse silently defaulted most
khan palettes).

Co-Authored-By: Imbas <imbas@eris.co>
Angelique's TypeScript port (dalib-ts, agent Fintan) surfaced these while
reconciling against a real Dark Ages 7.41 client and the darkages-741-re
docs. Each was re-verified here against the RE docs and, where possible,
against real retail assets before fixing.

1. ControlFileParser: <IMAGE> is an explicit ordered list of (name, frame)
   visual states, not a start/end range. Range-filling fabricated frames for
   sparse lists. Confirmed live on retail data: field001.txt has
   emot000.epf {0,1,3} and emot001.epf {0,1,7}, which the old code expanded
   to {0,1,2,3} and {0..7}, shifting every later emote button state.

2. Graphics.RenderTile: ground tiles are isometric diamonds. Inside the
   diamond palette index 0 is an ordinary opaque color (not transparent as
   for sprites), and pixels outside the diamond are masked transparent. The
   old path routed tiles through the sprite SimpleRender, dropping index-0
   ground pixels (holes) and applying no mask.

3. MapFile: cells are u16le. A foreground id above 32767 (stc ids are five
   digits) read as a negative short and was then treated as non-renderable.
   Also, the client accepts a longer-than-expected file and ignores the
   trailing bytes; the old strict-equality length check rejected it.
   MapTile id fields widened short -> ushort (breaking; wire-faithful).

4. HeaFile: a run's intensity byte carries flags in its top two bits; mask
   with & 0x3F at decode. MAX_LIGHT_VALUE is 0x20, so a flag bit corrupted
   the intensity.

5. SpfFile/SpfView: frame size is the visible rectangle (Right-Left x
   Bottom-Top), and source rows advance by ByteWidth (pitch). The old decode
   used absolute Right/Bottom and ignored pitch, so nonzero-origin or padded
   frames decoded wrong. No shipped 7.41 asset has such a frame, so this is
   robustness verified by synthetic tests.

6. PaletteTable: strip // comments and split on whitespace runs in both the
   range parser and ParseCyclingFile. The client skips blank lines and //
   comments; the old single-space split dropped commented/padded entries.

Adds 11 regression tests (945 total, all passing).

Co-Authored-By: Imbas <imbas@eris.co>
The 2026-07-23 code review of 523b149 confirmed a pre-existing bug this
commit now closes: colorized SPFs with more than one frame decoded garbage.
ReadColorized read frames sequentially and consumed only the first (RGB565)
of each frame's two on-disk pixel copies, so frame 1+ started inside frame
0's RGB555 copy. (The StartAddress seek that would have realigned it was on
a StreamSegment the loop never read from — dead code.)

- ReadColorized now seeks to dataSectionStart + StartAddress per frame,
  reads only the visible RGB565 rectangle (honoring pitch), and allocates
  ColorData compact (also removes a ~2x over-allocation).
- SaveColorized derives sizes/strides from the visible rectangle
  (PixelWidth/PixelHeight) instead of absolute Right/Bottom, so decode and
  save agree — closing the asymmetry 523b149's relative-dims change left for
  a nonzero-origin colorized frame.

For real assets (origin 0, pitch==width) every bound and size is unchanged;
verified by the full suite (947) plus clean setoa/national converts. Adds two
multi-frame colorized regression tests (decode-from-StartAddress and a
FromImages save/load round-trip). SpfFile and SpfView now agree on
multi-frame colorized output.

Co-Authored-By: Imbas <imbas@eris.co>
More testing wanted before 1.0. DALib carries the dalib-ts reconciliation
fixes (523b149) and the colorized-SPF rework (ee57157) since beta2; Foscail
bundles that DALib and gets its own beta bump.

Co-Authored-By: Imbas <imbas@eris.co>
Foscail is versioned and released independently of DALib but lives in the
same repo. This workflow mirrors dalib-package.yml, keyed to a separate
foscail-v* tag namespace so a tool release never collides with a DALib
library release. Packs only Foscail/Foscail.csproj (DALib.dll is bundled),
signs, and pushes Foscail.<version>.nupkg to nuget.org.

Co-Authored-By: Imbas <imbas@eris.co>
@baughj
baughj merged commit 45f170e into main Jul 25, 2026
1 check passed
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