Commit Graph
7612 Commits
Author SHA1 Message Date
06b115a293 drop EvalLua and the commented-out Lua init leftovers
EvalLua had no callers anywhere in the tree — it converted a wide string
to UTF-8, ran it as a chunk and printf'd the error, which was the hook for
a developer console that is not wired up. It was the only user of stdio,
MemMan and windows.h in this file.

InitializeLua and ShutdownLua are live, called from InitOverhead and
ShutdownOverhead. The calls in InitializeGame and ShutdownGame were
commented out when they moved there; remove them, and the commented
ACCESSOR_TABLE block in InitializeLua that refers to a macro no longer
defined anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:25:35 -03:00
76eda883a5 build Lua from vendored source instead of the prebuilt lua51.lib
The tree carried two prebuilt Lua static libraries and a copy of the Lua
public headers. lua51.lib is 5.1.2 and matched the headers; lua51.vc9.lib
is 5.1.4 and was dead weight — it came second in Ja2_Libraries, so the
linker resolved every Lua symbol out of lua51.lib and never pulled an
object from it. Just as well, since it asks for /DEFAULTLIB:MSVCRT while
we build /MT.

ext/lua-5.1.5 is the upstream tarball unmodified, built as lua51 the way
the other vendored libraries are, and its src directory replaces lua/ as
the home of lua.h, luaconf.h, lauxlib.h and lualib.h. Those four headers
were stock 5.1.2 retabbed, so 5.1.5 is bugfix-only against what the game
compiled against; the bytecode format is unchanged across 5.1.x and every
script under gamedir is plain source anyway. lua/lua.hpp had no includers
and returns to etc/ where upstream keeps it.

/SAFESEH:NO goes with it. Its two stated reasons were lua51.lib and the
smackw32 import library, and both are now gone: every remaining prebuilt
static library is SAFESEH-clean (libexpatMT.lib 5 of 5 members with
@feat.00 = 0x1, RakNetLibStatic.lib 79 of 79), and lld-link emits a
3155-entry SEHandlerTable without it.

The /MT comment blamed the wrong library. lua51.lib carried no linker
directives at all; RakNetLibStatic.lib is what pins us to the static
runtime, with /DEFAULTLIB:LIBCMT and /DEFAULTLIB:libcpmt.

Verified by building all four applications in Debug and Release, and by
linking the tarball's own lua.c against our lua51.lib with the build's
clang-cl flags and running it under Wine: 5.1 stdlib, GC, coroutines and
the x86 __asm fld/fistp lua_number2int fast path all behave. The game
itself could not be launched here — this checkout's gamedir has loose
Data directories but no SLF archives, so VFS aborts on Data\Ambient.slf
long before any script runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:25:35 -03:00
Marco Antonio J. Costaandmajcosta 7f7cf34e26 replaced by ext/libsmacker 2026-07-30 13:25:35 -03:00
Marco Antonio J. Costaandmajcosta 4e43d93623 bad idea 2026-07-30 13:25:35 -03:00
Marco Antonio J. Costaandmajcosta 80109ee036 not used anywhere 2026-07-30 13:25:35 -03:00
dc351889db assemble releases on ubuntu runners
The assemble job only ran on Windows for the case-insensitive filesystem, which
the overlays no longer depend on. Checking out the ~93000 game data files is
most of what the job does, and ubuntu runners are quicker and cheaper at it.

Naming the executable has to be exact now: the artifact is named after the
matrix entry, which is lowercase, while the executable inside it is named after
the CMake target, which is the uppercased application. Only NTFS was making
those agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 09:12:12 -03:00
c11014afa1 check game data for paths that differ only in case
Nothing stops the inconsistency the previous commit cleaned up from coming
straight back. The game reads its data through FileMan, which hands every path
to VFS, and VFS compares paths case-insensitively (vfs::Path::Less ->
vfs::String::less). A file added under Data/Mercedt/ therefore works perfectly
well beside Data/MercEdt/ right up until a release is assembled on a
case-sensitive filesystem and both of them survive into the archive, at which
point the game serves whichever the directory yields first.

Directories count as much as files: a translated-only file below Data/Mercedt/
collides with Data/MercEdt/ even when no file does, so each overlay is merged
onto gamedir the way a release does and every path prefix is compared.

Runs on pull requests only, and in its own workflow rather than in build.yml,
which release.yml also calls: a check has no business running again while a
release is assembled. It reads the git index rather than the files, so a
blobless sparse checkout is enough and no game data is fetched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 09:12:12 -03:00
b9ea3ef36e spell language overlay paths the way gamedir does
The overlays were authored on Windows, where a filesystem that folds case hid
the fact that thousands of translated files spell their path differently from
the gamedir file they are meant to replace: Data-1.13/Mercedt/170.EDT over
Data-1.13/MercEdt/170.EDT, 149_ATTN.wav over 149_ATTN.WAV. Copying an overlay
onto gamedir on a case-sensitive filesystem leaves both files instead of
replacing one, and VFS, which folds case itself, then serves whichever the
directory happened to yield first.

Every overlay path is now spelled component for component the way gamedir
spells it. gamedir was already self-consistent, so nothing there moves and no
new convention is invented. The transformation is mechanical and no file gains
or loses content:

    git diff --shortstat HEAD~1 HEAD
        5374 files changed, 0 insertions(+), 0 deletions(-)
    git diff --name-status -M HEAD~1 HEAD | cut -f1 | sort -u
        R100
    git ls-tree -r --name-only HEAD gamedir-languages | wc -l
        28884, the same as before

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 09:12:12 -03:00
95cf0621a0 grow libsmacker audio buffer when a frame exceeds the header max_buffer
The Smacker header declares a max audio chunk size, and libsmacker sized its
output buffer from it — then wrote each frame's audio trusting the frame's
own unpacked size, unchecked. The fan-localized intro videos (Chinese among
others) declare max_buffer=2304 but carry ~97KB audio frames: every decode
was a heap overflow, crashing the intro. The original SMACKW32.DLL played
these files, so treat the per-frame size as truth and grow the buffer,
bounded by a 16MB sanity cap; a chunk beyond that fails the frame as corrupt.
Covers both the raw-PCM and DPCM paths.

Verified with an ASan/UBSan harness over all 19 vanilla and Chinese intro
SMKs: previously all 8 Chinese files faulted, now all decode both passes
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 21:31:13 -03:00
858c149584 never upload or delete crash reports from unversioned local builds
A "build local" report has no released PDB behind it — the telemetry sink
cannot symbolize it. Skip these when draining reports at startup: not sent,
not reaped by the 30-day cleanup, left on disk for the developer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 20:58:11 -03:00
majcostaandGitHub 4bd709ea36 Merge pull request #677 from majcosta/monorepo
Monorepo
2026-07-28 20:25:26 -03:00
Marco Antonio J. Costa 0224174795 remove cross-repo dispatch workflows
These triggered source's release workflow on gamedir pushes. In the
monorepo a push to master triggers it directly, and GitHub ignores
workflow files outside the repository root anyway.
2026-07-28 19:08:24 -03:00
Marco Antonio J. Costa a1784fbaff adapt CI workflows to the monorepo layout
gamedir and gamedir-languages now live in this repository, so the build
no longer clones them separately or pins their commits in versions.env:
one commit SHA identifies everything. The compile job sparse-checks-out
the source only, and the assemble job the game data only, both with
blob:none so neither downloads the other half. Dist names drop the
G....L.... gamedir suffixes since GAME_VERSION now covers the data too.
2026-07-28 19:08:23 -03:00
Marco Antonio J. Costa 8366b00793 merge gamedir-languages repository into gamedir-languages/ subdirectory
Full history of majcosta/gamedir-languages imported via git filter-repo
--to-subdirectory-filter. Blobs unreachable from its HEAD whose content
is binary (11847, NUL-byte sniff as git does it) were stripped; text
file history is kept in full. Working tree content is identical to
gamedir-languages HEAD; verify with:
  git diff --stat gl/master HEAD -- gamedir-languages/
2026-07-28 19:08:02 -03:00
Marco Antonio J. Costa 80caf3c935 merge gamedir repository into gamedir/ subdirectory
Full history of majcosta/gamedir imported via git filter-repo
--to-subdirectory-filter. Blobs unreachable from its HEAD whose content
is binary (37314, NUL-byte sniff as git does it) were stripped; text
file history is kept in full. Working tree content is identical to
gamedir HEAD; verify with:
  git diff --stat gd/master HEAD -- gamedir/
2026-07-28 19:05:22 -03:00
a5c514d2be fix mismatched upper/lowercase filenames
Fix case of WinFont.h include in WinFont.cpp

Fix case of "Strategic Status.h" include in Queen Command.cpp

Fix case of "mapscreen.h" include in Strategic Merc Handler.cpp

Fix case of "Handle Items.h" include across Tactical/TileEngine/Utils

Mechanical: normalized every #include of handle items.h (any case) to
match the actual filename "Handle Items.h". Verify with:
grep -rn "handle items.h" -i --include=*.cpp --include=*.h . | grep -v "\"Handle Items.h\""

Fix case of "World Items.h" include across Tactical/Editor

Mechanical: normalized every #include of world items.h (any case) to
match the actual filename "World Items.h". Verify with:
grep -rn "world items.h" -i --include=*.cpp --include=*.h . | grep -v "\"World Items.h\""

Fix case of "Arms Dealer Init.h" include in Overhead.cpp

Fix case of "Meanwhile.h" include in TeamTurns.cpp

Fix case of "timer.h" include in Utils All.h and Event Pump.cpp

Fix case of "Store Inventory.h" include in XML_Items.cpp

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 15:53:25 -03:00
c3578335ab cmake: keep frame pointers
Frame pointers on: dbghelp's stack walker gets usable frames in crash
reports.  /Oy- rather than -fno-omit-frame-pointer -- clang-cl rejects
the GNU spelling:("unknown argument ignored"), and cl.exe takes /Oy-
too. x86-only option; a 64-bit target would ignore it and rely on
unwind data instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:22:16 -03:00
dcb4b5bfea cmake: do not optimize away nullptr checks
OBJECTTYPE::exists() and SOLDIERTYPE::exists()/DeleteSoldier() are
called on null pointers by design and guard with `this == NULL`. That
is undefined behaviour, so clang infers `this` is non-null: it deletes
the guard inside the callee *and* deletes null checks that follow a
call in the caller, which is an access violation at /O2 in code MSVC
has always compiled the naive way.  The caller-side inference happens
in every translation unit that calls one of these, so this has to be
global rather than per-file; there is no per-function attribute or
pragma for it. Drop it once nothing relies on a null `this`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:22:16 -03:00
majcostaandmajcosta c3c4fe2873 add telemetry INI for chinese version 2026-07-28 12:10:19 -03:00
a1576bcaaa fix dispatch after source CI refactor
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 11:20:55 -03:00
3e170e01a4 fix dispatch on the CI since source refactored
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:55:18 -03:00
dbc4772da1 fix warning about deprecated node version
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:55:18 -03:00
majcostaandmajcosta c2cf6331c5 add telemetry server URL 2026-07-28 10:36:20 -03:00
c3e2d1ae3d Cap a report at 32 KB on both sides of the upload
The client would send up to 256 KB and the sink answered a settling 400 above
64 KB, so the two disagreed about what a valid report is, and the client
deletes what it is told is invalid. Unreachable in practice — the module table
and backtrace are both bounded, which puts the ceiling near 10 KB — but the
two constants have to agree for the disagreement not to matter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
1d0b4beb4c Sanitize every field the channel summary quotes, not just the handle
The endpoint is public and unauthenticated, so the whole uploaded file is
attacker-chosen, not only the part the client copied from Ja2.ini. The build
field sat inside backticks a backtick closes, and the access-violation text
went in raw, so either could carry markdown or a link into the channel.

One clean(): printable ASCII minus what Discord reads as markup or a URL,
length-capped. It replaces the handle's own stripping and absorbs the .trim()
the field getters did, which also drops the CR that "(.+)$" captures off a
CRLF report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
90f5bff69b Report module names, not the paths they were loaded from
The module table is what makes a report symbolizable, but a full path also
carries the player's Windows account name and wherever they keep the game,
and the report leaves their machine. Symbolizing only ever matched on module
name and base — symbolize_crash already ran every path through baseName()
before printing it — so the directory was read by nothing.

Say so in the consent prompt too. "No personal information" was not true of
a table full of paths, and is only worth claiming if it holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
3794e3a274 Symbolize crash reports with a first-party tool
Adds tools/symbolize_crash.cpp, built alongside the game as a console exe:
it reads a crash_report_*.txt written by writeExceptionBacktrace and prints
the backtrace with function names, source lines and inlined frames, in call
order, coloured when standard output is a console.

Symbols come from DbgHelp against the build's PDB rather than an external
symbolizer. SymLoadModuleEx takes the runtime base out of the report's
module table, so the relocation arithmetic /DYNAMICBASE forces on us is
DbgHelp's problem now; a report predating the module table loads at the
image's preferred base, which is where it ran. Verified under Wine: PDB
line info and inline traces both resolve.

The tool wants C++23 (std::print, std::format) where the game is C++17, so
the standard is set on the target alone. Both MSVC 19.51 and clang-cl build
it clean in all three configurations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
58e00e0d66 Add the crash-telemetry sink, a Cloudflare Worker, to the repo
The other half of the feature, and the half that decides whether a player's report
survives: the endpoint CRASH_TELEMETRY_URL points at. It takes the POST from
processCrashTelemetry and forwards the report to a Discord webhook as a file
attachment. It stores nothing -- a report is only worth reading next to the PDB it
was built against, and that never leaves a developer's machine, so there is nothing
for a bucket to do here that the channel we already read bug reports in does not do
better. That also keeps the whole thing inside the Workers free tier, where the
10 ms budget is CPU, not wall clock, and waiting on Discord is not metered.

In the repo rather than in someone's home directory because the status codes are a
contract with the client and the two have to be changed together. reportIsSettled()
in sgp/crash_telemetry.cpp deletes the player's copy on 2xx and on 400/413/415 and
keeps it on everything else, so a settling 4xx returned for a failure on our side
silently destroys the report. Every failure path here is therefore a 503, each
naming its own cause in wrangler's console, and the one 4xx that is safe -- 429,
which the client does not settle -- is the one the rate limiter returns.

That rate limiter is a binding with a .limit() call, not a dashboard rule: WAF rate
limiting rules need a zone and a workers.dev subdomain is not one. Per-IP, 50 a
minute, which has to clear kMaxUploadsPerRun (20) in the client or a player draining
a backlog throttles themselves. It is checked before the body is read.

A report is attacker-controlled text arriving at a public, unauthenticated endpoint
whose URL ships in every player's Ja2.ini, so the summary line strips markdown from
the player handle and the payload sets allowed_mentions to nothing. The size cap and
the "*** CRASH" check keep drive-by scanners out; anything determined gets through,
and the blast radius is a message we delete.

test.mjs covers the whole contract against a stubbed fetch, no network and no
webhook needed. DISCORD_WEBHOOK is a secret and lives nowhere in this tree;
.dev.vars, which holds a live one for local development, is gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
f5782dccbd Upload pending crash reports as opt-in telemetry
A crash report is worth nothing sitting on the player's disk. On startup, drain
the crash_report_*.txt files the handler left behind to the endpoint named by
CRASH_TELEMETRY_URL in Ja2 Settings; an empty or absent key turns the feature off
entirely. The first launch asks the player once and remembers the answer in
telemetry.consent -- declined means the reports simply keep accumulating locally.

This lands in its own translation unit rather than in more of debug_win_util.cpp.
Everything in that file runs inside a faulting thread and may not allocate;
everything here runs at startup with a healthy heap and is ordinary code. Two
files keep the no-heap rule easy to see and easy to hold.

The draining runs on a detached thread. The uploads are synchronous WinHttp calls
with seconds-long timeouts, and this sits on the startup path, so on the main
thread an unreachable endpoint is a stall the player watches before the splash
screen. Nothing waits on the result: if the player quits first the process exits
from under the thread, which costs nothing, since an interrupted upload leaves the
file on disk and it goes out next launch. The consent prompt stays on the main
thread on purpose -- it is a question, and a question has to be asked before
anything is sent.

Which reports get deleted is chosen so that a mistake cannot destroy them. A file
goes away on 2xx, and on 400/413/415, i.e. content the server will never accept.
Everything else keeps it: no connection, 5xx, and notably the 403/404 of a
mistyped CRASH_TELEMETRY_URL, which would otherwise silently eat every player's
crash history. Bounds all round: every WinHttp phase has a timeout, a report over
256 KB is not one of ours and never goes on the wire, at most 20 uploads per
launch so a crash-looping build cannot turn startup into an upload session, and
reports older than 30 days are dropped unsent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
71cc553a07 Capture crashes with a first-chance vectored handler and dump them heap-free
Report crashes for every player, not only those running under Wine's WINEDEBUG.
A last-chance UnhandledExceptionFilter is no good: faults on the message-pump /
WindowProcedure path have no game __except on their stack, and under Wine the
WndProc dispatch swallows them before they ever reach "unhandled". Install a
vectored handler instead -- it runs first-chance, ahead of every frame handler,
records the fault and returns EXCEPTION_CONTINUE_SEARCH, so normal SEH is
unaffected.

The dump is deliberately heap-free. The crash most worth reporting is often heap
corruption, so anything that allocates (the game Logger, std::vector, DbgHelp's
Sym* family) would fault again and take the report down with it. Using only stack
buffers and raw Win32, writeExceptionBacktrace writes the registers, the faulting
address, a UTC timestamp, the build id, the loaded-module table and a
bounds-checked manual EBP walk into a fresh numbered crash_report_NNN.txt.

The module table is what makes the addresses mean anything. Our executables are
linked /DYNAMICBASE and keep their .reloc section, so the loader is free to move
the image: Wine leaves it at the preferred base, Windows ASLR does not. A report
listing only runtime addresses is symbolizable by luck, and silently wrong once
the luck runs out. Recording where each image actually landed turns the offset
into arithmetic:

    llvm-symbolizer --obj=JA2.exe --adjust-vma=$((<JA2.exe base> - 0x400000))

It is walked off the PEB loader list because both alternatives -- EnumProcessModules
and DbgHelp's module APIs -- allocate, and this path must not. Having the table
also frees the backtrace from restricting itself to our own module's return
addresses: a fault inside ddraw/fmod/bink is exactly the case worth seeing, and
an address belonging to no module is recognizable as the frame-pointer debris it
is.

Two things go into the report beside the machine state. czVersionString stamps
the build, so a report matches the exact PDB it has to be symbolized against, and
the optional HANDLE from Ja2 Settings names the player, so a report can be tied
to whoever raises it with us. The handle is sanitized where it is set rather than
where it is used: it is player input that lands in a line-oriented text report,
so anything that could forge a line (CR/LF, control and non-ASCII characters) is
dropped and the length is capped.

Finally the player is told. The message is composed in the handler, while the
fault details and the report's filename are still in hand, but shown from
SGPExit: first-chance means the exception may yet be handled downstream, and a
message box pumps messages, which on the heap that just faulted is a second
crash. It replaces the generic "Unhandled exception. Unable to recover." box
rather than adding to it -- it says more, and it names the file we need attached
to the bug report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
1e2ce70315 Stamp the build's identity into GameVersion.cpp from CMake
GameVersion.cpp becomes a configure_file template, so the version strings are a
build input rather than something a CI step seds into the working tree mid-run.
That is what lets a crash report be matched to its exact build's PDB for offline
symbolization.

The two strings get distinct jobs. czVersionString is the machine-readable build
identity: the bare short commit SHA and nothing else. A `git describe` string
cannot serve here -- czVersionString is 16 bytes because it is stored in the
savegame header as SAVED_GAME_HEADER::zGameVersionNumber and strcmp'd on load,
and describe puts the SHA last, so truncation ate exactly the discriminating part
and could make two different commits compare equal. CI passes 9 hex; anything
that would not fit is now a configure error rather than a silent clip.

zBuildInformation is the display string, untruncated, and it is no longer printed
beside czVersionString: that drops the duplication in the version line ("JA2 1.13
V4-555-G6A941C0 2026-07-25 V4-555-G6A941C06") and stops showing a token that is
now machine-only. CI's copy leads with GAME_VERSION, because the version line is
now the only place a player reads which release they are running.

Finish what the sed step's TODO asked for: the workflow pins GAME_BUILD_INFORMATION
and GIT_SHA once, next to the gamedir SHAs it already pins, and hands them to CMake
with -D. Every parallel build job therefore stamps one agreed identity, and the
compile checkout needs no git history at all.

A build nobody identifies is a local build and says so -- czVersionString reads
"local" -- rather than deriving a SHA from HEAD. A derived SHA could not stay
true: nothing re-runs configure once HEAD moves, so an incremental build would
keep whatever it was configured with, and keeping it honest would mean
reconfiguring on every commit. It would also buy nothing, because only packaged
builds are archived with their PDBs; a local build's crash reports are symbolized
against the PDB sitting next to the exe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 10:25:47 -03:00
Marco Antonio J. Costaandmajcosta 00dad8aa12 once we build with mingw, i'll worry about this 2026-07-28 10:25:47 -03:00
Marco Antonio J. Costaandmajcosta 9416e82fb5 remove last remnant of old exception handling code 2026-07-28 10:25:47 -03:00
765dabf419 Fix too-many-arguments fprintf in spread pattern debug log
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 08:55:30 -03:00
Marco Antonio J. Costaandmajcosta 3ba0aa71d7 initialize gubFOVDebugInfoInfo after allocation
otherwise it holds garbage
2026-07-28 07:55:10 -03:00
3a3d42d2a6 Fix sizeof-pointer memset in FOV debug clear
gubFOVDebugInfoInfo is a heap-allocated UINT8*, so sizeof() on it only
covered the pointer, not the WORLD_MAX-sized buffer it points to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 07:55:10 -03:00
8b80466a7d rebuild the bink import library on every toolchain, not just cross ones
binkw32.lib as shipped is a long-format ordinal import library built in
2002, and link.exe is the only linker left that binds it. lld-link takes
it without complaint and writes an import descriptor with an empty thunk
table, so every Bink call reaches whatever the stale IAT slot holds. The
first one is BinkSoundUseDirectSound, reached from BinkInitialize during
EnterIntroScreen, which is unconditional -- Intro.cpp constructs its
VideoPlayer with VT_SMK|VT_BINK in every application, so a Smacker-only
gamedir still runs the Bink init path. The process dies before the splash
with an access violation executing an unmapped address.

clang-cl.cmake and mingw.cmake already rebuilt the library from
binkw32.def for exactly this reason, but they are toolchain files, so the
rebuild only happened when someone cross-compiled. Which linker is in use
is not a cross-compilation question: a preset that points
CMAKE_CXX_COMPILER at clang-cl -- how you use it from Visual Studio --
loads no toolchain file and linked the broken library instead. The test
was a proxy for "is this lld-link", correct until it wasn't.

So the rebuild moves to the top-level CMakeLists and runs unconditionally,
and clang-cl.cmake loses its copy: it already sets CMAKE_AR to llvm-lib,
which is the same binary the new code invokes with the same arguments.
mingw.cmake keeps its own, because GNU ar cannot build an import library
at all and dlltool takes different flags.

Only the name shape differs between archivers. lib.exe prepends the x86
leading underscore to every name in the .def; llvm-lib and llvm-dlltool
take the name as written. The checked-in file keeps the underscore for the
llvm tools and the MSVC path strips it back off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 07:13:52 -03:00
ee183510d3 another ja2export error that wasn't caught
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:39:23 -03:00
ee4a6b1149 warnings: catalog what we emit today, error on anything new
No warning is fixed here. Every one we currently emit is harvested and
switched off one by one, and /WX goes on top, so both toolchains build
quiet and any *new* warning fails the build instead of scrolling past.

cmake/Warnings.cmake carries two independent lists, one per compiler,
one line per warning with its occurrence count. Deleting a single line
is the unit of work from here on: drop the line, fix the fallout in
both compilers, commit. The build stays honest in the meantime.

Picking a level had to come first. CMake stopped injecting /W3 of its
own accord (policy CMP0092), which had quietly left MSVC on the /W1
default and hid nearly everything; at /W4 it reports 89361 warnings
across 31 codes. clang-cl is pinned to /W3 -- clang's own -Wall, and
already broader than MSVC /W4 -- for 55 more. The two lists are not
comparable by length.

Counts cover all four applications, since the #ifdef forest means each
one compiles different code. The file is included after the ext/
subdirectories so that add_compile_options' directory scope leaves
vendored code on its default flags; ext/ still warns, deliberately,
and /WX does not reach it.

The existing /wd4838 moves here with the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:56:01 -03:00
f7faa076a9 name the application executable when copying to the gamedir
every build artifact also carries Ja2Export.exe, so the glob handed mv
three paths and it failed with "target 'gamedir/ja2.exe' is not a
directory". The assemble job runs on Windows, so the lowercase matrix
name matches the uppercase target file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:04:58 -03:00
3471b19fdc split the release jobs into their own workflow
A skipped job still shows up in a pull request's check list, so assemble
and release reported there as two permanently skipped checks. A job can
only be left out of a run graph it does not belong to, so move both into
release.yml, which triggers on master, v* tags and manual dispatch, and
gets its build by calling build.yml through workflow_call. A pull request
now runs build.yml directly and reports exactly the five checks it can
actually pass.

The global_vars step only existed to compute assemble_release for the two
jobs that left, and it read the workflow_dispatch input, which a called
workflow cannot see. Its logic moves to a condition on assemble itself,
unchanged: a manual run assembles only when asked, tags always assemble,
master assembles only upstream. The release job needs no condition since
a skipped assemble skips it.

Check out the source rather than cloning it by hand. A pull request
builds its merge commit, which is on no branch and so is absent from a
clone of branches and tags; it resolved only because --filter=tree:0 made
the clone partial and git fetched it from the promisor remote on demand.
The action fetches the merge ref itself, so nothing hinges on the filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 14:39:51 -03:00
d68f046ea3 build on pull requests, drop per-branch push builds
A push builds in the repository it was pushed to, so a contributor's
branch builds in their fork and the base repository sees nothing. Without
a run of its own there is no check on the pull request for branch
protection to require, so trigger on pull_request as well.

Push builds narrow to master and v* tags: with pull requests covered,
building every branch push only duplicated the same commit's build across
four Windows matrix jobs. Master still builds so the "latest" pre-release
keeps updating. A branch with no pull request open builds via
workflow_dispatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 14:39:51 -03:00
Marco Antonio J. Costaandmajcosta fd47a422b5 ja2export is not vendored, we own that + fix errors
stop lying about it by keeping it in ext/
fix three errors in clang-cl
2026-07-27 13:07:29 -03:00
fc1c1dfe77 remove stray 'typename' before non-qualified names
'typename' disambiguates a qualified dependent name, telling the
compiler that something like T::iterator names a type rather than a
value. Before a plain identifier there is nothing to disambiguate: P1
is already a type, and so is PopupIndex.

The grammar wants a qualified name after the keyword, so clang reports
"expected a qualified name after 'typename'", drops the keyword and
carries on -- 15175 times, since the offending declarations sit in
widely included headers. MSVC accepts them without a word.

Semantics do not change. Both compilers already ignored the keyword,
so the partial specialisations matched before this and match after it.

The diagnostic belongs to no -W group and therefore cannot be switched
off with -Wno-, which makes deleting the tokens the only way to quiet
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 13:05:23 -03:00
b6c79dbead make sccache available for all cmake users, not only CI
if you have it in your path, the build will use

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 06:16:42 -03:00
f99786e195 cache compiled objects with sccache
Compiler cache stored in the GitHub Actions cache, shared across runs.

Requires /Z7 instead of the CMake default /Zi, since sccache cannot cache
objects whose debug information lands in a shared PDB. That is a debug
information format change only, the generated code is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 06:16:42 -03:00
Marco Antonio J. Costaandmajcosta 9747a53e17 remove stale comments with CDrom cruft on them 2026-07-24 04:54:29 -03:00
Marco Antonio J. Costaandmajcosta 656651aeee put static FMOD_ErrorString where it's needed
it was in a _header_, being included everywhere. fix it.
2026-07-24 04:54:29 -03:00
Marco Antonio J. Costaandmajcosta 1054012acd cleanup: remove more old CDrom cruft from the code
now that the CDromEjectionErrorMessageBoxCallBack is no longer referenced
2026-07-24 04:54:29 -03:00
Marco Antonio J. Costaandmajcosta d9836f806e Do as the debug build does instead of "checking for CDROM"
A bit of a hack to allow removal of dead CDROM code. If the game can't
find the movies, you have bigger problems to worry about
2026-07-24 04:54:29 -03:00