Recommendation
decidedThis keeps the per-title engine flexibility the build system was designed for, preserves the option of third-party channels later, and leaves buyers with products that keep working if our infrastructure ever stops. The failure mode that produced the Catalina problem is closed off in CI rather than in packaging.
Where we stand
what already existsMost of the hard engineering is already done, and it is worth being explicit about that before planning more of it.
The genuinely unsolved question is narrow: how desktop products reach buyers without a distribution partner. Everything below serves that.
The real diagnosis
why this decision goes the way it doesIt is tempting to read the Catalina episode as a packaging problem — too many artifacts, too much rebuild cost. It was not. It was version drift. Titles became frozen artifacts pinned to an engine nobody was actively maintaining, and by the time the OS moved, the gap was years wide.
This distinction decides the architecture. If the problem were rebuild cost, the answer would be to collapse everything onto one shared runtime and ship content packs. But the build machines have already reduced rebuild cost to near zero, and that changes the calculus.
So the anti-drift property has to be enforced rather than hoped for: no title may ship against a non-current Fission SDK version, and the build fails if it tries. Encode it in the manifest schema and gate it in CI. That single rule does the work people usually buy a shared runtime to get.
Drift, measured
live issueThe CGameSpecific* files were created as per-title extension points. That intent never materialised: they are one lineage, copied forward by hand, where the newest title has the current version and older titles are retrofitted — when someone remembers.
Comparing them across eleven title repositories confirms it. The versions cluster by when the title was last touched, not by anything the title does:
| File | Distinct versions | Pattern |
|---|---|---|
| CGameSpecificCollectionListener.cpp | 3 across 9 | Six titles share one July 2026 copy; two share a later one. |
| CGameSpecificAchievementsComponent.cpp | 4 across 9 | Five titles on the July 2026 copy. halloween4 is on an August 2024 copy. |
| CGameSpecificMain.cpp | 6 across 10 | The most divergent, but still clustered — four titles share one copy. halloween4 is on a March 2024 copy. |
A third of the files are byte-identical everywhere already — CAchievementEffectScreen, CInGameShopGui, CWildlifeScreen, CGameSpecificInGameAds. Another group differs in exactly one title by two to four lines. The only genuine per-title code found was barrierReef's photo feature.
What to do about it
Promote the homogenised files into Fission and delete them from the titles. This is not a refactor — it is finishing the retrofit permanently, so it cannot lag again. The reconciliation is smaller than it looks: three versions to merge for CollectionListener, four for Achievements, six for Main. Thirteen diffs to read, not thirty files across eleven repositories.
Genuine variation follows the pattern the SDK already uses for FISSION_SPRITER and FISSION_3D: an optional module behind a flag, not per-title source. barrierReef's photo feature is the model.
Two cautions. The six versions of CGameSpecificMain.cpp need a human read to separate retrofit lag from genuine per-title configuration — that file is where real differences are most likely to be worth preserving as data. And halloween4 needs actual QA rather than a rubber stamp: every other title moves forward by weeks, halloween4 by two years in one step.
Two constraints that shape everything
structuralFission is not a HOG engine
The SDK carries gfHogCore, gfTimeCore, gfTileMapCore, gfMaze and gfMinigameCore as sibling modules. Any “single store runtime” therefore either carries every core in one fat binary, or exists in several genre variants. The moment it is several, the launcher has to resolve which runtime a given product needs — which is descriptor-driven dispatch.
The store may open to other producers
This one is decisive. A store hosting third-party products cannot require their code to be linked into our binary. That is structural, not technical:
Both constraints land on the same answer, which is a good sign it is the right one: the store's delivery contract must be producer-agnostic — a signed standalone artifact plus a product descriptor.
Delivery architecture
decidedOne manifest, one build machine, one artifact shape, several channels. The market axis already exists in game.yml; the store is another value on it, not a new kind of build.
product manifest
│
▼
build machine ──── engine-currency check → compile → sign → notarize
│
▼
standalone signed product + product.json
│
├──▶ our store (launcher installs, updates, launches)
├──▶ steam · epic · itch · gumroad · partners
└──▶ ios · android (native, same source)
Why standalone wins here
What we give up, honestly
Cross-cutting features — cloud saves, accessibility options, new entitlement logic — reach old titles only by rebuilding and re-shipping them, where a shared runtime would deliver them instantly to everything. Generic delta updating softens this considerably, since an engine change alters the executable and not the several hundred megabytes of assets beside it. It does not eliminate it.
One assumption worth heading off: a shared runtime would not have saved QA. Updating a runtime that thirty titles depend on still requires verifying thirty titles. The saving would have been in release management, not testing.
The product descriptor
core infrastructureBoth launchers currently guess what they installed. The Tauri client walks directories looking for a .app one level deep; the Rust client scans for launch candidates heuristically. Both are guessing at something the build system knows for certain.
The release step should emit a descriptor into the package, from data it already holds in buildSettings.yml and the parsed build name:
{
"product": "park-ranger-18",
"publisher": "CA",
"caption": "Park Ranger 18",
"version": "2.10.0",
"build": "park-ranger-18-CA-se-en-win32-ax3-2010000",
"platform": "win32",
"engine": "ax3",
"market": "store",
"runtime": null,
"executable": "parkRanger18.exe",
"entitlement": "store"
}
This is the contract between the build system and every client, and it is what makes the store producer-agnostic: a third-party product is just one whose descriptor we did not generate. The runtime field is how a first-party title later opts into a shared runtime without the store noticing the difference.
Shared runtime
deferred, not rejectedA single signed runtime with games as data downloads is genuinely attractive for our own catalogue: one binary to notarize, adding a title becomes a data upload, an OS break is one update that fixes the whole library, and licensing lives in one place.
The drift findings make it far more feasible than it first appeared. With the game-specific files promoted into Fission, per-title C++ approaches zero — there are not thirty implementations to disambiguate, so no namespacing pass and no product-keyed factory is needed. What remains is CGameSpecificMain.cpp, roughly 147 lines of wiring, which is a strong candidate to become manifest data.
It cannot be the store's architecture, for the reasons in Two constraints. But it works perfectly well as a first-party packaging option: adopt it per title, later, reversibly, with the store none the wiser because the descriptor's runtime field carries the difference. The standalone build continues to exist for every title, which answers the longevity objection by construction.
Engine backend
openThe portability seam already exists; this is a choice of what sits beneath it. The decision hinges on how high the seam sits — whether it speaks in scene graphs and sprites, or in windows and audio buffers.
| Backend | Fits when | Notes | Verdict |
|---|---|---|---|
| Axmol | Seam is 2D scene-graph shaped | Maintained cocos2d-x successor. Desktop, mobile, Xbox and WebAssembly. Built-in video player — worth real money given how much cutscene content the catalogue carries. Experimental D3D12 and Vulkan backends landed late 2025. | likely pick |
| SDL3 | Seam is low-level | The most durable portability layer available; Valve-backed, targets everything including Emscripten. SDL_GPU covers Metal, Vulkan and D3D12. More to build on top, correspondingly fewer surprises in a decade. | alternative |
| raylib | Seam is very thin | Pleasant and highly portable, but no scene graph, no UI layer, no video, and the softest mobile story. Thin for a commercial catalogue. | weak fit |
| cocos2d-x | — | No updates since 2019. Cocos have redirected all resources to Cocos Creator and confirmed there will be no further releases. No WebAssembly support. | ruled out |
Whichever we pick, the seam is the asset — protect it. Axmol is a comparatively small project and choosing it takes on that risk; the mitigation is not picking something larger but keeping the backend genuinely replaceable. Seams rot silently when only one implementation exists: backend types leak into game code, someone reaches around the interface for a deadline, and five years later the “swappable” layer is welded on.
One hard constraint regardless: do not let OpenGL be the only rendering path. It has been deprecated on macOS since Mojave and is the most plausible next thing to be withdrawn. See Current OS Targets for the supported-version picture.
Signing constraints
fixed costWe cannot fully self-distribute desktop software. Apple and Microsoft are unavoidable third parties — not for payment, but for permission to run. This is the one outsourcing that cannot be designed away, and it should be treated as a cost of doing business rather than a decision.
| Platform | Requirement | Indicative cost |
|---|---|---|
| macOS | Apple Developer Program, Developer ID certificate, notarization of every shipped binary. Sequoia removed the Control-click bypass for unsigned apps and Tahoe keeps it removed — blocked apps now send users into System Settings, which is fatal to conversion. Notarized software does not trigger Gatekeeper at all. | ~£80 / yr |
| Windows | Authenticode signing. Azure Artifact Signing (formerly Trusted Signing) is available to UK organisations, but Microsoft place it under the same reputation model as OV certificates: new files warn until history accumulates. Only an EV certificate buys immediate SmartScreen trust. | ~$10 / mo, or ~£400 / yr EV |
Sign every release with one consistent publisher identity so reputation compounds instead of resetting. The tooling — codesign, notarytool, signtool — is all CLI, so this is a pipeline stage on machines that already handle keys and manifests, not a project.
Opening the store to other producers adds a question here that does not exist today: whether we sign their builds or verify signatures they supply. That decision belongs with the marketplace planning, not this document.
Delivery & updates
decidedLicensing
decidedThe threat model is casual sharing — someone zipping their install folder for a friend — not determined piracy. That is worth stating plainly, because most of the machinery people build for this is aimed at an adversary who was never going to be stopped by it.
Because we own the engine, the entitlement check belongs in Fission itself, selected by a FISSION_MARKET define in the same way FISSION_IN_APP_PURCHASE already conditions the paywall sources: our API for the store, Steamworks for Steam, a null implementation for channels that handled it themselves. Written once, inherited by every title. Encrypted asset packs and a Big Fish-style trial timer become engine features rather than per-title work.
Deliberately not doing
Worth noting the browser build is the most piracy-resistant channel we have by construction: there is no redistributable artifact at all.
Mobile
openAxmol and SDL3 both target iOS and Android natively from the same C++ source, so the games ship as native builds. Capacitor stays useful for the storefront and library shell, reusing the lexFit pattern — but the games themselves should not go through it. Keep those two things separate in planning or they will get muddled.
On app store access: plan so the court outcomes do not matter. US anti-steering rules loosened in 2025 and the EU's DMA opened alternative distribution, so the picture is already less closed than “waiting on the courts” implies — but that area moves quickly and the specifics should be re-checked before anything is planned around them.
Rejected options
recorded so they need not be re-argued| Option | Why it was rejected |
|---|---|
| Shared runtime as the store's architecture | Rejected as the store's model, because a store open to other producers cannot require their code in our binary, and a multi-genre engine would need several runtime variants anyway. Not rejected as an idea — see Shared runtime, where it survives as a first-party packaging option. |
| Port the catalogue to a web runtime | Would mean rewriting a working, debugged C++ engine to buy a portability abstraction Fission already has. Emscripten gives us the browser target from the existing source anyway. |
| Per-download watermarked builds | Server-side repacking of large archives per buyer, to deter sharing by making copies traceable. Unnecessary once licensing moves into an engine we control. |
| Per-download stub executables | A tiny generated installer carrying the buyer's details. Every such binary is unique and would need signing and notarization individually, putting Apple's notary queue in the critical path of every purchase — and unsigned variants trip antivirus heuristics constantly. |
| Machine-fingerprint activation | Support load and broken installs cost more than the piracy it prevents, and it is defeated by anyone who would have defeated the simpler options. |
Open questions
needs an answerSequencing
ordered by dependency and lead time, not size| Step | Action | Why here |
|---|---|---|
| 01 | Start signing now, in prototype | Gates every install, has the longest lead time — certificate identity validation and SmartScreen reputation both take calendar time — and reputation only begins accruing once signed builds are shipping. Nothing else here is blocked by starting it today. |
| 02 | Fix the S3 path gap | Resolve the shop. / store. bucket inconsistency and point the shop at the versioned store/{publisher}/{project}/ prefix the release pipeline already writes. Until this is done, nothing else has anywhere to plug in. |
| 03 | Emit the product descriptor | The contract between build system and every client, and the thing that makes the store producer-agnostic. Both launchers stop guessing. |
| 04 | Promote game-specific files into Fission | Finishes the retrofit permanently and stops halloween4 recurring. Thirteen diffs to reconcile. Do it during the rebuild, while all titles are being touched anyway. |
| 05 | Enforce engine currency in CI | Add the current-engine assertion to the manifest schema and fail builds that violate it. Cheap while the pipeline is still being shaped, and it locks in the gain from step 04. |
| 06 | Close the backend decision | Resolve where the seam sits, pick Axmol or SDL3, and stand up the second CI backend at the same time so the seam is proven rather than assumed. |
| 07 | Launcher: deltas and integrity | Tauri updater against our own signed manifest, generic delta patching, ed25519 verification on every downloadable. Makes standalone products cheap to maintain in the field. |
| 08 | Licensing at the engine layer | Last, deliberately. Once Fission owns loading, the entitlement check and trial timer are a small amount of work in one place. |