Skip to main content

siglus_scene_vm/runtime/forms/
math.rs

1use anyhow::Result;
2
3use crate::runtime::forms::codes::elm_value;
4use crate::runtime::forms::prop_access;
5use crate::runtime::{CommandContext, Value};
6
7const TNM_ANGLE_UNIT: f64 = 10.0;
8
9fn round_half_away_from_zero(v: f64) -> i64 {
10    if v > 0.0 {
11        (v + 0.5) as i64
12    } else if v < 0.0 {
13        (v - 0.5) as i64
14    } else {
15        0
16    }
17}
18
19fn repeat_char(c: char, n: usize) -> String {
20    std::iter::repeat_n(c, n).collect()
21}
22
23fn tostr_pad(num: i64, len: i64, fill: char) -> String {
24    let len = len.max(0) as usize;
25
26    if num == 0 {
27        if len <= 1 {
28            return "0".to_string();
29        }
30        let mut out = repeat_char(fill, len - 1);
31        out.push('0');
32        return out;
33    }
34
35    if num > 0 {
36        let digits = num.to_string();
37        if len <= digits.len() {
38            return digits;
39        }
40        let mut out = repeat_char(fill, len - digits.len());
41        out.push_str(&digits);
42        return out;
43    }
44
45    // Negative number.
46    let abs = num.abs().to_string();
47    let total_digits = abs.len() + 1; // '-' + abs
48    if len <= total_digits {
49        return format!("-{}", abs);
50    }
51
52    let mut out = String::new();
53    out.push('-');
54    out.push_str(&repeat_char(fill, len - total_digits));
55    out.push_str(&abs);
56    out
57}
58
59fn to_zenkaku_ascii(s: &str) -> String {
60    s.chars()
61        .map(|ch| match ch {
62            '0'..='9' => char::from_u32('0' as u32 + (ch as u32 - '0' as u32)).unwrap(),
63            'A'..='Z' => char::from_u32('A' as u32 + (ch as u32 - 'A' as u32)).unwrap(),
64            'a'..='z' => char::from_u32('a' as u32 + (ch as u32 - 'a' as u32)).unwrap(),
65            ' ' => ' ',
66            '-' => '-',
67            '+' => '+',
68            '.' => '.',
69            ',' => ',',
70            ':' => ':',
71            ';' => ';',
72            '/' => '/',
73            '\\' => '\',
74            '(' => '(',
75            ')' => ')',
76            '[' => '[',
77            ']' => ']',
78            _ => ch,
79        })
80        .collect()
81}
82
83fn timetable_arg(v: &Value) -> Option<(f64, f64, f64, i64)> {
84    let Value::List(items) = v.unwrap_named() else {
85        return None;
86    };
87    if items.len() < 3 {
88        return None;
89    }
90    let start_time = items.first().and_then(Value::as_i64)? as f64;
91    let end_time = items.get(1).and_then(Value::as_i64)? as f64;
92    let end_value = items.get(2).and_then(Value::as_i64)? as f64;
93    let speed_type = items.get(3).and_then(Value::as_i64).unwrap_or(0);
94    Some((start_time, end_time, end_value, speed_type))
95}
96
97fn timetable_value(params: &[Value]) -> i64 {
98    let now_time = params.first().and_then(Value::as_i64).unwrap_or(0) as f64;
99    let rep_time = params.get(1).and_then(Value::as_i64).unwrap_or(0) as f64;
100    let mut start_value = params.get(2).and_then(Value::as_i64).unwrap_or(0) as f64;
101    let mut ret_value = start_value;
102    let now_time = now_time - rep_time;
103
104    for arg in params.iter().skip(3) {
105        let Some((start_time, end_time, end_value, speed_type)) = timetable_arg(arg) else {
106            continue;
107        };
108        if now_time < start_time {
109            ret_value = start_value;
110            break;
111        } else if now_time >= end_time {
112            ret_value = end_value;
113        } else {
114            let duration = end_time - start_time;
115            if duration == 0.0 {
116                ret_value = end_value;
117            } else if speed_type == 1 {
118                let t = now_time - start_time;
119                ret_value = (end_value - start_value) * t * t / duration / duration + start_value;
120            } else if speed_type == 2 {
121                let t = now_time - end_time;
122                ret_value = -(end_value - start_value) * t * t / duration / duration + end_value;
123            } else {
124                ret_value =
125                    (end_value - start_value) * (now_time - start_time) / duration + start_value;
126            }
127            break;
128        }
129        start_value = end_value;
130    }
131
132    round_half_away_from_zero(ret_value)
133}
134
135fn xorshift32(state: &mut u32) -> u32 {
136    if *state == 0 {
137        // Non-zero default seed.
138        *state = 0x1234_5678;
139    }
140    let mut x = *state;
141    x ^= x << 13;
142    x ^= x >> 17;
143    x ^= x << 5;
144    *state = x;
145    x
146}
147
148pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
149    let parsed = prop_access::parse_element_chain_ctx(ctx, form_id, args);
150    let mut chain_pos: Option<usize> = None;
151    let mut chain: Option<&[i32]> = None;
152    if let Some((pos, ch)) = parsed {
153        chain_pos = Some(pos);
154        chain = Some(ch);
155    }
156
157    let (op, params, al_id) = if let Some(pos) = chain_pos {
158        let ch = chain.unwrap();
159        let op = ch.get(1).copied();
160        let al_id = crate::runtime::forms::prop_access::current_vm_meta(ctx).0;
161        (
162            op,
163            crate::runtime::forms::prop_access::script_args(args, pos),
164            al_id,
165        )
166    } else {
167        let op = args.first().and_then(|v| v.as_i64()).map(|v| v as i32);
168        let params = if args.len() >= 2 { &args[1..] } else { &[] };
169        (op, params, None)
170    };
171
172    let p_int = |i: usize| -> i64 { params.get(i).and_then(|v| v.as_i64()).unwrap_or(0) };
173    let p_str = |i: usize| -> &str { params.get(i).and_then(|v| v.as_str()).unwrap_or("") };
174
175    let Some(op) = op else {
176        if let Some(direct_op) = args.first().and_then(|v| v.as_i64()) {
177            prop_access::store_or_push_direct_prop(ctx, form_id, direct_op as i32, args, 1);
178            return Ok(true);
179        }
180        ctx.push(Value::Int(0));
181        return Ok(true);
182    };
183
184    // MAX
185    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_max, elm_value::MATH_MAX) {
186        let a = p_int(0);
187        let b = p_int(1);
188        ctx.push(Value::Int(a.max(b)));
189        return Ok(true);
190    }
191    // MIN
192    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_min, elm_value::MATH_MIN) {
193        let a = p_int(0);
194        let b = p_int(1);
195        ctx.push(Value::Int(a.min(b)));
196        return Ok(true);
197    }
198    // LIMIT(min, value, max)
199    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_limit, elm_value::MATH_LIMIT)
200    {
201        let a = p_int(0);
202        let v = p_int(1);
203        let b = p_int(2);
204        ctx.push(Value::Int(v.clamp(a.min(b), a.max(b))));
205        return Ok(true);
206    }
207    // ABS
208    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_abs, elm_value::MATH_ABS) {
209        ctx.push(Value::Int(p_int(0).abs()));
210        return Ok(true);
211    }
212    // RAND(min, max)
213    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_rand, elm_value::MATH_RAND) {
214        let a = p_int(0);
215        let b = p_int(1);
216        let lo = a.min(b);
217        let hi = a.max(b);
218        if lo == hi {
219            ctx.push(Value::Int(lo));
220            return Ok(true);
221        }
222        let span = (hi - lo + 1) as u64;
223        let r = xorshift32(&mut ctx.globals.rng_state) as u64;
224        ctx.push(Value::Int(lo + (r % span) as i64));
225        return Ok(true);
226    }
227
228    // SQRT(num, scale)
229    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_sqrt, elm_value::MATH_SQRT) {
230        let num = p_int(0).max(0) as f64;
231        let scale = p_int(1) as f64;
232        let v = (num.sqrt() * scale) as i64;
233        ctx.push(Value::Int(v));
234        return Ok(true);
235    }
236    // LOG(num, scale)
237    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_log, elm_value::MATH_LOG) {
238        let num = p_int(0).max(1) as f64;
239        let scale = p_int(1) as f64;
240        let v = (num.ln() * scale) as i64;
241        ctx.push(Value::Int(v));
242        return Ok(true);
243    }
244    // LOG2
245    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_log2, elm_value::MATH_LOG2) {
246        let num = p_int(0).max(1) as f64;
247        let scale = p_int(1) as f64;
248        let v = (num.log2() * scale) as i64;
249        ctx.push(Value::Int(v));
250        return Ok(true);
251    }
252    // LOG10
253    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_log10, elm_value::MATH_LOG10)
254    {
255        let num = p_int(0).max(1) as f64;
256        let scale = p_int(1) as f64;
257        let v = (num.log10() * scale) as i64;
258        ctx.push(Value::Int(v));
259        return Ok(true);
260    }
261
262    // SIN(angle, scale)
263    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_sin, elm_value::MATH_SIN) {
264        let angle = p_int(0) as f64;
265        let scale = p_int(1) as f64;
266        let deg = angle / TNM_ANGLE_UNIT;
267        let rad = deg.to_radians();
268        ctx.push(Value::Int((rad.sin() * scale) as i64));
269        return Ok(true);
270    }
271    // COS(angle, scale)
272    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_cos, elm_value::MATH_COS) {
273        let angle = p_int(0) as f64;
274        let scale = p_int(1) as f64;
275        let deg = angle / TNM_ANGLE_UNIT;
276        let rad = deg.to_radians();
277        ctx.push(Value::Int((rad.cos() * scale) as i64));
278        return Ok(true);
279    }
280    // TAN(angle, scale)
281    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_tan, elm_value::MATH_TAN) {
282        let angle = p_int(0) as f64;
283        let scale = p_int(1) as f64;
284        let deg = angle / TNM_ANGLE_UNIT;
285        let rad = deg.to_radians();
286        ctx.push(Value::Int((rad.tan() * scale) as i64));
287        return Ok(true);
288    }
289
290    // ARCSIN(num, denom)
291    if crate::runtime::constants::matches_element_id(
292        op,
293        ctx.ids.math_arcsin,
294        elm_value::MATH_ARCSIN,
295    ) {
296        let num = p_int(0) as f64;
297        let denom = p_int(1) as f64;
298        let ret = if denom == 0.0 {
299            0
300        } else {
301            let mut x = num / denom;
302            x = x.clamp(-1.0, 1.0);
303            round_half_away_from_zero(x.asin().to_degrees() * TNM_ANGLE_UNIT)
304        };
305        ctx.push(Value::Int(ret));
306        return Ok(true);
307    }
308    // ARCCOS
309    if crate::runtime::constants::matches_element_id(
310        op,
311        ctx.ids.math_arccos,
312        elm_value::MATH_ARCCOS,
313    ) {
314        let num = p_int(0) as f64;
315        let denom = p_int(1) as f64;
316        let ret = if denom == 0.0 {
317            0
318        } else {
319            let mut x = num / denom;
320            x = x.clamp(-1.0, 1.0);
321            round_half_away_from_zero(x.acos().to_degrees() * TNM_ANGLE_UNIT)
322        };
323        ctx.push(Value::Int(ret));
324        return Ok(true);
325    }
326    // ARCTAN
327    if crate::runtime::constants::matches_element_id(
328        op,
329        ctx.ids.math_arctan,
330        elm_value::MATH_ARCTAN,
331    ) {
332        let num = p_int(0) as f64;
333        let denom = p_int(1) as f64;
334        let ret = if denom == 0.0 {
335            0
336        } else {
337            round_half_away_from_zero((num / denom).atan().to_degrees() * TNM_ANGLE_UNIT)
338        };
339        ctx.push(Value::Int(ret));
340        return Ok(true);
341    }
342
343    // DISTANCE(x1,y1,x2,y2)
344    if crate::runtime::constants::matches_element_id(
345        op,
346        ctx.ids.math_distance,
347        elm_value::MATH_DISTANCE,
348    ) {
349        let x1 = p_int(0);
350        let y1 = p_int(1);
351        let x2 = p_int(2);
352        let y2 = p_int(3);
353        let dx = (x2 - x1) as f64;
354        let dy = (y2 - y1) as f64;
355        ctx.push(Value::Int(((dx * dx + dy * dy).sqrt()) as i64));
356        return Ok(true);
357    }
358
359    // ANGLE(x1,y1,x2,y2)
360    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_angle, elm_value::MATH_ANGLE)
361    {
362        let x1 = p_int(0);
363        let y1 = p_int(1);
364        let x2 = p_int(2);
365        let y2 = p_int(3);
366        let dy = (y2 - y1) as f64;
367        let dx = (x2 - x1) as f64;
368        let mut ret = round_half_away_from_zero(dy.atan2(dx).to_degrees() * TNM_ANGLE_UNIT);
369        ret = (ret + (360.0 * TNM_ANGLE_UNIT) as i64).rem_euclid((360.0 * TNM_ANGLE_UNIT) as i64);
370        ctx.push(Value::Int(ret));
371        return Ok(true);
372    }
373
374    // LINEAR(x0,x1,y1,x2,y2)
375    if crate::runtime::constants::matches_element_id(
376        op,
377        ctx.ids.math_linear,
378        elm_value::MATH_LINEAR,
379    ) {
380        let x0 = p_int(0);
381        let x1 = p_int(1);
382        let y1 = p_int(2);
383        let x2 = p_int(3);
384        let y2 = p_int(4);
385        if x1 == x2 {
386            ctx.push(Value::Int(y1));
387        } else {
388            let v = ((y2 - y1) as f64 * (x0 - x1) as f64 / (x2 - x1) as f64 + y1 as f64) as i64;
389            ctx.push(Value::Int(v));
390        }
391        return Ok(true);
392    }
393
394    // TIMETABLE(now_time, rep_time, start_value, [start_time,end_time,end_value,speed_type]...)
395    if op == elm_value::MATH_TIMETABLE {
396        let ret = timetable_value(params);
397        if env_is_set!("SG_DEBUG")
398            && matches!(
399                ctx.current_scene_name.as_deref(),
400                Some("sys10_sm00") | Some("sys10_cf00") | Some("sys10_cf01")
401            )
402            && matches!(ctx.current_line_no, 140..=185 | 240..=285 | 700..=730 | 870..=895)
403        {
404            let scene = ctx.current_scene_name.as_deref().unwrap_or("<none>");
405            let scene_no = ctx
406                .current_scene_no
407                .map(|n| n.to_string())
408                .unwrap_or_else(|| "-".to_string());
409            eprintln!(
410                "[SG_DEBUG][CF_CONDITION_TRACE] scene={} scene_no={} line={} kind=MATH_TIMETABLE params={:?} result={}",
411                scene, scene_no, ctx.current_line_no, params, ret
412            );
413        }
414        ctx.push(Value::Int(ret));
415        return Ok(true);
416    }
417
418    // TOSTR
419    if crate::runtime::constants::matches_element_id(op, ctx.ids.math_tostr, elm_value::MATH_TOSTR)
420    {
421        let s = if matches!(al_id, Some(1)) {
422            let num = p_int(0);
423            let len = p_int(1);
424            tostr_pad(num, len, ' ')
425        } else {
426            p_int(0).to_string()
427        };
428        ctx.push(Value::Str(s));
429        return Ok(true);
430    }
431
432    // TOSTR_ZERO
433    if crate::runtime::constants::matches_element_id(
434        op,
435        ctx.ids.math_tostr_zero,
436        elm_value::MATH_TOSTR_ZERO,
437    ) {
438        let num = p_int(0);
439        let len = p_int(1);
440        ctx.push(Value::Str(tostr_pad(num, len, '0')));
441        return Ok(true);
442    }
443
444    // TOSTR_ZEN
445    if op == elm_value::MATH_TOSTR_ZEN {
446        let s = if matches!(al_id, Some(1)) {
447            let num = p_int(0);
448            let len = p_int(1);
449            tostr_pad(num, len, ' ')
450        } else {
451            p_int(0).to_string()
452        };
453        ctx.push(Value::Str(to_zenkaku_ascii(&s)));
454        return Ok(true);
455    }
456
457    // TOSTR_ZEN_ZERO
458    if op == elm_value::MATH_TOSTR_ZEN_ZERO {
459        let num = p_int(0);
460        let len = p_int(1);
461        ctx.push(Value::Str(to_zenkaku_ascii(&tostr_pad(num, len, '0'))));
462        return Ok(true);
463    }
464
465    // TOSTR_BY_CODE
466    if op == elm_value::MATH_TOSTR_BY_CODE {
467        let code = (p_int(0) & 0xffff) as u32;
468        let s = char::from_u32(code)
469            .map(|c| c.to_string())
470            .unwrap_or_default();
471        ctx.push(Value::Str(s));
472        return Ok(true);
473    }
474
475    Ok(false)
476}