diff --git a/.gitignore b/.gitignore index 08f2b68..251ac2e 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,12 @@ build.ninja *.dat *.MID *.mid +.DS_Store + +# Ripped game media — copyrighted, large, and not ours to redistribute +Full_Tilt_Pinball_ISO/ +*.img +*.iso +*.ccd +*.sub +*.cue diff --git a/CLAUDE.md b/CLAUDE.md index e8295a5..5b37136 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,3 +40,30 @@ Drop them in any of these; first match wins in the order `CADET.DAT`, `PINBALL.D - `~/Library/Application Support/SpaceCadetPinball/` ← preferred, keeps the repo clean Without them the binary exits with "Could not load game data" and lists these paths. + +## The other two Full Tilt tables (Dragon's Keep, Pirate's Cove) + +The retail CD carries `DRAGON.DAT` and `PIRATES.DAT` alongside `CADET.DAT`. **They do not play, and +making them play is a months-long reverse-engineering project per table — not a loader fix.** +Investigated 2026-07-28; evidence, so nobody re-derives it: + +- The container format is shared (`PARTOUT(4.0)RESOURCE`) and all three tables use the same five + component type ids (200, 201, 202, 300, 400). So the DAT *parses* fine. That is the easy half. +- Only ~35 of Dragon's 278 named components and ~36 of Pirate's 355 match Cadet's names. The + 88-entry `control::score_components` table in `control.cpp` binds behaviour to those names, and + `control.cpp` is 4600 lines of Cadet-specific rules (missions, ranks, wormholes, Gravity Well). + Each table needs its own equivalent written from scratch. +- Both tables use float attributes the engine has no semantics for: `409`, `1306`, `1406`, + `1600`–`1604` (plus `604`, `704`–`706` for Dragon, `1405` for Pirate). These are hard errors, not + missing-value errors — e.g. `loader::kicker` accepts only 401–406 and calls `error(10, 20)` on + anything else. Dragon's kickers carry `409`, meaning unknown. +- Upstream will not do it. Maintainer k4zmu2a, issue #148: the effort is "comparable to doing the + whole process all over again, once per table", and issue #20: "I most likely will not be adding + support for DRAGON and PIRATE tables." Cadet was reversed with the aid of the *public PDB symbols* + shipped for XP's `pinball.exe`; no equivalent symbols exist for the Full Tilt table binaries. + Four issues have requested it (#20, #148, #160, #256); none has ever produced a PR. + +If someone does take it on, `tools/datdump.py` dumps and diffs DAT structure, and +[DatPartoutExplorer](https://github.com/belaw/DatPartoutExplorer) has a `Resource Identifiers.txt` +mapping attribute ids to object types — including entries flagged as unused in Cadet, which is the +obvious first place to look up what `1600`–`1604` mean. diff --git a/SpaceCadetPinball/TRamp.cpp b/SpaceCadetPinball/TRamp.cpp index 795235d..bb65436 100644 --- a/SpaceCadetPinball/TRamp.cpp +++ b/SpaceCadetPinball/TRamp.cpp @@ -28,13 +28,18 @@ TRamp::TRamp(TPinballTable* table, int groupIndex) : TCollisionComponent(table, RampPlaneCount = static_cast(floor(*floatArr3Plane)); RampPlane = reinterpret_cast(floatArr3Plane + 1); - auto wall0Arr = loader::query_float_attribute(groupIndex, 0, 1303); - auto wall0CollisionGroup = 1 << static_cast(floor(wall0Arr[0])); - auto wall0Pts = reinterpret_cast(wall0Arr + 2); - Line1 = new TLine(this, &ActiveFlag, wall0CollisionGroup, wall0Pts->Pt1, wall0Pts->Pt0); - Line1->WallValue = nullptr; - Line1->place_in_grid(&AABB); - EdgeList.push_back(Line1); + // Full Tilt's Dragon's Keep and Pirate's Cove ramps omit wall0; 3DPB/Cadet always have it. + auto wall0Arr = loader::query_float_attribute_opt(groupIndex, 0, 1303); + Line1 = nullptr; + if (wall0Arr) + { + auto wall0CollisionGroup = 1 << static_cast(floor(wall0Arr[0])); + auto wall0Pts = reinterpret_cast(wall0Arr + 2); + Line1 = new TLine(this, &ActiveFlag, wall0CollisionGroup, wall0Pts->Pt1, wall0Pts->Pt0); + Line1->WallValue = nullptr; + Line1->place_in_grid(&AABB); + EdgeList.push_back(Line1); + } auto wall1Arr = loader::query_float_attribute(groupIndex, 0, 1301); Wall1CollisionGroup = 1 << static_cast(floor(wall1Arr[0])); diff --git a/SpaceCadetPinball/loader.cpp b/SpaceCadetPinball/loader.cpp index 283e983..7f8a28f 100644 --- a/SpaceCadetPinball/loader.cpp +++ b/SpaceCadetPinball/loader.cpp @@ -239,9 +239,21 @@ int16_t* loader::query_iattribute(int groupIndex, int firstValue, int* arraySize } float* loader::query_float_attribute(int groupIndex, int groupIndexOffset, int firstValue) +{ + return query_float_attribute_impl(groupIndex, groupIndexOffset, firstValue, false); +} + +float* loader::query_float_attribute_opt(int groupIndex, int groupIndexOffset, int firstValue) +{ + return query_float_attribute_impl(groupIndex, groupIndexOffset, firstValue, true); +} + +float* loader::query_float_attribute_impl(int groupIndex, int groupIndexOffset, int firstValue, bool optional) { if (groupIndex < 0) { + if (optional) + return nullptr; error(0, 22); return nullptr; } @@ -249,6 +261,8 @@ float* loader::query_float_attribute(int groupIndex, int groupIndexOffset, int f int stateId = state_id(groupIndex, groupIndexOffset); if (stateId < 0) { + if (optional) + return nullptr; error(16, 22); return nullptr; } @@ -263,6 +277,8 @@ float* loader::query_float_attribute(int groupIndex, int groupIndexOffset, int f return floatArr + 1; } + if (optional) + return nullptr; error(13, 22); return nullptr; } diff --git a/SpaceCadetPinball/loader.h b/SpaceCadetPinball/loader.h index ea4176c..55c46c2 100644 --- a/SpaceCadetPinball/loader.h +++ b/SpaceCadetPinball/loader.h @@ -102,7 +102,16 @@ public: static int query_visual(int groupIndex, int groupIndexOffset, visualStruct* visual); static char* query_name(int groupIndex); static float* query_float_attribute(int groupIndex, int groupIndexOffset, int firstValue); + + /* Returns nullptr instead of raising a load error when the attribute is absent. + Full Tilt's Dragon's Keep / Pirate's Cove omit attributes that Cadet always has. */ + static float* query_float_attribute_opt(int groupIndex, int groupIndexOffset, int firstValue); static float query_float_attribute(int groupIndex, int groupIndexOffset, int firstValue, float defVal); + +private: + static float* query_float_attribute_impl(int groupIndex, int groupIndexOffset, int firstValue, bool optional); + +public: static int16_t* query_iattribute(int groupIndex, int firstValue, int* arraySize); static float play_sound(int soundIndex, TPinballComponent *soundSource, const char* info); static DatFile* loader_table; diff --git a/tools/datdump.py b/tools/datdump.py new file mode 100644 index 0000000..e321cf8 --- /dev/null +++ b/tools/datdump.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Dump PARTOUT(4.0) pinball .DAT structure: groups, component type ids, float attribute ids.""" +import struct, sys, collections + +FIXED = {0: 2, 2: 2, 13: 0} # partman::_field_size; everything else has a uint32 length prefix + +def parse(path): + d = open(path, 'rb').read() + sig = d[0:21].split(b'\0')[0].decode() + app = d[21:71].split(b'\0')[0].decode() + desc = d[71:171].split(b'\0')[0].decode() + filesize, ngroups, sizeofbody, unknown = struct.unpack_from('= 4: + g['attrs'].append(int(struct.unpack_from(' 1: + base = 'CADET.DAT' if 'CADET.DAT' in results else list(results)[0] + bf, bsv, battr = results[base] + for name, (f, svs, attrs) in results.items(): + if name == base: + continue + print(f"--- {name} vs {base}") + print(f" component types only in {name}: {sorted(set(svs) - set(bsv))}") + print(f" component types missing from {name}: {sorted(set(bsv) - set(svs))}") + print(f" attrs only in {name}: {sorted(set(attrs) - set(battr))}") + print(f" attrs missing from {name}: {sorted(set(battr) - set(attrs))}") + bn = {g['name'] for g in bf['groups'] if g['name']} + fn = {g['name'] for g in f['groups'] if g['name']} + print(f" shared component names: {len(bn & fn)} / {len(fn)} in {name}") + print(f" sample names only in {name}: {sorted(fn - bn)[:25]}") + print()