neser 0.1.1

NESER - NES Emulator in Rust - is a NES emulator written in Rust. It aims to be a high-quality, hardware-accurate emulator that is also easy to use and extend. It supports a wide range of NES games and features, including various mappers, audio processing, and input handling. NESER is designed to be modular and extensible, allowing developers to easily add new features or support for additional hardware. It can be run using one of two frontends: a native desktop application using SDL2, or a web application using WebAssembly. The desktop application provides a high-performance, feature-rich experience with support for various input devices and display options, while the web application allows users to play NES games directly in their browsers without needing to install any software in a BYOR manner (Bring Your Own Roms).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
"""SQLite-backed ROM database for scraped NES Cart Database entries."""

from __future__ import annotations

import sqlite3
from typing import Dict, Optional
from enum import Enum
from enum import IntEnum

class HardwareType(IntEnum):
    """ ROM database hardware types (merged console type + region) """
    NES_NTSC = 0
    NES_PAL = 1
    FAMICOM = 2
    VS_SYSTEM = 3
    DENDY = 4
    PLAYCHOICE_10 = 5
    NES_MULTI_REGION = 6
    VT01_MONOCHROME = 7
    VT01_STN = 8
    VT02 = 9
    VT03 = 10
    VT09 = 11
    VT32 = 12
    VT369 = 13
    UMC_UM6578 = 14
    FAMICOM_NETWORK_SYSTEM = 15


# Mapping from iNES 2.0 extended console type to HardwareType
_EXTENDED_CONSOLE_MAP = {
    3: HardwareType.VT01_MONOCHROME,
    4: HardwareType.VT01_STN,
    5: HardwareType.VT02,
    6: HardwareType.VT03,
    7: HardwareType.VT09,
    8: HardwareType.VT32,
    9: HardwareType.VT369,
    10: HardwareType.UMC_UM6578,
    11: HardwareType.FAMICOM_NETWORK_SYSTEM,
}

# Mapping from iNES 2.0 region to HardwareType (for console type 0 = NES/Famicom)
# iNES 2.0 spec: 0=NTSC, 1=PAL, 2=Multi-region, 3=Dendy
_REGION_TEXT_MAP = {
    "ntsc": 0,
    "pal": 1,
    "universal": 2,
    "multi": 2,
    "dendy": 3,
}


def hardware_from_console_type_and_region(
    console_type_str: Optional[str], region_str: Optional[str],
    country: Optional[str] = None,
) -> Optional[int]:
    """Compute a HardwareType integer from iNES 2.0 console type and region strings.

    For console type 0 (NES/Famicom), the region normally selects between
    NES_NTSC, NES_PAL, NES_MULTI_REGION, and DENDY. If ``country`` contains
    "japan" (case-insensitive), NES_NTSC is upgraded to FAMICOM, but other
    region-derived types (including NES_PAL and NES_MULTI_REGION) are left
    unchanged.

    Returns None when the inputs cannot be mapped.
    """
    if console_type_str is None and region_str is None:
        return None

    try:
        ct = int(console_type_str) if console_type_str is not None else 0
    except (ValueError, TypeError):
        return None

    if ct == 1:
        return HardwareType.VS_SYSTEM.value
    if ct == 2:
        return HardwareType.PLAYCHOICE_10.value
    if ct >= 3:
        hw = _EXTENDED_CONSOLE_MAP.get(ct)
        return hw.value if hw is not None else None

    # ct == 0: NES/Famicom — determine from region
    if region_str is None:
        is_japan = country and "japan" in country.lower()
        return HardwareType.FAMICOM.value if is_japan else HardwareType.NES_NTSC.value
    try:
        cr = int(region_str)
    except (ValueError, TypeError):
        cr = _REGION_TEXT_MAP.get(str(region_str).strip().lower())
        if cr is None:
            return None

    # iNES 2.0 spec: region 2=Multi-region, 3=Dendy
    region_map = {
        0: HardwareType.NES_NTSC,
        1: HardwareType.NES_PAL,
        2: HardwareType.NES_MULTI_REGION,
        3: HardwareType.DENDY,
    }
    hw = region_map.get(cr)
    if hw is None:
        return None
    # NES_NTSC is upgraded to FAMICOM for Japan releases; NES_MULTI_REGION stays as-is
    is_japan = country and "japan" in country.lower()
    if is_japan and hw == HardwareType.NES_NTSC:
        return HardwareType.FAMICOM.value
    return hw.value

class NametableLayout(IntEnum):
    """ iNES 2.0 Hardwired nametable layouts """
    HORIZONTAL = 0
    VERTICAL = 1

class Battery(IntEnum):
    """ iNES 2.0 Battery presence """
    NO = 0
    YES = 1

class ControllerType(IntEnum):
    """ iNES 2.0 Controller types """
    UNSPECIFIED = 0x00
    STANDARD_CONTROLLERS = 0x01
    NES_FOUR_SCORE = 0x02
    FAMICOM_FOUR_PLAYERS_SIMPLE = 0x03
    VS_SYSTEM_4016 = 0x04
    VS_SYSTEM_4017 = 0x05
    RESERVED = 0x06
    VS_ZAPPER = 0x07
    ZAPPER_4017 = 0x08
    TWO_ZAPPERS = 0x09
    BANDAI_HYPER_SHOT = 0x0A
    POWER_PAD_SIDE_A = 0x0B
    POWER_PAD_SIDE_B = 0x0C
    FAMILY_TRAINER_SIDE_A = 0x0D
    FAMILY_TRAINER_SIDE_B = 0x0E
    ARKANOID_VAUS_NES = 0x0F
    ARKANOID_VAUS_FAMICOM = 0x10
    TWO_VAUS_DATA_RECORDER = 0x11
    KONAMI_HYPER_SHOT = 0x12
    COCONUTS_PACHINKO = 0x13
    EXCITING_BOXING_BAG = 0x14
    JISSEN_MAHJONG = 0x15
    YONEZAWA_PARTY_TAP = 0x16
    OEKA_KIDS_TABLET = 0x17
    SUNSOFT_BARCODE_BATTLER = 0x18
    MIRACLE_PIANO = 0x19
    POKKUN_MOGURAA_TAP_MAT = 0x1A
    TOP_RIDER = 0x1B
    DOUBLE_FISTED = 0x1C
    FAMICOM_3D_SYSTEM = 0x1D
    DOREMIKKO_KEYBOARD = 0x1E
    ROB_GYROMITE = 0x1F
    FAMICOM_DATA_RECORDER = 0x20
    ASCII_TURBO_FILE = 0x21
    IGS_STORAGE_BATTLE_BOX = 0x22
    FAMILY_BASIC_KEYBOARD_RECORDER = 0x23
    DONGDA_PEC_KEYBOARD = 0x24
    PUZE_BIT79_KEYBOARD = 0x25
    SUBOR_KEYBOARD = 0x26
    SUBOR_KEYBOARD_MACRO_MOUSE = 0x27
    SUBOR_KEYBOARD_SUBOR_MOUSE_4016 = 0x28
    SNES_MOUSE_4016 = 0x29
    MULTICART = 0x2A
    TWO_SNES_CONTROLLERS = 0x2B
    RACERMATE_BICYCLE = 0x2C
    U_FORCE = 0x2D
    ROB_STACK_UP = 0x2E
    CITY_PATROLMAN_LIGHTGUN = 0x2F
    SHARP_C1_CASSETTE = 0x30
    STANDARD_SWAP_DPAD_BA = 0x31
    EXCALIBUR_SUDOKU_PAD = 0x32
    ABL_PINBALL = 0x33
    GOLDEN_NUGGET_BUTTONS = 0x34
    KEDA_KEYBOARD = 0x35
    SUBOR_KEYBOARD_SUBOR_MOUSE_4017 = 0x36
    PORT_TEST_CONTROLLER = 0x37
    BANDAI_MULTI_GAME_PLAYER = 0x38
    VENOM_TV_DANCE_MAT = 0x39
    LG_TV_REMOTE = 0x3A
    FAMICOM_NETWORK_CONTROLLER = 0x3B
    KING_FISHING_CONTROLLER = 0x3C
    CROAKY_KARAOKE = 0x3D
    KEWANG_KINGWON_KEYBOARD = 0x3E
    ZECHENG_KEYBOARD = 0x3F
    SUBOR_KEYBOARD_PS2_MOUSE_4017_L90 = 0x40
    UM6578_PS2_KEYBOARD_MOUSE_4017 = 0x41
    UM6578_PS2_MOUSE = 0x42
    YUXING_MOUSE_4016 = 0x43
    SUBOR_KEYBOARD_YUXING_MOUSE_4016 = 0x44
    GIGGGLE_TV_PUMP = 0x45
    BBK_KEYBOARD_PS2_MOUSE_4017_R90 = 0x46
    MAGICAL_COOKING = 0x47
    SNES_MOUSE_4017 = 0x48
    ZAPPER_4016 = 0x49
    ARKANOID_VAUS_PROTO = 0x4A
    TV_MAHJONG_GAME = 0x4B
    MAHJONG_GEKITOU_DENSETU = 0x4C
    SUBOR_KEYBOARD_PS2_MOUSE_4017_XINV = 0x4D
    IBM_PC_XT_KEYBOARD = 0x4E
    SUBOR_KEYBOARD_MEGA_BOOK_MOUSE = 0x4F
    # Values not in iNES 2.0 spec:
    POWER_GLOVE = 0x51
    ALADDIN_DECK_ENHANCER = 0x52

class VsHardwareType(IntEnum):
    """ iNES 2.0 Vs Hardware types """
    VS_UNISYSTEM = 0x00
    VS_UNISYSTEM_RBI_BASEBALL = 0x01
    VS_UNISYSTEM_TKO_BOXING = 0x02
    VS_UNISYSTEM_SUPER_XEVIOUS = 0x03
    VS_UNISYSTEM_ICE_CLIMBER_JP = 0x04
    VS_DUALSYSTEM = 0x05
    VS_DUALSYSTEM_BUNGELING_BAY = 0x06

class VsPpuType(IntEnum):
    """ iNES 2.0 Vs PPU types """
    RP2C03_ANY = 0x00
    RP2C04_0001 = 0x02
    RP2C04_0002 = 0x03
    RP2C04_0003 = 0x04
    RP2C04_0004 = 0x05
    RC2C05_01 = 0x08
    RC2C05_02 = 0x09
    RC2C05_03 = 0x0A
    RC2C05_04 = 0x0B

class RomDbKey(str, Enum):
    """ROM database field keys."""
    ROM_ID = "rom_id"
    CRC = "crc"
    NAME = "name"
    COUNTRY = "country"
    HARDWARE = "hardware"
    CONSOLE_CLASS = "rom_class"
    MAPPER = "mapper"
    SUBMAPPER = "submapper"
    NAMETABLE_LAYOUT = "nametable_layout"
    PRG_ROM_SIZE = "prg_rom_size"
    CHR_ROM_SIZE = "chr_rom_size"
    PRG_NVRAM_SIZE = "prg_nvram_size"
    PRG_RAM_SIZE = "prg_ram_size"
    CHR_NVRAM_SIZE = "chr_nvram_size"
    CHR_RAM_SIZE = "chr_ram_size"
    PRG_ROM_CRC = "prg_rom_crc"
    CHR_ROM_CRC = "chr_rom_crc"
    BATTERY = "battery"
    VS_HARDWARE_TYPE = "vs_hardware_type"
    VS_PPU_TYPE = "vs_ppu_type"
    EXPANSION_TYPE = "expansion_type"

class RomDatabase:
    """Store and update ROM metadata in a SQLite database."""

    @staticmethod
    def _expected_columns() -> Dict[str, str]:
        return {
            RomDbKey.NAME.value: "TEXT",
            RomDbKey.COUNTRY.value: "TEXT",
            RomDbKey.CRC.value: "TEXT",
            RomDbKey.HARDWARE.value: "INTEGER",
            RomDbKey.CONSOLE_CLASS.value: "TEXT",
            RomDbKey.MAPPER.value: "INTEGER",
            RomDbKey.SUBMAPPER.value: "INTEGER",
            RomDbKey.NAMETABLE_LAYOUT.value: "TEXT",
            RomDbKey.PRG_ROM_SIZE.value: "INTEGER",
            RomDbKey.PRG_ROM_CRC.value: "TEXT",
            RomDbKey.PRG_NVRAM_SIZE.value: "INTEGER",
            RomDbKey.PRG_RAM_SIZE.value: "INTEGER",
            RomDbKey.CHR_ROM_SIZE.value: "INTEGER",
            RomDbKey.CHR_ROM_CRC.value: "TEXT",
            RomDbKey.CHR_NVRAM_SIZE.value: "INTEGER",
            RomDbKey.CHR_RAM_SIZE.value: "INTEGER",
            RomDbKey.BATTERY.value: "INTEGER",
            RomDbKey.VS_HARDWARE_TYPE.value: "INTEGER",
            RomDbKey.VS_PPU_TYPE.value: "INTEGER",
            RomDbKey.EXPANSION_TYPE.value: "INTEGER",
        }

    def __init__(self, db_path: str) -> None:
        """Open or create the SQLite database at ``db_path`` and ensure the
        primary `roms` table and expected columns exist.

        The connection is stored on ``self._conn`` for use by other methods.
        """
        self._db_path = db_path
        self._conn = sqlite3.connect(db_path)
        expected = self._expected_columns()
        schema_cols = ["rom_id INTEGER PRIMARY KEY"]
        schema_cols.extend([f"{name} {col_type}" for name, col_type in expected.items()])
        self._conn.execute(
            f"""
            CREATE TABLE IF NOT EXISTS roms (
                {', '.join(schema_cols)}
            ) STRICT
            """
        )
        self._ensure_columns(expected)
        self._conn.commit()

    def _ensure_columns(self, columns: Dict[str, str]) -> None:
        """Ensure the named columns exist on the `roms` table.

        `columns` is a mapping of column name to SQLite column type
        (e.g. ``{"foo": "TEXT"}``). Missing columns will be added.
        """
        cursor = self._conn.execute("PRAGMA table_info(roms)")
        existing = {row[1] for row in cursor.fetchall()}
        for name, col_type in columns.items():
            if name not in existing:
                self._conn.execute(f"ALTER TABLE roms ADD COLUMN {name} {col_type}")

    def close(self) -> None:
        """Commit any pending changes and close the DB connection."""
        self._conn.commit()
        self._conn.close()

    def list_columns_with_types(self) -> Dict[str, str]:
        """Return current column names mapped to their declared types."""
        cursor = self._conn.execute("PRAGMA table_info(roms)")
        return {row[1]: row[2].upper() for row in cursor.fetchall()}

    def list_columns(self) -> list[str]:
        """Return current column names for the ``roms`` table."""
        return list(self.list_columns_with_types().keys())

    @staticmethod
    def _coerce_value(value: Optional[str], col_type: str) -> Optional[object]:
        if value is None or value == "":
            return None
        if col_type == "INTEGER":
            if isinstance(value, bool):
                return int(value)
            if isinstance(value, int):
                return value
            try:
                return int(str(value), 10)
            except (TypeError, ValueError):
                return None
        return value

    def _coerce_params(self, params: Dict[str, Optional[str]], columns: list[str]) -> Dict[str, Optional[object]]:
        types = self.list_columns_with_types()
        return {col: self._coerce_value(params.get(col), types.get(col, "")) for col in columns}

    def get_rom(self, rom_id: int) -> Optional[Dict[str, Optional[str]]]:
        """Return a single ROM record by numeric ``rom_id``.

        The returned mapping uses the names defined in ``RomDbKey`` as keys.
        Returns ``None`` if no matching row is found.
        """
        cursor = self._conn.execute("PRAGMA table_info(roms)")
        cols = [row[1] for row in cursor.fetchall()]
        if not cols:
            return None
        cursor = self._conn.execute(
            f"SELECT {', '.join(cols)} FROM roms WHERE rom_id = ?",
            (rom_id,),
        )
        row = cursor.fetchone()
        if not row:
            return None
        return {col: row[idx] for idx, col in enumerate(cols)}

    def get_rom_by_crc(self, crc: str) -> Optional[Dict[str, Optional[str]]]:
        """Return a single ROM record matching the given ``crc`` string.

        The returned mapping uses the names defined in ``RomDbKey`` as keys.
        Returns ``None`` if no matching row is found.
        """
        cols = self.list_columns()
        if not cols:
            return None
        cursor = self._conn.execute(
            f"SELECT {', '.join(cols)} FROM roms WHERE crc = ?",
            (crc,),
        )
        row = cursor.fetchone()
        if not row:
            return None
        return {col: row[idx] for idx, col in enumerate(cols)}

    def upsert_rom(self, rom_id: int, data: Dict[str, Optional[str]]) -> None:
        """Insert or update a ROM identified by ``rom_id``.

        ``data`` should be a mapping keyed by the string values in
        ``RomDbKey``. Existing rows are updated on of ``rom_id``.
        """
        cursor = self._conn.execute("PRAGMA table_info(roms)")
        cols = [row[1] for row in cursor.fetchall()]
        cols = [col for col in cols if col != RomDbKey.ROM_ID.value]
        unknown_keys = sorted(set(data.keys()) - set(cols))
        if unknown_keys:
            print(
                f"upsert_rom: ignoring unknown fields {unknown_keys}",
                file=__import__("sys").stderr,
            )
        col_list = ", ".join([RomDbKey.ROM_ID.value] + cols)
        placeholder_list = ", ".join([f":{RomDbKey.ROM_ID.value}"] + [f":{c}" for c in cols])
        update_list = ", \n                ".join([f"{c}=excluded.{c}" for c in cols])
        sql = f"""
            INSERT INTO roms (
                {col_list}
            ) VALUES (
                {placeholder_list}
            )
            ON CONFLICT(rom_id) DO UPDATE SET
                {update_list}
            """
        params = {RomDbKey.ROM_ID.value: rom_id}
        params.update({c: data.get(c) for c in cols})
        coerced = self._coerce_params(params, [RomDbKey.ROM_ID.value] + cols)
        self._conn.execute(sql, coerced)
        self._conn.commit()

    def insert_rom_by_crc(self, data: Dict[str, Optional[str]]) -> None:
        """Insert a minimal row using the CRC and any minimal columns provided.

        This creates a row with the supplied columns; other columns remain
        NULL until updated.
        """
        cursor = self._conn.execute("PRAGMA table_info(roms)")
        existing = {row[1] for row in cursor.fetchall()}
        columns = [k for k in data.keys() if k in existing]
        if RomDbKey.CRC.value not in columns:
            columns.insert(0, RomDbKey.CRC.value)
        if not columns:
            return
        placeholders = ", ".join([":" + c for c in columns])
        params = {c: data.get(c) for c in columns}
        coerced = self._coerce_params(params, columns)
        self._conn.execute(
            f"""
            INSERT INTO roms ({", ".join(columns)})
            VALUES ({placeholders})
            """,
            coerced,
        )
        self._conn.commit()

    def update_rom_by_crc(self, crc: str, updates: Dict[str, Optional[str]]) -> None:
        """Apply in-place `updates` (column -> value) to the row matching ``crc``.

        No-op if ``updates`` is empty.
        """
        if not updates:
            return
        set_clause = ", ".join([f"{k} = :{k}" for k in updates.keys()])
        params = {**updates, "crc": crc}
        coerced = self._coerce_params(params, list(updates.keys()))
        coerced["crc"] = crc
        self._conn.execute(
            f"UPDATE roms SET {set_clause} WHERE crc = :crc",
            coerced,
        )
        self._conn.commit()

    def process_record_by_crc(self, data: Dict[str, str]) -> tuple[int, int, int, int]:
        """Process a single parsed record (identified by CRC) and update DB.

        Returns a tuple (added_count, updated_count, skipped_count, conflict_count)
        where exactly one element is 1 and the others are 0 describing the
        outcome for this single record.
        """
        crc = data.get(RomDbKey.CRC.value)
        if not crc:
            # No CRC => nothing done
            return (0, 0, 0, 1)

        existing = self.get_rom_by_crc(crc)
        if existing is None:
            # Insert minimal columns present in data
            self.insert_rom_by_crc(data)
            return (1, 0, 0, 0)

        updates: Dict[str, str] = {}
        has_conflict = False
        for key, value in data.items():
            if key == RomDbKey.CRC.value or value is None or value == "":
                continue
            old_value = existing.get(key)
            if old_value is None or (old_value == "" and value != ""):
                updates[key] = value
            elif str(old_value) != str(value):
                # Extra merge of controller types
                if key == RomDbKey.HARDWARE.value:
                    # NES_NTSC (0) is the least-specific fallback produced by the
                    # nescartdb.com scraper — treat it as a no-op so it never
                    # overwrites a more-specific value set by an XML import.
                    # NES_MULTI_REGION is authoritative from XML — any incoming
                    # value is silently ignored.
                    # FAMICOM (2) is a Japan-specific NES_NTSC — allow it to
                    # upgrade an existing generic NES_NTSC entry.
                    if str(value) == str(HardwareType.NES_NTSC.value):
                        pass  # keep existing value, no conflict
                    elif str(old_value) == str(HardwareType.NES_MULTI_REGION.value):
                        pass  # NES_MULTI_REGION is authoritative, keep it
                    elif (str(old_value) == str(HardwareType.NES_NTSC.value)
                            and str(value) == str(HardwareType.FAMICOM.value)):
                        updates[key] = value  # FAMICOM upgrades generic NES_NTSC
                    else:
                        print(f"\nConflict on CRC {crc}: column '{key}' has existing value '{old_value}', new value '{value}'")
                        has_conflict = True
                # Extra merge of controller types
                elif key == RomDbKey.EXPANSION_TYPE.value:
                    # if new value is multicart, update to that
                    if value == ControllerType.MULTICART.value:
                        updates[key] = value
                    # If old is multicaart, do nothing
                    elif old_value == ControllerType.MULTICART.value:
                        pass
                    # If old value says standard controller, select the other one
                    elif old_value == ControllerType.STANDARD_CONTROLLERS.value:
                        updates[key] = value
                    # If new value says standard controller, do nothing
                    elif value == ControllerType.STANDARD_CONTROLLERS.value:
                        pass
                    else:
                        print(f"\nConflict on CRC {crc}: column '{key}' has existing value '{old_value}', new value '{value}'")
                        has_conflict = True
                elif key == RomDbKey.PRG_RAM_SIZE.value or key == RomDbKey.CHR_RAM_SIZE.value or key == RomDbKey.PRG_NVRAM_SIZE.value or key == RomDbKey.CHR_NVRAM_SIZE.value:
                    # RAM checks are handled below in one go
                    pass
                else:
                    print(f"\nConflict on CRC {crc}: column '{key}' has existing value '{old_value}', new value '{value}'")
                    # print(f"Old={existing}, New={data}")
                    has_conflict = True

        # Special handling for RAM sizes
        def to_int(value: Optional[object]) -> Optional[int]:
            if value is None or value == "":
                return None
            if isinstance(value, int):
                return value
            try:
                return int(str(value), 10)
            except (TypeError, ValueError):
                return None

        def ram_sum(source: Dict[str, Optional[object]], ram_key: RomDbKey, nvram_key: RomDbKey) -> Optional[int]:
            ram = to_int(source.get(ram_key.value))
            nvram = to_int(source.get(nvram_key.value))
            if ram is None and nvram is None:
                return None
            return (ram or 0) + (nvram or 0)

        existing_prg_sum = ram_sum(existing, RomDbKey.PRG_RAM_SIZE, RomDbKey.PRG_NVRAM_SIZE)
        update_prg_sum = ram_sum(data, RomDbKey.PRG_RAM_SIZE, RomDbKey.PRG_NVRAM_SIZE)
        if existing_prg_sum is not None and update_prg_sum is not None:
            if existing_prg_sum != update_prg_sum:
                print(f"\nConflict on CRC {crc}: PRG RAM+NVRAM size sum mismatch existing={existing_prg_sum}, new={update_prg_sum}")
                has_conflict = True

        existing_chr_sum = ram_sum(existing, RomDbKey.CHR_RAM_SIZE, RomDbKey.CHR_NVRAM_SIZE)
        update_chr_sum = ram_sum(data, RomDbKey.CHR_RAM_SIZE, RomDbKey.CHR_NVRAM_SIZE)
        if existing_chr_sum is not None and update_chr_sum is not None:
            if existing_chr_sum != update_chr_sum:
                print(f"\nConflict on CRC {crc}: CHR RAM+NVRAM size sum mismatch existing={existing_chr_sum}, new={update_chr_sum}")
                has_conflict = True

        if len(updates) > 0 and not has_conflict:
            self.update_rom_by_crc(crc, updates)
            return (0, 1, 0, 0)
        if not has_conflict:
            return (0, 0, 1, 0)
        return (0, 0, 0, 1)

    def list_roms(self) -> list[Dict[str, Optional[str]]]:
        """Return all ROM rows ordered by ``rom_id`` ascending.

        Each row is returned as a dict keyed by the string values from
        ``RomDbKey``.
        """
        cols = self.list_columns()
        if not cols:
            return []
        cursor = self._conn.execute(
            f"SELECT {', '.join(cols)} FROM roms ORDER BY rom_id ASC"
        )
        rows = []
        for row in cursor.fetchall():
            rows.append({col: row[idx] for idx, col in enumerate(cols)})
        return rows

    def reset_schema(self) -> None:
        """Drop existing tables and recreate the schema for a fresh database.

        This removes the `roms` table if present and recreates it with the
        same columns used in `__init__`.
        """
        self._conn.execute("DROP TABLE IF EXISTS roms")
        expected = self._expected_columns()
        schema_cols = ["rom_id INTEGER PRIMARY KEY"]
        schema_cols.extend([f"{name} {col_type}" for name, col_type in expected.items()])
        self._conn.execute(
            f"""
            CREATE TABLE IF NOT EXISTS roms (
                {', '.join(schema_cols)}
            ) STRICT
            """,
        )
        self._ensure_columns(expected)
        self._conn.commit()