Skip to main content

box3d_rust/
name_cache.rs

1// Port of the name cache from box3d-cpp-reference/src/name_cache.h/.c.
2// Remaining load/replay helpers land with the recording slice.
3//
4// SPDX-FileCopyrightText: 2025 Erin Catto
5// SPDX-License-Identifier: MIT
6
7use crate::constants::NULL_NAME;
8use std::collections::HashMap;
9
10/// A single name entry. (b3NameEntry)
11#[derive(Debug, Clone, Default)]
12pub struct NameEntry {
13    pub hash: u32,
14    pub length: i32,
15    pub name: String,
16}
17
18/// Name cache for shape and body names. Works with recording.
19/// (b3NameCache)
20///
21/// C stores a verstable map as `void* map`; Rust uses a std HashMap keyed by
22/// name id for the same id→entry association. Observable name↔id behavior will
23/// match once the logic slice lands.
24#[derive(Debug, Clone, Default)]
25pub struct NameCache {
26    pub entries: Vec<NameEntry>,
27    /// Maps name id → entry index. Id 0 is [`NULL_NAME`] and is never inserted.
28    pub map: HashMap<u32, i32>,
29}
30
31impl NameCache {
32    /// (b3CreateNameCache)
33    pub fn new() -> NameCache {
34        NameCache {
35            entries: Vec::new(),
36            map: HashMap::new(),
37        }
38    }
39
40    /// Sentinel unused by real names. (B3_NULL_NAME)
41    pub const NULL: u32 = NULL_NAME;
42
43    /// FNV-1a + Murmur3 finalizer. Changing this breaks recordings. (b3Hash32)
44    pub fn hash32(data: &[u8]) -> u32 {
45        const FNV32_OFFSET_BASIS: u32 = 0x811c_9dc5;
46        const FNV32_PRIME: u32 = 0x0100_0193;
47
48        let mut h = FNV32_OFFSET_BASIS;
49        for &b in data {
50            h ^= b as u32;
51            h = h.wrapping_mul(FNV32_PRIME);
52        }
53
54        h ^= h >> 16;
55        h = h.wrapping_mul(0x85eb_ca6b);
56        h ^= h >> 13;
57        h = h.wrapping_mul(0xc2b2_ae35);
58        h ^= h >> 16;
59        h
60    }
61
62    /// Insert or look up a name, returning its id. Empty names yield NULL.
63    /// (b3AddName)
64    pub fn add_name(&mut self, name: &str) -> u32 {
65        if name.is_empty() {
66            return NULL_NAME;
67        }
68
69        let mut id = Self::hash32(name.as_bytes());
70        if id == 0 {
71            id = 1;
72        }
73
74        if let Some(&index) = self.map.get(&id) {
75            if self.entries[index as usize].name != name {
76                // Different name, same hash — C logs; keep returning the id.
77            }
78            return id;
79        }
80
81        let index = self.entries.len() as i32;
82        self.entries.push(NameEntry {
83            hash: id,
84            length: name.len() as i32,
85            name: name.to_string(),
86        });
87        self.map.insert(id, index);
88        id
89    }
90
91    /// (b3FindName)
92    pub fn find_name(&self, id: u32) -> Option<&str> {
93        let index = *self.map.get(&id)?;
94        Some(&self.entries[index as usize].name)
95    }
96}