Skip to main content

rustynes_core/
vs_db.rs

1//! Vs. System per-game database (v2.7.0).
2//!
3//! Closes two gaps in Vs. System support:
4//!
5//! 1. **PPU palette.** iNES-1.0 dumps carry no NES 2.0 byte-13, so the cartridge
6//!    parser defaults every Vs. cart to [`VsPpuType::Rp2C03`]
7//!    ([`crate::rustynes_mappers::parse`] §arcade detection). Many Vs. games used a
8//!    2C04-000x or RC2C03 PPU whose colour LUT differs from the 2C03's, so an
9//!    iNES-1.0 dump renders with the wrong colours. This table supplies the
10//!    correct [`VsPpuType`] keyed on the ROM SHA-256; the frontend applies it
11//!    via [`crate::Nes::set_vs_ppu_type`] (always — the DB is authoritative for
12//!    the palette).
13//!
14//! 2. **DIP-switch presets.** Vs. arcade games read an 8-bit DIP bank through
15//!    the upper bits of `$4016`/`$4017` (coinage, difficulty, lives, PPU-type,
16//!    etc.). The frontend's default DIP is `0`, which is not always the
17//!    factory-shipment setting; this table supplies each game's documented
18//!    default so the frontend can apply it when the user has not set an explicit
19//!    `[vs] dip` (precedence: explicit config dip > DB > 0).
20//!
21//! ## DIP-byte encoding
22//!
23//! The `vs_dip` byte here is in **this emulator's** encoding: DIP switch 1 =
24//! bit 0 .. DIP switch 8 = bit 7. The bus overlay maps DIP1 -> `$4016` bit 3,
25//! DIP2 -> `$4016` bit 4, and DIP3..8 -> `$4017` bits 2..7 (see
26//! `LockstepBus::vs_overlay_4016` / `vs_overlay_4017`). Each value below is
27//! MAME's documented `DSW0` factory default for the corresponding game
28//! (the bitwise-OR of the per-field `PORT_DIPNAME` defaults in MAME's
29//! `src/mame/nintendo/vsnes.cpp`), which is exactly the `vs_dip` byte the
30//! overlay consumes (switch 1 = bit 0). On the real dual-system boards there is
31//! a second DIP bank (`DSW1`, for the sub-CPU); this single-CPU model only
32//! exposes `DSW0`.
33//!
34//! ## Sources
35//!
36//! - DIP defaults: MAME `src/mame/nintendo/vsnes.cpp` `INPUT_PORTS_START`
37//!   blocks (`PORT_DIPNAME(<mask>, <default>, ...)`).
38//! - PPU types: MAME `src/mame/nintendo/vsnes.cpp` — the per-game `ROM_START`
39//!   block's `PALETTE_2C04_000x` / `PALETTE_STANDARD` macro is the authoritative
40//!   PPU assignment (each `.pal` ROM is dumped from real hardware). The fceux
41//!   `src/vsuni.cpp` "Games/PPU list. Information copied from MAME" table is a
42//!   secondary cross-check; both agree for every game below. Verified
43//!   2026-06-11 against MAME `master`. (For dual-system carts the `ppu1`
44//!   master-CPU PALETTE is used.)
45//!
46//! ## Caveats
47//!
48//! - The dual-system games (Balloon Fight / Tennis / Mahjong / Wrecking Crew)
49//!   run two CPUs/PPUs, modelled since v2.0.0 beta.5 by
50//!   [`crate::VsDualSystem`] (routed via [`crate::Emu::from_rom`], which
51//!   consults this table's `dual_system` flag). Their `DSW0` defaults are
52//!   `0x00` (Balloon Fight / Tennis / Mahjong) per MAME.
53//! - Two titles (Balloon Fight, Wrecking Crew) additionally carry a
54//!   **second, COMBINED-dump entry** with a different SHA-256: the
55//!   pre-existing 32 KiB-PRG entry (main-CPU program only -- boot cannot
56//!   complete, the sub-CPU program is absent) coexists with a new 64 KiB-PRG
57//!   entry (main half + sub half concatenated) once the sub-CPU program was
58//!   located. See `docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md`. Tennis and
59//!   Mahjong remain 32 KiB-only -- no sub-CPU dump has been located for
60//!   either.
61//!
62//! The table is `&'static`, sorted by SHA-256, and binary-searched. It is
63//! `no_std`-safe (const data only).
64
65use rustynes_mappers::VsPpuType;
66
67/// A single Vs. System per-game database entry.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub struct VsDbEntry {
70    /// The game's factory-default DIP-switch bank, in this emulator's encoding
71    /// (switch 1 = bit 0 .. switch 8 = bit 7). MAME `DSW0` default.
72    pub vs_dip: u8,
73    /// The correct Vs. System PPU type (output palette + 2C05 quirks). Supplies
74    /// the right colour LUT for iNES-1.0 dumps that default to the 2C03.
75    pub vs_ppu_type: VsPpuType,
76    /// `true` for a Vs. **`DualSystem`** cart (two CPUs + two PPUs sharing an
77    /// inter-CPU latch — Tennis / Mahjong / Wrecking Crew / Balloon Fight).
78    /// v2.0.0 beta.5: [`crate::Emu::from_rom`] routes these to the full
79    /// two-console [`crate::VsDualSystem`] wrapper. This flag is the
80    /// load-bearing detection source for the circulating iNES-1.0 dumps,
81    /// whose headers carry no NES 2.0 byte-13 Vs. hardware type (see
82    /// `docs/audit/vs-dualsystem-design-2026-06-11.md`).
83    pub dual_system: bool,
84}
85
86/// Internal key+value record. Kept private; [`lookup`] returns the value only.
87struct Record {
88    sha256: [u8; 32],
89    entry: VsDbEntry,
90}
91
92const fn entry(sha256: [u8; 32], vs_dip: u8, vs_ppu_type: VsPpuType) -> Record {
93    Record {
94        sha256,
95        entry: VsDbEntry {
96            vs_dip,
97            vs_ppu_type,
98            dual_system: false,
99        },
100    }
101}
102
103/// Like [`entry`] but flags the cart as a Vs. **`DualSystem`** title.
104const fn entry_dual(sha256: [u8; 32], vs_dip: u8, vs_ppu_type: VsPpuType) -> Record {
105    Record {
106        sha256,
107        entry: VsDbEntry {
108            vs_dip,
109            vs_ppu_type,
110            dual_system: true,
111        },
112    }
113}
114
115/// The embedded database, sorted ascending by SHA-256 (enforced by a unit test).
116///
117/// Each comment records the game and the MAME `DSW0` default / PPU source.
118static DB: &[Record] = &[
119    // Vs. Duck Hunt (hack) -- MAME duckhunt DSW0=0x28
120    // PPU RC2C03: vsnes.cpp ROM_START(duckhunt) -> PALETTE_STANDARD (rp2c0x.pal).
121    entry(
122        [
123            0x16, 0x0d, 0x43, 0xde, 0x97, 0x7f, 0xbc, 0x8e, 0x24, 0x11, 0x23, 0x54, 0xe7, 0x0e,
124            0x40, 0xcf, 0xf8, 0x43, 0x10, 0x16, 0x7b, 0x07, 0xfb, 0x0b, 0x65, 0xeb, 0xc8, 0x19,
125            0xfc, 0xf0, 0xca, 0x0c,
126        ],
127        0x28,
128        VsPpuType::Rp2C03,
129    ),
130    // Vs. Gradius (hack) -- MAME vsgradus DSW0=0x80
131    // PPU RP2C04-0001: vsnes.cpp ROM_START(vsgradus) -> PALETTE_2C04_0001.
132    entry(
133        [
134            0x29, 0x01, 0x11, 0x92, 0x9a, 0x34, 0x10, 0x5e, 0x73, 0x56, 0x2d, 0xba, 0x99, 0x69,
135            0x01, 0x1d, 0x65, 0x2a, 0xb8, 0x17, 0x67, 0xee, 0x1c, 0x41, 0x74, 0xd2, 0x7b, 0x7c,
136            0x33, 0x95, 0xaa, 0x78,
137        ],
138        0x80,
139        VsPpuType::Rp2C04_0001,
140    ),
141    // Vs. Super Mario Bros. -- MAME suprmrio DSW0=0x10
142    // PPU RP2C04-0004: vsnes.cpp ROM_START(suprmrio) -> PALETTE_2C04_0004.
143    entry(
144        [
145            0x2f, 0xaa, 0xb7, 0xa4, 0x83, 0xf9, 0xa3, 0x33, 0x06, 0x6c, 0x54, 0x27, 0xee, 0x78,
146            0xb8, 0xf0, 0x0a, 0xe3, 0x00, 0x31, 0x84, 0xbe, 0xd4, 0xad, 0x2f, 0xd0, 0xe0, 0xad,
147            0xa3, 0xb0, 0xb1, 0x9b,
148        ],
149        0x10,
150        VsPpuType::Rp2C04_0004,
151    ),
152    // Vs. Excitebike (hack) -- MAME excitebk DSW0=0x00
153    // PPU RP2C04-0003: vsnes.cpp ROM_START(excitebk/excitebko) -> PALETTE_2C04_0003.
154    // (US "palette 3" set; the Japanese excitebkj set is RP2C04-0004 -- a
155    // different ROM, not staged here.)
156    entry(
157        [
158            0x31, 0xff, 0x4e, 0x40, 0xe0, 0xac, 0x32, 0x9d, 0x20, 0x7b, 0xb6, 0xa0, 0xc3, 0xaa,
159            0x65, 0x98, 0x1e, 0xa9, 0xa3, 0x67, 0x63, 0xf6, 0x79, 0x5d, 0x14, 0xf0, 0x3f, 0x87,
160            0x4c, 0xb2, 0x35, 0xc2,
161        ],
162        0x00,
163        VsPpuType::Rp2C04_0003,
164    ),
165    // Vs. Pinball (hack) -- MAME vspinbal DSW0=0x01
166    // PPU RP2C04-0001: vsnes.cpp ROM_START(vspinbal) [US set] -> PALETTE_2C04_0001.
167    // (The Japanese vspinbalj set is RC2C03B/PALETTE_STANDARD -- not staged here.)
168    entry(
169        [
170            0x44, 0xf4, 0x34, 0x07, 0x0b, 0xf9, 0x10, 0xf4, 0xe5, 0x13, 0x5d, 0x22, 0xba, 0x65,
171            0xb8, 0xc7, 0x49, 0x2c, 0xca, 0xf3, 0x25, 0xaa, 0xc1, 0x91, 0xd0, 0xab, 0xf9, 0xac,
172            0xb3, 0xa3, 0xce, 0xc9,
173        ],
174        0x01,
175        VsPpuType::Rp2C04_0001,
176    ),
177    // Vs. Wrecking Crew (DualSystem, COMBINED 64 KiB dump: main PRG half +
178    // sub-CPU PRG half, MAME `.6d/.6c/.6b/.6a`) -- MAME wrecking DSW0=0xF8.
179    // PPU RP2C04-0002: vsnes.cpp ROM_START(wrecking) ppu1 -> PALETTE_2C04_0002.
180    // Distinct SHA-256 from the pre-existing 32 KiB-only "GVS Wrecking
181    // Crew.nes" entry below (same game, same DIP/PPU -- the dump
182    // completeness is what differs). See
183    // `docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md` for how this dump was
184    // assembled and verified.
185    entry_dual(
186        [
187            0x4c, 0xde, 0x98, 0x3b, 0x9d, 0x03, 0x40, 0x2b, 0x32, 0x4e, 0x28, 0x15, 0x18, 0x92,
188            0x68, 0x04, 0x1b, 0x31, 0x3a, 0x93, 0x77, 0xd7, 0xf0, 0x85, 0x6c, 0xf8, 0xd3, 0xa9,
189            0x79, 0x07, 0x08, 0xfe,
190        ],
191        0xF8,
192        VsPpuType::Rp2C04_0002,
193    ),
194    // Vs. Stroke & Match Golf -- MAME smgolf DSW0=0x21
195    // PPU RP2C04-0002: vsnes.cpp ROM_START(smgolf) -> PALETTE_2C04_0002.
196    entry(
197        [
198            0x50, 0x65, 0xc6, 0x9c, 0x1e, 0x8b, 0x09, 0x81, 0x8f, 0x37, 0x4d, 0xd5, 0xa3, 0xb3,
199            0x43, 0xa8, 0xde, 0x28, 0x36, 0x3a, 0xf5, 0x91, 0x60, 0x91, 0x53, 0x66, 0x33, 0x95,
200            0x52, 0x02, 0x01, 0x4c,
201        ],
202        0x21,
203        VsPpuType::Rp2C04_0002,
204    ),
205    // Vs. Tennis (DualSystem) -- MAME vstennis DSW0=0x00
206    // PPU RC2C03: vsnes.cpp ROM_START(vstennis) ppu1 -> PALETTE_STANDARD.
207    entry_dual(
208        [
209            0x52, 0x93, 0x4e, 0x98, 0x16, 0x7d, 0xf4, 0x7d, 0xe5, 0x2a, 0xbc, 0x2c, 0x1f, 0x56,
210            0x53, 0xb5, 0x32, 0x93, 0xba, 0x66, 0x7b, 0x91, 0xd2, 0xdf, 0x2d, 0x58, 0x27, 0x41,
211            0xf5, 0x0a, 0x45, 0x9c,
212        ],
213        0x00,
214        VsPpuType::Rp2C03,
215    ),
216    // Vs. Mahjong (DualSystem) -- MAME vsmahjng DSW0=0x00
217    // PPU RC2C03: vsnes.cpp ROM_START(vsmahjng) ppu1 -> PALETTE_STANDARD.
218    entry_dual(
219        [
220            0x63, 0x47, 0x05, 0x57, 0xaf, 0xb7, 0xb9, 0xd5, 0x76, 0x63, 0xcc, 0xc6, 0xe9, 0xb4,
221            0xd6, 0xcd, 0x70, 0x02, 0x6e, 0xf0, 0x1a, 0x77, 0xdb, 0xb4, 0x66, 0xab, 0xa1, 0xb1,
222            0xd3, 0xe4, 0xf5, 0x12,
223        ],
224        0x00,
225        VsPpuType::Rp2C03,
226    ),
227    // Vs. Wrecking Crew (DualSystem) -- MAME wrecking DSW0=0xF8
228    // PPU RP2C04-0002: vsnes.cpp ROM_START(wrecking) ppu1 -> PALETTE_2C04_0002.
229    entry_dual(
230        [
231            0x85, 0xd5, 0xf1, 0x74, 0xfe, 0x94, 0xcc, 0xba, 0x9d, 0x70, 0x2e, 0x01, 0xc0, 0xf7,
232            0x2d, 0xcc, 0x56, 0x9b, 0xc2, 0x44, 0x70, 0xd3, 0x4a, 0x36, 0xbb, 0xd5, 0x9a, 0xed,
233            0x9b, 0xb2, 0x9d, 0x7d,
234        ],
235        0xF8,
236        VsPpuType::Rp2C04_0002,
237    ),
238    // Vs. Balloon Fight (DualSystem, COMBINED 64 KiB dump: main PRG half +
239    // sub-CPU PRG half, MAME `.6d/.6c/.6b/.6a`) -- MAME balonfgt DSW0=0x00.
240    // PPU RP2C04-0003: vsnes.cpp ROM_START(balonfgt) ppu1 -> PALETTE_2C04_0003.
241    // Distinct SHA-256 from the pre-existing 32 KiB-only "GVS Balloon
242    // Fight.nes" entry above (same game, same DIP/PPU -- the dump
243    // completeness is what differs). See
244    // `docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md` for how this dump was
245    // assembled and verified.
246    entry_dual(
247        [
248            0x8b, 0x32, 0x80, 0xe6, 0x51, 0xf3, 0xf8, 0xd9, 0x9d, 0xe0, 0x46, 0xcd, 0xd2, 0x71,
249            0x0e, 0x2f, 0xf5, 0xf9, 0x4b, 0x5d, 0xd4, 0x22, 0x49, 0x82, 0xcc, 0xa4, 0xa9, 0xa7,
250            0x26, 0x44, 0x41, 0xcb,
251        ],
252        0x00,
253        VsPpuType::Rp2C04_0003,
254    ),
255    // Vs. The Goonies (hack) -- MAME goonies DSW0=0x80
256    // PPU RP2C04-0003: vsnes.cpp ROM_START(goonies) -> PALETTE_2C04_0003.
257    entry(
258        [
259            0xae, 0xe9, 0x8d, 0xa8, 0x5b, 0xe8, 0x10, 0x2d, 0x41, 0xbd, 0x21, 0x2a, 0xe1, 0x5d,
260            0x11, 0x40, 0x35, 0xc0, 0x8d, 0x52, 0x2b, 0xaf, 0x22, 0x2e, 0xdb, 0x12, 0x56, 0xb8,
261            0xc9, 0x3f, 0x3b, 0x7d,
262        ],
263        0x80,
264        VsPpuType::Rp2C04_0003,
265    ),
266    // Vs. T.K.O. Boxing (hack) -- MAME tkoboxng DSW0=0x00
267    // PPU RP2C04-0003: vsnes.cpp ROM_START(tkoboxng) -> PALETTE_2C04_0003.
268    entry(
269        [
270            0xb8, 0x15, 0x8a, 0x64, 0xa1, 0xc8, 0xb6, 0x7b, 0x53, 0x0a, 0x01, 0x06, 0x78, 0x77,
271            0x2c, 0x43, 0xb0, 0xae, 0xd7, 0x20, 0xbb, 0x28, 0xf2, 0x09, 0x4a, 0xce, 0xe2, 0xf5,
272            0x92, 0x78, 0x9c, 0xc9,
273        ],
274        0x00,
275        VsPpuType::Rp2C04_0003,
276    ),
277    // Vs. Castlevania -- MAME cstlevna DSW0=0x00
278    // PPU RP2C04-0002: vsnes.cpp ROM_START(cstlevna) -> PALETTE_2C04_0002.
279    entry(
280        [
281            0xca, 0xbc, 0x23, 0x0a, 0x7f, 0x8c, 0x36, 0x6e, 0xd4, 0x05, 0xfb, 0x83, 0x1e, 0x42,
282            0xc3, 0x58, 0xa1, 0xf1, 0x40, 0x8a, 0x42, 0x77, 0x17, 0x16, 0x1c, 0xd1, 0xdd, 0x3d,
283            0x86, 0x8d, 0x35, 0x1b,
284        ],
285        0x00,
286        VsPpuType::Rp2C04_0002,
287    ),
288    // Vs. Ice Climber (hack) -- MAME iceclimb DSW0=0x00
289    // PPU RP2C04-0004: vsnes.cpp ROM_START(iceclimb) -> PALETTE_2C04_0004.
290    entry(
291        [
292            0xda, 0x2d, 0x91, 0xc8, 0x47, 0xbf, 0x59, 0x56, 0xeb, 0xe2, 0x6a, 0x0d, 0x64, 0x38,
293            0x20, 0x18, 0x9a, 0x3f, 0xa5, 0xe1, 0xdd, 0x71, 0x5d, 0xd7, 0x76, 0x77, 0x0a, 0x61,
294            0x5e, 0xf2, 0x69, 0x8e,
295        ],
296        0x00,
297        VsPpuType::Rp2C04_0004,
298    ),
299    // Vs. Excitebike -- MAME excitebk DSW0=0x00
300    // PPU RP2C04-0003: vsnes.cpp ROM_START(excitebk/excitebko) -> PALETTE_2C04_0003.
301    // (Staged dump's PRG bank-0 CRC32 = 7e54df1d = MAME `excitebko`, palette 3.)
302    entry(
303        [
304            0xea, 0x27, 0x8a, 0x35, 0xa0, 0x50, 0x17, 0xa8, 0x04, 0x9f, 0x0b, 0xa9, 0x6e, 0x06,
305            0x0a, 0x26, 0xf5, 0x50, 0xed, 0x92, 0x02, 0xf8, 0xee, 0x62, 0x50, 0x2b, 0xef, 0x50,
306            0xcb, 0x04, 0x5b, 0x23,
307        ],
308        0x00,
309        VsPpuType::Rp2C04_0003,
310    ),
311    // Vs. Clu Clu Land -- MAME cluclu DSW0=0x10
312    // PPU RP2C04-0004: vsnes.cpp ROM_START(cluclu) -> PALETTE_2C04_0004.
313    entry(
314        [
315            0xfb, 0x43, 0x24, 0x81, 0x06, 0xa4, 0x20, 0x25, 0x90, 0x84, 0x0c, 0xca, 0x68, 0x89,
316            0x5a, 0xb4, 0xb2, 0xe9, 0x4c, 0x49, 0xf8, 0x2a, 0xa1, 0x5c, 0x7c, 0x23, 0x26, 0x99,
317            0xed, 0x7a, 0xb9, 0x0a,
318        ],
319        0x10,
320        VsPpuType::Rp2C04_0004,
321    ),
322    // Vs. Balloon Fight (DualSystem) -- MAME balonfgt DSW0=0x00
323    // PPU RP2C04-0003: vsnes.cpp ROM_START(balonfgt) ppu1 -> PALETTE_2C04_0003.
324    // (Not in the fceux vsuni.cpp PPU list -- fceux skips the DualSystem carts;
325    // MAME `balonfgt` is the authoritative source.)
326    entry_dual(
327        [
328            0xfd, 0xa8, 0x4d, 0x8d, 0xcd, 0xe6, 0x90, 0xb1, 0x5a, 0xcf, 0x8f, 0x11, 0xb2, 0x7d,
329            0x61, 0x3d, 0x57, 0x1a, 0x65, 0xb2, 0xb3, 0x47, 0x19, 0xc6, 0xe0, 0x3e, 0x7f, 0x00,
330            0xe3, 0xb7, 0x09, 0x6b,
331        ],
332        0x00,
333        VsPpuType::Rp2C04_0003,
334    ),
335];
336
337/// Look up a Vs. System per-game database entry by ROM SHA-256.
338///
339/// Returns `Some(entry)` when the hash is in the embedded table, `None`
340/// otherwise. Binary search over the sorted table; `no_std`-safe.
341#[must_use]
342pub fn lookup(sha256: &[u8; 32]) -> Option<VsDbEntry> {
343    DB.binary_search_by(|rec| rec.sha256.cmp(sha256))
344        .ok()
345        .map(|i| DB[i].entry)
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn db_is_sorted_by_sha256() {
354        for w in DB.windows(2) {
355            assert!(
356                w[0].sha256 < w[1].sha256,
357                "vs_db must be sorted ascending by SHA-256 (binary search invariant)"
358            );
359        }
360    }
361
362    #[test]
363    fn lookup_hit_returns_entry() {
364        // Vs. Castlevania -- the first byte 0xca entry.
365        let sha = [
366            0xca, 0xbc, 0x23, 0x0a, 0x7f, 0x8c, 0x36, 0x6e, 0xd4, 0x05, 0xfb, 0x83, 0x1e, 0x42,
367            0xc3, 0x58, 0xa1, 0xf1, 0x40, 0x8a, 0x42, 0x77, 0x17, 0x16, 0x1c, 0xd1, 0xdd, 0x3d,
368            0x86, 0x8d, 0x35, 0x1b,
369        ];
370        let e = lookup(&sha).expect("castlevania present");
371        assert_eq!(e.vs_dip, 0x00);
372        assert_eq!(e.vs_ppu_type, VsPpuType::Rp2C04_0002);
373    }
374
375    #[test]
376    fn lookup_miss_returns_none() {
377        assert_eq!(lookup(&[0u8; 32]), None);
378        assert_eq!(lookup(&[0xffu8; 32]), None);
379    }
380
381    #[test]
382    fn exactly_six_dualsystem_rows_across_the_four_carts_are_flagged() {
383        // Tennis / Mahjong / Wrecking Crew / Balloon Fight are the four
384        // DualSystem *games*; every other entry (single-system) is not
385        // flagged. The flag lets the frontend warn instead of
386        // black-screening on a two-CPU cart -- and lets `Emu::from_rom`
387        // route to the two-console wrapper.
388        //
389        // The row COUNT is 6, not 4: Tennis and Mahjong have only the
390        // original 32 KiB-PRG (main-CPU-only) dump staged (no sub-CPU
391        // program has been located for either), while Balloon Fight and
392        // Wrecking Crew each additionally have a 64 KiB-PRG COMBINED dump
393        // (main half + sub half) once the sub-CPU program was located --
394        // see the "Caveats" doc section above and
395        // `docs/audit/vs-dualsystem-combined-dumps-2026-07-02.md`. The 32 KiB-only
396        // entries for those two titles are kept: loading that specific
397        // (incomplete) dump still correctly flags `dual_system` (and thus
398        // still routes to the wrapper), it just can't complete the boot
399        // handshake -- which is expected/harmless, not a bug.
400        let dual = DB.iter().filter(|r| r.entry.dual_system).count();
401        assert_eq!(
402            dual, 6,
403            "expected exactly 6 DualSystem rows (4 games, 2 of which have both an incomplete and a complete dump entry)"
404        );
405        // And the flag survives a lookup round-trip for every flagged record.
406        for rec in DB.iter().filter(|r| r.entry.dual_system) {
407            assert!(lookup(&rec.sha256).is_some_and(|e| e.dual_system));
408        }
409    }
410
411    #[test]
412    fn every_entry_is_findable() {
413        for rec in DB {
414            let e = lookup(&rec.sha256).expect("entry findable");
415            assert_eq!(e, rec.entry);
416        }
417    }
418}