Asset and script architecture¶
The design for moving Coney off the original's file formats and onto its own asset store and script runner. The rule stands: the move happens after the game plays 1:1. Nothing here is built yet. This page draws the boundaries now, so that the faithful work going on today does not add new ties to the PS2 formats, and so that each later step changes one layer.
It covers four things: the asset layers and how they fit the installer, the split of the script system, what each modernisation really buys and what it needs first, and what to do now versus later. The texture resolver and the render backend are on Enhancements plan (F4, F5); the Xbox side is on Xbox assets. This page links to them and does not repeat them.
Where Coney stands¶
Counted on main at 7300c566 (2026-10-07). A planned repo check rule (N4) is to keep the first four
rows current.
| What | Count | Where |
|---|---|---|
| Legacy file layer | 7 units, 1,183 lines | src/fileio/: disc, wad, wad_index, executable, file_stream, stream, reader |
Engine files that include a fileio/ header |
57 (platform 21, characters 10, world 5, audio 3, graphics 3, scripting 3, 12 others) |
plus 58 test files |
Public functions and constructors that take const io::Wad& |
48 in 32 headers (platform 17 headers, characters 5, audio 2, 8 others) |
the loaders' signatures |
Direct archive or disc calls (lookup, openEntry, index().find/findHash/entries, disc(), Disc::open, Wad::open, readExecutableWords) |
44 in 21 engine files (main.cpp 9, scene_disc.cpp 4); 123 in 45 test files |
|
| Direct reads of a disc file outside the WAD | 3: IOP/BFW.SND and IOP/MUSIC.SND (audio/sound_stream.cpp), PSS/*.BIK (platform/ffmpeg_movie_decoder.cpp), SLUS_212.15 for the random table (platform/main.cpp) |
|
Modes that need --disc |
8 (--load, --render-references, --view-txd, --view-sheet, --view-text, --view-world, --view-character, --play-level) and the front end |
src/core/options.h |
| Seams that already hide the source | 5 function objects: ScriptSource, SceneRecordSource, CaptionSource, SoundFiles, MovieOpener |
made from the WAD in 9 places (main.cpp 7, audio_output.cpp, play_level_scene.cpp) |
| Format parsers that take bytes or a stream and return a Coney struct | 37 in core directories (Lua chunks, animation clips, characters, scenes, sound tables, worlds, level objects, path maps, captions, particle pages, ...) | they stay as they are |
Files using librw (rw::) |
49, all in src/platform/; texture dictionaries, sprite sheets, world atomics and level objects come back as librw objects |
|
| Script natives registered | 466 registerFunction calls in 31 files, 26 of them *_bindings.cpp |
src/scripting/ |
Code outside src/scripting/ that touches the VM |
debug/ 7 files, gamemodes/ 3; at least 27 calls of ScriptSystem (call, schedule, runFile, enterLevel) |
|
| Process-wide mutable state | 8 tuning singletons (followTuning, followDefaults, combatTuning, bodyTuning, climbTuning, jumpTuning, locomotionTuning, staminaTuning) and the tunable registry |
|
| Threads | audio only (mixer, pcm_stream, spsc_queue, sdl_audio_device) and sdl_input |
|
Simulation inside src/platform/ |
PlayLevelMode::update steps the player, the humans, pick-ups, objects, scenes, streaming and lights (play_level_*.cpp, 2,105 lines) |
|
| Per-user writes (SDL pref path) | profiles/ (profile_folder.cpp), and also coney-tunables.ini and coney-imgui.ini (debug_menus.cpp) |
What this means: the parsers are already clean. Bytes go in and Coney structs come out, with no WAD in sight. What is tied to the PS2 is how a loader finds its bytes. 48 signatures pass the WAD itself around, and 44 calls look things up in it by WAD name. That is the layer to cut.
Assets¶
Layers¶
4 Game code gamemodes, world, characters, audio, scenes, script bindings
asks for engine-native data by id; never sees a file, a WAD or a disc
3 AssetStore resolves an id against the sources in priority order; one per session
2 Typed loaders bytes -> engine-native struct: the 37 parsers of today, unchanged
1 Sources Overrides | Xbox | PS2 install | Disc (--disc) | Memory (tests)
0 fileio Stream, Reader, Disc, Wad, WadIndex, Executable: only layer 1 includes
disc/wad/wad_index/executable; Reader and Stream stay shared byte tools
Rules that keep the layers apart:
- Only sources open files. A loader takes an
io::Stream&or a byte span and returns a struct. It never opens a file by name. - Game code asks by id, not by path. The id is the original's own key, so nothing has to be renamed and every research page still applies. For a WAD entry the key is the name hash (Name hash), for a disc file its path, and for a resource inside a pack its resource hash and chunk type. The Xbox resolver needs that last one (Xbox assets).
- Each asset kind is a simulation kind or a presentation kind. Presentation kinds (textures, movies, sounds, music, later models' looks) can be replaced by a mod. Simulation kinds can only be replaced in a session marked as modded: scripts, levels, object lists, collision, worlds' streaming data, animation clips (their timing drives combat), scenes, character data and executable tables. This extends the Xbox policy (assets only, never behaviour) to every source. The store reports a simulation digest over the simulation kinds it served, which test mode and multiplayer compare.
- Test mode uses the PS2 data only.
--headless,--input-scriptand the disc tests build a store with the PS2 source alone, so reference comparisons never see an override or an Xbox texture. - librw stays a renderer detail. Presentation loaders return CPU data (an image with its palette and mip
levels, a mesh).
src/platform/uploads that data. Today librw both parses and uploads RenderWare streams; the boundary moves in two steps (Benefits).
Interface sketch¶
A new directory src/assets/ (core, no OS or librw code). The names are proposals.
namespace coney::assets {
/// A WAD entry, by the hash of `./ee_files/<name>` (docs/research/name-hash.md).
struct FileId {
std::uint32_t nameHash = 0;
[[nodiscard]] static FileId of(std::string_view wadName); // "level99.lua" -> its hash
};
/// A file of the disc outside the WAD: "IOP/BFW.SND", "PSS/INTRO.BIK", "SLUS_212.15".
struct DiscFileId { std::string path; };
/// A resource inside a pack, as the chunk system and the Xbox resolver key it.
struct ResourceId { std::uint32_t resourceHash = 0; std::uint32_t chunkType = 0; };
/// Where a served asset came from; logged, and part of the simulation digest.
enum class Origin : std::uint8_t { Override, Xbox, Ps2, Memory };
/// One source of game files. NotFound means "ask the next source".
class AssetSource {
public:
virtual ~AssetSource() = default;
[[nodiscard]] virtual Origin origin() const = 0;
[[nodiscard]] virtual std::expected<std::unique_ptr<io::Stream>, Error> open(const FileId& id) const = 0;
[[nodiscard]] virtual std::expected<std::unique_ptr<io::Stream>, Error> open(const DiscFileId& id) const = 0;
/// Every WAD entry this source holds (what playableLevelNames() and the viewers list).
[[nodiscard]] virtual std::vector<FileId> files() const = 0;
};
/// The session's sources, in priority order, and the policy above.
class AssetStore {
public:
AssetStore(std::vector<std::unique_ptr<AssetSource>> sources, StorePolicy policy);
[[nodiscard]] std::expected<Opened, Error> open(const FileId& id, AssetKind kind) const; // Opened: stream + origin
[[nodiscard]] std::expected<std::vector<std::byte>, Error> read(const FileId& id, AssetKind kind) const;
[[nodiscard]] std::expected<Opened, Error> open(const DiscFileId& id, AssetKind kind) const;
[[nodiscard]] bool has(const FileId& id) const;
[[nodiscard]] std::uint64_t simulationDigest() const;
};
} // namespace coney::assets
Loaders keep their parsers and change only their first parameter. For example,
loadSoundBank(const io::Wad&, name, ...) becomes loadSoundBank(const assets::AssetStore&, name, ...). The five
existing seams get store-based factories (storeScriptSource(store) and so on) in place of wadScriptSource(wad).
Resource-level replacement (an Xbox texture for a PS2 dictionary) is a second interface, ResourceResolver, that
the texture loader asks after it has parsed the PS2 dictionary, as F4 describes. Keeping it out of AssetSource means
whole-file sources stay simple.
Sources¶
| Source | Reads | Built from | When |
|---|---|---|---|
DiscSource |
a mounted disc, a copy of its files, or an ISO image | today's io::Disc + io::Wad, unchanged |
--disc (development, CI disc tests) |
Ps2InstallSource |
<install>/game/ps2/: the disc's files copied as they are |
the same Disc + Wad over a folder; Disc already reads "a copy of its files" |
the installed game |
XboxSource |
<install>/game/xbox/: XBoxWad.idx, the volumes and the movies, copied |
a C++ port of coney-tools xbox (XDVDFS reader not needed after install) |
after 1:1, presentation kinds only |
OverrideSource |
<install>/overrides/, loose files by the extract tool's names |
open-format loaders (PNG, WAV, BIK) | after 1:1 |
MemorySource |
byte buffers | tests | now, with the store |
Because the PS2 install keeps the original files, it is the same code as --disc. The engine can play from an
installed folder as soon as the store exists, with no new parser.
The install folder¶
Maintainer decision (2026-10-07): portable. Everything lives in the folder the player picks in the installer. Only save data goes to the per-user location (SDL's pref path). No disc or ISO is needed after install. The Xbox ISO is preferred for assets, and the PS2 disc fills the gaps and is always the behaviour reference.
<install>/ picked by the player; movable as a whole
coney(.exe), licences
game/ps2/ the PS2 disc's files as they are (WARRIORS.DIR, WARRIORS.WAD, IOP/, PSS/, SLUS_212.15)
game/xbox/ optional: the Xbox disc's archive and movies as they are
overrides/ mods and texture packs
packs/<pack>/ a texture pack as `coney-tools` exports it: manifest + images, keyed by texture hash
textures/<dictionary>/<texture>.png loose files: the same names as `coney-tools extract` writes
audio/sounds/<name>.wav audio/music/<track>.wav
movies/<name>.bik
files/<wad name> power users: a whole WAD entry replaced (simulation kinds mark the session modded)
open/ optional modding kit: `coney-tools extract` output (PNG, WAV, Lua chunks, index)
index/
install.json sources, disc SHA-1s, Coney version, per-type counts and digests
xbox-resources.bin cache of the Xbox resource index (resource hash, chunk type -> location)
config/ proposed: tunables, overlay layout (see Maintainer decisions)
<SDL pref path>/profiles/ save data only
- The installer is a front end over one library call per step: copy the disc's files and check them against
the known SHA-1s, write
index/install.json, and optionally build the Xbox index and the modding kit. The first version can be aconey-tools installcommand next toextract. The installer study track owns the launcher (coney-tools: extract owns the names). - Name and lookup per kind:
| Kind | Order | Note |
|---|---|---|
| Textures | overrides (packs by the original texture's hash; an entry matching no texture on the disc is ignored), Xbox (by resource hash, equal counts only), PS2 | F4; Texture packs |
| Movies | overrides, Xbox _hd, PS2 |
|
| Sounds, music | overrides, PS2 | Xbox mapping is open (Xbox assets) |
| Fonts, HUD and particle sheets | overrides, PS2 | presentation |
| Scripts, levels, object lists, worlds, collision, animations, scenes, characters, executable tables, game text | PS2 (overrides only in a modded session) | simulation kinds |
- Command line.
--data <install>opens the install folder, and--discstays for development and CI. With neither, Coney looks forgame/ps2/next to its executable. All three build the same store.
The legacy loaders, one by one¶
What changes for each original format. Seam means a function object already hides the source; only its factory moves. Signature means the loader takes the WAD and its first parameter changes.
| Format | Loader (file) | Parser kept | Today | Change |
|---|---|---|---|---|
| WAD index, archive, disc | fileio/wad, wad_index, disc |
WadIndex::parse, readIsoRoot |
used everywhere | used by DiscSource and Ps2InstallSource only |
| Executable tables | fileio/executable (readExecutableWords) |
yes | main.cpp reads the random table from the disc |
DiscFileId{"SLUS_212.15"} through the store |
Lua chunks, _objs.txt, global strings |
scripting/config_strings (wadScriptSource, loadGlobalStrings) |
parseLuaChunk |
seam | storeScriptSource(store) |
Scene list and records (.scn) |
scenes/scene_disc (loadSceneList, wadSceneSource) |
SceneList::parse, parseSceneHeader, parseSceneSegment |
seam + signature | store |
| Captions | movies/disc_captions |
parseSubtitles |
seam | store |
| Movies (Bink) | platform/ffmpeg_movie_decoder (discMovieOpener) |
FFmpeg | seam, opens PSS/ on the disc |
opener over store.open(DiscFileId) |
| Sound tables, banks, streams | audio/sound_data, sound_bank, sound_stream (DiscSoundFiles) |
parseSoundList, parseIndex, ADPCM |
2 signatures + seam | store |
| Characters | characters/character_list, character_assets, character_data, character_model, dynamic_clips; human/player |
CharacterList::parse, decodeCharacterModel, readCharacterClump |
6 signatures | store |
| Animations | characters/dynamic_clips (loadAnimResource) |
parseAnimClip |
signature | store |
| Generic chunk containers | gamemodes/load_entry_mode (loadWadEntry), core/chunk_system |
loadContainer, loadGroupedContainer |
signature | store |
| Level objects | world_objects/object_list, placed_objects_file, world/level_object, path_map |
yes | 1 signature | store |
| Worlds and level files | platform/world_set, level_file, world_viewer_mode, play_scenery |
readWorldManifest, readSectorPluginData, ... |
8 signatures, librw objects | store, then Benefits |
| Textures, sprite sheets | platform/texture_dictionary, sprite_sheets, object_models, texture_lookup |
librw's RenderWare reader | 6 signatures, librw objects | one texture function (F4), then CPU images |
| Particles, HUD sheets, smoke | platform/particle_renderer, hud_layer, room_smoke_overlay |
parseParticlePage, parseSpriteSheetTable |
4 signatures | store |
| Viewers, modes | platform/*_viewer_mode, play_level_mode, front_end_scene, reference_renderer, audio_output |
pass the WAD on | pass the store on | |
| Profiles | warriors/disk_profile_store |
ProfileRecord::parse |
not a disc format | unchanged: save data stays in the pref path |
Scripts¶
Today ScriptSystem (src/scripting/script_system.h) does three jobs in one object. It reads chunks through
ScriptSource. It owns the one Lua 4.0 state, whose bindings installBindings registers straight on LuaVm. And it
runs the schedule of delayed calls and the update function. Message handlers, animation callbacks and trigger
boxes call into the same state. The split below keeps that behaviour exactly (Scripting: life of the Lua
state). Only the seams move.
Three parts¶
Script source which program a name means: original chunk | Coney's level script | (later) a mod
|
Runtime Coney's Lua 4.0 VM (lua_vm, lua_chunk, lua_value, lua_libraries); one state per level, as now
|
Native API the 956 original natives + Coney's own, as data (name, signature, status, callback)
|
Scheduler when scripts run: level entry, update function, schedule, message handlers, anim callbacks, triggers
Script source. A ScriptCatalog maps a script's name (level99.lua) to a provider:
- original: the compiled chunk from the store (today's only path);
- Coney: Coney's own rewrite of that level's script, written from the level walks on
Scripting. Recommended form: Lua 4.0 source text that Coney compiles with
its own compiler into the same
LuaProtothe VM runs. Then there is one runtime, one binding table and one scheduler, and a rewrite can be compared call for call with the original chunk; - mod: a file from
overrides/scripts/(this marks the session modded).
The level flow asks the catalog, never the store, so a level can switch from the original chunk to Coney's rewrite with a one-line change in the catalog. That switch is allowed only when the parity test passes: same input script, same natives called in the same order with the same arguments, compared on a trace.
A compiler from Lua 4.0 source to LuaProto replaces the console's evaluator (debug/lua_console, expressions and
assignments only). It also gives the console if, loops, local and function. A modern Lua (5.4) runtime for
mods would be a second runtime behind the same native API, with its own value bridge. Defer it. One runtime is
enough until mods need features that 4.0 lacks.
Native API. Today each *_bindings.cpp captures a BindingContext and calls vm.registerFunction(name,
lambda). Bindings reach the game only through host interfaces (BindingHost, AiBindingHost, SoundHost,
CrimeServices, ...), and that is the boundary to keep. The proposed change is small:
namespace coney::script {
/// One native, independent of any VM: what the Natives page, the masterlist and the VM all read.
struct Native {
std::string_view name; // "HuCreate"; Coney's own live in a `Coney` table, never as bare globals
NativeOrigin origin; // Original | Coney
BindingKind status; // implemented | partial | stub (today's BindingKind)
NativeFunction call; // std::span<const Value> -> std::expected<std::vector<Value>, Error>
};
/// Built once per state from the BindingContext; the runtime installs it, the debug menus list it.
[[nodiscard]] std::vector<Native> buildNatives(const BindingContext& context);
}
The binding bodies do not change. Only the registration moves from the VM to a list. A second runtime or a call
tracer can then wrap every native in one place. Coney's own natives go in a Coney table, so they can never clash
with one of the 956 original names.
Scheduler. Scripts run only on the simulation thread, inside a game mode's update(), at the points the
research fixes. Everything that starts a script (level entry, update function, due scheduled calls, message
handlers, animation callbacks, trigger events) goes through one ScriptScheduler::run(entry) that owns the order
and the instruction budget. Coroutines later become one more entry kind ("resume coroutine n"). Lua 4.0 has none,
and the research shows the original's scripts waiting only through the schedule and callbacks, never inside a call.
Coney native candidates¶
The registry of what Coney's engine can do that the original's does not, and that a script could plausibly want.
Each one would become a native of Coney's own, in the Coney table of the Native API, once mods
exist (Script mods, Enhancements: Coney natives).
Nothing here is a script binding yet: every entry is a C++ capability today, and none has an @orig tag, because
none reimplements the original. These natives are Coney's own and never mixed with the original's 956: they are
not in research/bindings/, not in Script bindings and not in
src/debug/native_signatures.cpp (all of which describe the original's table), and a mod calls them as
Coney.Name, never as a bare global.
Why a table here and not a research/bindings/coney.yaml. The masterlist's schema is built around facts about
the original (the wrapper's address, the functions it calls, an evidence level, usage counts in the shipped
scripts), and natives render, natives cpp and the Natives page all read it as the original's table. A Coney file
would need every one of those fields faked or made optional, and a second origin threaded through three generators,
for natives of which Coney has none. A table here costs no machinery. When the first one is exposed, the move is the
Native struct above (its origin field already says Coney), and the generated pages can then grow a "Coney"
section from that data; this table is its seed.
Status is engine-only (the C++ exists and no script can reach it) or exposed (registered in the Coney
table). Names are proposals and may change before the first is exposed; the C++ entry point is the part to keep
current. Where a capability has a debug-menu page, the page calls the same entry point, so a native would be one more
caller, not new logic (Debug menus: adding a feature).
| Proposed native | C++ entry point today | What it does | Status |
|---|---|---|---|
Coney.SetFlight(human, speed, climb) |
human::Human::setFlight(std::optional<Flight>), human::noclipFlight() (the debug noclip change, not on main yet) |
Flies a human by the stick, through walls and floors, with no gravity; nil ends it and drops him with the normal physics |
engine-only, on its branch |
Coney.SetNoclip(on) |
PlayLevelMode::setNoclip(), human::Player::setNoclip() (the debug noclip change, not on main yet) |
The player's flight as the debug noclip drives it (pad 1 flies him) | engine-only, on its branch |
Coney.TeleportPlayer(x, y, z, heading) |
human::Player::teleport(mesh, PlayerStart), PlayLevelMode::teleport() |
Puts the player on the ground below a spot, with the camera behind him and nothing to blend from | engine-only |
Coney.SetPlayerFrozen(on) |
PlayLevelMode::setPlayerFrozen() |
Makes the player ignore the pad, or listen again | engine-only |
Coney.ChangeCharacter(type) |
human::Player::changeCharacter(), PlayLevelMode::changeCharacter() |
Rebuilds the player in place as a CfgChar character type: model, moves, class damage, power class, full health |
engine-only |
Coney.SetFreeCamera(on) |
PlayLevelMode::setFreeCamera(), world::DebugCamera |
A camera flown by pad 1 while the player stands still | engine-only |
Coney.ResetCamera() |
human::Player::resetCamera(), camera::FollowCamera::place() |
Places the follow camera behind the player again, with nothing to blend from | engine-only |
Coney.SpawnObject(kind, ...), Coney.ClearSpawned() |
PlayLevelMode::spawn(), clearSpawned(), debug::spawnables() |
Adds a solid sandbox primitive (crate, wall, ramp, stairs) ahead of the player, or removes them; sandbox only | engine-only |
Coney.SpawnFighter(x, y, z, heading [, model]), Coney.ClearFighters() |
PlayLevelMode::spawnFighter(), spawnFighterAs(), clearFighters(); ai::AiHumans |
Adds or removes AI fighters that fight the player, as the scene's character or any human model the configuration names | engine-only |
Coney.SetFightersEngage(on) |
ai::AiHumans::setEngaging() |
Whether idle fighters take the player on | engine-only |
Coney.PlayAnim(human, set, id, loop, speed, inPlace), Coney.StopAnim(human) |
human::Human::playDebugClip(), stopDebugClip(); PlayLevelControls::playAnim(), stopAnim() (debug tools) |
Plays any clip of an anim set (the human's own, the generic set or another character's) from its first frame, once or looping, at a speed, then gives the human its own animation back | engine-only |
Coney.SetHudOverride(player, field, value) |
hud::Hud::overrides() (hud::PanelOverrides) |
Shows a chosen rage, score, money or item count on a player's panel instead of his own | engine-only |
Coney.SetPaused(on), Coney.StepOnce(), Coney.SetTimeScale(scale) |
debug::TimeControl, FrameClock::setSpeed() |
Stops the game's steps, runs one, or runs more or fewer of them a second (0.05x to 4x); every step is still 1/30 s | engine-only |
Coney.GetTunable(path), Coney.SetTunable(path, value), Coney.ListTunables() |
debug::TunableRegistry::value(), set() (applied between steps by applyPending()), categories() |
Reads and writes any named, typed value a subsystem registered, by its Category/Name key |
engine-only |
Coney.GetOption(key), Coney.SetOption(key, value) |
debug::TunableRegistry and the Display/<name> keys of Enhancements |
The enhancement options (the cutscene letterbox now, more later) by their stable key | engine-only |
Coney.StartLevel(name, checkpoint) |
StartUpFlow::jumpToLevel() |
Jumps to a level at a checkpoint from the front end or from a level in play | engine-only |
Coney.SetDebugDraw(option, on) |
debug::DebugDrawOptions, PlayLevelMode::setSceneLines(), PlayLevelControls::sceneLines() (debug tools) |
Draws collision, headings, camera and places as lines in the scene | engine-only |
Coney.SetVolume(bus, volume), Coney.SetTestTone(on) |
debug::AudioControls::setVolume(), setTestTone() |
Sets the master or a bus volume; plays Coney's synthesised test tone | engine-only |
Coney.Notice(text) |
debug::DebugSession::notice() (the debug noclip change, not on main yet) |
Prints a line and shows it as a short notice under the corner lines | engine-only, on its branch |
Coney.TraceCalls(path) |
script::ScriptSystem::traceCalls() (--script-trace) |
Writes every binding call the scripts make, and every call into them, to a log | engine-only |
Not candidates, because a script has no use for them or the original already has the native: the debug menus'
navigation and front ends; the Lua console (a script can run Lua itself); the test aids --start, --frames,
--input-script and --screenshot; the cheat codes (the original's DbgEnterCheat callback already is the native);
and position, health and gait reads, which the original's Hu bindings give.
Debug console needs¶
What the console (Debug menus: Lua console) needs from these parts:
- Pick the state: the game's state or the sandbox state, as the Natives page already does.
- Full statements: the source compiler above (
if, loops,local,function);printcaptured to the console. - Errors with lines: the compiler keeps line information for Coney's chunks. The original chunks have none, and
parseLuaChunkdrops it. - Introspection: globals, natives with their status (the native list), the schedule queue, the update function.
- A call trace: the natives a script called this step, with their arguments. The parity tests need the same trace.
- Reload: re-run a Coney script file in the running state, outside test mode only.
- Safety: every console run has its own instruction budget (
LuaVmOptions::maxInstructions), so a typed loop cannot hang the game.
Benefits and prerequisites¶
An honest list: what each modernisation buys, what has to exist first, and what in today's code stands in the way.
| Goal | What it buys | Needs first | In the way today | Verdict |
|---|---|---|---|---|
| Multithreading | Load and decode on worker threads (world parts, textures, sound banks): fewer hitches when streaming | The store; loaders that are pure (bytes in, struct out); decode split from GPU upload; completed loads applied at a step boundary, in request order (test mode: synchronous) | 49 librw files decode and upload in one go on the main thread; 8 tuning singletons and the tunable registry are process-wide (harmless while only the simulation thread reads them) | Worth it for loading only. The simulation stays on one thread, as the original's does: that keeps it deterministic and easy to compare |
| Modern render pipeline | F2-F5 on Enhancements plan: a post chain, SDL3 GPU (Vulkan, D3D12, Metal) | Presentation loaders that return CPU data; the simulation moved out of src/platform/ |
PlayLevelMode::update (platform) steps the simulation; texture dictionaries, sprite sheets, atomics and level objects come back as rw:: objects |
Do it in two steps: first a platform-side upload interface with librw still parsing; much later, Coney's own RenderWare readers. Only then can librw go |
| Memory hardcodes | Bigger levels, more humans for mods and multiplayer | Each limit marked as faithful (it changes behaviour) or as Coney's own; faithful limits gathered in one Limits struct with the original's values, forced in test mode |
kSectorPoolSize (17,217,536: what streams in when), kGangSlots 32, kGangMemberSlots 16, kFormationPool 42, kRoutePoolSize 32, kMaxEnemies 16, kLightCapacity 512, kMaxShards 79 and others are constexpr in their headers |
Raising any of them changes play (Memory: what to keep). Gather them now as documentation; turn them into options only for mods and multiplayer |
| Async scripting | Level scripts written as straight lines ("wait until the fence is climbed") for rewrites and mods | The scheduler as the only way in; a VM that can suspend (today's execute recurses through C++ for every call); a fixed resume order |
Nothing for the original's scripts, which never wait | Defer. Faithful scripts do not need it, and rewrites must match the original's timing anyway |
| Multiplayer | Roadmap: online multiplayer | Fixed step (done), seeded randomness (GameRandom, done), input scripts (done), one input path for pads and brains; simulation and presentation separate; state snapshots; matching simulation data (the store's simulation digest) |
Simulation inside src/platform/; no snapshot of simulation state; pools sized for one player |
After the whole game. The store's digest and the platform split are the parts worth preparing |
| Open formats (PNG, WAV, glTF) | Modders edit what they understand; overrides load without PS2 encoders | Typed loaders chosen by asset kind, not by file format; one naming scheme (the extract tool's) | Textures are 4- or 8-bit palettised, so a PNG round trip must be lossless (indexed PNG, or exact RGBA, compared on the texel hash); 9.5 GB for a full extract, mostly WAV | Overrides in open formats: yes. The engine's own data stays in the original formats: they are smaller, already parsed, and they are what the behaviour was researched on |
Legal¶
- Converting a file does not change who owns it. A game texture saved as PNG, upscaled, renamed or packed is
still the game's texture. The same goes for a sound decoded to WAV or a model exported to glTF. Everything in
game/,open/,index/and any override made from game assets is the player's local copy. It never goes into the repository, a release, a CI cache, an issue or a test fixture (LEGAL.md). - What keeps Coney clean is not the format. It is that Coney ships no game data and extracts it from the
player's own disc at install, on the player's machine. Tests use synthetic fixtures (
MemorySource), and disc checks print counts and hashes only. The repository may hold expected counts and SHA-1s, asextract.pyalready does. - Nothing the project hosts or links holds game pixels. The installer and
coney-toolsdownload Coney and, for upscaling, model weights (maintainer decision, 2026-10-07), never game data. Texture packs are shared as original art or as recipes that each player rebuilds from their own disc; players may import a pre-made upscale, but the project never hosts or links one (Texture packs).
Maintainer decisions¶
These are open. This page assumes the first option of each, and a maintainer decides. Sharing texture packs is decided (Enhancements plan: Decided).
- Coney's level scripts in the repository. Coney's Lua rewrites are written from research walks, not copied. May they ship under GPL-3.0-or-later, and how closely may their structure follow the original's (function names, order of calls)? LEGAL.md forbids the game's script source, and a rewrite is not that source, but where the line falls is a legal call.
- Game text (dialogue, subtitles, menu strings): extracted at install and never shipped (current recommendation; pending).
- Settings location. The decision is that only save data goes to the pref path, but
coney-tunables.iniandconey-imgui.iniare written there today. Proposal: move them to<install>/config/, and keep the pref path forprofiles/only. - The modding kit (
open/, about 9.5 GB): optional at install, or left toconey-tools extract.
Timing¶
Now¶
Each item is one engineer, one feature commit, and engine behaviour does not change: the same bytes reach the
same parsers. Order: N1, then N2-N5 in any order, then N6-N7. Coordinate platform files with "main", since other
engineers are working in src/platform/ now.
| Id | Item | Files | Size |
|---|---|---|---|
| N1 | src/assets/: FileId, DiscFileId, ResourceId, AssetSource, AssetStore, DiscSource (over Disc + Wad), MemorySource; Catch2 tests with synthetic WADs; main.cpp builds the store after opening --disc and passes nothing new yet |
new src/assets/*, platform/main.cpp, tests/assets/* |
S |
| N2 | Store-based factories for the 5 seams (ScriptSource, SceneRecordSource, CaptionSource, SoundFiles, MovieOpener), used at their 9 construction sites; the random table read as DiscFileId{"SLUS_212.15"} |
scripting/config_strings, scenes/scene_disc, movies/disc_captions, audio/sound_stream, platform/ffmpeg_movie_decoder, platform/main.cpp, audio_output.cpp, play_level_scene.cpp |
S |
| N3 | Core loaders from const io::Wad& to const assets::AssetStore&, one directory per commit: characters + human/player (6), audio (2), scenes, world_objects, gamemodes/load_entry_mode (4) |
15 headers in core directories, their tests | 3 x S |
| N4 | coney-tools repo check rule: outside src/fileio/, src/assets/ and a shrinking allowlist, no #include of fileio/disc.h, wad.h, wad_index.h or executable.h (reader.h and stream.h stay free). The allowlist starts with today's files and only ever shrinks, so the 1:1 work adds no new direct use |
python/src/coney_tools/repo_checks.py, its test |
S |
| N5 | One function for every texture load (Enhancements choice 5), called by texture_dictionary, sprite_sheets and object_models; F4 attaches there later |
src/platform/ texture files |
S |
| N6 | Platform loaders and modes to the store, after N3 and when the platform's maintainers agree: 17 headers, about 30 signatures (world_set, level_file, play_level_*, viewers, hud_layer, particle_renderer, ...) |
src/platform/ |
2 x M |
| N7 | --data <install> opening <install>/game/ps2/ through the same Disc + Wad (a folder copy already works), and the default lookup next to the executable; docs/guides/building.md updated |
core/options, platform/main.cpp |
S |
Not now, but rules for all work from today on: no new const io::Wad& parameter (N4 enforces this once it
lands); a new loader takes a stream or bytes; new script entry points go through ScriptSystem, not LuaVm; new
pool limits get a // faithful limit or // Coney limit comment; nothing new goes in the pref path except save
data.
Deferred, in order¶
After the game plays 1:1 (The whole game for the core; earlier where a maintainer says):
- Installer:
coney-tools install(copy, verify,index/install.json), then the launcher. - Overrides for presentation kinds, with PNG and WAV loaders; test mode stays PS2-only.
- Xbox source: textures via the
ResourceResolver(F4) and HD movies (Xbox assets). - Native list (
buildNatives), theScriptScheduleras the one entry, and the call tracer. - Lua 4.0 source compiler, then the console on it, then Coney's level scripts, one level at a time, each behind its parity test.
- Simulation out of
src/platform/(PlayLevelMode::updateinto a core gameplay mode; platform keeps the drawing), and simulation snapshots. - Async loading: decode on workers, upload on the main thread, apply at step boundaries.
Limitsstruct; the render backend (F5); Coney's own RenderWare readers if librw is to go.- Coroutines in the VM, the
Coneynatives table for mods, a modern Lua runtime if mods need it. - Multiplayer.
Risks¶
- Merge conflicts with the engineers active today. Change the core directories first and
src/platform/last, one directory per commit, and agree each platform step with "main". - Behaviour drift from a source. An override or an Xbox file reaching the simulation would change play. The split into simulation and presentation kinds, PS2-only test mode and the simulation digest guard against it.
- Two naming schemes. Overrides must use the extract tool's names, or modders will face two sets of names. The extract tool owns them; the engine reads them.
- Xbox mismatches. 65 texture resources have different counts on the two discs. They stay PS2 until mapped (Xbox assets: open questions).
- Rewritten scripts drifting from the original. No level switches to a rewrite without a passing parity trace under the same input script.
- The compiler's size. Lua 4.0's grammar is small, but a compiler with correct code generation is still an M-to-L item. It is the gate for every Coney level script, so schedule it first among the script items.