Commit Graph
4850 Commits
Author SHA1 Message Date
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
Marco Antonio J. Costaandmajcosta 916893ff66 cleanup: remove low hanging LibraryDatabase.* nonsense 2026-07-24 04:54:29 -03:00
Marco Antonio J. Costaandmajcosta bbfd88c414 update mingw toolchain file while we're at it
still does not compile, it'll require its own set of changes to make
the code gcc-compliant
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 5fd1d5f567 build with clang-cl from a toolchain file and a preset
The tree compiles and links under clang-cl, so make that a configuration
anyone can ask for rather than a command line to reconstruct:

  MSVC_DIRECTORY=/path/to/msvc cmake --preset '1dot13 Release clang-cl'

It is a check on the source, not a port -- MSVC still builds what ships. What
it buys is a second front end over every translation unit, which is how the
inline assembly with no operand size and the resource string with a raw 0xA9
were found, and it runs natively: clang-cl, lld-link, llvm-rc and llvm-lib
need no Wine, so a full JA2.exe takes a fraction of the time.

The toolchain file names every MSVC and SDK include directory, because
clang-cl does not read INCLUDE and LIB the way cl does, and picks the newest
version of each tree rather than pinning one. MSVC_DIRECTORY says where they
live and is reported plainly when it is missing or wrong.

build a bindable Bink import library when not building with MSVC

The shipped binkw32.lib is an old ordinal-based long-format import library.
MSVC's link.exe binds it, but clang-cl's lld-link does not: it leaves every
Bink import unresolved, so at run time the calls jump into arbitrary code and
crash the game at startup, six access violations deep in unrelated code before
the game's own handler gives up.

Rebuild the import library from binkw32.def with llvm-dlltool on any front end
other than MSVC. The library imports by ordinal, for the reasons binkw32.def
records: binkw32.dll exports decorated stdcall names that keep their leading
underscore (_BinkOpen@8), and llvm-dlltool strips that underscore from the
imported name whatever --no-leading-underscore says, so a by-name import would
look up a name the DLL does not export and Wine would substitute a builtin stub
that aborts ("unimplemented function binkw32.dll.BinkSetSoundSystem@8").
Ordinals have no such ambiguity and match the retail DLL. MSVC keeps binding the
shipped library, so the retail build is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

refactor toolchain

make the preset template something CMake will read

The template carries two // comments. JSON has no comments and neither does
the preset format, so CMake refuses the whole file:

  CMakePresets.json:10: Missing '}' or object member name

Which means the file CopyUserPresetTemplate drops into the source directory
has never worked as handed over -- "cmake --preset '1dot13 Release'" fails
until you delete the comments yourself.

The text they carried is worth keeping, so it moves to the description field
the format does have, and cmake --preset '1dot13 Release' now configures.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta c2379acd7a link the executable the same way whatever generator builds it
Two link settings were conditional on the Visual Studio generator, though
what they compensate for is the prebuilt libraries in the tree, which are the
same libraries under every generator. Ninja builds got away with it because
link.exe is quiet about both; lld-link is not.

legacy_stdio_definitions.lib: RakNetLibStatic.lib still calls _vsnprintf,
which the UCRT only exports through that compatibility library.

/SAFESEH:NO: lua51.lib and the smackw32 import library carry no safe
exception handler table, so no linker can emit one. The image never had it.
Before and after, JA2.exe reports SEHandlerTable 0x0 and SEHandlerCount 0,
with DYNAMIC_BASE, NX_COMPAT and TERMINAL_SERVER_AWARE unchanged -- the flag
states what link.exe was already doing silently.

Dbghelp.lib and Winmm.lib are also spelled as the SDK ships them, since a
cross-compile looks for them on a case sensitive filesystem.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 4d04a86bdf drop the non-editor screen stubs the editor never contributes
editscreen.cpp and LoadScreen.cpp each end in an "#else //non-editor
version" branch defining EditScreenInit/Handle/Shutdown and the LoadSaveScreen
trio. jascreens.cpp defines the same six under "#ifndef JA2EDITOR", so a
non-editor build compiles both.

Nothing linked the editor copies. The map file for JA2.exe resolves all six
to JA2_Ja2:jascreens.cpp.obj, link.exe having taken the first archive that
answered and never pulled the other. lld-link pulls both and calls it what it
is: six duplicate symbols.

The two sets are not identical -- the editor Handle stubs return ERROR_SCREEN
where jascreens returns TRUE -- so this removes the copies that lose, leaving
the behaviour the linked image already had.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 0fd48acf5d spell the copyright sign in the version resource as an escape
The one byte in ja2.rc above ASCII is the 0xA9 in "Copyright (c) 1998",
written raw. What it means depends on the codepage the resource compiler
happens to run under, and llvm-rc will not guess: "Non-ASCII 8-bit codepoint
can't be interpreted in the current codepage".

An escape in a wide string says it outright. The escape alone is not enough
-- a byte above 127 cannot appear in a narrow resource string either -- but
the value block is stored as wide characters regardless, so the L costs
nothing.

rc.exe produces a byte-identical ja2.rc.res before and after.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 2c6202334e say what size the pixelation pattern compare reads
The pattern compare in Blt16BPPBufferPixelateRectWithColor gives neither
operand a size: a memory reference with no register to take the width from,
against an untyped zero. MSVC picks a byte, which is right -- Pattern is
UINT8[8][8], and every other access to it in the block goes through al --
but the source never says so, and clang-cl refuses to guess.

Naming the size changes nothing. cl emits 80 3C 1E 00 before and after.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 8495af4b00 spell the Chinese percent sign as an escape instead of raw bytes
Four string literals carry the bytes A3 A5, which is GBK for U+FF05, the
fullwidth percent sign. All four sit in g_lang == i18n::Lang::zh branches
added by "Chinese specific update (by zww from tbs)", and each has an ASCII
sibling in the else branch using an ordinary percent.

clang-cl decodes source as UTF-8, where A3 is not a valid lead byte:

  error: illegal character encoding in string literal

A raw high byte in a literal means whatever the compiler's source charset
says it means, so this text has never been portable. Asking the cl we build
with what it makes of L"%d%<A3><A5>%%" gives a 7-character string whose
fourth character is 163 -- it reads the bytes as Latin-1 and produces two
characters, £ and ¥, not the one the author wrote. The same probe on
L"%d%%%%" gives 6 characters with U+FF05 in that position.

So the Chinese text has been rendering as garbage in any build not made on a
CP936 machine, this one included. Writing the character as % says
exactly which character is meant regardless of the compiler's charset, and
restores what the branch was written to display.

The format grammar is untouched. In each of these strings the character is
preceded by a percent, making %<fullwidth percent> an unknown conversion
specification that prints the character following it -- the same trick the
else branches spell as %%. Where a string is formatted twice, once into a
buffer and again as the format argument of mprintf, the doubling in the
sibling literal (%%%%) is what survives two passes; that pairing is
unchanged.

Both files are now pure ASCII:

  python3 -c "print(sum(1 for b in open(F,'rb').read() if b>0x7f))"

The parse sweep drops from 5 error sites in 3 files to 1 in 1 -- the inline
assembly in sgp/vobject_blitters.cpp is all that is left.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta b147e47856 let the text drawing path take a string it will not write to
DrawTextToScreen takes STR16, a mutable wchar_t*, for a string it only
measures and prints. Quest Debug System.cpp picks its text with a conditional:

  DrawTextToScreen( gubFact[ usLoop1 ] ? L"True" : L"False", ... );

which has type const wchar_t*. clang-cl's MSVC compatibility lets a bare
string literal decay to wchar_t*, but not a const wchar_t* expression, so it
reports

  error: no matching function for call to 'DrawTextToScreen'
  note: candidate function not viable: 1st argument ('const wchar_t *')
  would lose const qualifier

The parameter is const-correct in every function DrawTextToScreen hands the
string on to, so the change follows the string down the call chain rather
than stopping at the top:

  DrawTextToScreen, ShadowText          Utils/WordWrap.h
  WFStringPixLength                     Utils/Font Control.h
  mprintf, StringPixLength,             sgp/Font.h
  VarFindFontCenterCoordinates,
  VarFindFontRightCoordinates
  WinFontStringPixLength                sgp/WinFont.h
  gprintfdirty                          TileEngine/Render Dirty.h

The varargs members of that list copy the format string into a local buffer
before doing anything with it, and the measuring ones walk it and return a
width; none of the nine assigns through the parameter. Where StringPixLength
casts the string to UINT16* to walk it, the cast and the local now carry the
const rather than dropping it.

Callers are unaffected: every one already passes something that converts to
const CHAR16*. Both configurations build with no new diagnostics, and the
parse sweep drops from 9 error sites in 7 files to 5 in 3.

To check the claim that nothing writes through these parameters, read the
nine bodies; each is short.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta c6a4fdb393 give the drug item draw an engine it can take by reference
std::uniform_int_distribution::operator() takes its engine by non-const
reference, and the call passes a freshly constructed std::mt19937 temporary.
MSVC binds it anyway; clang-cl reports

  error: no matching function for call to object of type
  'std::uniform_int_distribution<>'
  note: candidate function not viable: expects an lvalue for 1st argument

Naming the engine is enough. Behaviour is unchanged: the engine is still
seeded from std::random_device and still discarded after the single draw.
2026-07-23 19:29:55 -03:00
Marco Antonio J. Costaandmajcosta 3cc3ade169 declare the table specialisations before anything instantiates one
TestTableTemplate declares SetRefresh and Init but never defines them; both
exist only as explicit specialisations, one set per table, at the bottom of
FacilityProduction.cpp and MilitiaWebsite.cpp.

That is too late for tables 3 and 4. Both files call SetRefresh on their
table hundreds of lines above the specialisation, and a call is a point of
instantiation, so the compiler has already committed to instantiating the
primary template by the time the specialisation shows up. A specialisation
declared after the first use that would instantiate it is ill-formed; MSVC
takes it anyway, clang-cl reports

  error: explicit specialization of 'SetRefresh' after instantiation

Declaring the specialisations after the includes fixes it. Tables 1 and 2 are
only ever reached through the base class pointer, so they were not diagnosed,
but they are declared alongside the others rather than left to depend on that.

The definitions stay where they are.
2026-07-23 19:29:55 -03:00
6d2df6addc let FileWrite take a source buffer it will not write to
FileWrite took its source as PTR, a mutable void*, though it only reads it:
the buffer goes straight to vfs::tWritableFile::write, which takes a const
vfs::Byte*. STIConvert.cpp pads a file a byte at a time with

    FileWrite(hFile, "", 1, NULL);

and a string literal is a const char[1], which PTR cannot hold.

Declaring the parameter const void* matches what the function does and what
the layer below it already asks for. The cast inside becomes const as well.
None of the 431 calls change: every pointer, const or not, converts to
const void*.

JA2EncryptedFileWrite and NewJA2EncryptedFileWrite have the same shape but
transform the buffer on the way out, so they keep PTR.

Verification:

    ninja -C build parse         # STIConvert.cpp clean
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
a7dcdcaec3 let PlayJA2SampleFromFile take a filename it will not write to
The parameter was STR8, a mutable char*, for a filename the function only
reads: it runs strstr over it and hands it to SoundPlay. environment.cpp picks
between two of them at runtime,

    PlayJA2SampleFromFile( Chance( 50 ) ? "Sounds\\WATERSNAKE_ATTACK_01.WAV"
                                        : "Sounds\\WATERSNAKE_ATTACK_02.WAV", ... );

and the conditional is a const char*, which STR8 has no room for. Literals
passed on their own are still accepted under the MSVC rules, which is why only
these two calls ever failed.

The cast at the call to SoundPlay was already written as (STR) and is left as
it is. SoundPlay, SoundPlayStreamed and SoundLoadSample below it all still
take STR, so making them const is a separate piece of work and a much larger
one; until then that cast is where const stops.

No call site changes. The (char*) cast at LuaInitNPCs.cpp:11640 is now
unnecessary but still valid, and is left for whoever next edits that line.

Verification:

    ninja -C build parse         # both environment.cpp sites gone
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
aac53ce71d let ScreenMsg take a format string it will not write to
STR16 is CHAR16*, so ScreenMsg asked for a mutable format string it has no
intention of mutating: the body only hands it to va_start and vswprintf, and
vswprintf takes it as const.

Calls that pass a literal are accepted anyway under the MSVC rules clang-cl
applies. Calls that pass anything else are not, and DynamicDialogue.cpp picks
the format at runtime:

    ScreenMsg( FONT_MCOLOR_LTGREEN, MSG_INTERFACE,
               ...sOpinionModifier >= 0 ? L"%s: %s +%d" : L"%s: %s %d", ... );

The conditional has type const wchar_t*, and there is nowhere for it to go.

Declaring the parameter const CHAR16* is what the function always meant. Every
existing caller still compiles: a literal and a CHAR16* both convert.

Verification:

    ninja -C build parse         # DynamicDialogue.cpp clean
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
e1ae72a5f6 remove an array declared with neither a size nor an initializer
char team_names[][30];//hayden need client_names with AI

This is not a declaration of anything. C would treat it as a tentative
definition and settle on one element; C++ requires a size or an initializer,
and clang says so. MSVC accepts it quietly.

Nothing in the tree reads or writes team_names, and the comment reads as a
note to self rather than a feature. Rather than guess a size for an array
nobody uses, it goes.

Verification:

    grep -rn 'team_names' --include=*.cpp --include=*.h .   # nothing
    ninja -C build parse         # client.cpp clean
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
ee01103b67 pass a string, not a std::wstring object, to a %s
The AI deadlock message hands ScreenMsg an object where its format string
promises a pointer:

    ScreenMsg(..., L"Aborting AI deadlock for [%d] %s %s data %d",
              pSoldier->ubID.i, pSoldier->GetName(),
              utf8_to_wstring(std::string(szAction[...])), ...);

utf8_to_wstring returns std::wstring by value. ScreenMsg is variadic, so the
whole object is pushed and %s then reads the first bytes of it as a pointer.
What that prints, or whether it survives printing it, is not defined. clang
says as much: the call will abort at runtime.

Adding .c_str() passes what the format asked for. The temporary lives until
the end of the statement, so the pointer is good for the duration of the call.

The only other use of utf8_to_wstring in this file has the same shape and is
commented out; it is left as it is.

Verification:

    ninja -C build parse         # AIMain.cpp clean
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
17d0c9f672 read the gear template item into the node, not into a temporary
LoadEquipmentTemplate parsed each line of a saved equipment template like
this:

    if ( iss >> num && iss >> (UINT16)node.item )

node.item is already a UINT16, so the cast does nothing except turn the member
into a temporary. The stream then extracts into that temporary, which is
thrown away at the semicolon, and node.item keeps the 0 its constructor gave
it. node.ammoitem, cast the same way on the next line, is lost with it.

This is not only a parse error. The tokens are still consumed, so the line
parses and the node is appended, but every node comes back with item 0. The
consumer skips those:

    if ( node.slot >= 0 && node.slot < NUM_INV_SLOTS && node.item != NOTHING )

so loading an equipment template has been quietly doing nothing at all. Only
the attachments, read through a real variable, ever survived, and they were
attached to a node that got skipped.

Removing the two casts makes the extraction write where it was always meant
to. slot is still read through num, as it must be, since it is an INT8 and the
stream would take it for a character.

This changes behaviour: gear templates will start applying. That is the point,
but it is worth knowing before this ships.

Verification:

    ninja -C build parse         # both Handle Items.cpp sites gone
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
31a2202c8a rename a local called not, which is a keyword
not is an alternative spelling of ! in ISO C++, so it cannot name a variable:

    XML_Char const* not = GetAttribute("not", atts);

MSVC only treats it as an operator when iso646.h is included, so it accepts
this as an identifier and the file compiles. clang cannot parse any of the
three lines that mention it.

Renamed to notOp, matching cmpOp thirty lines above, which reads the "op"
attribute the same way. The attribute name in the XML is untouched.

Verification:

    grep -rnE '\*[ \t]*(and|or|not|xor)\b' --include=*.cpp --include=*.h .   # nothing
    ninja -C build parse         # parser-error class gone: 21 -> 17 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
897d3737c4 drop typename from two non-type template parameters
AutoCArray takes its allocator and deallocator as template parameters:

    template<typename T, typename void*(*Alloc)(size_t) = malloc,
                         typename void(*Dealloc)(void*) = free>

Alloc and Dealloc are function pointers, so they are values, not types, and
typename has no business in front of them. MSVC ignores the keyword there;
clang stops at it and cannot parse the parameter list, which fails every
translation unit that reaches this header.

Removing the two stray keywords leaves the parameters exactly as they were
meant to be read, with the same defaults of malloc and free. Nothing in the
tree instantiates AutoCArray, so nothing depends on the spelling either way.

Verification:

    grep -rn 'AutoCArray' --include=*.cpp --include=*.h .   # only this header
    ninja -C build parse         # sgp_auto_memory.h clean
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
42b947d470 give three temporaries a name before taking their address
Each of these takes the address of an object that has no name:

    bool b = client->Startup(1,30,&SocketDescriptor(), 1);
    OutputToStream(msg, &(SGP_LOG(s_log.id)));

MSVC allows it as an extension. The temporary lives to the end of the full
expression, which is past the call, so all three happen to work; clang rejects
the address-of outright.

Naming the object changes nothing about when it is built or what is passed,
only that the pointer now refers to something that outlives the statement.
MemMan.cpp already does this with the same logger call:

    sgp::Logger::LogInstance memLeak = SGP_LOG(log_id);

Verification:

    ninja -C build parse         # address-of-temporary class gone: 24 -> 21 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
e7095999df delete four out-of-line declarations that repeat the class body
Each of these says again, outside the class, what the class already declares:

    template <int N>
    void TestTableTemplate<N>::SetRefresh();

SetRefresh and Init are declared as members a few dozen lines above, so these
add nothing. A member named out of line has to be a definition; MSVC lets the
declaration through, clang does not.

None of the three templates ever defines these members generically. Every
instantiation supplies its own, as an explicit specialization next to the
screen that uses it, and those are untouched:

    template<> void DropDownTemplate<DROPDOWNNR_APPEARANCE>::SetRefresh()
                                            { gfIMPPrejudice_Redraw = TRUE; }

The mpSelf lines that follow each of these look similar but are definitions of
a static data member, so they stay.

Verification:

    grep -rn 'Template<N>::' --include=*.h Laptop/    # only mpSelf definitions
    ninja -C build parse         # out-of-line class gone: 28 -> 24 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
1bf28bc07a ask whether strstr found the tag, not whether it is positive
strstr returns a pointer into the string or NULL, and these four tests compare
that pointer to 0 with >. Ordering a pointer against a null pointer constant
is not something C++ defines; MSVC allows it, clang rejects it.

The intent is plainly "did this attribute name contain the tag", and the
result is the same either way, since the only pointer these can produce that
is not greater than zero is the null one. != NULL says it directly, and reads
like the strcmp(name, "...") == 0 tests it sits between.

strstr rather than strcmp is deliberate here: the attributes it looks for are
numbered, usAttachment1 through usAttachment4 and usResult1 upwards, so an
exact comparison would not match them. That is unchanged.

Verification:

    grep -rn 'strstr([^)]*)[ \t]*[><]' --include=*.cpp --include=*.h .   # nothing
    ninja -C build parse         # pointer-ordering class gone: 32 -> 28 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
1d6ff6be3c let CIniReader take its strings as pointers to const
STR8 is char*, so const STR8 is char* const: a const pointer to a mutable
string, which is not what any of these sixty parameters wanted. What they
wanted is const CHAR8*, a pointer to a string they will not write to, and
that is what all of them do.

Under the MSVC compatibility rules clang-cl applies, a string literal still
converts to char*, so most calls survive this mistake untouched. An expression
of type const char* does not, and Intro.cpp has thirteen of them:

    inireader.ReadString("INTRO_BEGINNING", "INTRO_REBEL_CRDT",
                         no_defaults ? "" : "INTRO\\Rebel_cr");

The conditional is a const char*, not a literal, so it has nowhere to bind and
the call does not compile. The other two arguments are literals and are
accepted, which is why only the third one was ever reported.

The invariant: in these two files, every parameter spelled const STR8 becomes
const CHAR8*, and nothing else changes. No body assigns to one of them, so
none needed rewriting; STR8 input_buffer in the five-argument ReadString is
genuinely an output and keeps its type. Three commented-out lines still say
const STR8 and were left as they are.

Verification:

    grep -n 'const[ \t]*STR8' Utils/INIReader.h Utils/INIReader.cpp
        # three hits, all on lines beginning with //

    git show HEAD -U0 | grep '^[+-][^+-]' |
      sed -E 's/const[ \t]+(STR8|CHAR8\*)/const/g; s/^[+-]//' |
      sort | uniq -c | awk '$1 % 2'          # no output

    ninja -C build parse         # ReadString class gone: 45 -> 32 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
c99f939fe6 stop pasting a directive name onto the comma after it
DIRECTIVE_TEXT builds four enumerator names from one directive name:

    #define DIRECTIVE_TEXT(id)  RCDT_##id##, RCDT_##id##_EFFECT, ...

The last ## on the first name has nothing to paste onto but the comma that
follows, and RCDT_GATHER_SUPPLIES, is not a token. The other three pastes are
well formed; only this one runs off the end of the name.

MSVC concatenates the text and re-lexes it, so it recovers the identifier and
the comma and produces the right enumerators; clang diagnoses it. Removing the
stray ## gives the same ten enumerations, and MISSION_TEXT just below already
spells it this way.

Verification:

    grep -rn '##[A-Za-z_]*##[,)]' --include=*.h --include=*.cpp .   # nothing
    ninja -C build parse         # token-paste class gone: 55 -> 45 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
3db2d60a4d drop the extern "C" wrapper whose other half is commented out
DEBUG.H once wrapped its declarations in extern "C", and DEBUG.cpp wrapped the
matching definitions to agree. At some point the header's pair was commented
out, both the opening brace and the closing one:

    /*
    #ifdef __cplusplus
    extern "C" {
    #endif
    */

That had to happen, because the block would otherwise have swallowed
<stdexcept>, <list> and namespace sgp further down the header. The wrapper in
DEBUG.cpp was left behind, so seven variables are declared with C++ linkage in
the header and defined with C linkage in the source. That is ill-formed. MSVC
takes the linkage of the first declaration it sees, which is the header's, so
the mangled names still agree and the tree still links; clang refuses.

The header is the only place these seven are declared, so removing the
wrapper leaves everything with the C++ linkage it already had. Nothing here
needs to be callable from C.

A second such wrapper remains further down the file, inside #ifdef SGP_DEBUG.
Nothing in the build defines SGP_DEBUG and nothing it contains is declared in
a header, so it is neither compiled nor mismatched, and it is left alone.

Verification:

    ninja -C build parse         # language-linkage class gone: 60 -> 55 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

The seven diagnostics sit on five lines, three of them sharing a single
declaration, and the parse target counts one site per line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
e56a372871 give the XML tag map and the writer buffer a name
Both of these bound a non-const reference to a temporary, which MSVC accepts
and clang does not, and which leaves the reference dangling once the full
expression ends.

PropertyContainer::initFromXMLFile and writeToXMLFile take their tag map by
non-const reference because the accessors on it insert default tag names as
they are asked for, so it cannot be const and it cannot be a temporary. All
eight call sites passed a freshly built one; they now declare it. Each keeps
its own object, so a map filled in by one call cannot leak into the next, the
same as when each call built its own temporary.

XMLWriter::writeToFile bound std::string& to what stringstream::str() returns
by value. Declared as a value it is initialised straight from the return with
no copy, and const because only c_str() and length() are asked of it.

Verification:

    grep -rn 'PropertyContainer::TagMap()' .   # nothing
    ninja -C build parse         # no i18n or XMLWriter sites remain
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
b5ea70aa44 declare the postal service iterators as values, not as references
PostalService.h names every type twice, once plainly and once as a reference:
RefToDestinationStruct is DestinationStruct&, RefToShipmentList is
ShipmentList&. That scheme was extended to the iterators, and there it does
not work. Every one of the seventeen uses looks like this:

    RefToShipmentListIterator sli = _Shipments.begin();

begin() returns a value, so this binds a non-const reference to a temporary.
MSVC allows it as an extension; the temporary still dies at the end of the
full expression, which leaves sli referring to a dead object for the whole
rest of the function. It works only because nothing has reused the stack slot
yet. clang rejects it outright.

Iterators are already handles into their container, so a reference to one buys
nothing: the plain typedef says exactly what these seventeen variables want to
be. DESTINATION and SHIPMENT now take theirs by value, which changes nothing
for either, as both only dereference the iterator they are given.

Two of the reference typedefs, RefToDestinationDeliveryInfoTableIterator and
RefToDeliveryCallbackDataListIterator, had no uses at all. They are removed
rather than left behind, since their only possible use is the pattern this
commit removes.

Verification:

    grep -rn 'RefTo[A-Za-z]*Iterator' --include=*.cpp --include=*.h .   # nothing
    ninja -C build parse         # bind-to-temporary class gone: 91 -> 60 sites
    ninja -C build -k 0          # Release, four applications, green
    ninja -C build-debug -k 0    # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
70b11ee120 make narrowing conversions in braced initialisers explicit
A braced initialiser does not allow a narrowing conversion unless the value
is a constant expression the compiler can prove fits. Every site here holds a
runtime value: WORLD_COLS is the global guiWorldCols, the laptop screen
coordinates are built from iScreenWidthOffset, the sector limits come from the
externalised options. MSVC accepts all of it silently; clang rejects it, and
it is ill-formed.

Nothing about the generated code changes. MSVC was already performing these
conversions; the casts only say so out loud, at the 73 places where it was
happening implicitly.

The invariant: each site is wrapped in a static_cast to the element type the
initialiser already had, and nothing else is touched. No type is widened, no
expression is reassociated, no value is clamped or checked. static_cast rather
than a C-style cast so that a later reader can grep for the narrowings, and so
that none of these can quietly become a reinterpret_cast if a type changes.

Verification:

    # every changed line differs only by inserted casts and their parentheses
    git show HEAD -U0 | grep '^[+-][^+-]' |
      sed -E 's/static_cast<[A-Za-z0-9_]+>//g; s/[()]//g; s/^[+-]//' |
      sort | uniq -c | awk '$1 % 2'          # no output

    git show HEAD -U0 | grep -c '^+[^+]'     # 38 lines, 73 casts

    ninja -C build parse                     # narrowing class gone: 129 -> 91 sites
    ninja -C build -k 0                      # Release, four applications, green
    ninja -C build-debug -k 0                # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
5e2982c509 take popup option names by const reference, not by pointer
POPUP_OPTION copies the name it is given -- `this->name = *newName` into a
std::wstring member -- but its constructor took a std::wstring*, so 64 call
sites spelled the argument `&std::wstring( pStr )`. Taking the address of a
temporary is ill-formed, and clang refuses it outright:

    error: taking the address of a temporary object of type 'std::wstring'

It happens to work under MSVC because the temporary outlives the call, dying
at the end of the full expression rather than before the copy. Nothing was
corrupt; the code was just spelling "pass me a string" in a way the language
does not allow.

A const reference says what these functions actually want, so the call sites
lose the &, and the two places that allocated a string purely to have an
address to pass -- POPUP_SUB_POPUP_OPTION's default constructor, which carried
a "TODO: possible memmory leak!" saying as much, and a "Dummy generator"
option -- stop leaking one.

Converted: POPUP_OPTION's constructor and setName, POPUP::addOption and
addSubMenuOption, and both POPUP_SUB_POPUP_OPTION constructors. The popupDef
family in popup_definition.* keeps its std::wstring* because it owns what it
is handed and stores the pointer; that is a different design and a different
change. The three calls it makes into the converted API now dereference.

Verification:
    grep -rn '&std::wstring' --include=*.cpp --include=*.h .   # nothing
    ninja -C build parse             # no popup_* or SkillMenu sites remain
    ninja -C build -k 0              # Release, four applications, green
    ninja -C build-debug -k 0        # Debug, four applications, green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:29:55 -03:00
83b637aa73 libsmacker: allow overlapping copies in the 0x40 palette opcode
The color-shift opcode copies a run of palette entries from the previous
frame's palette to the current one. libsmacker rejected the copy when the
source and destination ranges overlapped, aborting the whole palette decode
and leaving the palette half-updated -- so every frame from the first such
opcode on rendered with a mix of the new and stale palette.

The overlap check is bogus: the copy reads from oldPalette, a snapshot taken
at the top of the function, and writes into s->palette, a separate buffer, so
overlapping ranges are harmless (and it is a memmove regardless). ffmpeg and
the original SMACKW32.DLL have no such check.

Several of the videos that ship with the game (Rebel_cr, Omerta, Prague) use
these overlapping copies, and showed washed-out colours with the previous
frame's palette bleeding through. Drop the overlap clause, keep the real
256-entry bounds checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:01:37 -03:00
Marco Antonio J. Costaandmajcosta 4ff2853b70 play smacker video with libsmacker instead of SMACKW32.DLL
Drops the last piece of the proprietary Smacker SDK: SMACKW32.LIB, its
header sgp/SMACK.H, and with them the SMACKW32.DLL the game needed at
runtime. Bink is untouched and still needs binkw32.

The two libraries divide the work differently, so Cinematics.cpp changes
more than the call names. SMACKW32.DLL played a file: it kept the playback
clock (SmackWait), blitted converted 16bpp pixels into a surface we handed
it (SmackToBuffer) and pushed the audio to DirectSound itself. libsmacker
only decodes, so this module now:

- keeps the clock. Frames are advanced against GetTickCount() from the
  moment the flic starts, skipping frames if the game fell behind, which
  is what SmackWait/SmackDoFrame did internally.
- blits itself. Frames come out 8bpp palettised, so the palette is
  converted with Get16BPPColor() and the frame is written into the frame
  buffer, clipped to the screen. The Y-scaling flags are not handled;
  none of the videos shipping with the game set them.
- plays the audio itself, through the SoundPlayFromBuffer() added earlier.
  libsmacker hands out audio a frame at a time, so the whole track is
  decoded up front, with the video track disabled to keep that pass cheap,
  and given a WAV header so the sound module can take it.

libsmacker reads from memory, which lets the file be read straight out of
the VFS. That removes the dance where the .smk was first copied to a real
file under Temp/ because SmackOpen() could only open a path.

SmkInitialize() loses its window and screen size arguments, which only
existed for SmackBufferOpen(). SmkOpenFlic(), SmkSetBlitPosition() and
SmkGetFreeFlic() are no longer declared in the header; nothing outside the
module called them. SMKFLIC is now opaque, Intro.cpp only holds pointers.

The unused SMACK.H include in Cinematics Bink.cpp goes away as well.

Tested by running the Sirtech splash screen (SPLASHSCREEN.SMK, 640x480,
15 fps, one 44.1kHz 16 bit stereo track) under Wine: picture, palette,
placement and playback speed are right.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

refactor
2026-07-23 19:01:37 -03:00
13c4be7382 soundman: allow playing a sound file held in memory
Smacker video carries its audio as PCM that the video decoder produces
itself, so there is no file for the sound module to open. Add
SoundPlayFromBuffer(), which streams from a caller-owned memory buffer.

It deliberately does not go through the sample cache: a full length intro
video decodes to close to 20 MB of PCM, which does not fit in the cache's
memory budget and would evict everything else on the way to failing. The
channel bookkeeping already has a notion of a stream with no cache slot
behind it (uiSample == -1), which is what streamed files use, so the buffer
simply stays the caller's property for as long as the sound plays.

To share the parameter handling, everything SoundStartStream() did after
opening the stream moves unchanged into SoundStartOpenedStream().

No caller yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:01:37 -03:00
de78480c9c vendor libsmacker 1.2
libsmacker is an open reimplementation of the parts of smackw32.dll needed
to get frames and audio out of an .smk file. Vendoring it here is the first
step to dropping the proprietary SMACKW32.LIB/SMACKW32.DLL dependency.

Nothing links against it yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:01:37 -03:00
9c1403b4ec do not hand clang the MSVC-only forward declaration in VFS
vfs_debug.h forward declares std::exception under #ifdef _MSC_VER. clang-cl
defines _MSC_VER too, and rejects the declaration:

    vfs_debug.h(34,15): error: forward declaration of class cannot have a
                               nested name specifier

so any tool built on clang stops at the first file that reaches sgp/DEBUG.H,
which is nearly all of them. Guarding on __clang__ as well leaves the MSVC
build byte for byte identical and lets include-what-you-use parse the tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:14:21 -03:00
aad39ac3bd declare gFullBaseTileValues extern like every other global here
tiledef.h is included by 1900 objects, and each one was compiling a tentative
definition of gFullBaseTileValues rather than a declaration of it. The array
is defined once in TileDat.cpp, and every other global in the same block is
already declared extern; this one was missed.

MSVC accepts it and merges the tentative definitions, so nothing was visibly
broken. A conforming front end rejects it outright:

    tiledef.h(180,12): error: definition of variable with array type needs
                              an explicit size or an initializer

which is what turned it up.

Verification: ninja -C build -k 0, all four applications link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:14:21 -03:00
Marco Antonio J. Costaandmajcosta 3d3e1bbfc6 refactor Translate()
it returns true but no consumers use it. its return type is void, then
2026-07-21 18:39:50 -03:00
Marco Antonio J. Costaandmajcosta 738225d2a2 move ImportStrings to i18n 2026-07-21 18:39:50 -03:00
Marco Antonio J. Costaandmajcosta 4be9b106f4 give more local functions internal linkage 2026-07-21 18:39:50 -03:00
Marco Antonio J. Costaandmajcosta af4eeb09f4 make local-only functions static 2026-07-21 18:39:50 -03:00
Marco Antonio J. Costaandmajcosta 15821498a3 text declarations go into text.h 2026-07-21 18:39:50 -03:00