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::{bail, Context, Result};
4use encoding_rs::SHIFT_JIS;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8#[derive(Debug, Clone, Default)]
9pub struct GanPat {
10    pub pat_no: i32,
11    pub x: i32,
12    pub y: i32,
13    pub z: i32,
14    pub tr: u8,
15    pub wait: i32,
16    pub keika_time: i32,
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct GanSet {
21    pub pat_list: Vec<GanPat>,
22    pub total_time: i32,
23}
24
25#[derive(Debug, Clone, Default)]
26pub struct GanData {
27    pub g00_file_name: String,
28    pub set_list: Vec<GanSet>,
29}
30
31impl GanData {
32    pub fn load(path: &Path) -> Result<Self> {
33        let buf = std::fs::read(path).with_context(|| format!("read gan: {:?}", path))?;
34        if buf.len() < 8 {
35            bail!("gan too short: {:?}", path);
36        }
37        let mut data = GanData::default();
38        data.analize(&buf)?;
39        Ok(data)
40    }
41
42    fn analize(&mut self, buf: &[u8]) -> Result<()> {
43        let mut sp = 0usize;
44        let code = read_i32(buf, &mut sp)?;
45        if code != 10000 {
46            bail!("gan bad version code: {code}");
47        }
48        let version = read_i32(buf, &mut sp)?;
49        if version != 10000 {
50            bail!("gan unsupported version: {version}");
51        }
52
53        loop {
54            let code = read_i32(buf, &mut sp)?;
55            if code == 10100 {
56                let len = read_i32(buf, &mut sp)? as usize;
57                let s = read_bytes(buf, &mut sp, len)?;
58                self.g00_file_name = decode_sjis(s);
59                continue;
60            }
61            if code == 20000 {
62                let set_cnt = read_i32(buf, &mut sp)?;
63                if set_cnt > 0 {
64                    for _ in 0..set_cnt {
65                        let mut set = GanSet::default();
66                        let mut keika_time = 0i32;
67                        analize_set(buf, &mut sp, &mut set, &mut keika_time)?;
68                        self.set_list.push(set);
69                    }
70                }
71                return Ok(());
72            }
73
74            bail!("gan unexpected code: {code}");
75        }
76    }
77}
78
79fn analize_set(buf: &[u8], sp: &mut usize, set: &mut GanSet, keika_time: &mut i32) -> Result<()> {
80    let code = read_i32(buf, sp)?;
81    if code != 30000 {
82        bail!("gan set missing PAT_COUNT: {code}");
83    }
84    let pat_cnt = read_i32(buf, sp)?;
85    for _ in 0..pat_cnt {
86        let pat = analize_pat(buf, sp, keika_time)?;
87        set.pat_list.push(pat);
88    }
89    set.total_time = *keika_time;
90    Ok(())
91}
92
93fn analize_pat(buf: &[u8], sp: &mut usize, keika_time: &mut i32) -> Result<GanPat> {
94    let mut pat = GanPat {
95        tr: 255,
96        ..Default::default()
97    };
98    loop {
99        let code = read_i32(buf, sp)?;
100        if code == 999999 {
101            break;
102        }
103        match code {
104            30100 => pat.pat_no = read_i32(buf, sp)?,
105            30101 => pat.x = read_i32(buf, sp)?,
106            30102 => pat.y = read_i32(buf, sp)?,
107            30103 => pat.wait = read_i32(buf, sp)?,
108            30104 => {
109                let v = read_i32(buf, sp)?;
110                pat.tr = v.clamp(0, 255) as u8;
111            }
112            30105 => pat.z = read_i32(buf, sp)?,
113            _ => bail!("gan unexpected pat code: {code}"),
114        }
115    }
116    *keika_time += pat.wait;
117    pat.keika_time = *keika_time;
118    Ok(pat)
119}
120
121fn decode_sjis(bytes: &[u8]) -> String {
122    let (cow, _, had_err) = SHIFT_JIS.decode(bytes);
123    if had_err {
124        String::from_utf8_lossy(bytes).into_owned()
125    } else {
126        cow.into_owned()
127    }
128}
129
130fn read_i32(buf: &[u8], sp: &mut usize) -> Result<i32> {
131    let bytes = read_bytes(buf, sp, 4)?;
132    Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
133}
134
135fn read_bytes<'a>(buf: &'a [u8], sp: &mut usize, len: usize) -> Result<&'a [u8]> {
136    if *sp + len > buf.len() {
137        bail!("gan out of range");
138    }
139    let out = &buf[*sp..*sp + len];
140    *sp += len;
141    Ok(out)
142}
143
144#[derive(Debug, Default, Clone)]
145pub struct GanState {
146    data: Option<Arc<GanData>>,
147    current_pat: Option<GanPat>,
148
149    gan_name: String,
150    now_time: i32,
151    anm_set_no: i32,
152    next_anm_set_no: i32,
153
154    anm_start: bool,
155    anm_pause: bool,
156    anm_loop_flag: bool,
157    anm_real_time_flag: bool,
158    next_anm_flag: bool,
159    next_anm_loop_flag: bool,
160    next_anm_real_time_flag: bool,
161}
162
163impl GanState {
164    pub fn reset(&mut self) {
165        *self = GanState::default();
166    }
167
168    pub fn current_pat(&self) -> Option<&GanPat> {
169        self.current_pat.as_ref()
170    }
171
172    pub fn is_active(&self) -> bool {
173        self.data.is_some() && self.anm_start && !self.anm_pause
174    }
175
176    pub fn load_gan(&mut self, project_dir: &Path, append_dir: &str, name: &str) -> Result<()> {
177        self.reset();
178        self.load_gan_only(project_dir, append_dir, name)
179    }
180
181    pub fn load_gan_only(
182        &mut self,
183        project_dir: &Path,
184        append_dir: &str,
185        name: &str,
186    ) -> Result<()> {
187        if name.trim().is_empty() {
188            return Ok(());
189        }
190        self.gan_name = name.to_string();
191        let path = resolve_gan_path(project_dir, append_dir, name)
192            .with_context(|| format!("resolve gan path: {name}"))?;
193        let data = GanData::load(&path)?;
194        self.data = Some(Arc::new(data));
195        Ok(())
196    }
197
198    pub fn start_anm(&mut self, set_no: i32, loop_flag: bool, real_time_flag: bool) {
199        self.now_time = 0;
200        self.anm_start = true;
201        self.anm_pause = false;
202        self.anm_set_no = set_no;
203        self.anm_loop_flag = loop_flag;
204        self.anm_real_time_flag = real_time_flag;
205        self.next_anm_flag = false;
206    }
207
208    pub fn next_anm(&mut self, set_no: i32, loop_flag: bool, real_time_flag: bool) {
209        if self.anm_start {
210            if self.next_anm_flag {
211                self.start_anm(self.next_anm_set_no, false, self.next_anm_real_time_flag);
212            } else {
213                self.anm_loop_flag = false;
214            }
215            self.next_anm_flag = true;
216            self.next_anm_set_no = set_no;
217            self.next_anm_loop_flag = loop_flag;
218            self.next_anm_real_time_flag = real_time_flag;
219        } else {
220            self.start_anm(set_no, loop_flag, real_time_flag);
221        }
222    }
223
224    pub fn pause_anm(&mut self) {
225        self.anm_pause = true;
226    }
227
228    pub fn resume_anm(&mut self) {
229        self.anm_pause = false;
230    }
231
232    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
233        let mut game = past_game_time.max(0);
234        let mut real = past_real_time.max(0);
235
236        let Some(data) = self.data.as_ref() else {
237            self.current_pat = None;
238            return;
239        };
240        if self.anm_set_no < 0 || self.anm_set_no as usize >= data.set_list.len() {
241            self.current_pat = None;
242            return;
243        }
244        let set = &data.set_list[self.anm_set_no as usize];
245        if set.pat_list.is_empty() {
246            self.current_pat = None;
247            return;
248        }
249        let total_time = set.total_time;
250        let first_pat = set.pat_list[0].clone();
251        let last_pat = set.pat_list[set.pat_list.len() - 1].clone();
252        if !self.anm_start || total_time <= 0 {
253            self.current_pat = Some(first_pat);
254            return;
255        }
256
257        if !self.anm_pause {
258            if self.anm_real_time_flag {
259                self.now_time += real;
260            } else {
261                self.now_time += game;
262            }
263        }
264
265        if !self.anm_loop_flag {
266            if self.now_time >= total_time {
267                if self.next_anm_flag {
268                    self.start_anm(
269                        self.next_anm_set_no,
270                        self.next_anm_loop_flag,
271                        self.next_anm_real_time_flag,
272                    );
273                    let overshoot = self.now_time - total_time;
274                    game -= overshoot;
275                    real -= overshoot;
276                    self.update_time(game, real);
277                } else {
278                    self.now_time = total_time;
279                    self.current_pat = Some(last_pat);
280                }
281                return;
282            }
283        }
284
285        if set.total_time > 0 {
286            self.now_time %= set.total_time;
287        }
288
289        for pat in &set.pat_list {
290            if pat.keika_time >= self.now_time {
291                self.current_pat = Some(pat.clone());
292                break;
293            }
294        }
295    }
296}
297
298fn resolve_gan_path(project_dir: &Path, append_dir: &str, name: &str) -> Result<PathBuf> {
299    let mut candidates: Vec<PathBuf> = Vec::new();
300    let mut norm = name.replace('\\', "/");
301    if !norm.ends_with(".gan") && !norm.contains('.') {
302        norm.push_str(".gan");
303    }
304
305    let path = PathBuf::from(&norm);
306    if path.is_absolute() {
307        if path.exists() {
308            return Ok(path);
309        }
310    }
311
312    if !append_dir.is_empty() {
313        candidates.push(project_dir.join(append_dir).join("gan").join(&norm));
314        candidates.push(project_dir.join(append_dir).join(&norm));
315    }
316    candidates.push(project_dir.join("gan").join(&norm));
317    candidates.push(project_dir.join(&norm));
318    candidates.push(project_dir.join("dat").join(&norm));
319
320    for cand in candidates {
321        if cand.exists() {
322            return Ok(cand);
323        }
324    }
325
326    bail!("gan file not found: {name}")
327}