chip8_db/
lib.rs

1//! Easily access the data from the [CHIP-8 Database][] from your own [CHIP-8] implementation
2//! written in Rust.
3//!
4//! Use this data to automatically apply the needed quirks for a specific ROM, or give the user more
5//! information about individual games. You can show the name of the game in the titlebar, or
6//! display a description of the ROM underneath the main window.
7//!
8//! You can also provide descriptions of the various CHIP-8 platforms out there, or describe the
9//! effects of enabling various quirks.
10//!
11//! ## Usage
12//!
13//! ```rust
14//! # use chip8_db::Database;
15//! #
16//! # let rom = [0u8; 4096];
17//! #
18//! // The CHIP-8 Database is included in this library, no need to open or download files
19//! let db = Database::new();
20//!
21//! // Get metadata from a rom directly
22//! let metadata = db.get_metadata(&rom);
23//!
24//! // Get metadata from a hash string
25//! let metadata = db.get_metadata_from_hash("0df2789f661358d8f7370e6cf93490c5bcd44b01");
26//! let program = metadata.program.unwrap();
27//!
28//! println!("Title: {} ({})", program.title, metadata.hash);
29//!
30//! // Most fields are optional in the base schema
31//! if let Some(description) = program.description {
32//!     println!("Description: {description}");
33//! }
34//! ```
35//!
36//! ## Features
37//!
38//! While the ROM database is always enabled, there is additional data from `platforms.json` and
39//! `quirks.json` that you can choose to include with the `extra-data` feature.
40//!
41//! ```toml
42//! chip_8_database_rs = { version = "2.0.0", features = ["extra-data"] }
43//! ```
44//!
45//! [CHIP-8]: https://chip-8.github.io/links/
46//! [CHIP-8 Database]: https://github.com/chip-8/chip-8-database
47
48pub mod color;
49pub mod font;
50pub mod input;
51pub mod origin;
52pub mod platform;
53pub mod program;
54pub mod quirk;
55pub mod rom;
56pub mod rotation;
57
58use program::Program;
59use rom::Rom;
60use sha1::{Digest, Sha1};
61use std::collections::HashMap;
62
63#[cfg(feature = "extra-data")]
64use platform::PlatformDetails;
65
66#[cfg(feature = "extra-data")]
67use quirk::QuirkDetails;
68
69/// Database contains the full contents of the CHIP-8 database, minus any disabled features.
70#[derive(Clone, Debug, Default)]
71pub struct Database {
72    /// A list of all known programs written for a CHIP-8 platform.
73    pub programs: Vec<Program>,
74
75    /// A map of all known ROM hashes, used to index into [programs].
76    pub hashes: HashMap<String, usize>,
77
78    /// A list of all known CHIP-8 variants.
79    #[cfg(feature = "extra-data")]
80    pub platforms: Vec<PlatformDetails>,
81
82    /// A list of common quirks in CHIP-8 implementations.
83    #[cfg(feature = "extra-data")]
84    pub quirks: Vec<QuirkDetails>,
85}
86
87impl Database {
88    /// Create a new instance of the DB. Does not touch the filesystem or network.
89    pub fn new() -> Self {
90        // Updating note: Panics if the `.json` files in `../chip-8-database/database/` are not in the
91        // expected schema. Update the tests with the new schema and try again.
92        let programs = {
93            let json = include_str!("../chip-8-database/database/programs.json");
94
95            serde_json::from_str(json)
96                .expect("programs.json is hardcoded and should never be in an invalid state")
97        };
98
99        let hashes = {
100            let json = include_str!("../chip-8-database/database/sha1-hashes.json");
101
102            serde_json::from_str(json)
103                .expect("sha1-hashes.json is hardcoded and should never be in an invalid state")
104        };
105
106        #[cfg(feature = "extra-data")]
107        let platforms = {
108            let json = include_str!("../chip-8-database/database/platforms.json");
109
110            serde_json::from_str(json)
111                .expect("platforms.json is hardcoded and should never be in an invalid state")
112        };
113
114        #[cfg(feature = "extra-data")]
115        let quirks = {
116            let json = include_str!("../chip-8-database/database/quirks.json");
117
118            serde_json::from_str(json)
119                .expect("quirks.json is hardcoded and should never be in an invalid state")
120        };
121
122        Database {
123            programs,
124            hashes,
125
126            #[cfg(feature = "extra-data")]
127            platforms,
128
129            #[cfg(feature = "extra-data")]
130            quirks,
131        }
132    }
133
134    /// Lookup the metadata for a specific ROM file by hashing it.
135    pub fn get_metadata(&self, rom: &[u8]) -> Metadata {
136        let mut hasher = Sha1::new();
137        hasher.update(rom);
138        let hash = hasher.finalize();
139        let mut buf = [0u8; 40];
140        let hash = base16ct::lower::encode_str(&hash, &mut buf).unwrap();
141
142        self.get_metadata_from_hash(hash)
143    }
144
145    /// Lookup the metadata for a specific hash string.
146    pub fn get_metadata_from_hash(&self, hash: &str) -> Metadata {
147        let hash = hash.to_owned();
148        let program = self.hashes.get(&hash).map(|i| self.programs[*i].clone());
149        let rom = program
150            .as_ref()
151            .and_then(|prog| prog.roms.get(&hash).cloned());
152
153        Metadata { hash, program, rom }
154    }
155}
156
157/// Metadata results from a ROM lookup
158#[derive(Clone, Debug, Default)]
159pub struct Metadata {
160    /// During ROM lookup, this will be populated with the hash used.
161    pub hash: String,
162
163    /// The program matching the listed hash.
164    pub program: Option<Program>,
165
166    /// Any ROM-specific metadata, otherwise defaulting to the values in program.
167    pub rom: Option<Rom>,
168}
169
170#[cfg(test)]
171mod test {
172    use super::*;
173
174    mod program {
175        use super::*;
176
177        use crate::{
178            font::FontStyle,
179            input::{Keymap, TouchInputMode},
180            origin::OriginType,
181            platform::Platform,
182            quirk::Quirk,
183            rotation::ScreenRotation,
184        };
185        use std::io::Result;
186
187        #[test]
188        fn deserialize_full() -> Result<()> {
189            let input = r##"{
190                "title": "Test Program",
191                "origin": {
192                    "type": "manual",
193                    "reference": "What's this supposed to be?"
194                },
195                "description": "A description of the program",
196                "release": "2023-06-24",
197                "copyright": "Probably copyrighted or something",
198                "license": "MIT",
199                "authors": ["Someone"],
200                "images": ["https://example.com/chip8/test-program.png"],
201                "urls": ["https://example.com/chip8/test-program.html"],
202                "roms": {
203                    "0123456789abcdef0123456789abcdef01234567": {
204                        "file": "test-program.ch8",
205                        "embeddedTitle": "Test Program Embedded",
206                        "description": "The test program to test all programs",
207                        "release": "2023-06-24",
208                        "platforms": ["originalChip8"],
209                        "quirkyPlatforms": {
210                            "originalChip8": {
211                                "shift": true,
212                                "memoryIncrementByX": false,
213                                "memoryLeaveIUnchanged": true,
214                                "wrap": false,
215                                "jump": true,
216                                "vblank": false,
217                                "logic": true
218                            }
219                        },
220                        "authors": ["Someone Else"],
221                        "images": ["https://example.com/chip8/test-program-detail.png"],
222                        "urls": ["https://example.com/chip8/test-program.ch8"],
223                        "tickrate": 10,
224                        "startAddress": 512,
225                        "screenRotation": 0,
226                        "keys": {
227                            "up": 0,
228                            "down": 1,
229                            "left": 2,
230                            "right": 3,
231                            "a": 4,
232                            "b": 5,
233                            "player2Up": 16,
234                            "player2Down": 17,
235                            "player2Left": 18,
236                            "player2Right": 19,
237                            "player2A": 20,
238                            "player2B": 21
239                        },
240                        "touchInputMode": "none",
241                        "fontStyle": "vip",
242                        "colors": {
243                            "pixels": ["#000000", "#ff0000", "#00ff00", "#0000ff"],
244                            "buzzer": "#cccccc",
245                            "silence": "#555555"
246                        }
247                    }
248                }
249            }"##;
250
251            let program: Program = serde_json::from_str(input)?;
252
253            assert_eq!(program.title, "Test Program");
254            assert_eq!(
255                &OriginType::Manual,
256                program
257                    .origin
258                    .as_ref()
259                    .unwrap()
260                    .origin_type
261                    .as_ref()
262                    .unwrap()
263            );
264            assert_eq!(
265                "What's this supposed to be?",
266                program.origin.unwrap().reference.unwrap()
267            );
268            assert_eq!(
269                "A description of the program",
270                &program.description.unwrap()
271            );
272            assert_eq!("2023-06-24", &program.release.unwrap());
273            assert_eq!(
274                "Probably copyrighted or something",
275                &program.copyright.unwrap()
276            );
277            assert_eq!("MIT", &program.license.unwrap());
278            assert_eq!(vec!["Someone".to_owned()], program.authors.unwrap());
279            assert_eq!(
280                vec!["https://example.com/chip8/test-program.png"],
281                program.images.unwrap()
282            );
283            assert_eq!(
284                vec!["https://example.com/chip8/test-program.html"],
285                program.urls.unwrap()
286            );
287
288            let rom = program.roms["0123456789abcdef0123456789abcdef01234567"].clone();
289
290            assert_eq!("test-program.ch8", &rom.file_name.unwrap());
291            assert_eq!("Test Program Embedded", &rom.embedded_title.unwrap());
292            assert_eq!(
293                "The test program to test all programs",
294                rom.description.unwrap()
295            );
296            assert_eq!("2023-06-24", rom.release.unwrap());
297            assert_eq!(vec![Platform::OriginalChip8], rom.platforms);
298
299            let quirks = rom.quirky_platforms.unwrap()[&Platform::OriginalChip8].clone();
300
301            assert!(quirks[&Quirk::Shift]);
302            assert!(!quirks[&Quirk::MemoryIncrementByX]);
303            assert!(quirks[&Quirk::MemoryLeaveIUnchanged]);
304            assert!(!quirks[&Quirk::Wrap]);
305            assert!(quirks[&Quirk::Jump]);
306            assert!(!quirks[&Quirk::VBlank]);
307            assert!(quirks[&Quirk::Logic]);
308
309            assert_eq!(vec!["Someone Else"], rom.authors.unwrap());
310            assert_eq!(
311                vec!["https://example.com/chip8/test-program-detail.png"],
312                rom.images.unwrap()
313            );
314            assert_eq!(
315                vec!["https://example.com/chip8/test-program.ch8"],
316                rom.urls.unwrap()
317            );
318            assert_eq!(10, rom.tickrate.unwrap());
319            assert_eq!(0x200, rom.start_address.unwrap());
320            assert_eq!(ScreenRotation::Landscape, rom.screen_rotation.unwrap());
321
322            let keys = rom.keys.unwrap();
323
324            assert_eq!(0x00, keys[&Keymap::P1Up]);
325            assert_eq!(0x01, keys[&Keymap::P1Down]);
326            assert_eq!(0x02, keys[&Keymap::P1Left]);
327            assert_eq!(0x03, keys[&Keymap::P1Right]);
328            assert_eq!(0x04, keys[&Keymap::P1A]);
329            assert_eq!(0x05, keys[&Keymap::P1B]);
330
331            assert_eq!(0x10, keys[&Keymap::P2Up]);
332            assert_eq!(0x11, keys[&Keymap::P2Down]);
333            assert_eq!(0x12, keys[&Keymap::P2Left]);
334            assert_eq!(0x13, keys[&Keymap::P2Right]);
335            assert_eq!(0x14, keys[&Keymap::P2A]);
336            assert_eq!(0x15, keys[&Keymap::P2B]);
337
338            assert_eq!(TouchInputMode::None, rom.touch_input_mode.unwrap());
339            assert_eq!(FontStyle::VIP, rom.font_style.unwrap());
340
341            let colors = rom.colors.unwrap();
342
343            assert_eq!(
344                vec!["#000000", "#ff0000", "#00ff00", "#0000ff"],
345                colors.pixels.unwrap()
346            );
347            assert_eq!("#cccccc", colors.buzzer.unwrap());
348            assert_eq!("#555555", colors.silence.unwrap());
349
350            Ok(())
351        }
352
353        #[test]
354        fn deserialize_minimal() -> Result<()> {
355            let input = r##"{
356                "title": "Minimal",
357                "roms": {
358                    "0123456789abcdef0123456789abcdef01234567": {
359                        "platforms": ["originalChip8"]
360                    }
361                }
362            }"##;
363
364            let program: Program = serde_json::from_str(input)?;
365
366            assert_eq!("Minimal", program.title);
367
368            let rom = program.roms["0123456789abcdef0123456789abcdef01234567"].clone();
369
370            assert_eq!(vec![Platform::OriginalChip8], rom.platforms);
371
372            Ok(())
373        }
374    }
375
376    #[cfg(feature = "extra-data")]
377    mod platform {
378        use crate::{platform::Platform, quirk::Quirk};
379
380        use super::*;
381
382        use std::io::Result;
383
384        #[test]
385        fn deserialize_minimal() -> Result<()> {
386            let input = r##"{
387                "id": "originalChip8",
388                "name": "Minimal Platform Example",
389                "displayResolutions": ["64x32"],
390                "defaultTickrate": 15,
391                "quirks": {
392                    "shift": false,
393                    "memoryIncrementByX": false,
394                    "memoryLeaveIUnchanged": false,
395                    "wrap": false,
396                    "jump": false,
397                    "vblank": true,
398                    "logic": true
399                }
400            }"##;
401
402            let platform: PlatformDetails = serde_json::from_str(input)?;
403
404            assert_eq!(Platform::OriginalChip8, platform.id);
405            assert_eq!("Minimal Platform Example", platform.name);
406            assert_eq!(vec!["64x32"], platform.display_resolutions);
407            assert_eq!(15, platform.default_tickrate);
408
409            assert!(!platform.quirks[&Quirk::Shift]);
410            assert!(!platform.quirks[&Quirk::MemoryIncrementByX]);
411            assert!(!platform.quirks[&Quirk::MemoryLeaveIUnchanged]);
412            assert!(!platform.quirks[&Quirk::Wrap]);
413            assert!(!platform.quirks[&Quirk::Jump]);
414            assert!(platform.quirks[&Quirk::VBlank]);
415            assert!(platform.quirks[&Quirk::Logic]);
416
417            Ok(())
418        }
419
420        #[test]
421        fn deserialize_full() -> Result<()> {
422            let input = r##"{
423                "id": "hybridVIP",
424                "name": "Platform Example",
425                "description": "A description goes here",
426                "release": "1999-12-31",
427                "authors": ["Who Knows?"],
428                "urls": ["https://example.com"],
429                "copyright": "Probably copyrighted or something",
430                "license": "GPL",
431                "displayResolutions": ["128x64"],
432                "defaultTickrate": 999,
433                "quirks": {
434                    "shift": true,
435                    "memoryIncrementByX": false,
436                    "memoryLeaveIUnchanged": true,
437                    "wrap": false,
438                    "jump": true,
439                    "vblank": false,
440                    "logic": true
441                }
442            }"##;
443
444            let platform: PlatformDetails = serde_json::from_str(input)?;
445
446            assert_eq!(Platform::HybridVIP, platform.id);
447            assert_eq!("Platform Example", platform.name);
448
449            assert_eq!("A description goes here", &platform.description.unwrap());
450            assert_eq!("1999-12-31", &platform.release.unwrap());
451            assert_eq!(vec!["Who Knows?".to_owned()], platform.authors.unwrap());
452
453            assert_eq!(
454                vec!["https://example.com".to_owned()],
455                platform.urls.unwrap()
456            );
457
458            assert_eq!(
459                "Probably copyrighted or something",
460                &platform.copyright.unwrap()
461            );
462
463            assert_eq!("GPL", &platform.license.unwrap());
464            assert_eq!(vec!["128x64"], platform.display_resolutions);
465            assert_eq!(999, platform.default_tickrate);
466
467            assert!(platform.quirks[&Quirk::Shift]);
468            assert!(!platform.quirks[&Quirk::MemoryIncrementByX]);
469            assert!(platform.quirks[&Quirk::MemoryLeaveIUnchanged]);
470            assert!(!platform.quirks[&Quirk::Wrap]);
471            assert!(platform.quirks[&Quirk::Jump]);
472            assert!(!platform.quirks[&Quirk::VBlank]);
473            assert!(platform.quirks[&Quirk::Logic]);
474
475            Ok(())
476        }
477    }
478
479    #[cfg(feature = "extra-data")]
480    mod quirks {
481        use crate::quirk::Quirk;
482
483        use super::*;
484
485        use std::io::Result;
486
487        #[test]
488        fn deserialize_minimal() -> Result<()> {
489            let input = r##"{
490                "id": "shift",
491                "name": "Minimal Quirk Example",
492                "default": false,
493                "ifTrue": "Do some thing",
494                "ifFalse": "Do some other thing"
495            }"##;
496
497            let quirk: QuirkDetails = serde_json::from_str(input)?;
498
499            assert_eq!(Quirk::Shift, quirk.id);
500            assert_eq!("Minimal Quirk Example", quirk.name);
501
502            assert!(!quirk.default);
503
504            assert_eq!("Do some thing", quirk.if_true);
505            assert_eq!("Do some other thing", quirk.if_false);
506
507            Ok(())
508        }
509
510        #[test]
511        fn deserialize_full() -> Result<()> {
512            let input = r##"{
513                "id": "jump",
514                "name": "Quirk Example",
515                "description": "An example of a quirk",
516                "default": true,
517                "ifTrue": "Do some more things",
518                "ifFalse": "Do some more other things"
519            }"##;
520
521            let quirk: QuirkDetails = serde_json::from_str(input)?;
522
523            assert_eq!(Quirk::Jump, quirk.id);
524            assert_eq!("Quirk Example", quirk.name);
525            assert_eq!("An example of a quirk", quirk.description.unwrap());
526
527            assert!(quirk.default);
528
529            assert_eq!("Do some more things", quirk.if_true);
530            assert_eq!("Do some more other things", quirk.if_false);
531
532            Ok(())
533        }
534    }
535}