Skip to content

Updated TAassets and HPIView for MacOS 26 and ARM#1

Open
csilvertooth wants to merge 55 commits into
loganjones:mainfrom
csilvertooth:main
Open

Updated TAassets and HPIView for MacOS 26 and ARM#1
csilvertooth wants to merge 55 commits into
loganjones:mainfrom
csilvertooth:main

Conversation

@csilvertooth

Copy link
Copy Markdown

Support for modern Mac hardware and latest OS. Added some useful features to the programs and mod compatibility for viewing mod units.

csilvertooth and others added 30 commits April 21, 2026 18:41
Bumps MACOSX_DEPLOYMENT_TARGET from 10.12 to 10.13 (the new floor for
Xcode 26) on both HPIView and TAassets. Disambiguates five Data.withUnsafeBytes
call sites in TdfParser now that Swift requires an explicit buffer-pointer
type. Implements the empty extractAll action in HPIView so the archive
root is enumerated and handed to the existing extractItems recursion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces PieceHierarchyView (outline of piece names, primitive and
vertex counts, and per-piece script references) in both HPIView and
TAassets. Piece references are decoded from each unit's COB via a new
UnitScript.pieceReferences walk. Selecting a piece tints it in the
Metal renderer: a new highlightedPieceIndex uniform and a flat piece
index interpolant drive a yellow mix in the fragment shader in both
targets.

Adds mod support. FileSystem.init gains a modDirectory argument that
overlays mod archives on top of the base. TaassetsDocument scans
<base>/mods for mod subdirectories and exposes them through a dynamic
top-level Mods menu that rebuilds the filesystem and reloads the
current browser on selection.

Adds camera controls. Scroll-wheel and trackpad pinch adjust zoom;
= / - / 0 act as keyboard zoom and reset. Reset also restores rotation.
Shift-drag pitches the camera via a new rotateX step in the Metal
view matrix. The HPIView preview container no longer wastes its lower
third on a centered title/size block: the filename and byte count now
sit as a compact footer so the 3D view and piece outline get the rest
of the pane.

Adds auto-fit. UnitModel.maxWorldExtent walks the piece tree once on
load and the view sizes the scene around it, so large buildings like
armasy no longer open zoomed past the viewport. Window resizes and
piece changes re-run the fit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces PlaybackControlsView below the 3D preview in the unit
detail pane. A speed slider scales the script's deltaTime from 0x to
2x, a Pause toggle freezes the timeline without touching the model
state, Step advances one thirtieth of a second of script execution
while paused, and a pull-down menu launches any COB module by name so
building internals like the Activate sequence can be observed piece
by piece. UnitViewController gains the matching API
(setPlaybackSpeed, stepOnce, startScript, availableScriptFunctions)
and updateAnimatingState scales deltaTime accordingly, short-
circuiting when speed is zero so Metal still renders the frozen frame.

Fixes a long-standing layout bug where the detail controller's view
was given autoresizingMask = [.width, .width] instead of
[.width, .height], so the right pane didn't grow vertically with the
window; that kept the 3D viewport small and pushed the piece outline
into a cramped strip at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mods like TAESC and UH nest additional unit definitions under
units/<category>/*.fbi, but FileSystem.Directory.files(withExtension:)
only walked the top level, so those units never reached the unit
browser. Adds an allFiles(withExtension:) walker that recurses into
every subdirectory and switches both UnitBrowser and
UnitInfo.collectUnits to use it. Deduplicates by base name so a mod
override of a vanilla unit still appears exactly once, and logs the
resolved unit count so mod troubleshooting from the console is
obvious.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously File → Open required the selected directory to supply
gamedata/sidedata.tdf, so a standalone folder of ufo/gp3/ccx files
(e.g. ~/tafiles/mods/taesc on its own) failed to load the document.
Now TaassetsDocument treats the sidedata as optional: if the file is
missing or malformed, it logs a note and proceeds with an empty side
list.

Adds a palette fallback in the unit browser that tries
Palette.texturePalette first, then Palette.standardTaPalette, and
finally a neutral Palette() so the 3D preview still renders when the
mod folder does not carry matching palette data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Palette.init() built a 255-entry color array, but palette lookups
index from a UInt8 so anything with byte 255 crashed the moment the
new mod-folder fallback was exercised (Adv. Aircraft Plant's texture
atlas hit it on selection). Bumps the default to a full 256 entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mods regularly ship unit thumbnails outside the vanilla
unitpics/<NAME>.PCX convention: TAESC builds keep JPEGs in
anims/buildpic/, other packs use PNG or BMP. Broadens the lookup to
try PCX, BMP, PNG, JPG, JPEG, and TGA under unitpics/ and then
JPG/JPEG/PNG/BMP under anims/buildpic/ before giving up, so mod units
get thumbnails in the list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some mod archives stash their unit FBIs outside a units/ directory.
When nothing shows up under units/, UnitBrowser now falls back to
walking the entire filesystem for *.fbi so mod-supplied units still
surface in the list. Adds startup logs listing the root directory
entries and the resolved unit count, which is the quickest way to
eyeball whether the selected mod actually overlaid.

Relabels the first Mods-menu entry from "Vanilla (no mod)" to
"Base only: <folder>". When the user opens a mod folder directly the
label now matches what is actually loaded instead of implying a
vanilla install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TaassetsDocument.read now inspects the opened folder's location. If
the folder sits directly under a "mods" directory whose grandparent
contains TA archives, the grandparent loads as the base and the
opened folder is treated as the active mod. Opening
~/tafiles/mods/taesc directly therefore behaves the same as opening
~/tafiles and choosing taesc from the Mods menu, so the mod can
resolve textures and palettes that live in the vanilla archives.

Hardens the COB VM against division by zero. Some mod-supplied
scripts (confirmed in TAESC) exercise the divide opcode against a
zero right-hand side, which trapped Swift's / operator and crashed
the app the moment the unit was selected. The operator is now guarded
to return 0 on zero divisor so the animation VM keeps running.

Expands unit and buildpic discovery so mod-only folders populate
their lists. UnitBrowser walks the entire merged filesystem for
*.fbi instead of just units/, catching TAESC's unitsE/ tree. The
buildpic search iterates every root directory whose name starts with
unitpic (unitpics, unitpicE, unitpicsE) with PCX plus common image
formats, falling back to anims/buildpic/. AppDelegate routes the
Mods-menu action through its own IBAction so dispatch no longer
depends on NSDocument's validator chain, and the first menu entry
now reads "Base only: <folder>" instead of a generic "Vanilla" label.

Extends notes/SwiftTA_Apple_Silicon_Bootstrap.md with a feature-work
section documenting the piece inspector, camera controls, playback
controls, mod support, and outstanding follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings SwiftTA up on current Xcode/macOS on Apple silicon plus a
batch of feature work targeting TA archive inspection and mod
browsing: piece hierarchy and script-reference outline in both
HPIView and TAassets, COB playback controls with pause/step/speed/run,
Metal piece highlight, camera zoom and pitch, mod-aware filesystem
with a dynamic Mods menu and mod-folder auto-detect, tolerant
standalone mod-folder loading, and COB divide-by-zero hardening.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the new TAassets and HPIView features at the top of the
README so the fork's intent is obvious from the repo landing page.
Points at notes/SwiftTA_Apple_Silicon_Bootstrap.md for the full
write-up, summarizes the piece inspector, COB playback, camera
controls, and mod-aware filesystem, and gives copy-paste build and
usage instructions for both apps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reworks the map detail pane so the selected map fills the available
viewport. The map name moves from a giant centered label sitting
below a 62% content box to a compact header strip that also shows
planet, player count, wind, tidal, and gravity from the OTA. The
detail container pins content to the full remaining height and the
sidebar view's autoresizing mask is corrected ([.width, .width] was a
typo mirroring the one already fixed on the unit browser).

Swaps the sidebar icons from stock AppKit images to SF Symbols where
available: cube.fill for Units, scope for Weapons, map.fill for
Maps, folder.fill for Files. Guarded by #available(macOS 11) so the
10.13 deployment target still builds on older systems.

Adds a filter search field above the unit and map lists. The unit
search matches name, title, description, and object; the map search
matches the file base name. Typing filters live.

MapView now auto-fits the loaded map to the viewport instead of
always opening at 1:1 magnification (some maps are 16k pixels wide,
and a 500px pane at 1:1 was useless). Wraps the scroll view's
document view in a new MapOverlayView that draws numbered, numbered
start-position markers from the OTA schema. Markers scroll and zoom
with the map and expose a showMarkers toggle for future UI wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applies 28pt of top padding to the sidebar stack so the Units icon no
longer overlaps the red/yellow/green window controls. Bottom inset is
also bumped so spacing stays balanced.

Reworks UnitDetailViewController's title strip to match the map
detail pane: the object name sits as a small header alongside a
secondary detail line listing unit.title, description, side,
tedclass, footprint, and maxVelocity when present. The 3D preview
now pins directly under the header instead of floating below an
oversized title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Raises DynamicTileMetalTntViewRenderer's maximumDisplaySize from 4096
to 8192 so maps like [ESC] Dark Prime render cleanly on 4K-class
Retina displays. Bigger maps (any resolution) still work fine; they
simply scroll through the 8192-pixel working window. The grid
clamping in computeTileGrid also caps the requested tile area at
maximumGridSize so a viewport larger than the pool no longer
overflows the pre-sized index or slice buffers.

Replaces the empty Weapons sidebar pane with a working browser that
walks every weapons/*.tdf recursively, parses each top-level block
with TdfParser, and lists the discovered weapons in a searchable
table. Selecting a weapon shows its key, source file, weapon type,
range, damage table, and full property set. Matches the UX of the
unit and map browsers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Metal map view only refreshed its viewport when the scrollView
posted a bounds-changed notification, which would occasionally get
dropped around a reload so the second map stopped redrawing even as
the overlay markers scrolled. Each draw() now pulls the current
clip view bounds directly so scrolling and magnification stay
consistent across map swaps.

Surfaces a console warning when feature loading throws (commonly due
to a missing TA_Features_2013.ccx alongside the base archives)
instead of swallowing the error with try?.

Broadens the weapons browser to any top-level directory whose name
starts with "weapon" (covers the weaponE/weaponsE mod variants that
already caused trouble in the unit and unitpic paths) and walks the
parsed TDF recursively, identifying weapon blocks by characteristic
properties (weapontype, range, reloadtime, damage subobject, …)
instead of assuming a flat list. Logs the directories and TDF count
on load so empty results are easier to diagnose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The map fragment shaders used the default clamp_to_edge sampler, so
any time the viewport or a partial tile extended beyond the map's
pixel area the last row/column of terrain smeared to fill the rest.
Both shaders now discard fragments that fall outside the map: the
single-quad variant checks texCoord against [0,1], the dynamic tile
variant compares world-space position against a new mapSize uniform
supplied by both renderers. Samplers also switch to clamp_to_zero so
nothing bleeds in if the bounds check misses.

TaassetsDocument now opens its window at a reasonable default size
(70% of screen width, 80% height, capped at 1600x1100, floored at
1100x750) and persists future size and position under the
"TaassetsMainWindow" frame autosave name. A 900x600 minimum stops it
from collapsing smaller than the asset browsers can use.

The Weapons browser no longer filters blocks through a narrow
"looks-like-a-weapon" heuristic. Every top-level block with
properties is shown, and the user can narrow the list with the
existing search field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends notes/SwiftTA_Apple_Silicon_Bootstrap.md with a new section
covering the browser chrome changes, map viewer improvements,
mod-unit discovery, the weapons browser, and the COB divide-by-zero
fix, all with links to the relevant source files so future spelunking
is fast. Mirrors the highlights into the README fork callout so the
GitHub landing page reflects the current feature set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Metal uniform only carried room for 40 piece matrices, but
mod-supplied units (TAESC's CORMKL spider among them) have more, so
the extra transforms overflowed the uniform buffer, clobbering later
fields and leaving the shader reading zeros for those pieces. Result:
legs and other limbs rendered collapsed on the base or vanished. The
uniform now carries 128 slots, and the Swift side caps the per-frame
copy to that capacity with a one-shot warning if a future unit
exceeds the limit.

Reworks the unit-view auto-fit to consider the current viewport
aspect ratio. The previous implementation multiplied the model extent
by 2.3 to pick a scene width, but scene height = sceneWidth ·
aspectRatio so a wide-but-short window would crop tall mod units. The
new computation picks whichever of modelDiameter and
modelDiameter / aspectRatio is larger, guaranteeing both axes show
the full 2.4·extent box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends notes/SwiftTA_Apple_Silicon_Bootstrap.md with why the
pieces[40] uniform was biting TAESC's CORMKL and what changed on the
renderer side, plus the revised computeSceneSize math that now
guarantees the full unit fits the viewport in either orientation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UnitModel.init always threw away all but the first sibling of the
root node (root = model.roots.first!) while UnitModel.roots was kept
internal. The BFS parser correctly accumulates every top-level
sibling from offset 0 in the 3DO, but the renderers only ever walked
from model.root, so any piece that lived on a sibling tree — a
pattern several TAESC mod units use for extra appendages — never
reached the vertex buffer or the transformation list. CORMKL in
particular kept its legs on sibling roots, which is why the body
rendered cleanly while the legs disappeared.

Exposes the roots array on UnitModel and walks every root in:

  - TAassets' Metal unit renderer (vertex + outline + transform
    passes)
  - HPIView's Metal model renderer (vertex + outline passes)
  - the piece-hierarchy outline in both apps
  - UnitModel.maxWorldExtent so the auto-fit camera accounts for
    pieces on secondary roots

Adds a startup print of per-unit piece/primitive/script-module
counts plus the full piece name list, so future "X is missing its Y"
reports can be diagnosed straight from /tmp/taassets.log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records why CORMKL's legs were missing (UnitModel.init dropped every
root past the first) and lists the call sites that were updated to
walk model.roots so future mod units with sibling trees render
completely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getUnitValue and getFunctionResult both just pushed 0 regardless of
what the COB script asked for. TA spider units (confirmed with
TAESC's CORMKL) lean heavily on the IK trio — PIECE_XZ to query a
leg's geometric offset, XZ_ATAN to derive a rotation angle from that
packed offset, and XZ_HYPOT for the matching distance — and then
turn-now the leg into its starting pose during Create. With
everything returning 0 the atan was 0 and every leg that needed this
path collapsed onto the body origin, matching the "side legs
missing" symptom.

Adds UnitModel.parents (ancestor chain per piece) and
UnitModel.pieceStaticOffset so the VM can sum a piece's offsets
without re-evaluating the whole transform chain. Stores the
UnitModel on UnitScript.Context so Instructions can reach the piece
tree. Implements packXZ / unpackXZ along with taAtan2 and taHypot
(TA encodes a full turn as 65536 angle units). getUnitValue now
returns sane defaults for activation, health, standing orders, and
position queries; getFunctionResult handles PIECE_XZ, PIECE_Y,
XZ_ATAN, XZ_HYPOT, ATAN, HYPOT, and the zero-returning unit/ground
queries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records why CORMKL's side legs collapsed (PIECE_XZ / XZ_ATAN /
XZ_HYPOT all returning 0) and the VM changes that now handle the
packed-xz encoding, TA's 65536-unit angle space, and the piece
ancestor chain used to resolve world-space piece offsets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stack.pop(count: n) used suffix(from: n - 1) where it should have
used suffix(n). suffix(from:) returns everything from that index to
the end, so the returned array count varied with the current stack
depth — you only got exactly n elements when the stack happened to
hold 2·n - 1 items. Everywhere else it either truncated or over-read
the stack, making getFunctionResult, startScript, and callScript pop
a buggy mix of params from the wrong positions.

Switching to suffix(n) fixes getFunctionResult for real — CORMKL's
Create IK (get PIECE_XZ / XZ_ATAN) now receives the piece index at
params[0] instead of whatever silent garbage the old implementation
returned. Also fixes start-script / call-script parameter passing
for any script that supplies more than one argument.

Also routes PIECE_XZ / PIECE_Y through the UnitModel's SIMD axis
convention: UnitModel remaps 3DO (x, y, z) -> SIMD (x, z, y), so
offset.y is TA's Z (horizontal depth) and offset.z is TA's Y
(vertical height). The getter now packs (offset.x, offset.y) for
PIECE_XZ and returns offset.z for PIECE_Y.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With Stack.pop(count:) now returning items in push order (oldest
first) — matching how TA's compiler lays parameters on the stack —
the .reversed() in getFunctionResult, startScript, and callScript
was flipping params[0] to the last argument instead of the first.
For calls like get(PIECE_XZ, piece, 0, 0, 0) that meant the piece
index landed in params[3] and params[0] carried a trailing zero, so
every PIECE_XZ resolved to whatever piece the script happened to
pad with first (typically 0). CORMKL's Create leg-positioning math
always computed the same angle for every leg as a result.

Dropping .reversed() lines up with the decompiler's existing
convention where params[0] is the first written argument. Also dumps
each selected unit's decompiled COB to /tmp/taassets-last-cob.txt so
future script issues can be compared against the bytecode directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TA Create scripts (CORMKL and every other walker in TAESC) do a
binary-search IK where each iteration does `turn Leg1-2 now;` and
immediately reads back the resulting end-effector position via
get PIECE_XZ / PIECE_Y / HYPOT. Our VM was appending `setAngle`
animations to Context.animations that only flushed in the
post-run applyAnimations step, so every iteration read the same
stale state and the bisection never converged — leaving the legs
folded straight under the body.

Adds UnitModel.pieceWorldTransform and pieceWorldPosition that walk
the piece's ancestor chain from the root, multiplying each piece's
local translation + Euler-rotation matrix. Mirrors the renderer's
matrix convention so the VM and display agree on where a piece is.

Threads the instance through run() as inout / UnsafeMutablePointer
so turnPieceNow and movePieceNow update piece.turn / piece.move
directly on the model. Subsequent PIECE_XZ / PIECE_Y queries in the
same script tick now observe the just-applied angles. Animated
(with-speed) turns and moves still flow through Context.animations
and the per-frame applyAnimations step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
waitForTurn and waitForMove parked the thread in a .waitingForTurn /
.waitingForMove status but nothing ever released them, so every
walker loop stalled on its first synchronization point and kept
queueing animations against a frozen piece state. CORMKL's walklegs
queue visibly ballooned past 1000 pending rotations while nothing
on screen moved.

applyAnimations now checks each waiting thread after processing the
animation list: if no rotation / spin / translation matching the
waited piece and axis is still in flight, the thread flips back to
.running for the next run() tick. waitForTurn / waitForMove also
shortcircuit when no matching animation is pending at the call site
so scripts that wait on something already complete don't stall.

Adds startScript + per-second heartbeat logs (module found, thread
count, pending animations) so future regressions are easy to eyeball
from /tmp/taassets.log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Create's binary-search IK (CORMKL and every other TAESC walker)
bisects the leg-segment angle by comparing the resulting end-effector
distance against a target distance. With PIECE_XZ / PIECE_Y packing
integer pixels, single-degree increments often produced the same
rounded hypot so consecutive bisection iterations made no progress
and the loop halved down to local8 = 0 having never moved off the
starting angle.

Scales piece world positions by 256 before packing, giving the IK
eight bits of sub-pixel precision while staying well within 16-bit
signed int range for typical unit footprints. XZ_ATAN and XZ_HYPOT
only care about the ratio and scale consistency of the packed
arguments, so the script comparisons keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
csilvertooth and others added 25 commits April 22, 2026 14:44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With pitch-outermost (R_x·R_y·R_z), a non-zero shoulder turn.x rotated
the knee's local x-axis toward vertical, and CORMKL's bisection IK
became unimodal — the "bend more → shorter reach" relationship broke
and the search froze at the degenerate near-zero angle. Switching the
composition to R_z(turn.y)·R_y(turn.z)·R_x(turn.x) keeps a child's
pitch axis in the horizontal plane regardless of the parent's pitch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes that together let CORMKL's Create-time bisection converge:

- PIECE_XZ / PIECE_Y return native TA integer units again; the
  256× sub-pixel scaling from 785890d broke every walker that
  compared XZ_HYPOT against fixed integer thresholds.
- taAtan2 returns unsigned [0, 65536) instead of signed
  [-32768, 32768]. LegGroups' quadrant checks — `XZ_ATAN(...) > 0
  && < 32768` and `> 16384 && < 49152` — relied on the full
  unsigned range; with signed output every third-quadrant angle
  failed both checks and the stride-target bisection rolled to
  the ±200 game-unit edge.
- GROUND_HEIGHT returns the world Y of the piece whose current
  world XZ matches the query (falling through to 0 otherwise).
  The zero stub made PositionLegs' `move Point to y-axis
  [GROUND_HEIGHT(PIECE_XZ(Point)) - PIECE_Y(Point)] speed [...]`
  oscillate every tick; returning the queried piece's own Y
  collapses the expression to zero so the Point targets sit
  still and the per-leg IK has a stationary aim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CORMKL's Create() spawns PositionLegs/LegGroups/SmokeUnit threads
with no signal mask, so nothing can kill them and they run a
forever-gait over the viewer's empty scene. Once the Create thread
returns we drop every remaining thread; user-triggered scripts
still work because they create new threads after the freeze point.

Also drops the 1-second auto-StartMoving and adds `d` to dump each
piece's offset/turn/move/world position/parent chain — needed to
diagnose which legs Create's bisection failed to converge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs on pushes to main, pull requests against main, version tags
(v*), and manual dispatch. Both apps build Release-unsigned on a
macos-14 runner and the .app bundles ship as zipped workflow
artifacts. Pushing a v-prefixed tag additionally attaches them to
a draft GitHub release so non-Xcode users can download the build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new segmented control in the map detail header toggles between
no overlay, per-cell elevation tinting, and a passability heatmap
(green-to-yellow for passable terrain, red where the max slope to
any neighbor exceeds a threshold, blue for under-sea cells, and
orange where a feature occupies the cell). The threshold slider
appears only in passability mode. Intended to help line up AEX's
passability computation against the actual TNT heightmap and sea
level for each map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main-branch pushes now update a single 'latest' prerelease with the
current TAassets and HPIView app zips attached, so anyone can grab
an up-to-date build from the Releases page without a GitHub login
or waiting on tag cadence. Version tags (v*) still cut their own
permanent entries, and the draft step is gone — tagged releases
publish immediately now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous README mixed three audiences (fork maintainers, the
original game-client experiment, and end users trying to run
TAassets / HPIView) into one wall of bullets. Split them:

- README.md is now a download-first user guide — where to get the
  apps from the Releases page, first-launch Gatekeeper steps, what
  TA files the apps need, and how each browser / overlay works.
- docs/FORK_NOTES.md keeps the summary of fork additions.
- docs/ORIGINAL_README.md preserves the upstream loganjones/SwiftTA
  README verbatim (Swift 4.2 / Ubuntu 16.04 era game-client notes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a conditional signing + notarization pipeline so Apple-blessed
builds land on the Releases page and downloaders can open the apps
with a double-click instead of right-click-Open.

When MACOS_CERTIFICATE_P12_BASE64 and the four notarization secrets
are present, the workflow imports the Developer ID cert into a
temporary keychain, builds with hardened runtime and --timestamp
--options=runtime flags, submits each .app to xcrun notarytool,
and staples the ticket onto the bundle before zipping for release.
The release notes mention whether the drop is signed/notarized.

PR builds from forks and any run without secrets still produce
unsigned artifacts instead of failing — useful for contributors who
can't see the repository secrets.

docs/SIGNING.md walks a maintainer through the one-time Developer
ID certificate export, Team ID and app-specific password lookup,
and the five secrets the workflow needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… diagnosable.

Previous failure mode printed only "Developer ID Application identity
not found" with no indication of what was actually imported — a .p12
with just the certificate (no private key) looked identical in the log
to a .p12 with the wrong certificate type. Log now prints every
codesigning identity and every certificate label present in the keychain
before the match, and the error message explicitly calls out the
"export both the cert and its key" gotcha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… settings don't pin signing to the upstream maintainer's team.

HPIView.xcodeproj carries DEVELOPMENT_TEAM=544Z2URGY9 from the
upstream loganjones fork, so xcodebuild refused to sign with our
Developer ID cert: "No certificate for team 544Z2URGY9 matching
'Developer ID Application: Azimuth Systems LLC (D96…)' found."
TAassets happened to sign because its xcodeproj had no team baked
in. Parse the team ID out of the imported identity's Common Name
and pass DEVELOPMENT_TEAM=<parsed> on every xcodebuild invocation
so both apps sign under the team that owns the cert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e diagnosable.

Notarization for TAassets landed with status Invalid and the stapler
step then failed with no further information. notarytool returns
exit code 0 when Apple accepts then rejects a submission, so the
workflow believed it had succeeded until stapler complained; and
it never pulled the log that would have said *why* Apple rejected
it. Switched to --output-format json, parsed the status, ran
notarytool log unconditionally so Invalid + Accepted both show
their log in CI output, and error-out on anything other than
Accepted before stapling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-task-allow.

Apple's notary service returned Invalid with:
  "The executable requests the com.apple.security.get-task-allow entitlement."

When xcodebuild signs a target that has no explicit
CODE_SIGN_ENTITLEMENTS set, it injects a default plist that
includes get-task-allow=true (so debuggers can attach). That flag
is never permitted in notarized binaries, so every submission
failed validation.

ci/release.entitlements is a minimal plist that pins
get-task-allow to false. Both TAassets and HPIView release builds
now pass CODE_SIGN_ENTITLEMENTS pointing at it, so Xcode stops
injecting the debug-only entitlement and notarization has a clean
signature to validate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CORMKL (and most TAESC units) iterate over nearby units in Detect()
with the TA Recorder / Unofficial Patch 3.9 extensions:

  for id := get MIN_ID() to get MAX_ID() do
    if not get UNIT_ALLIED(id) then … check height, distance, …

Our VM fell through on all the extended IDs and returned 0, which
meant MAX_ID also returned 0, the loop ran once with id=0, and
UNIT_ALLIED(0) returning 0 classified id=0 as hostile — so every
mod unit thought it was surrounded by enemies and flipped armor /
shield state accordingly on load.

Adds the six IDs CORMKL actually hits (MIN_ID=69, MAX_ID=70,
MY_ID=71, UNIT_BUILD_PERCENT_LEFT=73, UNIT_ALLIED=74,
UNIT_IS_ON_THIS_COMP=75) to UnitScript.UnitValue, wires them into
both getUnitValue and getFunctionResult dispatchers, and teaches
the decompiler to render them by name. In the no-sim viewer:

 - MIN_ID = 1, MAX_ID = 0 → iteration loop exits without entering
 - MY_ID = 0
 - UNIT_BUILD_PERCENT_LEFT = 0 (fully built)
 - UNIT_ALLIED = 1 (always allied — "skip" branch wins)
 - UNIT_IS_ON_THIS_COMP = 1

The full TADR extension set (~150 IDs) is the reference spec for a
real sim like AEX; SwiftTA only implements enough to keep mod
target-scan logic from misfiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s the asset inspectors.

Removes the SwiftTA macOS, SwiftTA iOS, and SwiftTA Linux source
trees, their xcodeproj/Package.swift entries, and the workspace
references that pointed at them. CI never built them; they weren't
imported by TAassets or HPIView; they were just clutter inherited
from the original loganjones/SwiftTA. Anyone who wants the game
client can clone upstream directly.

The Swift packages the game client once shared with the inspectors
(SwiftTA-Core, SwiftTA-Ctypes, SwiftTA-Metal, SwiftTA-OpenGL3) all
stay — TAassets and HPIView depend on them.

Also relocates the legacy screenshots (SwiftTA.jpg, TAassets.gif,
HpiView.jpg) under docs/images/ and fixes the references in
docs/ORIGINAL_README.md, so the preserved upstream README still
renders intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core adds:
- TaMapModel.writeTnt() — serializes a TA-version TNT map to a binary
  blob byte-equivalent at the section level to what Cavedog writes
  (header/ext-header/tile-index/map-info/tile-array/features/minimap).
  Validates invariants (sample counts, tile sizes, feature indices)
  before emission.
- TdfParser.Object.serializeAsTdf() and a Dictionary extension for the
  top-level .ota / .fbi / .tdf shape, so OTA round-trips happen at the
  lossless parser-object level instead of through the MapInfo view.
  Keys are sorted for determinism; the emitter is stable under
  repeated round-trips.

Tests cover both writers with synthetic maps, no-feature edge cases,
error paths (oversized feature names, mismatched sample counts),
mutation-then-round-trip, and an env-gated real-map round-trip
(SWIFTTA_TEST_MAPS_DIR) that stays skipped until someone points it at
loose .tnt files.

CI now runs `swift test` before building the apps so writer
regressions fail fast instead of getting masked by a clean app build.

No UI changes yet — that's Phase 2, which creates the AEX-MapEditor
app target and the height-brush MVP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The editor is a new macOS app target, linked against SwiftTA-Core to
reuse Phase 1's TNT writer. Open a loose .tnt file, left-drag to
raise heights, right-click to toggle erase (lower), Cmd-Z/Cmd-Shift-Z
undo/redo, Cmd-S saves back to the file with a <name>.tnt.bak
snapshot on first write. Any .ota sibling gets loaded and
round-tripped through the TDF writer on save so existing metadata
doesn't get discarded even though we aren't editing it yet.

Rendering in this MVP is Core Graphics grayscale — Phase 4 (tile
painting) will promote to the Metal map renderer shared with
TAassets. Five source files total:

- AppDelegate programmatic menu + File → Open
- EditableMap wrapping TaMapModel + OTA with save-with-backup
- HeightBrushCommand and HeightBrushStroke for the brush tool +
  one-stroke-per-undo semantics
- MapCanvasView for the raster + brush-footprint overlay + mouse
- MapEditorWindowController owning the tool palette + undo manager

CI now builds / signs / notarizes the third app and ships
AEX-MapEditor-macOS.zip to the rolling Latest release alongside the
existing two. README lists it as early access.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setting a title on the NSMenu but not the NSMenuItem that contains
it left the menu bar showing only the app-name menu — the File,
Edit, and Window headers were empty strings. macOS renders the
first top-level item using the process name regardless of what we
set, but every subsequent item needs its own title string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…u bar.

Without NSMainNibFile and without an @main stub that calls
NSApplicationMain explicitly, nothing in the launch path sets the
process activation policy to .regular. macOS then kept the previous
app's menu bar and AEX-MapEditor ran as an accessory with no UI
focus. Setting the policy and activating at launch gives it a
proper menu bar and foreground presence.

Also drops the private _setMenuName: selector fiddle for the Open
Recent menu — AppKit populates that from NSDocumentController
automatically for document-typed apps; we can wire a custom recent-
documents menu later if needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@main on an NSApplicationDelegate calls NSApplicationMain under the
hood, which reads NSMainNibFile from Info.plist to locate the
delegate. This app ships without a MainMenu.xib and therefore
without that key, so NSApplicationMain fell through to the default
delegate — nothing. applicationWill/DidFinishLaunching never fired,
installMainMenu() never ran, and the menu bar stayed at the
minimum two system items (Apple + process name).

main.swift now instantiates AppDelegate directly, assigns it to
NSApplication.shared, sets the activation policy, and calls run().
Delegate callbacks fire, the programmatic four-menu bar installs
cleanly, and the app activates as a regular foreground app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applicationShouldTerminateAfterLastWindowClosed was returning true
unconditionally, and AppKit counts the File → Open panel as a
window. So dismissing the panel — cancel OR even the "just opened,
haven't picked yet" state — triggered the last-window-closed rule
and the app exited before anything useful happened.

Gate the terminate-on-close behaviour on "has an actual map window
ever been opened" so a freshly-launched editor stays running until
the user has opened at least one map and then closed it. Cancelling
the open panel now leaves the app idle in the menu bar, as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Most TA-format maps ship inside Cavedog archives, not loose on
disk. Requiring users to extract with an external tool before they
could edit anything was an onboarding cliff. File → Open now
accepts both .tnt and the five archive formats. When the user
picks an archive:

- ArchiveMapPicker walks the archive's directory tree, collects
  every .tnt file, and pairs it with its same-basename .ota sidecar
  when present.
- A modal popup picker lets the user choose among the contained
  maps, sorted by name.
- The selected .tnt and (if present) .ota are extracted to a per-
  archive folder under Application Support/AEX-MapEditor/Extracted/
  so future opens of the same archive find the files already staged
  and the user has a known-writable location for edits.
- The editor then opens the extracted .tnt like any loose map. Save
  writes back to that extracted copy; Save As can move it elsewhere.

OTA-extract failures log but don't block the map from opening —
they just mean the editor treats the map as having no metadata.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Palette now has a Heights / Features tool switcher. Features mode
surfaces a popup of every feature type already declared in the
map's feature table plus an "Add feature type…" button for typing
a new name (e.g. Tree01, MetalPatch). Left-click assigns the
selected feature to a cell, right-click erases whatever feature is
at the cell. The map canvas draws an orange square on every cell
carrying a feature so the layout is visible at a glance.

Every edit — cell assignment, cell erasure, feature-type append —
routes through its own MapCommand and lands on the window's undo
stack, so Cmd-Z rolls back in the same order the user painted.
Undoing an append correctly leaves existing assigns intact unless
they pointed at the removed index; future hardening can validate
that invariant explicitly.

Rendering the actual feature sprite (loading the planet's feature
pack GAFs) is Phase 3.x; the orange-cell overlay is enough for
laying out obstacles and lining up with AEX passability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pieces land together:

- MapRasterizer assembles a full-resolution RGBA image of the
  map's current tile layout by walking the tile-index map and
  blitting each referenced tile from the tileset with the supplied
  palette applied. Cached on the canvas and invalidated whenever a
  tile command or undo/redo lands.
- A Tiles tool joins Heights and Features on the palette. In Tiles
  mode the canvas renders the actual painted map in colour, a
  dropdown lists every tile in the map's tileset (with 32×32
  previews as menu-item images), and clicking on the map replaces
  the tile at the clicked 32×32 cell with whatever the dropdown
  has selected. Each paint is a TilePaintCommand on the undo stack.
- The vanilla Cavedog palette (PALETTE.PAL, 256 RGBA entries)
  ships as a bundled resource and loads lazily when a map opens.
  EditableMap.palette exposes it so later phases can substitute a
  per-planet palette when we wire feature-pack loading.

Known gaps — no minimap regeneration on tile edits yet (minimap
is cached as the author shipped it), no tile-picker grid (dropdown
is functional but not a true palette), no bucket-fill. Those stay
on the Phase 4.x list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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