Skip to content

Android port - #6

Open
timskap wants to merge 19 commits into
nival:mainfrom
timskap:android-port
Open

Android port#6
timskap wants to merge 19 commits into
nival:mainfrom
timskap:android-port

Conversation

@timskap

@timskap timskap commented Aug 19, 2026

Copy link
Copy Markdown

Silent Storm — Android port

An Android build of the Silent Storm engine, made from the 2003 source release in
this repository (Soft/Andy/Jan03/a5dll). The original targets Win32, DirectX 8/9,
MSVC .NET 2003 and STLport; this directory builds the same code with the Android
NDK (clang/libc++) for arm64-v8a, armeabi-v7a and x86_64.

Status: the whole engine builds, links and runs on Android; the game reaches
its first screen.
File I/O, the chunk serialiser, the .res package reader,
the object model, the Lua 4 VM, texture loading and the retail game.db
importer are verified against real game data on device. Main — the renderer,
scene, AI, UI and game logic, 154k lines — compiles for arm64 in all 269 of its
files; the Direct3D 9 backend runs on a D3D9-on-GLES 3.0 implementation
(compat/d3d9gles/, all 155 engine shaders translated). On a Galaxy Z Fold7 the
main menu comes up — logo, version string, localised text, cursor — at
~120 fps into a 1024×768 virtual back buffer; the menu's 3D scene and touch
navigation are the next steps. See docs/PORTING.md for the state of each
piece and what comes next.

The app currently boots, mounts your game data, exercises those subsystems and
shows the result on screen through GLES 2.0:

== FileIO: game data packages
[ok]   Globals.res: 5 entries (0.1 ms)
         entry -1: read 20 bytes, first byte 0x01
[ok]   Chapters.res: 3 entries (0.0 ms)
...
== Script: the game's own Lua sources
[ok]   Constants.l: 1399 bytes executed (0.1 ms)
         POSE_RUN = 3, as defined in the file
[ok]   20 passed, 0 failed

Verified on a Galaxy Z Fold7 (SM-F966B, Android 16, Adreno 830), on the Android
emulator, and headlessly on macOS.

Quick start

cd android
./scripts/build_apk.sh          # stage sources, build all ABIs, package an APK
./scripts/push_data.sh          # copy a subset of Complete/ to the device
./scripts/run.sh                # install, launch, print the engine's report

Requirements: Android SDK with build-tools and an NDK (r27 and r29 are both
tested), CMake 3.22+, Ninja, Python 3, a JDK for apksigner. No Gradle needed —
though android/ is also a valid Gradle project if you prefer Android Studio.

To run the engine checks on your development machine instead of a device:

cmake -S . -B build/host -G Ninja && cmake --build build/host
./build/host/silentstorm_hosttest ../Complete

See docs/RUNNING.md for where the game data has to live.

How the port is organised

android/
  compat/        Win32 -> POSIX/Android compatibility layer
    include/       windows.h, hash_map, crtdbg.h ... the vocabulary the engine expects
    include-host/  headers only the host build needs
    src/           implementations (files, threads, time, paths, CRT gaps)
  tools/
    prepare_sources.py   stages the engine sources into gen/, applying documented rewrites
    preview_font.py      renders the boot console font to a PNG for checking
  gen/           generated: the staged engine modules (not checked in)
  platform/      Android entry point, GLES boot console, data mounting, boot harness
  app/           AndroidManifest.xml (a NativeActivity; there is no Java code)
  scripts/       build.sh, build_apk.sh, push_data.sh, run.sh
  docs/          PORTING.md, RUNNING.md, WIN32_SURFACE.md

The original sources are never modified. tools/prepare_sources.py copies the
modules it builds into gen/ and applies a set of named, individually documented
rewrites on the way through — include paths, x86 assembly, MSVC-only C++, and a
handful of genuine 64-bit bugs. Run it with --report to see every change it
makes:

python3 tools/prepare_sources.py --report

That list is the port's diff against 2003. Everything else is new code in
compat/ and platform/.

What is ported

Module What it is State
Misc refcounting, math, strings, timing, RNG ported, verified
FileIO streams, chunk serialiser, .res packages ported, verified
Script Lua 4.0 + the engine's C++ wrapper ported, verified
MiscDll console variables/commands, log streams ported (builds)
DBFormat the game.db schema (130 record classes) ported; platform/db_retail.cpp imports the shipped (retail-format) game.db: 130/130 tables, 222k records
Image + libpng BMP/TGA/PNG and MMP/DXT textures ported, verified (real textures decode to their stored average colour)
Main renderer, scene, AI, UI, game logic (154k lines) 269/269 files build and link; runs on device over compat/d3d9gles (D3D9 on GLES 3.0, 155 shaders) — see RENDERER.md
Input DirectInput platform/input_android.cpp implements Input.h from Android keys and touch
FModSound FMOD 3 wrapper NFMSound implemented as a silent null back end

Notable things the port had to fix

Three bugs in the original code only appear on a 64-bit target, and all three
crash immediately rather than degrade:

  • CBufferedStream::LoadBufferForced shifted two pointers by nBufferStart - nPos
    computed in unsigned int. On Win32 the arithmetic wrapped around and gave
    the intended negative offset; on arm64 it moves the pointer 4 GB away. This is
    what made every .res package segfault.

  • CBufferedStream::SetNewBufferSize truncated a pointer difference to int.

  • CRandomGenerator::FillRandRsl seeded ISAAC by walking C:\ for a random file
    and looping until it found one — an infinite loop anywhere else.

  • CStructureSaver used 4 bytes of an object's address as its on-disk
    reference ID. On 64-bit that truncates on write and half-fills a pointer on
    read; the port keys references as uint32 and numbers objects densely on
    write. Format unchanged.

And one that is not a bug but a trap: Float2Int was x87 fld/fistp, which
rounds. A naive (int) replacement truncates and silently shifts geometry;
the port uses lrintf.

Licence

The engine sources are covered by the repository's LICENSE.md
non-commercial use only. The porting layer in compat/, platform/, tools/
and scripts/ is part of this repository and carries the same terms.

timskap and others added 19 commits August 18, 2026 17:12
Builds the 2003 engine sources for Android with the NDK for arm64-v8a,
armeabi-v7a and x86_64, plus a headless host target for debugging.

Misc, FileIO, Script (Lua 4.0) and MiscDll are ported and verified on
device against real game data: .res packages open and read, the chunk
serialiser round-trips, and the game's own Lua sources execute. The
renderer, audio and the game layer (Main) are not ported yet; the
roadmap is in android/docs/PORTING.md.

The historical sources under Soft/ are not modified. Instead
android/tools/prepare_sources.py stages the modules being built into
android/gen/ and applies 45 named, individually documented rewrites
(include paths, x86 inline assembly, MSVC-only C++, 64-bit fixes);
--report prints the complete list, which is the port's diff against
2003. Everything else is new code under android/compat (a Win32 ->
POSIX shim) and android/platform (NativeActivity entry point, GLES
boot console, data mounting, boot harness).

Three original bugs only surface on a 64-bit target and are fixed by
those rewrites:

  * CBufferedStream::LoadBufferForced shifted pointers by
    nBufferStart - nPos computed in unsigned int, relying on 32-bit
    pointer wraparound; on arm64 it moves the pointer 4GB away, which
    made every .res package segfault.
  * CBufferedStream::SetNewBufferSize truncated a pointer difference
    to int.
  * CRandomGenerator::FillRandRsl seeded ISAAC by walking C:\ for a
    random file and looped forever when it found none.

Float2Int is also worth noting: the x87 original rounds rather than
truncates, so it maps to lrintf, not a cast.

Verified on a Galaxy Z Fold7 (Android 16, Adreno 830), on the emulator,
and headlessly on macOS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wide strings.  The engine's wide strings are UTF-16 (2-byte wchar_t on
Win32) and the on-disk format depends on that width; Android's wchar_t is
4 bytes.  The staging tool now rewrites the sources to char16_t /
std::u16string, WCHAR is char16_t in the compat windows.h, and a new
compat/src/wide_char.cpp supplies the char16_t CRT forms together with
generated windows-1251/1252 tables behind MultiByteToWideChar and
WideCharToMultiByte (the original relied on CP_ACP of a Russian machine).

DBFormat.  The 130 game-object record classes plus the ADOFake database
stub now build and run.  Getting there needed a set of mechanical passes
in prepare_sources.py for MSVC 7 leniencies that recur across the tree
and will cover Main as well: forward-declared enums given ': int', typename
on dependent iterator types, `if ( CDynamicCast<T> p( x ) )` condition
declarations rewritten to '= x' with balanced-paren parsing, typeid on
incomplete types in CClassFactory (now keyed by typeid(T*)), and the
CPtr<T> == T* overload ambiguity (member operator templated on the
pointee).

Serialiser.  CStructureSaver stored 4 bytes of an object's *address* as
its on-disk reference ID and mapped them back on load; on 64-bit that
truncated on write and left half a void* unwritten on read, so every
reference resolved to nothing.  References are now uint32 throughout and
stored objects are numbered densely on write.  Format unchanged.

Result.  game.db parses and all tables register -- but every game.db in
the repository was written by a later build than this January 2003
source: tables are heap objects of an unregistered class 0xA1843130 with
a column layout this source has no schema for.  The serialiser reports
unknown types precisely and the harness shows this as a warning, not a
failure.  Documented in docs/PORTING.md with the two ways forward.

Verified: 22/22 host, 21/21 on the Galaxy Z Fold7 (arm64-v8a).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Builds libpng 1.0.9 from the tree (NDK zlib, x86 assembler paths off) and
the engine's Image module: BMP/TGA/PNG loaders and the MMP mipmapped
texture container.  ImagePack.cpp -- the DXT encoder on a proprietary
s3tc.h, used only by the TexConv tool -- stays out of the runtime build.

Adds a software DXT1/3/5 decoder under platform/ for GPUs without
GL_EXT_texture_compression_s3tc and for verification: the harness loads
real textures through NImage::LoadImageMMP, decodes the top mip and
compares its mean colour to the dwAverageColor the 2003 tools stored in
the header.  They agree to within 1/255 on every texture tried.

Verified: 29/29 host, 28/28 on the Galaxy Z Fold7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Starts compiling the engine+game module.  The initial survey produced
26,478 errors across all 271 files; one root cause -- Main/Time.h
shadowing the system <time.h> when Main/ is on the include path -- was
72% of that.  Main/ is no longer an -I directory (engine sources include
siblings relative to themselves, which needs none).

The rest was recurring MSVC 7 leniencies, now handled once in
prepare_sources.py so they will not come back: `typename` on any
qualified iterator inside a template body (242 sites), single-argument
insert(end()) (34), unqualified member-function names passed as
pointer-to-member (11), dependent-base member access in class templates
(the CObjectBase macros now use this->; using-declarations elsewhere),
typeid on incomplete types in the event registry (keyed by T* like the
factory), and unions with non-trivial members (SMove).  -fno-operator-
names because the engine has variables named `or`.

Also: Input is staged for its headers (Input.h is a clean, DirectInput-
free seam for the touch layer; Bind.h/Bind.cpp are portable), and the
char16_t CRT gains wcstol/_wtol.

Main is an OBJECT library behind -DA5_BUILD_MAIN=ON; nothing links it
yet.  87 files still fail: LifeStudio:HEAD SDK headers (proprietary,
absent), a handful of dependent-base and conversion one-offs, and the
D3D-facing files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main.vcproj is now the authority on which files build (the directory
holds two sources MSVC never compiled: Interface.cpp and iPopupMenu.cpp
reference types that no longer exist).  prepare_sources.py emits a
SOURCES.cmake per module from the vcproj.

The MMX inline-assembly skinning routines in GCombiner.cpp -- the only
path for skinned normals and tangents -- are replaced by the same fixed-
point arithmetic in portable C++ (16-bit saturating adds, same shifts,
same normalisation table).  A standalone check against a float
rotate+normalise gives mean error 0.0042, max 0.0096; Nival's own
commented-out check tolerated 0.02.

LifeStudio:HEAD (facial animation, proprietary, absent from the release)
gets a stub under compat/include/thirdparty-stubs: dialogue heads render
in their neutral pose instead of animating, and the stub says so once in
the log.

More MSVC 7 leniencies handled once for the whole tree: converting
constructors between smart-pointer kinds (CObj<Derived> -> CPtr<Base>),
CPtr == 0, address-of-temporary hoisted into locals, unqualified
member-function names as pointer-to-member arguments, private nested
types named from outside the class, non-const reference parameters bound
to temporaries, implicit int, and a friend declaration relied on for
ordinary lookup.  _findfirst/_findnext/_findclose and the VK_ key codes
join the compat layer.

Remaining: FMOD (SoundFormat/SoundEffect/Sound), a few mixed-type min/max
calls, swscanf on char16_t, CDGPtr conversions, and a handful of one-offs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…end remains

The last x86 inline assembly is gone.  Four MMX sites replaced with the
same arithmetic in C++: the AABB accumulator in Bound.h (state moves from
mm4-mm7 to a thread-local), the bilinear resample in SWTexture.cpp and
the 2D 'over' blend in 2DSceneSW.cpp (Nival left the C++ in a comment
beside the asm), and the particle colour modulate in GSceneParticles.h,
which is transcribed lane by lane and verified bit-exact against an
emulation of the instruction sequence over 800k random inputs.

FMOD: FModSound/FMsound.h is staged as the audio seam and
platform/audio_null.cpp implements the whole NFMSound interface silently,
so Sound.cpp/SoundEffect.cpp/SoundFormat.cpp build.

Remaining MSVC 7 leniencies handled once for the tree: min/max as macros
(the engine never set NOMINMAX and mixes types in ways std::min rejects),
insert(end()) through dotted/arrow container expressions (41 sites),
converting assignment on CDGPtr, and a handful of one-offs -- swscanf on
char16_t rewritten to explicit strtol/suffix parsing, `struct _stat` to
POSIX stat, wcsftime narrowed, `{ (0,0) }` as an aggregate initialiser,
implicit-int declarations, GetCursorPos/SPI_GETMOUSE to a compat pointer
hook.  The member-argument pass no longer touches free functions.  BMP
headers, GetDoubleClickTime and _findfirst join the compat layer.

Both toolchains agree: 264/269 on aarch64-linux-android and on the
macOS host.  The five files that do not compile are exactly the Direct3D
9 backend (Gfx.cpp, GfxBuffers.cpp, GfxRender.cpp, GfxEffects.cpp,
GfxShadersDescr.h): ~6,500 lines using ~40 IDirect3DDevice9 methods,
which docs/PORTING.md now lists as the scope of the next chapter.

Boot harness: 29/29 host, 28/28 on the Galaxy Z Fold7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/RENDERER.md writes down what the five remaining D3D9 files must
provide behind the engine's NGfx interface: the CRenderContext state
model and its Apply<> translation tables, the atlas sub-allocation the
2D/UI path depends on, the LRU vertex-buffer cache with its base-vertex
emulation, the three vertex formats byte by byte, the VS constant
register map, all 77 vertex and 78 pixel shaders by name (their assembly
source is embedded in the bytecode's DBUG chunks), the ps1.x features
that need hand-porting, the screen-sized register bank, render-target
and cube-map consumers, and a suggested order of work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/extract_shaders.py pulls the vs.1.1 / ps.1.1 / ps.1.4 assembly out
of the DBUG chunks in GfxShaders.cpp's bytecode -- 77 vertex and 78 pixel
shaders as text under shaders/d3d/, plus an index recording which pixel
shaders enable alpha test (13) and which have a ps.1.4 variant that
differs from the ps.1.1 one (34; the difference is that ps.1.4 spells
projective reads out as t0_dw, so those variants are the ones translated).

tools/d3dasm2glsl.py is a translator, not a hand port: the assembly is a
small register language and the look depends on ps.1.x semantics that
are easy to get subtly wrong by hand -- the _x2/_x4/_bx2/_sat modifiers,
the [-1,1] register range after every instruction, co-issued colour and
alpha ops reading their sources simultaneously, texm3x2/texm3x3vspec and
texreg2ar dependent reads.  Encoding those once gives every shader the
same treatment and lets the GLSL be regenerated as the rules improve.
The vertex template also carries the D3D->GL clip fixups (z range, y
flip for render-to-texture, half-pixel offset) and the D3DCOLOR .zyxw
attribute swizzle.  Samplers are typed per stage by a cube mask supplied
by the caller, since ps.1.x `tex` samples whatever is bound.

shaders/glsl/ holds the output; tools/validate_glsl.sh checks all 155
with the NDK's glslc (155/155 pass as ES 3.10 layout-annotated copies;
the on-device compile is the real test and comes with the backend).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…evice

compat/d3d9gles/ implements the slice of Direct3D 9 the engine's renderer
uses (Gfx.cpp, GfxBuffers.cpp, GfxRender.cpp: ~40 IDirect3DDevice9
methods and ten resource types) on OpenGL ES 3.0, so those 3,800 lines
of Nival's renderer -- LRU-cached vertex buffers, the UI texture atlas,
the render-target pool, index batching, redundant-state filtering -- can
be kept as they are instead of rewritten.

Conventions: framebuffer memory is always in D3D layout (row 0 = top);
every draw goes to an FBO with clip-space y flipped and culling inverted
to match, so render targets the engine samples back have the layout its
projective UVs expect, and Present blits the virtual back buffer to the
window with a vertical flip and a letterbox.  The back buffer is an FBO
of the size the engine asks for (4:3 modes are enumerated; the game's UI
is fixed-pixel).  Textures keep a CPU shadow so LockRect behaves like the
managed pool; DXT goes up as S3TC where the GPU has it, decoded otherwise;
A8R8G8B8/1555/4444 are converted to GL layouts on upload.  Vertex and
index buffers keep a shadow with dirty ranges (MarkDirty, a hint the
engine's sub-allocator will supply), uploaded on Unlock.  Shaders:
CreateVertexShader/CreatePixelShader receive the engine's own bytecode,
find the assembly text in its DBUG chunk and look the GLSL up by FNV-1a
hash in the generated table; programs are cached per (vs, ps, cube-mask)
with per-register uniform locations, since the GLSL compiler trims
constant arrays to the highest index a shader uses.

platform/d3d_selftest.cpp drives the shim from the boot harness on the
live context: creates the device, compiles all 155 shaders from
GfxShaders.cpp's real bytecode tables, links 154 (vs, ps) programs and
draws with each, and checks pixels through GetFrontBufferData and a
render-target LockRect.  On the Galaxy Z Fold7 (Adreno 830, S3TC present):
154/154 programs, D3D row order confirmed, RTT and texture upload correct.
36/36 harness checks pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…me loop, RTTI/LONG fixes

- platform/db_retail.cpp replaces ADOFake: reads the retail game.db generic
  column-store tables (class 0xA1843130) and runs the record classes' own
  ADO-style Import() over them; Complete/game.db loads 130/130 tables,
  222k records in ~0.5 s on device (A5_DB_DUMP=1 lists columns)
- prepare_sources: rules for the D3D9 backend files (269/269 compile),
  MarkDirty hints, GfxBuffers/GfxRender/GfxEffects fixes, link-time
  Interpolate/At rules, texture-cache/MakeTexture diagnostics
- CDynamicCast and (CObjectBase*)pContext casts go through
  a5_cast_opaque<> (compat/src/rtti_compat.cpp): MSVC's RTTI tolerated
  dynamic_cast from a reinterpreted pointer, the Itanium ABI does not
- compat: LONG is 32-bit (RECT/POINT must match CTRect<int>/CTPoint<int>),
  pointer-position API, dinput.h stub
- platform: game_entry (WinMain as init/step/shutdown), input_android
  (NInput over Android keys/touch), android_main game loop with hooks and
  periodic step/present diagnostics, audio_null in the game build
- CMake: a5main static lib, A5_BUILD_MAIN; push_data pushes Complete/game.db
- docs: PORTING.md/README status -- the game loop runs on the Z Fold7 and
  presents its first frames

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k, console echo, diagnostics

- d3d9gles: cull mapping was mirrored the wrong way (D3D winds in projected
  space, y up; our y-flipped FBO mirrors it): D3DCULL_CW -> glFrontFace(GL_CW).
  Every 2D quad had been culled.  Frame stats (A5D3DGetFrameStats), per-draw
  trace + back-buffer histogram (A5_D3D_TRACE), state overrides (A5_D3D_FORCE),
  LockRect diagnostics
- CMake: engine static libs linked whole-archive -- START_REGISTER /
  REGISTER_SAVELOAD_CLASS objects that nothing references were being dropped
  ('mainmenu' unknown)
- game_entry: no start.cfg -> straight to the main menu; step logs stats
- MiscDll rule: console lines echoed to logcat as UTF-8 ('console: ...';
  '%S' meant UTF-16 on MSVC), a5_u16_to_utf8 in compat
- android_main: <external files>/env.txt sets debug environment variables
- db_retail: A5_DB_DUMP_ROWS=<table>; harness: main-menu UI container check
  (warns: retail UI layout differs from this source's iMainMenu.cpp)
- docs: PORTING.md/README -- main menu (logo, version, localised text, cursor)
  renders on the Z Fold7; next steps listed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g; menus navigable by touch

- BasicFactory: pointer-typed table lookup compared type_info by identity;
  the Itanium ABI gives typeid(T*) for an incomplete T internal linkage (name
  starts with '*'), so every DB cross-reference imported from a file that only
  forward-declares the target class was null (no objects on any map).  Compare
  mangled names.
- Particle effects: SParticle file records hold 32-bit offsets in pointer
  fields; decode into native records instead of reinterpreting (crash on arm64)
- Retail-data adapters (rules): main-menu buttons and side-menu NEXT/BACK laid
  out on the templates' text lines; 'clientview' -> 'view'; cursor UITexture
  ids aliased by name (db_retail UserName index); VapourSwitchTime 0 no longer
  divides by zero; VapourDensity read as per-100-units
- SWTexture: DXT1/3/5 MMPs decoded in software (terrain painting)
- Input: absolute pointer applied before the interface turns a button message
  into a positioned event -- one tap clicks; c:\a5\ paths mapped to the data root
  (autoexec now loads input.cfg/config.cfg)
- Harness: object-chain check for the main-menu template; d3d9gles:
  A5_D3D_SKIP, A5_D3D_TRACE_CONST; db_retail: A5_DB_DUMP_ROWS multi-line
  strings, A5_DB_TRACE_REF

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s count as existing

The retail data ships Geometries/ and AIGeometries/ as loose files (7,790
of them), not as .res packages.  This source's release build only asked
the package whether a file exists (the loose-file check in
CResourceFileOpener::DoesExist is under _MAPEDIT), so CGameView::
AddModelPart / CreateOccluder / GBuilding skipped every model part and no
object was ever submitted to the renderer: the main menu drew 15 terrain
patches, fog and particles over an empty platform (frame trace showed
zero object draws; the DB chain for all 18 template elements resolved).

prepare_sources rule set 17: DoesExist falls back to a5_stat_exists on
the same "<Res>\<id>" name CFileResource opens.  On the Z Fold7 the menu
now shows the station platform, locomotive and tender, buildings, posts,
rubble and fire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lias, build-root override

- d3d9gles: A5D3DDrawsByShader (A5_D3D_SHADERS=1 prints the top vs+ps pairs
  every 5 s next to the game stats line)
- boot_harness: object-chain check covers the allies hero screen (2999) too,
  reports skinned models
- db_retail: cursor alias 492 -> "Normal Pen" (options / chapter map cursor)
- prepare_sources: A5_DEBUG_SKIN diagnostic in CGameView::CreateSkin
- scripts: A5_BUILD_ROOT / A5_CMAKE_EXTRA so two sessions can build side by
  side (build-*/ ignored)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sic plays on device

- platform/audio_android.cpp replaces the null back end: 64 sample voices +
  streams mixed in float at the device rate (linear resampling) on a mixer
  thread that writes to AAudio with blocking writes; AAudio is dlopen'ed so
  minSdk stays 24.  Semantics follow the FMOD wrapper (handle drop stops the
  voice, clip-space 3D attenuation with FMOD's log rolloff, 0..255 volumes,
  sample fades in source samples, stream fade-out in seconds).  One deliberate
  divergence: PlayStream() shares a stream already playing the same file --
  every pushed menu interface creates its own sound scene and would otherwise
  layer another copy of the ambient track.
- platform/audio_decode.cpp: WAV (PCM, Microsoft ADPCM, IMA ADPCM) and Ogg
  Vorbis via vorbisfile over memory.  The retail assets were encoded with
  libVorbis 1.0 beta3/RC1 and use floor type 0, which stb_vorbis cannot decode
  (6901 of 6910 samples, all 30 music tracks) -- so libogg 1.3.5 + libvorbis
  1.3.7 are vendored under thirdparty/ and the stb copy goes.
- tools/audio_decode_test.cpp (host: silentstorm_audiotest): all 1584 WAVs are
  bit-exact against ffmpeg (MS ADPCM prediction must truncate toward zero, not
  shift), all 6940 Ogg files decode to their declared length.
- android_main: pause/resume the mixer with the activity, audio stats every
  5 s (voices, streams, output peak, underruns); game_entry: A5_START_CFG;
  push_data.sh --full also pushes Versions/Current/res/Music.
- Verified on the Z Fold7: Res\Music\Mainmenu.wav streams at 48 kHz, 0
  underruns, peak 0.26.  3D voices need a mission (currently crashes in
  NAI::CPathNetwork::GetCP on unit creation, not audio).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…when idle, PORTING.md status

- iHeroMenu adapter: BACK / CUSTOM CHARACTER / NEXT along the template's
  single 'line' (retail container 354); MakeLineControl accepts 'line'
- input_android: an empty queue yields a CT_TIME message like DirectInput did,
  so the game clock advances without input (scene animation ran only while
  the cursor moved)
- PORTING.md: status as of 2026-08-19, three engine-wide traps documented

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… errors logged with their Lua stack

- platform/script_prelude.cpp, run after the auto-load scripts (wMain.cpp
  rule): the retail scripts call ~100 functions this snapshot's
  ScriptFunctions.cpp lacks (list from diffing every call in game.db Scripts
  and scripts/*.l against the registered names); in Lua 4 one nil call aborts
  the whole script.  WaitForObject/Random/TableGetSize/Get+SetGlobalGameVar in
  Lua on the existing API, the rest no-ops that report themselves once
- Script/ldo.cpp rule: script errors go to the log with source/function/line

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Lua has none); harness loads and exercises it

Co-Authored-By: Claude Fable 5 <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