Casual Arts · architecture decision record

Catalogue Delivery Plan

How we rebuild the catalogue and get desktop products to buyers ourselves — without a distribution partner, and without repeating Catalina in five years' time.

Status  Draft for review · rev. 2 Date  3 September 2026 Scope  Build, delivery, licensing Outsourced: Stripe · Apple · Microsoft

Recommendation

decided
Decision Ship standalone per-product builds as the consumer artifact, hold every title to one enforced-current Fission SDK version in CI, and make the launcher an optional convenience rather than a dependency.

This 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.

Rev. 2 Two findings sharpen this. The per-title C++ is not per-title at all — it is one lineage propagated by hand, and the measurements are in Drift, measured. And the prospect of opening the store to other producers makes the product descriptor core infrastructure rather than a tidy-up. The shared runtime moves out of “rejected” and into a first-party optimisation.

Where we stand

what already exists

Most of the hard engineering is already done, and it is worth being explicit about that before planning more of it.

sourceFull C++ across the catalogue, on the shared Fission SDK with a deliberate portability seam. Titles are largely data-driven. engineFission is multi-genre by designgfHogCore, gfTimeCore, gfTileMapCore, gfMaze and gfMinigameCore sit alongside gfGameCore, with Torsion as a separate SDK. buildManifest-driven build machines can produce the entire catalogue for any target. game.yml already parameterises market (steam, epic, itch, gumroad, apple_store, aws_upload) and framework; buildSettings.yml already carries per-market identity blocks. storeFastAPI on Lambda, DynamoDB, S3, Stripe. Purchase, entitlement and download-history plumbing all exist. clientsTwo desktop store clients — a Tauri app sharing the shop's web UI, and a native egui app with the better install model. One needs to be retired.

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 does

It 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.

The trap Automated builds solve build cost, not verification cost. Being able to rebuild thirty titles cheaply does not mean anyone did. Without a forcing function, titles get rebuilt selectively under deadline pressure and drift returns quietly.

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 issue

The 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:

FileDistinct versionsPattern
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.

Now halloween4 is carrying two-year-old copies of files the rest of the catalogue updated in 2026. That is not a hypothetical drift risk — it is the Catalina mechanism, live in the tree today. Hand-propagation across thirty repositories does not fail because anyone did it wrong; it fails because it silently misses stragglers.

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

structural

Fission 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:

legalThey will not hand over source, and we would not want it — we would be distributing a derivative work and assuming responsibility for it. cadenceOur release schedule would gate every one of their releases. riskTheir crash happens inside our signed, notarized binary, under our Developer ID.

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

decided

One 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

producersThird-party capable. The only artifact shape another producer can ship us. Everything else follows from this. enginesEngine optionality. A title can move to a new engine independently. A shared runtime would force the whole catalogue to move in lockstep. channelsChannel optionality. Steam, GOG, itch or a publisher deal all want an installable product, not “install our launcher first.” lifespanLongevity. A signed standalone build keeps running if our service stops — which is precisely what Big Fish did to its own Game Manager v3 customers in April 2025. funnelNo launcher at purchase. Buy, download, run.

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 infrastructure

Both 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.

Blocking gap The release pipeline already promotes to store/{publisher}/{project}/{major}.{minor}/{platform}/, correctly versioned. The shop does not look there — it reads flat, unversioned s3_paths from DynamoDB, which currently name two different buckets (shop. and store.) in the same record. That looks unintentional and should be checked first. Until the shop resolves the versioned prefix, nothing else here has anywhere to plug in.

Shared runtime

deferred, not rejected

A 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

open

The 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.

BackendFits whenNotesVerdict
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.

Rule Keep two backends building in CI from day one — a headless or software-rendered second implementation used only for tests is enough. That is what proves the seam is real, and it converts the next OS transition from a rewrite into a weekend.

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 cost

We 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.

PlatformRequirementIndicative 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.

Do not build on this An app unzipped by our own launcher will often not carry the quarantine flag, and so appears to run unsigned. That is Gatekeeper evasion. Apple tighten it regularly, and the entire catalogue would break on a point release with no way to reach already-installed copies. Notarize properly.

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

decided
hostingS3 behind CloudFront, roughly $0.085/GB egress — about 4p per download of a 500 MB title. Noise at casual-game price points, but it is the recurring cost of not outsourcing, so model it. updatesTauri's updater plugin against a signed manifest on our own domain, minisign-verified client-side. No third party in the loop; nobody can deplatform us, change revenue terms, or sunset our client. deltasBuild delta patching in early. An engine-forced rebuild changes the executable, not the assets — deltas keep a catalogue-wide fix to tens of megabytes per title instead of hundreds. integritySign every downloadable with an ed25519 key and verify in the launcher, so a compromised bucket cannot push anything harmful. scopeThe launcher owns all updating. Self-updating titles would put N update mechanisms in the field.

Licensing

decided

The 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

rejectedMachine-fingerprint activation. Costs real revenue in support load and broken installs — new laptops, offline play, reinstalls — and is defeated by the same person who would defeat anything simpler. rejectedPer-download watermarking and stub installers. These existed only to compensate for not owning the binary. We own it.

Worth noting the browser build is the most piracy-resistant channel we have by construction: there is no redistributable artifact at all.

Mobile

open

Axmol 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
OptionWhy 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 answer
engineWhere does the seam sit? Scene-graph level points to Axmol; window-and-audio level points to SDL3. The one input still needed to close the engine decision. wiringIs CGameSpecificMain.cpp lag or configuration? Six versions need a human read to separate retrofit lag from genuine per-title config. An afternoon, and it decides whether titles can become pure data. clientsWhich launcher survives? Carrying both the Tauri and egui clients costs twice on every change. cadenceHow often do we expect a forced catalogue-wide rebuild? Every five years or so and standalone wins comfortably. If something makes it annual, the shared runtime's release-management advantage starts to earn its keep. taxVAT and merchant of record. “Just Stripe” does not discharge this. A UK seller supplying digital services to EU consumers generally registers under non-Union OSS from the first sale, with no small-seller threshold. Opening the store to other producers sharpens it considerably — taking payment for other people's goods generally makes us merchant of record whether we planned to be or not. One question for an accountant, not two. marketWhat does opening the store actually commit us to? Review process, producer entitlement SDK, payouts, liability for code we did not write, support for products we cannot debug. A business decision deserving its own planning pass.

Sequencing

ordered by dependency and lead time, not size
StepActionWhy 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.