Skip to main content

siglus_assets/
dbs.rs

1//! DBS (Siglus database) loader.
2//!
3//! This module implements the `.dbs` expansion and table access logic used by the engine runtime.
4//!
5//! File layout (on disk):
6//! - `i32 type` (little-endian)
7//! - `payload[...]` : obfuscated LZSS stream
8//!
9//! Expansion algorithm (`tnm_database_expand`):
10//! 1) XOR payload as u32 stream with `XORCODE[2]`.
11//! 2) LZSS-unpack payload into `unpack_data`.
12//! 3) Apply a tiled binary mask split into A/B using `tile_copy` semantics.
13//! 4) XOR A with `XORCODE[0]`, XOR B with `XORCODE[1]`.
14//! 5) Re-composite A/B back into the final expanded buffer using the same mask.
15//!
16//! Expanded buffer layout (in memory):
17//! - `S_tnm_database_header` at offset 0
18//! - row headers table (array of `S_tnm_database_row_header`)
19//! - column headers table (array of `S_tnm_database_column_header`)
20//! - data table (row_cnt * column_cnt DWORDs)
21//! - string table
22//!
23//! String encoding:
24//! - If `type == 0`: strings are multibyte (typically Shift-JIS) and NUL-terminated.
25//! - Else: strings are UTF-16LE (`TCHAR`) and NUL-terminated.
26
27use crate::lzss;
28use crate::util::read_i32_le;
29use anyhow::{anyhow, bail, Result};
30use encoding_rs::SHIFT_JIS;
31use std::fs;
32use std::path::Path;
33
34const MAP_WIDTH: usize = 16;
35const TILE_WIDTH: usize = 5;
36const TILE_HEIGHT: usize = 5;
37
38// Tile mask pattern used by the DBS expander.
39const TILE: [u8; TILE_WIDTH * TILE_HEIGHT] = [
40    255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 255, 0, 255, 0, 0, 255, 0, 0, 0, 0, 0, 255,
41    255,
42];
43
44const XORCODE: [u32; 3] = [0x753A4098, 0x4A673CCC, 0xFE6215AF];
45
46#[derive(Debug, Clone, Copy)]
47pub struct DbsRowHeader {
48    pub call_no: i32,
49}
50
51#[derive(Debug, Clone, Copy)]
52pub struct DbsColumnHeader {
53    pub call_no: i32,
54    pub data_type: i32,
55}
56
57#[derive(Debug, Clone, Copy)]
58struct DbsHeader {
59    data_size: i32,
60    row_cnt: i32,
61    column_cnt: i32,
62    row_header_offset: i32,
63    column_header_offset: i32,
64    data_offset: i32,
65    str_offset: i32,
66}
67
68#[derive(Debug, Clone)]
69pub struct DbsDatabase {
70    db_type: i32,
71    expanded: Vec<u8>,
72    header: DbsHeader,
73    rows: Vec<DbsRowHeader>,
74    cols: Vec<DbsColumnHeader>,
75    data: Vec<u32>,
76    str_base: usize,
77}
78
79impl DbsDatabase {
80    /// Load and decode a `.dbs` file from disk.
81    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
82        let bytes = fs::read(path)?;
83        Self::from_bytes(&bytes)
84    }
85
86    /// Load and decode a `.dbs` file from an in-memory buffer.
87    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
88        if bytes.len() < 4 {
89            bail!("DBS: file too short");
90        }
91        let db_type = i32::from_le_bytes(bytes[0..4].try_into().unwrap());
92        let payload = &bytes[4..];
93        let expanded = tnm_database_expand(payload)?;
94        Self::parse_expanded(db_type, expanded)
95    }
96
97    pub fn db_type(&self) -> i32 {
98        self.db_type
99    }
100
101    pub fn row_count(&self) -> usize {
102        self.rows.len()
103    }
104
105    pub fn column_count(&self) -> usize {
106        self.cols.len()
107    }
108
109    pub fn rows(&self) -> &[DbsRowHeader] {
110        &self.rows
111    }
112
113    pub fn columns(&self) -> &[DbsColumnHeader] {
114        &self.cols
115    }
116
117    /// Mimics `C_elm_database::get_data(int,int,int*)`.
118    pub fn get_data_int(&self, item_call_no: i32, column_call_no: i32) -> Result<Option<i32>> {
119        let item_no = self.get_item_no(item_call_no);
120        let col_no = self.get_column_no(column_call_no);
121        if item_no < 0 || col_no < 0 {
122            return Ok(None);
123        }
124        let col_no = col_no as usize;
125        if self.cols[col_no].data_type as u8 != b'V' {
126            bail!("DBS: column_call_no={column_call_no} is not numeric");
127        }
128        let idx = (item_no as usize)
129            .checked_mul(self.cols.len())
130            .and_then(|v| v.checked_add(col_no))
131            .ok_or_else(|| anyhow!("DBS: data index overflow"))?;
132        let v = self
133            .data
134            .get(idx)
135            .ok_or_else(|| anyhow!("DBS: data index out of range"))?;
136        Ok(Some(*v as i32))
137    }
138
139    /// Mimics `C_elm_database::get_data(int,int,TSTR&)`.
140    pub fn get_data_str(&self, item_call_no: i32, column_call_no: i32) -> Result<Option<String>> {
141        let item_no = self.get_item_no(item_call_no);
142        let col_no = self.get_column_no(column_call_no);
143        if item_no < 0 || col_no < 0 {
144            return Ok(None);
145        }
146        let col_no = col_no as usize;
147        if self.cols[col_no].data_type as u8 != b'S' {
148            bail!("DBS: column_call_no={column_call_no} is not string");
149        }
150        let idx = (item_no as usize)
151            .checked_mul(self.cols.len())
152            .and_then(|v| v.checked_add(col_no))
153            .ok_or_else(|| anyhow!("DBS: data index overflow"))?;
154        let off = self
155            .data
156            .get(idx)
157            .ok_or_else(|| anyhow!("DBS: data index out of range"))?;
158        Ok(Some(self.get_str(*off as usize)?))
159    }
160
161    /// Return the column type: 0 = missing, 1 = numeric ('V'), 2 = string ('S').
162    pub fn check_column_no(&self, column_call_no: i32) -> i32 {
163        let col_no = self.get_column_no(column_call_no);
164        if col_no < 0 {
165            return 0;
166        }
167        match self.cols[col_no as usize].data_type as u8 {
168            b'V' => 1,
169            b'S' => 2,
170            _ => 0,
171        }
172    }
173
174    /// Return 1 if the item call number exists, otherwise 0.
175    pub fn check_item_no(&self, item_call_no: i32) -> i32 {
176        if self.get_item_no(item_call_no) >= 0 {
177            1
178        } else {
179            0
180        }
181    }
182
183    /// Mimics `C_elm_database::find_num`.
184    pub fn find_num(&self, column_call_no: i32, num: i32) -> Result<i32> {
185        let col_no = self.get_column_no(column_call_no);
186        if col_no < 0 {
187            return Ok(-1);
188        }
189        let col_no = col_no as usize;
190        if self.cols[col_no].data_type as u8 != b'V' {
191            bail!("DBS: column_call_no={column_call_no} is not numeric");
192        }
193        for row in 0..self.rows.len() {
194            let idx = row * self.cols.len() + col_no;
195            if (self.data[idx] as i32) == num {
196                return Ok(self.rows[row].call_no);
197            }
198        }
199        Ok(-1)
200    }
201
202    /// Mimics `C_elm_database::find_str` (case-insensitive ASCII).
203    pub fn find_str(&self, column_call_no: i32, s: &str) -> Result<i32> {
204        let col_no = self.get_column_no(column_call_no);
205        if col_no < 0 {
206            return Ok(-1);
207        }
208        let col_no = col_no as usize;
209        if self.cols[col_no].data_type as u8 != b'S' {
210            bail!("DBS: column_call_no={column_call_no} is not string");
211        }
212        let needle = s.to_ascii_lowercase();
213        for row in 0..self.rows.len() {
214            let idx = row * self.cols.len() + col_no;
215            let off = self.data[idx] as usize;
216            let got = self.get_str(off)?;
217            if got.to_ascii_lowercase() == needle {
218                return Ok(self.rows[row].call_no);
219            }
220        }
221        Ok(-1)
222    }
223
224    /// Mimics `C_elm_database::find_str_real` (case-sensitive).
225    pub fn find_str_real(&self, column_call_no: i32, s: &str) -> Result<i32> {
226        let col_no = self.get_column_no(column_call_no);
227        if col_no < 0 {
228            return Ok(-1);
229        }
230        let col_no = col_no as usize;
231        if self.cols[col_no].data_type as u8 != b'S' {
232            bail!("DBS: column_call_no={column_call_no} is not string");
233        }
234        for row in 0..self.rows.len() {
235            let idx = row * self.cols.len() + col_no;
236            let off = self.data[idx] as usize;
237            let got = self.get_str(off)?;
238            if got == s {
239                return Ok(self.rows[row].call_no);
240            }
241        }
242        Ok(-1)
243    }
244
245    fn parse_expanded(db_type: i32, expanded: Vec<u8>) -> Result<Self> {
246        let mut off = 0usize;
247        let data_size = read_i32_le(&expanded, &mut off)?;
248        let row_cnt = read_i32_le(&expanded, &mut off)?;
249        let column_cnt = read_i32_le(&expanded, &mut off)?;
250        let row_header_offset = read_i32_le(&expanded, &mut off)?;
251        let column_header_offset = read_i32_le(&expanded, &mut off)?;
252        let data_offset = read_i32_le(&expanded, &mut off)?;
253        let str_offset = read_i32_le(&expanded, &mut off)?;
254
255        if data_size <= 0 {
256            bail!("DBS: invalid data_size={data_size}");
257        }
258        if data_size as usize > expanded.len() {
259            bail!(
260                "DBS: header data_size out of range (data_size={}, buf_len={})",
261                data_size,
262                expanded.len()
263            );
264        }
265        if row_cnt < 0 || column_cnt < 0 {
266            bail!("DBS: negative counts (row_cnt={row_cnt}, column_cnt={column_cnt})");
267        }
268
269        let row_cnt_u = row_cnt as usize;
270        let col_cnt_u = column_cnt as usize;
271
272        let row_header_off = row_header_offset as usize;
273        let col_header_off = column_header_offset as usize;
274        let data_off = data_offset as usize;
275        let str_off = str_offset as usize;
276
277        if row_header_off > expanded.len()
278            || col_header_off > expanded.len()
279            || data_off > expanded.len()
280            || str_off > expanded.len()
281        {
282            bail!("DBS: one or more offsets out of range");
283        }
284
285        let row_headers_bytes = row_cnt_u
286            .checked_mul(4)
287            .ok_or_else(|| anyhow!("DBS: row headers size overflow"))?;
288        let col_headers_bytes = col_cnt_u
289            .checked_mul(8)
290            .ok_or_else(|| anyhow!("DBS: col headers size overflow"))?;
291        let data_bytes = row_cnt_u
292            .checked_mul(col_cnt_u)
293            .and_then(|v| v.checked_mul(4))
294            .ok_or_else(|| anyhow!("DBS: data table size overflow"))?;
295
296        if row_header_off + row_headers_bytes > expanded.len() {
297            bail!("DBS: row header table truncated");
298        }
299        if col_header_off + col_headers_bytes > expanded.len() {
300            bail!("DBS: column header table truncated");
301        }
302        if data_off + data_bytes > expanded.len() {
303            bail!("DBS: data table truncated");
304        }
305        if str_off > expanded.len() {
306            bail!("DBS: string table offset out of range");
307        }
308
309        let mut rows = Vec::with_capacity(row_cnt_u);
310        let mut roff = row_header_off;
311        for _ in 0..row_cnt_u {
312            let call_no = i32::from_le_bytes(expanded[roff..roff + 4].try_into().unwrap());
313            rows.push(DbsRowHeader { call_no });
314            roff += 4;
315        }
316
317        let mut cols = Vec::with_capacity(col_cnt_u);
318        let mut coff = col_header_off;
319        for _ in 0..col_cnt_u {
320            let call_no = i32::from_le_bytes(expanded[coff..coff + 4].try_into().unwrap());
321            let data_type = i32::from_le_bytes(expanded[coff + 4..coff + 8].try_into().unwrap());
322            cols.push(DbsColumnHeader { call_no, data_type });
323            coff += 8;
324        }
325
326        let mut data = Vec::with_capacity(row_cnt_u * col_cnt_u);
327        let mut doff = data_off;
328        for _ in 0..(row_cnt_u * col_cnt_u) {
329            let v = u32::from_le_bytes(expanded[doff..doff + 4].try_into().unwrap());
330            data.push(v);
331            doff += 4;
332        }
333
334        Ok(Self {
335            db_type,
336            expanded,
337            header: DbsHeader {
338                data_size,
339                row_cnt,
340                column_cnt,
341                row_header_offset,
342                column_header_offset,
343                data_offset,
344                str_offset,
345            },
346            rows,
347            cols,
348            data,
349            str_base: str_off,
350        })
351    }
352
353    fn get_item_no(&self, item_call_no: i32) -> i32 {
354        for (i, r) in self.rows.iter().enumerate() {
355            if r.call_no == item_call_no {
356                return i as i32;
357            }
358        }
359        -1
360    }
361
362    fn get_column_no(&self, column_call_no: i32) -> i32 {
363        for (i, c) in self.cols.iter().enumerate() {
364            if c.call_no == column_call_no {
365                return i as i32;
366            }
367        }
368        -1
369    }
370
371    fn get_str(&self, str_offset: usize) -> Result<String> {
372        let base = self
373            .str_base
374            .checked_add(str_offset)
375            .ok_or_else(|| anyhow!("DBS: string offset overflow"))?;
376        if base >= self.expanded.len() {
377            bail!("DBS: string offset out of range");
378        }
379
380        if self.db_type == 0 {
381            // Multibyte NUL-terminated.
382            let end = self.expanded[base..]
383                .iter()
384                .position(|&b| b == 0)
385                .map(|p| base + p)
386                .unwrap_or(self.expanded.len());
387            let bytes = &self.expanded[base..end];
388            let (cow, _, _) = SHIFT_JIS.decode(bytes);
389            Ok(cow.into_owned())
390        } else {
391            // UTF-16LE (TCHAR) NUL-terminated.
392            let mut cur = base;
393            let mut u16s: Vec<u16> = Vec::new();
394            loop {
395                if cur + 2 > self.expanded.len() {
396                    break;
397                }
398                let w = u16::from_le_bytes([self.expanded[cur], self.expanded[cur + 1]]);
399                cur += 2;
400                if w == 0 {
401                    break;
402                }
403                u16s.push(w);
404            }
405            Ok(String::from_utf16_lossy(&u16s))
406        }
407    }
408}
409
410fn tnm_database_expand(src_payload: &[u8]) -> Result<Vec<u8>> {
411    if src_payload.is_empty() {
412        bail!("DBS: empty payload");
413    }
414
415    // Step 1: XORCODE[2] over DWORD stream.
416    let mut payload = src_payload.to_vec();
417    xor_u32_in_place(&mut payload, XORCODE[2]);
418
419    // Step 2: LZSS unpack.
420    let unpack_data = lzss::lzss_unpack(&payload)?;
421    let unpack_size = unpack_data.len();
422    if unpack_size == 0 {
423        bail!("DBS: unpack_size=0");
424    }
425    if unpack_size % (MAP_WIDTH * 4) != 0 {
426        bail!(
427            "DBS: unpack_size not aligned to map width (unpack_size={}, map_stride={})",
428            unpack_size,
429            MAP_WIDTH * 4
430        );
431    }
432
433    let yl = unpack_size / (MAP_WIDTH * 4);
434
435    // Step 3: split by mask.
436    let mut temp_a = vec![0u8; unpack_size];
437    let mut temp_b = vec![0u8; unpack_size];
438    mask_copy_u32(
439        &mut temp_a,
440        &unpack_data,
441        MAP_WIDTH,
442        yl,
443        &TILE,
444        TILE_WIDTH,
445        TILE_HEIGHT,
446        0,
447        128,
448    )?;
449    mask_copy_u32(
450        &mut temp_b,
451        &unpack_data,
452        MAP_WIDTH,
453        yl,
454        &TILE,
455        TILE_WIDTH,
456        TILE_HEIGHT,
457        1,
458        128,
459    )?;
460
461    // Step 4: XOR A/B with XORCODE[0]/[1].
462    xor_u32_in_place(&mut temp_a, XORCODE[0]);
463    xor_u32_in_place(&mut temp_b, XORCODE[1]);
464
465    // Step 5: composite back.
466    let mut dst = vec![0u8; unpack_size];
467    mask_copy_u32(
468        &mut dst,
469        &temp_a,
470        MAP_WIDTH,
471        yl,
472        &TILE,
473        TILE_WIDTH,
474        TILE_HEIGHT,
475        0,
476        128,
477    )?;
478    mask_copy_u32(
479        &mut dst,
480        &temp_b,
481        MAP_WIDTH,
482        yl,
483        &TILE,
484        TILE_WIDTH,
485        TILE_HEIGHT,
486        1,
487        128,
488    )?;
489
490    Ok(dst)
491}
492
493#[inline]
494fn xor_u32_in_place(buf: &mut [u8], xor_code: u32) {
495    let n = buf.len() / 4;
496    for i in 0..n {
497        let off = i * 4;
498        let v = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap()) ^ xor_code;
499        buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
500    }
501}
502
503/// Copy 4-byte units from `src` to `dst` using a repeated mask tile.
504///
505/// This implements the mask-tiled copy step used by the DBS expander.
506fn mask_copy_u32(
507    dst: &mut [u8],
508    src: &[u8],
509    xl: usize,
510    yl: usize,
511    mask: &[u8],
512    m_xl: usize,
513    m_yl: usize,
514    reverse: i32,
515    limit: u8,
516) -> Result<()> {
517    if dst.len() != src.len() {
518        bail!("DBS: mask_copy size mismatch");
519    }
520    let expect_len = xl
521        .checked_mul(yl)
522        .and_then(|v| v.checked_mul(4))
523        .ok_or_else(|| anyhow!("DBS: mask_copy size overflow"))?;
524    if expect_len != dst.len() {
525        bail!(
526            "DBS: mask_copy unexpected buffer length (expect={}, got={})",
527            expect_len,
528            dst.len()
529        );
530    }
531    if mask.len() != m_xl * m_yl {
532        bail!("DBS: mask tile size mismatch");
533    }
534
535    for y in 0..yl {
536        for x in 0..xl {
537            let mx = x % m_xl;
538            let my = y % m_yl;
539            let mv = mask[my * m_xl + mx];
540            // Exact `tile_copy` semantics for this expander:
541            // - reverse==0 : copy where tile value >= limit
542            // - reverse!=0 : copy where tile value < limit
543            let cond = if reverse == 0 {
544                mv >= limit
545            } else {
546                mv < limit
547            };
548            if !cond {
549                continue;
550            }
551            let p = (y * xl + x) * 4;
552            dst[p..p + 4].copy_from_slice(&src[p..p + 4]);
553        }
554    }
555
556    Ok(())
557}