Skip to main content

siglus_scene_vm/runtime/
gan.rs

1//! GAN animation support (ported from the original the original implementation implementation).
2
3use anyhow::{Context, Result, bail};
4use encoding_rs::SHIFT_JIS;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex, OnceLock, Weak};
8
9#[derive(Debug, Clone, Default)]
10pub struct GanPat {
11    pub pat_no: i32,
12    pub x: i32,
13    pub y: i32,
14    pub z: i32,
15    pub tr: u8,
16    pub wait: i32,
17    pub keika_time: i32,
18}
19
20#[derive(Debug, Clone, Default)]
21pub struct GanSet {
22    pub pat_list: Vec<GanPat>,
23    pub total_time: i32,
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct GanData {
28    pub g00_file_name: String,
29    pub set_list: Vec<GanSet>,
30}
31
32impl GanData {
33    pub fn load(path: &Path) -> Result<Self> {
34        let buf = crate::resource::read_file_bytes(path)
35            .with_context(|| format!("read gan: {:?}", path))?;
36        if buf.len() < 8 {
37            bail!("gan too short: {:?}", path);
38        }
39        let mut data = GanData::default();
40        data.analize(&buf)?;
41        Ok(data)
42    }
43
44    fn analize(&mut self, buf: &[u8]) -> Result<()> {
45        let mut sp = 0usize;
46        let code = read_i32(buf, &mut sp)?;
47        if code != 10000 {
48            bail!("gan bad version code: {code}");
49        }
50        let version = read_i32(buf, &mut sp)?;
51        if version != 10000 {
52            bail!("gan unsupported version: {version}");
53        }
54
55        loop {
56            let code = read_i32(buf, &mut sp)?;
57            if code == 10100 {
58                let len = read_i32(buf, &mut sp)? as usize;
59                let s = read_bytes(buf, &mut sp, len)?;
60                self.g00_file_name = decode_sjis(s);
61                continue;
62            }
63            if code == 20000 {
64                let set_cnt = read_i32(buf, &mut sp)?;
65                if set_cnt > 0 {
66                    for _ in 0..set_cnt {
67                        let mut set = GanSet::default();
68                        let mut keika_time = 0i32;
69                        analize_set(buf, &mut sp, &mut set, &mut keika_time)?;
70                        self.set_list.push(set);
71                    }
72                }
73                return Ok(());
74            }
75
76            bail!("gan unexpected code: {code}");
77        }
78    }
79}
80
81fn analize_set(buf: &[u8], sp: &mut usize, set: &mut GanSet, keika_time: &mut i32) -> Result<()> {
82    let code = read_i32(buf, sp)?;
83    if code != 30000 {
84        bail!("gan set missing PAT_COUNT: {code}");
85    }
86    let pat_cnt = read_i32(buf, sp)?;
87    for _ in 0..pat_cnt {
88        let pat = analize_pat(buf, sp, keika_time)?;
89        set.pat_list.push(pat);
90    }
91    set.total_time = *keika_time;
92    Ok(())
93}
94
95fn analize_pat(buf: &[u8], sp: &mut usize, keika_time: &mut i32) -> Result<GanPat> {
96    let mut pat = GanPat {
97        tr: 255,
98        ..Default::default()
99    };
100    loop {
101        let code = read_i32(buf, sp)?;
102        if code == 999999 {
103            break;
104        }
105        match code {
106            30100 => pat.pat_no = read_i32(buf, sp)?,
107            30101 => pat.x = read_i32(buf, sp)?,
108            30102 => pat.y = read_i32(buf, sp)?,
109            30103 => pat.wait = read_i32(buf, sp)?,
110            30104 => {
111                let v = read_i32(buf, sp)?;
112                pat.tr = v.clamp(0, 255) as u8;
113            }
114            30105 => pat.z = read_i32(buf, sp)?,
115            _ => bail!("gan unexpected pat code: {code}"),
116        }
117    }
118    *keika_time += pat.wait;
119    pat.keika_time = *keika_time;
120    Ok(pat)
121}
122
123fn decode_sjis(bytes: &[u8]) -> String {
124    let (cow, _, had_err) = SHIFT_JIS.decode(bytes);
125    if had_err {
126        String::from_utf8_lossy(bytes).into_owned()
127    } else {
128        cow.into_owned()
129    }
130}
131
132fn read_i32(buf: &[u8], sp: &mut usize) -> Result<i32> {
133    let bytes = read_bytes(buf, sp, 4)?;
134    Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
135}
136
137fn read_bytes<'a>(buf: &'a [u8], sp: &mut usize, len: usize) -> Result<&'a [u8]> {
138    if *sp + len > buf.len() {
139        bail!("gan out of range");
140    }
141    let out = &buf[*sp..*sp + len];
142    *sp += len;
143    Ok(out)
144}
145
146#[derive(Debug, Default, Clone)]
147pub struct GanState {
148    data: Option<Arc<GanData>>,
149    current_pat: Option<GanPat>,
150
151    gan_name: String,
152    now_time: i32,
153    anm_set_no: i32,
154    next_anm_set_no: i32,
155
156    anm_start: bool,
157    anm_pause: bool,
158    anm_loop_flag: bool,
159    anm_real_time_flag: bool,
160    next_anm_flag: bool,
161    next_anm_loop_flag: bool,
162    next_anm_real_time_flag: bool,
163}
164
165impl GanState {
166    pub(crate) fn read_short_work(
167        &mut self,
168        reader: &mut crate::original_save::OriginalStreamReader<'_>,
169    ) -> Result<()> {
170        self.now_time = reader.i32()?;
171        self.anm_start = reader.bool()?;
172        self.anm_pause = reader.bool()?;
173        self.anm_set_no = reader.i32()?;
174        self.anm_loop_flag = reader.bool()?;
175        self.anm_real_time_flag = reader.bool()?;
176        Ok(())
177    }
178
179    pub(crate) fn read_original_work(
180        &mut self,
181        reader: &mut crate::original_save::OriginalStreamReader<'_>,
182    ) -> Result<()> {
183        self.now_time = reader.i32()?;
184        self.anm_set_no = reader.i32()?;
185        self.next_anm_set_no = reader.i32()?;
186        self.anm_start = reader.bool()?;
187        self.anm_pause = reader.bool()?;
188        self.anm_loop_flag = reader.bool()?;
189        self.anm_real_time_flag = reader.bool()?;
190        self.next_anm_flag = reader.bool()?;
191        self.next_anm_loop_flag = reader.bool()?;
192        self.next_anm_real_time_flag = reader.bool()?;
193        Ok(())
194    }
195
196    pub fn reset(&mut self) {
197        *self = GanState::default();
198    }
199
200    pub fn current_pat(&self) -> Option<&GanPat> {
201        self.current_pat.as_ref()
202    }
203
204    pub fn is_active(&self) -> bool {
205        self.data.is_some() && self.anm_start && !self.anm_pause
206    }
207
208    pub fn load_gan(&mut self, project_dir: &Path, append_dir: &str, name: &str) -> Result<()> {
209        self.reset();
210        self.load_gan_only(project_dir, append_dir, name)
211    }
212
213    pub fn load_gan_only(
214        &mut self,
215        project_dir: &Path,
216        append_dir: &str,
217        name: &str,
218    ) -> Result<()> {
219        if name.trim().is_empty() {
220            return Ok(());
221        }
222        self.gan_name = name.to_string();
223        let path = resolve_gan_path(project_dir, append_dir, name)
224            .with_context(|| format!("resolve gan path: {name}"))?;
225        self.data = Some(load_cached_gan_data(&path)?);
226        Ok(())
227    }
228
229    pub fn start_anm(&mut self, set_no: i32, loop_flag: bool, real_time_flag: bool) {
230        self.now_time = 0;
231        self.anm_start = true;
232        self.anm_pause = false;
233        self.anm_set_no = set_no;
234        self.anm_loop_flag = loop_flag;
235        self.anm_real_time_flag = real_time_flag;
236        self.next_anm_flag = false;
237    }
238
239    pub fn next_anm(&mut self, set_no: i32, loop_flag: bool, real_time_flag: bool) {
240        if self.anm_start {
241            if self.next_anm_flag {
242                self.start_anm(self.next_anm_set_no, false, self.next_anm_real_time_flag);
243            } else {
244                self.anm_loop_flag = false;
245            }
246            self.next_anm_flag = true;
247            self.next_anm_set_no = set_no;
248            self.next_anm_loop_flag = loop_flag;
249            self.next_anm_real_time_flag = real_time_flag;
250        } else {
251            self.start_anm(set_no, loop_flag, real_time_flag);
252        }
253    }
254
255    pub fn pause_anm(&mut self) {
256        self.anm_pause = true;
257    }
258
259    pub fn resume_anm(&mut self) {
260        self.anm_pause = false;
261    }
262
263    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
264        let mut game = past_game_time.max(0);
265        let mut real = past_real_time.max(0);
266
267        let Some(data) = self.data.as_ref() else {
268            self.current_pat = None;
269            return;
270        };
271        if self.anm_set_no < 0 || self.anm_set_no as usize >= data.set_list.len() {
272            self.current_pat = None;
273            return;
274        }
275        let set = &data.set_list[self.anm_set_no as usize];
276        if set.pat_list.is_empty() {
277            self.current_pat = None;
278            return;
279        }
280        let total_time = set.total_time;
281        let first_pat = set.pat_list[0].clone();
282        let last_pat = set.pat_list[set.pat_list.len() - 1].clone();
283        if !self.anm_start || total_time <= 0 {
284            self.current_pat = Some(first_pat);
285            return;
286        }
287
288        if !self.anm_pause {
289            if self.anm_real_time_flag {
290                self.now_time += real;
291            } else {
292                self.now_time += game;
293            }
294        }
295
296        if !self.anm_loop_flag && self.now_time >= total_time {
297            if self.next_anm_flag {
298                self.start_anm(
299                    self.next_anm_set_no,
300                    self.next_anm_loop_flag,
301                    self.next_anm_real_time_flag,
302                );
303                let overshoot = self.now_time - total_time;
304                game -= overshoot;
305                real -= overshoot;
306                self.update_time(game, real);
307            } else {
308                self.now_time = total_time;
309                self.current_pat = Some(last_pat);
310            }
311            return;
312        }
313
314        if set.total_time > 0 {
315            self.now_time %= set.total_time;
316        }
317
318        for pat in &set.pat_list {
319            if pat.keika_time >= self.now_time {
320                self.current_pat = Some(pat.clone());
321                break;
322            }
323        }
324    }
325}
326
327fn load_cached_gan_data(path: &Path) -> Result<Arc<GanData>> {
328    // Original Siglus/Tona3 routes C_tnm_gan::load_gan_only() through the
329    // process-global G_gan_manager.  Objects that reference the same GAN file
330    // therefore share one parsed C_gan_data instead of re-reading and parsing
331    // the file for every child object.
332    //
333    // Keep weak entries here: active GanState instances own the strong Arc,
334    // while an unused GAN can disappear naturally. This is equivalent to the
335    // original manager's periodic organize() dropping entries whose data is no
336    // longer referenced by any C_tnm_gan.
337    static CACHE: OnceLock<Mutex<HashMap<PathBuf, Weak<GanData>>>> = OnceLock::new();
338    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
339
340    if let Some(data) = cache
341        .lock()
342        .unwrap_or_else(|poisoned| poisoned.into_inner())
343        .get(path)
344        .and_then(Weak::upgrade)
345    {
346        return Ok(data);
347    }
348
349    let data = Arc::new(GanData::load(path)?);
350    cache
351        .lock()
352        .unwrap_or_else(|poisoned| poisoned.into_inner())
353        .insert(path.to_path_buf(), Arc::downgrade(&data));
354    Ok(data)
355}
356
357fn resolve_gan_path(project_dir: &Path, append_dir: &str, name: &str) -> Result<PathBuf> {
358    let mut candidates: Vec<PathBuf> = Vec::new();
359    let mut norm = name.replace('\\', "/");
360    if !norm.ends_with(".gan") && !norm.contains('.') {
361        norm.push_str(".gan");
362    }
363
364    let path = PathBuf::from(&norm);
365    if path.is_absolute()
366        && let Some(path) = crate::resource::resolve_game_file(&path)?
367    {
368        return Ok(path);
369    }
370
371    if !append_dir.is_empty() {
372        candidates.push(project_dir.join(append_dir).join("gan").join(&norm));
373        candidates.push(project_dir.join(append_dir).join(&norm));
374    }
375    candidates.push(project_dir.join("gan").join(&norm));
376    candidates.push(project_dir.join(&norm));
377    candidates.push(project_dir.join("dat").join(&norm));
378
379    for cand in candidates {
380        if let Some(path) = crate::resource::resolve_game_file(&cand)? {
381            return Ok(path);
382        }
383    }
384
385    bail!("gan file not found: {name}")
386}