Skip to main content

siglus_scene_vm/runtime/
graphics.rs

1//! Graphics runtime: bridges VM stage/object operations to `LayerManager` + `ImageManager`.
2//!
3//! This layer maps stage/object operations onto renderable sprites while preserving
4//! stable sprite identities for the VM runtime.
5
6use anyhow::{Context, Result, bail};
7use std::collections::HashMap;
8
9use crate::image_manager::{ImageHandle, ImageManager};
10use crate::layer::{
11    ClipRect, LayerId, LayerManager, Sprite, SpriteBlend, SpriteFit, SpriteId, SpriteSizeMode,
12};
13
14fn sg_cgm_coord_trace_enabled() -> bool {
15    env_is_set!("SG_DEBUG")
16}
17
18/// Logs `format!(...)` arguments only when tracing (they are not built
19/// otherwise: object_set_pos runs for moving objects every frame).
20macro_rules! sg_cgm_coord_trace {
21    ($($arg:tt)*) => {
22        if sg_cgm_coord_trace_enabled() {
23            eprintln!("[SG_DEBUG][CGM_COORD_TRACE][GFX] {}", format!($($arg)*));
24        }
25    };
26}
27
28fn cgm_file_interesting(file: Option<&str>) -> bool {
29    file.map(|name| name.to_ascii_lowercase().contains("cgm_"))
30        .unwrap_or(false)
31}
32
33#[derive(Debug, Clone)]
34struct ObjectState {
35    // Every Siglus OBJECT, including BACK.OBJECT[0], owns an ordinary stage sprite.
36    layer_id: Option<LayerId>,
37    sprite_id: Option<SpriteId>,
38
39    // Logical properties.
40    is_mesh: bool,
41    file: Option<String>,
42    patno: i64,
43    // Resource binding cached on the backing sprite. Script/render parameters
44    // change every frame; the loaded album does not. Keeping these separate
45    // mirrors C_elm_object::m_album + m_op.obp in the original engine.
46    bound_file: Option<String>,
47    bound_patno: i64,
48    disp: bool,
49    x: i64,
50    y: i64,
51    layer_no: i64,
52    order: i64,
53    alpha: i64,
54    /// Stored but not used for sorting (Siglus draw order follows tree traversal).
55    z: i64,
56    center_x: i64,
57    center_y: i64,
58    scale_x: i64,
59    scale_y: i64,
60    rotate_z: i64,
61    clip_use: i64,
62    clip_left: i64,
63    clip_top: i64,
64    clip_right: i64,
65    clip_bottom: i64,
66    src_clip_use: i64,
67    src_clip_left: i64,
68    src_clip_top: i64,
69    src_clip_right: i64,
70    src_clip_bottom: i64,
71    tr: i64,
72    mono: i64,
73    reverse: i64,
74    bright: i64,
75    dark: i64,
76    color_rate: i64,
77    color_add_r: i64,
78    color_add_g: i64,
79    color_add_b: i64,
80    color_r: i64,
81    color_g: i64,
82    color_b: i64,
83    blend: i64,
84    light_no: i64,
85    fog_use: i64,
86}
87
88impl Default for ObjectState {
89    fn default() -> Self {
90        Self {
91            layer_id: None,
92            sprite_id: None,
93            is_mesh: false,
94            file: None,
95            patno: 0,
96            bound_file: None,
97            bound_patno: -1,
98            disp: false,
99            x: 0,
100            y: 0,
101            layer_no: 0,
102            order: 0,
103            alpha: 255,
104            z: 0,
105            center_x: 0,
106            center_y: 0,
107            scale_x: 1000,
108            scale_y: 1000,
109            rotate_z: 0,
110            clip_use: 0,
111            clip_left: 0,
112            clip_top: 0,
113            clip_right: 0,
114            clip_bottom: 0,
115            src_clip_use: 0,
116            src_clip_left: 0,
117            src_clip_top: 0,
118            src_clip_right: 0,
119            src_clip_bottom: 0,
120            tr: 255,
121            mono: 0,
122            reverse: 0,
123            bright: 0,
124            dark: 0,
125            color_rate: 0,
126            color_add_r: 0,
127            color_add_g: 0,
128            color_add_b: 0,
129            color_r: 0,
130            color_g: 0,
131            color_b: 0,
132            blend: 0,
133            light_no: -1,
134            fog_use: 0,
135        }
136    }
137}
138
139#[derive(Debug, Clone)]
140pub struct DebugObjectSpriteBinding {
141    pub stage: usize,
142    pub obj_idx: usize,
143    pub is_bg: bool,
144    pub layer_id: Option<LayerId>,
145    pub sprite_id: Option<SpriteId>,
146    pub file: Option<String>,
147    pub patno: i64,
148    pub disp: bool,
149    pub x: i64,
150    pub y: i64,
151    pub layer_no: i64,
152    pub order: i64,
153    pub alpha: i64,
154    pub z: i64,
155    pub tr: i64,
156    pub clip_use: i64,
157    pub clip_left: i64,
158    pub clip_top: i64,
159    pub clip_right: i64,
160    pub clip_bottom: i64,
161    pub scale_x: i64,
162    pub scale_y: i64,
163    pub rotate_z: i64,
164}
165
166#[derive(Debug, Clone, Default)]
167struct StageState {
168    layer_id: Option<LayerId>,
169    objects: HashMap<usize, ObjectState>,
170}
171
172#[derive(Debug, Default)]
173pub struct GfxRuntime {
174    /// Current logical layer number selected by named commands (LAYER/LAYER_SET).
175    /// Used as a default for CHR/object operations when scripts omit an explicit layer.
176    pub current_layer: i32,
177    stages: [StageState; 3],
178}
179
180impl GfxRuntime {
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    fn ensure_stage(&mut self, stage: usize) -> &mut StageState {
186        &mut self.stages[stage]
187    }
188
189    fn ensure_stage_layer(&mut self, layers: &mut LayerManager, stage: usize) -> LayerId {
190        let st = self.ensure_stage(stage);
191        if let Some(id) = st.layer_id {
192            return id;
193        }
194        let id = layers.create_layer();
195        st.layer_id = Some(id);
196        id
197    }
198
199    /// Expose stage layer allocation for non-Gfx backends (e.g., movie sprites).
200    pub fn ensure_stage_layer_id(
201        &mut self,
202        layers: &mut LayerManager,
203        stage: i64,
204    ) -> Option<LayerId> {
205        if !(0..=2).contains(&stage) {
206            return None;
207        }
208        Some(self.ensure_stage_layer(layers, stage as usize))
209    }
210
211    fn ensure_object_mut(&mut self, stage: usize, obj_idx: usize) -> &mut ObjectState {
212        self.ensure_stage(stage).objects.entry(obj_idx).or_default()
213    }
214
215    fn object(&self, stage: usize, obj_idx: usize) -> Option<&ObjectState> {
216        self.stages.get(stage)?.objects.get(&obj_idx)
217    }
218
219    fn reset_object_for_create(&mut self, layers: &mut LayerManager, stage: usize, obj_idx: usize) {
220        let (layer_id, sprite_id) = {
221            let obj = self.ensure_object_mut(stage, obj_idx);
222            (obj.layer_id, obj.sprite_id)
223        };
224
225        {
226            let obj = self.ensure_object_mut(stage, obj_idx);
227            *obj = ObjectState::default();
228            obj.layer_id = layer_id;
229            obj.sprite_id = sprite_id;
230        }
231
232        if let (Some(lid), Some(sid)) = (layer_id, sprite_id)
233            && let Some(sprite) = layers
234                .layer_mut(lid)
235                .and_then(|layer| layer.sprite_mut(sid))
236        {
237            *sprite = Sprite::default();
238        }
239    }
240
241    pub fn debug_object_snapshot(
242        &self,
243        stage: usize,
244        obj_idx: usize,
245    ) -> Option<DebugObjectSpriteBinding> {
246        let obj = self.object(stage, obj_idx)?;
247        Some(DebugObjectSpriteBinding {
248            stage,
249            obj_idx,
250            // Kept in the debug ABI for existing HUD callers. Siglus OBJECTs are never
251            // redirected to LayerManager::bg; BACK.OBJECT[0] is an ordinary object.
252            is_bg: false,
253            layer_id: obj.layer_id,
254            sprite_id: obj.sprite_id,
255            file: obj.file.clone(),
256            patno: obj.patno,
257            disp: obj.disp,
258            x: obj.x,
259            y: obj.y,
260            layer_no: obj.layer_no,
261            order: obj.order,
262            alpha: obj.alpha,
263            z: obj.z,
264            tr: obj.tr,
265            clip_use: obj.clip_use,
266            clip_left: obj.clip_left,
267            clip_top: obj.clip_top,
268            clip_right: obj.clip_right,
269            clip_bottom: obj.clip_bottom,
270            scale_x: obj.scale_x,
271            scale_y: obj.scale_y,
272            rotate_z: obj.rotate_z,
273        })
274    }
275
276    pub fn object_sprite_binding(&self, stage: i64, obj_idx: i64) -> Option<(LayerId, SpriteId)> {
277        let stage_i = stage as isize;
278        if !(0..3).contains(&stage_i) || obj_idx < 0 {
279            return None;
280        }
281        let obj = self.object(stage_i as usize, obj_idx as usize)?;
282        match (obj.layer_id, obj.sprite_id) {
283            (Some(lid), Some(sid)) => Some((lid, sid)),
284            _ => None,
285        }
286    }
287
288    /// Cheap guard for the compatibility repair pass in `CommandContext`.
289    ///
290    /// Normal PCT objects keep their image binding for their whole lifetime.
291    /// Walking every Stage/MWND/child tree just to discover that nothing is
292    /// missing is much more expensive than checking the compact GfxRuntime
293    /// object table first.  This deliberately mirrors the repair pass' scope:
294    /// only Gfx objects with an existing sprite binding can be repaired there.
295    pub fn has_missing_bound_object_image(&self, layers: &LayerManager) -> bool {
296        self.stages.iter().any(|stage| {
297            stage.objects.values().any(|obj| {
298                let Some(file) = obj.file.as_deref() else {
299                    return false;
300                };
301                if obj.is_mesh || file.is_empty() {
302                    return false;
303                }
304                let (Some(layer_id), Some(sprite_id)) = (obj.layer_id, obj.sprite_id) else {
305                    return false;
306                };
307                layers
308                    .layer(layer_id)
309                    .and_then(|layer| layer.sprite(sprite_id))
310                    .is_some_and(|sprite| sprite.image_id.is_none())
311            })
312        })
313    }
314
315    fn load_any_image(images: &mut ImageManager, file: &str, patno: i64) -> Result<ImageHandle> {
316        // Siglus delegates descriptors containing `|` to Tona3's composed-G00
317        // loader. The whole descriptor is not a resource file name and composed
318        // textures intentionally do not fall back to bg/png/jpeg resources.
319        if file.contains('|') {
320            if patno != 0 {
321                bail!("composed g00 has one texture; invalid pattern {patno}");
322            }
323            return images
324                .load_g00_composed(file)
325                .with_context(|| format!("failed to load composed g00: {file}"));
326        }
327
328        // Engine preference: g00 first, then bg fallback.
329        let pat_u32 = if patno < 0 { 0 } else { patno as u32 };
330        match images.load_g00(file, pat_u32) {
331            Ok(id) => Ok(id),
332            Err(_) => images
333                .load_bg(file)
334                .with_context(|| format!("failed to load image as g00/bg: {file}")),
335        }
336    }
337
338    fn ensure_bound_sprite(
339        &mut self,
340        layers: &mut LayerManager,
341        stage: usize,
342        obj_idx: usize,
343    ) -> Result<(LayerId, SpriteId)> {
344        let st_layer = self.ensure_stage_layer(layers, stage);
345        let obj = self.ensure_object_mut(stage, obj_idx);
346
347        if let (Some(lid), Some(sid)) = (obj.layer_id, obj.sprite_id) {
348            return Ok((lid, sid));
349        }
350
351        let sid = {
352            let layer = layers
353                .layer_mut(st_layer)
354                .context("stage layer not found")?;
355            layer.create_sprite()
356        };
357
358        // Initialize with sane defaults.
359        if let Some(layer) = layers.layer_mut(st_layer)
360            && let Some(sprite) = layer.sprite_mut(sid)
361        {
362            sprite.visible = true;
363            sprite.alpha = 255;
364            sprite.fit = SpriteFit::PixelRect;
365            sprite.size_mode = SpriteSizeMode::Intrinsic;
366            sprite.x = 0;
367            sprite.y = 0;
368            sprite.order = 0;
369        }
370
371        obj.layer_id = Some(st_layer);
372        obj.sprite_id = Some(sid);
373        Ok((st_layer, sid))
374    }
375
376    fn sync_object_sprite(
377        &mut self,
378        images: &mut ImageManager,
379        layers: &mut LayerManager,
380        stage: usize,
381        obj_idx: usize,
382    ) -> Result<()> {
383        // Read in place (this runs for every object property write; a clone
384        // copied its file names each time). The binding is written back last.
385        self.ensure_object_mut(stage, obj_idx);
386        let (lid, sid) = self.ensure_bound_sprite(layers, stage, obj_idx)?;
387        let obj = self.object(stage, obj_idx).context("object not found")?;
388        let sprite = layers
389            .layer_mut(lid)
390            .and_then(|l| l.sprite_mut(sid))
391            .context("sprite not found")?;
392
393        sprite.visible = obj.disp;
394        sprite.x = obj.x as i32;
395        sprite.y = obj.y as i32;
396        sprite.alpha = obj.alpha.clamp(0, 255) as u8;
397        sprite.scale_x = obj.scale_x as f32 / 1000.0;
398        sprite.scale_y = obj.scale_y as f32 / 1000.0;
399        sprite.rotate = obj.rotate_z as f32 * std::f32::consts::PI / 1800.0;
400        sprite.pivot_x = obj.center_x as f32;
401        sprite.pivot_y = obj.center_y as f32;
402        sprite.dst_clip = clip_rect(
403            obj.clip_use,
404            obj.clip_left,
405            obj.clip_top,
406            obj.clip_right,
407            obj.clip_bottom,
408        );
409        sprite.src_clip = clip_rect(
410            obj.src_clip_use,
411            obj.src_clip_left,
412            obj.src_clip_top,
413            obj.src_clip_right,
414            obj.src_clip_bottom,
415        );
416        sprite.tr = obj.tr.clamp(0, 255) as u8;
417        sprite.mono = obj.mono.clamp(0, 255) as u8;
418        sprite.reverse = obj.reverse.clamp(0, 255) as u8;
419        sprite.bright = obj.bright.clamp(0, 255) as u8;
420        sprite.dark = obj.dark.clamp(0, 255) as u8;
421        sprite.color_rate = obj.color_rate.clamp(0, 255) as u8;
422        sprite.color_add_r = obj.color_add_r.clamp(0, 255) as u8;
423        sprite.color_add_g = obj.color_add_g.clamp(0, 255) as u8;
424        sprite.color_add_b = obj.color_add_b.clamp(0, 255) as u8;
425        sprite.color_r = obj.color_r.clamp(0, 255) as u8;
426        sprite.color_g = obj.color_g.clamp(0, 255) as u8;
427        sprite.color_b = obj.color_b.clamp(0, 255) as u8;
428        sprite.blend = SpriteBlend::from_i64(obj.blend);
429        sprite.light_no = obj.light_no as i32;
430        sprite.fog_use = obj.fog_use != 0;
431
432        // Order: stage layer_no is treated as a coarse z, order as fine z.
433        let coarse = obj.layer_no.clamp(-10000, 10000) as i32;
434        let fine = obj.order.clamp(-100000, 100000) as i32;
435        sprite.order = coarse.saturating_mul(1000).saturating_add(fine);
436
437        if obj.is_mesh {
438            sprite.image_id = None;
439            sprite.mesh_file_name = obj.file.clone();
440            sprite.mesh_kind = 1;
441            sprite.camera_enabled = true;
442            sprite.shadow_cast = true;
443            sprite.shadow_receive = true;
444            sprite.object_anchor = false;
445            sprite.texture_center_x = 0.0;
446            sprite.texture_center_y = 0.0;
447            let (file, patno) = (obj.file.clone(), obj.patno);
448            let state = self.ensure_object_mut(stage, obj_idx);
449            state.bound_file = file;
450            state.bound_patno = patno;
451            return Ok(());
452        }
453
454        let Some(file) = obj.file.as_deref() else {
455            return Ok(());
456        };
457
458        // C_elm_object owns a loaded C_d3d_album independently from its mutable
459        // render parameters. X/Y/TR/ROTATE/etc. writes therefore must not run
460        // resource lookup again. Only CREATE/CHANGE_FILE or a PATNO transition
461        // changes the texture binding. For PATNO, select another cut directly
462        // from the already-loaded album, matching m_album->get_texture(pat_no).
463        let same_file = obj.bound_file.as_deref() == Some(file);
464        if same_file && obj.bound_patno == obj.patno && sprite.image_id.is_some() {
465            return Ok(());
466        }
467
468        let next_image = if same_file {
469            sprite.image_id.as_ref().and_then(|image| {
470                usize::try_from(obj.patno)
471                    .ok()
472                    .and_then(|cut| image.album_cut(cut))
473            })
474        } else {
475            None
476        };
477
478        let image_id = match next_image {
479            Some(image_id) => image_id,
480            None => match Self::load_any_image(images, file, obj.patno) {
481                Ok(image_id) => image_id,
482                Err(err) if is_probable_mesh_path(file) => {
483                    let _ = err;
484                    sprite.image_id = None;
485                    sprite.object_anchor = false;
486                    sprite.texture_center_x = 0.0;
487                    sprite.texture_center_y = 0.0;
488                    return Ok(());
489                }
490                Err(err) => return Err(err),
491            },
492        };
493        set_object_sprite_image(sprite, images, image_id);
494        let (file, patno) = (file.to_string(), obj.patno);
495        let state = self.ensure_object_mut(stage, obj_idx);
496        state.bound_file = Some(file);
497        state.bound_patno = patno;
498
499        Ok(())
500    }
501
502    pub fn object_set_center(
503        &mut self,
504        images: &mut ImageManager,
505        layers: &mut LayerManager,
506        stage: i64,
507        obj_idx: i64,
508        x: i64,
509        y: i64,
510    ) -> Result<()> {
511        let stage_i = stage as isize;
512        if !(0..3).contains(&stage_i) || obj_idx < 0 {
513            return Ok(());
514        }
515        let stage_u = stage_i as usize;
516        let obj_u = obj_idx as usize;
517        {
518            let obj = self.ensure_object_mut(stage_u, obj_u);
519            obj.center_x = x;
520            obj.center_y = y;
521        }
522        self.sync_object_sprite(images, layers, stage_u, obj_u)
523    }
524
525    pub fn object_set_scale(
526        &mut self,
527        images: &mut ImageManager,
528        layers: &mut LayerManager,
529        stage: i64,
530        obj_idx: i64,
531        x: i64,
532        y: i64,
533    ) -> Result<()> {
534        let stage_i = stage as isize;
535        if !(0..3).contains(&stage_i) || obj_idx < 0 {
536            return Ok(());
537        }
538        let stage_u = stage_i as usize;
539        let obj_u = obj_idx as usize;
540        {
541            let obj = self.ensure_object_mut(stage_u, obj_u);
542            obj.scale_x = x;
543            obj.scale_y = y;
544        }
545        self.sync_object_sprite(images, layers, stage_u, obj_u)
546    }
547
548    pub fn object_set_rotate(
549        &mut self,
550        images: &mut ImageManager,
551        layers: &mut LayerManager,
552        stage: i64,
553        obj_idx: i64,
554        z: i64,
555    ) -> Result<()> {
556        let stage_i = stage as isize;
557        if !(0..3).contains(&stage_i) || obj_idx < 0 {
558            return Ok(());
559        }
560        let stage_u = stage_i as usize;
561        let obj_u = obj_idx as usize;
562        {
563            let obj = self.ensure_object_mut(stage_u, obj_u);
564            obj.rotate_z = z;
565        }
566        self.sync_object_sprite(images, layers, stage_u, obj_u)
567    }
568
569    pub fn object_set_clip(
570        &mut self,
571        images: &mut ImageManager,
572        layers: &mut LayerManager,
573        stage: i64,
574        obj_idx: i64,
575        use_flag: i64,
576        left: i64,
577        top: i64,
578        right: i64,
579        bottom: i64,
580    ) -> Result<()> {
581        let stage_i = stage as isize;
582        if !(0..3).contains(&stage_i) || obj_idx < 0 {
583            return Ok(());
584        }
585        let stage_u = stage_i as usize;
586        let obj_u = obj_idx as usize;
587        {
588            let obj = self.ensure_object_mut(stage_u, obj_u);
589            obj.clip_use = use_flag;
590            obj.clip_left = left;
591            obj.clip_top = top;
592            obj.clip_right = right;
593            obj.clip_bottom = bottom;
594        }
595        self.sync_object_sprite(images, layers, stage_u, obj_u)
596    }
597
598    pub fn object_set_src_clip(
599        &mut self,
600        images: &mut ImageManager,
601        layers: &mut LayerManager,
602        stage: i64,
603        obj_idx: i64,
604        use_flag: i64,
605        left: i64,
606        top: i64,
607        right: i64,
608        bottom: i64,
609    ) -> Result<()> {
610        let stage_i = stage as isize;
611        if !(0..3).contains(&stage_i) || obj_idx < 0 {
612            return Ok(());
613        }
614        let stage_u = stage_i as usize;
615        let obj_u = obj_idx as usize;
616        {
617            let obj = self.ensure_object_mut(stage_u, obj_u);
618            obj.src_clip_use = use_flag;
619            obj.src_clip_left = left;
620            obj.src_clip_top = top;
621            obj.src_clip_right = right;
622            obj.src_clip_bottom = bottom;
623        }
624        self.sync_object_sprite(images, layers, stage_u, obj_u)
625    }
626
627    pub fn stage_clear(
628        &mut self,
629        images: &mut ImageManager,
630        layers: &mut LayerManager,
631        stage: i64,
632    ) -> Result<()> {
633        let stage_i = stage as isize;
634        if !(0..3).contains(&stage_i) {
635            return Ok(());
636        }
637        let stage_u = stage_i as usize;
638        let mut indices: Vec<usize> = self.stages[stage_u].objects.keys().copied().collect();
639        indices.sort_unstable();
640        for idx in indices {
641            {
642                let obj = self.ensure_object_mut(stage_u, idx);
643                obj.disp = false;
644            }
645            let _ = self.sync_object_sprite(images, layers, stage_u, idx);
646        }
647        Ok(())
648    }
649
650    fn object_create_impl(
651        &mut self,
652        images: &mut ImageManager,
653        layers: &mut LayerManager,
654        stage: i64,
655        obj_idx: i64,
656        file: &str,
657        disp: i64,
658        x: i64,
659        y: i64,
660        patno: i64,
661        reinit: bool,
662    ) -> Result<()> {
663        let stage_i = stage as isize;
664        if !(0..3).contains(&stage_i) {
665            bail!("invalid stage: {stage}");
666        }
667        if obj_idx < 0 {
668            bail!("invalid obj idx: {obj_idx}");
669        }
670
671        let stage_u = stage_i as usize;
672        let obj_u = obj_idx as usize;
673        let current_layer = self.current_layer;
674
675        if reinit {
676            self.reset_object_for_create(layers, stage_u, obj_u);
677        }
678
679        {
680            let obj = self.ensure_object_mut(stage_u, obj_u);
681            obj.is_mesh = false;
682            obj.file = Some(file.to_string());
683            obj.patno = patno;
684            obj.disp = disp != 0;
685            obj.x = x;
686            obj.y = y;
687
688            // Default layer number from current selection (can be overridden by scripts).
689            if obj.layer_no == 0 {
690                obj.layer_no = current_layer as i64;
691            }
692        }
693
694        // C++ C_elm_object has no special BACK.OBJECT[0] background slot: every
695        // picture object participates in the normal stage sprite tree.
696        let _ = self.ensure_bound_sprite(layers, stage_u, obj_u)?;
697
698        if cgm_file_interesting(Some(file)) || (30..=59).contains(&obj_u) {
699            let obj = self.object(stage_u, obj_u);
700            sg_cgm_coord_trace!(
701                "object_create stage={} obj={} file={} disp={} x={} y={} patno={} reinit={} layer_no={:?} binding={:?}/{:?}",
702                stage,
703                obj_idx,
704                file,
705                disp,
706                x,
707                y,
708                patno,
709                reinit,
710                obj.map(|o| o.layer_no),
711                obj.and_then(|o| o.layer_id),
712                obj.and_then(|o| o.sprite_id)
713            );
714        }
715
716        self.sync_object_sprite(images, layers, stage_u, obj_u)
717    }
718
719    pub fn object_create(
720        &mut self,
721        images: &mut ImageManager,
722        layers: &mut LayerManager,
723        stage: i64,
724        obj_idx: i64,
725        file: &str,
726        disp: i64,
727        x: i64,
728        y: i64,
729        patno: i64,
730    ) -> Result<()> {
731        self.object_create_impl(
732            images, layers, stage, obj_idx, file, disp, x, y, patno, true,
733        )
734    }
735
736    pub fn object_create_billboard(
737        &mut self,
738        images: &mut ImageManager,
739        layers: &mut LayerManager,
740        stage: i64,
741        obj_idx: i64,
742        file: &str,
743        disp: i64,
744        x: i64,
745        y: i64,
746        patno: i64,
747    ) -> Result<()> {
748        let result = self.object_create_impl(
749            images, layers, stage, obj_idx, file, disp, x, y, patno, true,
750        );
751        if let Some((lid, sid)) = self.object_sprite_binding(stage, obj_idx)
752            && let Some(sprite) = layers
753                .layer_mut(lid)
754                .and_then(|layer| layer.sprite_mut(sid))
755        {
756            sprite.billboard = true;
757            sprite.camera_enabled = true;
758        }
759        result
760    }
761
762    /// Mirror of C++ `C_elm_object::restruct_pct` (and the `restruct_type`
763    /// dispatch around it) executed at the tail of `C_elm_object::load`. After
764    /// a save-file load, the per-object gfx runtime (sprite binding, image
765    /// asset, transform/color state) is empty - the saved stream restores
766    /// `globals::ObjectState` but the rendering side has no equivalent storage
767    /// in the save format. This rebuilds the gfx side from the loaded globals
768    /// so the next render frame sees the same picture the save captured.
769    ///
770    /// Caller filters: only invoke for Gfx-backed objects whose `file_name` is
771    /// non-empty; everything else (mesh, movie, weather, number, string) needs
772    /// its own backend-specific path and is no-op here.
773    pub fn restore_gfx_object_from_globals(
774        &mut self,
775        images: &mut ImageManager,
776        layers: &mut LayerManager,
777        stage: i64,
778        obj_idx: i64,
779        src: &crate::runtime::globals::ObjectState,
780    ) -> Result<()> {
781        let stage_i = stage as isize;
782        if !(0..3).contains(&stage_i) || obj_idx < 0 {
783            return Ok(());
784        }
785        let file = match src.file_name.as_deref() {
786            Some(f) if !f.is_empty() => f.to_string(),
787            _ => return Ok(()),
788        };
789        let stage_u = stage_i as usize;
790        let obj_u = obj_idx as usize;
791
792        let current_layer = self.current_layer;
793        self.reset_object_for_create(layers, stage_u, obj_u);
794        {
795            let pe = &src.runtime.prop_events;
796            let dst = self.ensure_object_mut(stage_u, obj_u);
797            dst.is_mesh = false;
798            dst.file = Some(file);
799            dst.patno = src.base.patno;
800            dst.disp = src.base.disp != 0;
801            dst.x = pe.x.get_total_value() as i64;
802            dst.y = pe.y.get_total_value() as i64;
803            dst.z = pe.z.get_total_value() as i64;
804            dst.layer_no = src.base.layer;
805            dst.order = src.base.order;
806            dst.center_x = pe.center_x.get_total_value() as i64;
807            dst.center_y = pe.center_y.get_total_value() as i64;
808            dst.scale_x = pe.scale_x.get_total_value() as i64;
809            dst.scale_y = pe.scale_y.get_total_value() as i64;
810            dst.rotate_z = pe.rotate_z.get_total_value() as i64;
811            dst.clip_use = src.base.clip_use;
812            dst.clip_left = pe.clip_left.get_total_value() as i64;
813            dst.clip_top = pe.clip_top.get_total_value() as i64;
814            dst.clip_right = pe.clip_right.get_total_value() as i64;
815            dst.clip_bottom = pe.clip_bottom.get_total_value() as i64;
816            dst.src_clip_use = src.base.src_clip_use;
817            dst.src_clip_left = pe.src_clip_left.get_total_value() as i64;
818            dst.src_clip_top = pe.src_clip_top.get_total_value() as i64;
819            dst.src_clip_right = pe.src_clip_right.get_total_value() as i64;
820            dst.src_clip_bottom = pe.src_clip_bottom.get_total_value() as i64;
821            let tr = pe.tr.get_total_value() as i64;
822            dst.alpha = tr;
823            dst.tr = tr;
824            dst.mono = pe.mono.get_total_value() as i64;
825            dst.reverse = pe.reverse.get_total_value() as i64;
826            dst.bright = pe.bright.get_total_value() as i64;
827            dst.dark = pe.dark.get_total_value() as i64;
828            dst.color_rate = pe.color_rate.get_total_value() as i64;
829            dst.color_add_r = pe.color_add_r.get_total_value() as i64;
830            dst.color_add_g = pe.color_add_g.get_total_value() as i64;
831            dst.color_add_b = pe.color_add_b.get_total_value() as i64;
832            dst.color_r = pe.color_r.get_total_value() as i64;
833            dst.color_g = pe.color_g.get_total_value() as i64;
834            dst.color_b = pe.color_b.get_total_value() as i64;
835            dst.blend = src.base.blend;
836            dst.light_no = src.base.light_no;
837            dst.fog_use = src.base.fog_use;
838            if dst.layer_no == 0 {
839                dst.layer_no = current_layer as i64;
840            }
841        }
842
843        let _ = self.ensure_bound_sprite(layers, stage_u, obj_u)?;
844        self.sync_object_sprite(images, layers, stage_u, obj_u)
845    }
846
847    /// Release only the type-owned visual resource while preserving the
848    /// object's render parameters. This mirrors C_elm_object::free_type(false):
849    /// CHANGE_FILE must not reset position/color/alpha/layer/order/etc.
850    pub fn object_release_type_backing(
851        &mut self,
852        layers: &mut LayerManager,
853        stage: i64,
854        obj_idx: i64,
855    ) -> Result<()> {
856        let stage_i = stage as isize;
857        if !(0..3).contains(&stage_i) || obj_idx < 0 {
858            return Ok(());
859        }
860        let stage_u = stage_i as usize;
861        let obj_u = obj_idx as usize;
862        let (layer_id, sprite_id) = {
863            let obj = self.ensure_object_mut(stage_u, obj_u);
864            obj.file = None;
865            obj.is_mesh = false;
866            obj.bound_file = None;
867            obj.bound_patno = -1;
868            (obj.layer_id, obj.sprite_id)
869        };
870
871        if let (Some(lid), Some(sid)) = (layer_id, sprite_id)
872            && let Some(sprite) = layers
873                .layer_mut(lid)
874                .and_then(|layer| layer.sprite_mut(sid))
875        {
876            sprite.image_id = None;
877            sprite.mesh_file_name = None;
878            sprite.mesh_kind = 0;
879            sprite.billboard = false;
880            sprite.emote_render = None;
881        }
882        Ok(())
883    }
884
885    pub fn object_change_file(
886        &mut self,
887        images: &mut ImageManager,
888        layers: &mut LayerManager,
889        stage: i64,
890        obj_idx: i64,
891        file: &str,
892        disp: i64,
893        x: i64,
894        y: i64,
895        patno: i64,
896    ) -> Result<()> {
897        self.object_create_impl(
898            images, layers, stage, obj_idx, file, disp, x, y, patno, false,
899        )
900    }
901
902    pub fn object_create_mesh(
903        &mut self,
904        layers: &mut LayerManager,
905        stage: i64,
906        obj_idx: i64,
907        file: &str,
908        disp: i64,
909        x: i64,
910        y: i64,
911        patno: i64,
912    ) -> Result<()> {
913        let stage_i = stage as isize;
914        if !(0..3).contains(&stage_i) {
915            bail!("invalid stage: {stage}");
916        }
917        if obj_idx < 0 {
918            bail!("invalid obj idx: {obj_idx}");
919        }
920
921        let stage_u = stage_i as usize;
922        let obj_u = obj_idx as usize;
923        let current_layer = self.current_layer;
924
925        self.reset_object_for_create(layers, stage_u, obj_u);
926
927        {
928            let obj = self.ensure_object_mut(stage_u, obj_u);
929            obj.is_mesh = true;
930            obj.file = Some(file.to_string());
931            obj.patno = patno;
932            obj.disp = disp != 0;
933            obj.x = x;
934            obj.y = y;
935            if obj.layer_no == 0 {
936                obj.layer_no = current_layer as i64;
937            }
938        }
939
940        if cgm_file_interesting(Some(file)) || (30..=59).contains(&obj_u) {
941            sg_cgm_coord_trace!(
942                "object_create_mesh stage={} obj={} file={} disp={} x={} y={} patno={}",
943                stage,
944                obj_idx,
945                file,
946                disp,
947                x,
948                y,
949                patno
950            );
951        }
952
953        let (lid, sid) = self.ensure_bound_sprite(layers, stage_u, obj_u)?;
954        let sprite = layers
955            .layer_mut(lid)
956            .and_then(|l| l.sprite_mut(sid))
957            .context("mesh sprite not found")?;
958        sprite.visible = disp != 0;
959        sprite.image_id = None;
960        sprite.x = x as i32;
961        sprite.y = y as i32;
962        sprite.alpha = 255;
963        sprite.tr = 255;
964        sprite.fit = SpriteFit::PixelRect;
965        sprite.size_mode = SpriteSizeMode::Intrinsic;
966        sprite.mesh_file_name = Some(file.to_string());
967        sprite.mesh_kind = 1;
968        sprite.camera_enabled = true;
969        sprite.shadow_cast = true;
970        sprite.shadow_receive = true;
971
972        Ok(())
973    }
974
975    /// Mesh counterpart of object_change_file(): rebuild the mesh backing
976    /// without resetting render parameters that survive free_type(false).
977    pub fn object_change_mesh_file(
978        &mut self,
979        layers: &mut LayerManager,
980        stage: i64,
981        obj_idx: i64,
982        file: &str,
983        disp: i64,
984        x: i64,
985        y: i64,
986        patno: i64,
987    ) -> Result<()> {
988        let stage_i = stage as isize;
989        if !(0..3).contains(&stage_i) {
990            bail!("invalid stage: {stage}");
991        }
992        if obj_idx < 0 {
993            bail!("invalid obj idx: {obj_idx}");
994        }
995        let stage_u = stage_i as usize;
996        let obj_u = obj_idx as usize;
997        let current_layer = self.current_layer;
998
999        {
1000            let obj = self.ensure_object_mut(stage_u, obj_u);
1001            obj.is_mesh = true;
1002            obj.file = Some(file.to_string());
1003            obj.patno = patno;
1004            obj.disp = disp != 0;
1005            obj.x = x;
1006            obj.y = y;
1007            if obj.layer_no == 0 {
1008                obj.layer_no = current_layer as i64;
1009            }
1010        }
1011
1012        let (lid, sid) = self.ensure_bound_sprite(layers, stage_u, obj_u)?;
1013        let sprite = layers
1014            .layer_mut(lid)
1015            .and_then(|l| l.sprite_mut(sid))
1016            .context("mesh sprite not found")?;
1017        sprite.visible = disp != 0;
1018        sprite.image_id = None;
1019        sprite.x = x as i32;
1020        sprite.y = y as i32;
1021        sprite.fit = SpriteFit::PixelRect;
1022        sprite.size_mode = SpriteSizeMode::Intrinsic;
1023        sprite.mesh_file_name = Some(file.to_string());
1024        sprite.mesh_kind = 1;
1025        sprite.billboard = false;
1026        sprite.camera_enabled = true;
1027        sprite.shadow_cast = true;
1028        sprite.shadow_receive = true;
1029        Ok(())
1030    }
1031
1032    pub fn object_set_disp(
1033        &mut self,
1034        images: &mut ImageManager,
1035        layers: &mut LayerManager,
1036        stage: i64,
1037        obj_idx: i64,
1038        disp: i64,
1039    ) -> Result<()> {
1040        let stage_i = stage as isize;
1041        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1042            return Ok(());
1043        }
1044        let stage_u = stage_i as usize;
1045        let obj_u = obj_idx as usize;
1046        {
1047            let obj = self.ensure_object_mut(stage_u, obj_u);
1048            obj.disp = disp != 0;
1049        }
1050        self.sync_object_sprite(images, layers, stage_u, obj_u)
1051    }
1052
1053    pub fn object_set_pos(
1054        &mut self,
1055        images: &mut ImageManager,
1056        layers: &mut LayerManager,
1057        stage: i64,
1058        obj_idx: i64,
1059        x: i64,
1060        y: i64,
1061    ) -> Result<()> {
1062        let stage_i = stage as isize;
1063        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1064            return Ok(());
1065        }
1066        let stage_u = stage_i as usize;
1067        let obj_u = obj_idx as usize;
1068        let current_layer = self.current_layer;
1069        {
1070            let obj = self.ensure_object_mut(stage_u, obj_u);
1071            obj.x = x;
1072            obj.y = y;
1073            if obj.layer_no == 0 {
1074                obj.layer_no = current_layer as i64;
1075            }
1076        }
1077        if let Some(obj) = self.object(stage_u, obj_u)
1078            && (cgm_file_interesting(obj.file.as_deref()) || (30..=59).contains(&obj_u))
1079        {
1080            sg_cgm_coord_trace!(
1081                "object_set_pos stage={} obj={} file={:?} x={} y={} layer_no={} binding={:?}/{:?}",
1082                stage,
1083                obj_idx,
1084                obj.file.as_deref(),
1085                x,
1086                y,
1087                obj.layer_no,
1088                obj.layer_id,
1089                obj.sprite_id
1090            );
1091        }
1092        self.sync_object_sprite(images, layers, stage_u, obj_u)
1093    }
1094
1095    pub fn object_set_x(
1096        &mut self,
1097        images: &mut ImageManager,
1098        layers: &mut LayerManager,
1099        stage: i64,
1100        obj_idx: i64,
1101        x: i64,
1102    ) -> Result<()> {
1103        let stage_i = stage as isize;
1104        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1105            return Ok(());
1106        }
1107        let stage_u = stage_i as usize;
1108        let obj_u = obj_idx as usize;
1109        {
1110            let obj = self.ensure_object_mut(stage_u, obj_u);
1111            obj.x = x;
1112        }
1113        self.sync_object_sprite(images, layers, stage_u, obj_u)
1114    }
1115
1116    pub fn object_set_y(
1117        &mut self,
1118        images: &mut ImageManager,
1119        layers: &mut LayerManager,
1120        stage: i64,
1121        obj_idx: i64,
1122        y: i64,
1123    ) -> Result<()> {
1124        let stage_i = stage as isize;
1125        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1126            return Ok(());
1127        }
1128        let stage_u = stage_i as usize;
1129        let obj_u = obj_idx as usize;
1130        {
1131            let obj = self.ensure_object_mut(stage_u, obj_u);
1132            obj.y = y;
1133        }
1134        self.sync_object_sprite(images, layers, stage_u, obj_u)
1135    }
1136
1137    pub fn object_set_patno(
1138        &mut self,
1139        images: &mut ImageManager,
1140        layers: &mut LayerManager,
1141        stage: i64,
1142        obj_idx: i64,
1143        patno: i64,
1144    ) -> Result<()> {
1145        let stage_i = stage as isize;
1146        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1147            return Ok(());
1148        }
1149        let stage_u = stage_i as usize;
1150        let obj_u = obj_idx as usize;
1151        {
1152            let obj = self.ensure_object_mut(stage_u, obj_u);
1153            obj.patno = patno;
1154        }
1155        self.sync_object_sprite(images, layers, stage_u, obj_u)
1156    }
1157
1158    pub fn object_set_layer(
1159        &mut self,
1160        images: &mut ImageManager,
1161        layers: &mut LayerManager,
1162        stage: i64,
1163        obj_idx: i64,
1164        layer_no: i64,
1165    ) -> Result<()> {
1166        let stage_i = stage as isize;
1167        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1168            return Ok(());
1169        }
1170        let stage_u = stage_i as usize;
1171        let obj_u = obj_idx as usize;
1172        {
1173            let obj = self.ensure_object_mut(stage_u, obj_u);
1174            obj.layer_no = layer_no;
1175        }
1176        self.sync_object_sprite(images, layers, stage_u, obj_u)
1177    }
1178
1179    pub fn object_set_order(
1180        &mut self,
1181        images: &mut ImageManager,
1182        layers: &mut LayerManager,
1183        stage: i64,
1184        obj_idx: i64,
1185        order: i64,
1186    ) -> Result<()> {
1187        let stage_i = stage as isize;
1188        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1189            return Ok(());
1190        }
1191        let stage_u = stage_i as usize;
1192        let obj_u = obj_idx as usize;
1193        {
1194            let obj = self.ensure_object_mut(stage_u, obj_u);
1195            obj.order = order;
1196        }
1197        self.sync_object_sprite(images, layers, stage_u, obj_u)
1198    }
1199
1200    pub fn object_set_alpha(
1201        &mut self,
1202        images: &mut ImageManager,
1203        layers: &mut LayerManager,
1204        stage: i64,
1205        obj_idx: i64,
1206        alpha: i64,
1207    ) -> Result<()> {
1208        let stage_i = stage as isize;
1209        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1210            return Ok(());
1211        }
1212        let stage_u = stage_i as usize;
1213        let obj_u = obj_idx as usize;
1214        {
1215            let obj = self.ensure_object_mut(stage_u, obj_u);
1216            obj.alpha = alpha;
1217        }
1218        self.sync_object_sprite(images, layers, stage_u, obj_u)
1219    }
1220
1221    pub fn object_set_tr(
1222        &mut self,
1223        images: &mut ImageManager,
1224        layers: &mut LayerManager,
1225        stage: i64,
1226        obj_idx: i64,
1227        tr: i64,
1228    ) -> Result<()> {
1229        let stage_i = stage as isize;
1230        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1231            return Ok(());
1232        }
1233        let stage_u = stage_i as usize;
1234        let obj_u = obj_idx as usize;
1235        {
1236            let obj = self.ensure_object_mut(stage_u, obj_u);
1237            obj.tr = tr;
1238        }
1239        self.sync_object_sprite(images, layers, stage_u, obj_u)
1240    }
1241
1242    pub fn object_set_mono(
1243        &mut self,
1244        images: &mut ImageManager,
1245        layers: &mut LayerManager,
1246        stage: i64,
1247        obj_idx: i64,
1248        mono: i64,
1249    ) -> Result<()> {
1250        let stage_i = stage as isize;
1251        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1252            return Ok(());
1253        }
1254        let stage_u = stage_i as usize;
1255        let obj_u = obj_idx as usize;
1256        {
1257            let obj = self.ensure_object_mut(stage_u, obj_u);
1258            obj.mono = mono;
1259        }
1260        self.sync_object_sprite(images, layers, stage_u, obj_u)
1261    }
1262
1263    pub fn object_set_reverse(
1264        &mut self,
1265        images: &mut ImageManager,
1266        layers: &mut LayerManager,
1267        stage: i64,
1268        obj_idx: i64,
1269        reverse: i64,
1270    ) -> Result<()> {
1271        let stage_i = stage as isize;
1272        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1273            return Ok(());
1274        }
1275        let stage_u = stage_i as usize;
1276        let obj_u = obj_idx as usize;
1277        {
1278            let obj = self.ensure_object_mut(stage_u, obj_u);
1279            obj.reverse = reverse;
1280        }
1281        self.sync_object_sprite(images, layers, stage_u, obj_u)
1282    }
1283
1284    pub fn object_set_bright(
1285        &mut self,
1286        images: &mut ImageManager,
1287        layers: &mut LayerManager,
1288        stage: i64,
1289        obj_idx: i64,
1290        bright: i64,
1291    ) -> Result<()> {
1292        let stage_i = stage as isize;
1293        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1294            return Ok(());
1295        }
1296        let stage_u = stage_i as usize;
1297        let obj_u = obj_idx as usize;
1298        {
1299            let obj = self.ensure_object_mut(stage_u, obj_u);
1300            obj.bright = bright;
1301        }
1302        self.sync_object_sprite(images, layers, stage_u, obj_u)
1303    }
1304
1305    pub fn object_set_dark(
1306        &mut self,
1307        images: &mut ImageManager,
1308        layers: &mut LayerManager,
1309        stage: i64,
1310        obj_idx: i64,
1311        dark: i64,
1312    ) -> Result<()> {
1313        let stage_i = stage as isize;
1314        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1315            return Ok(());
1316        }
1317        let stage_u = stage_i as usize;
1318        let obj_u = obj_idx as usize;
1319        {
1320            let obj = self.ensure_object_mut(stage_u, obj_u);
1321            obj.dark = dark;
1322        }
1323        self.sync_object_sprite(images, layers, stage_u, obj_u)
1324    }
1325
1326    pub fn object_set_color_rate(
1327        &mut self,
1328        images: &mut ImageManager,
1329        layers: &mut LayerManager,
1330        stage: i64,
1331        obj_idx: i64,
1332        rate: i64,
1333    ) -> Result<()> {
1334        let stage_i = stage as isize;
1335        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1336            return Ok(());
1337        }
1338        let stage_u = stage_i as usize;
1339        let obj_u = obj_idx as usize;
1340        {
1341            let obj = self.ensure_object_mut(stage_u, obj_u);
1342            obj.color_rate = rate;
1343        }
1344        self.sync_object_sprite(images, layers, stage_u, obj_u)
1345    }
1346
1347    pub fn object_set_color_add(
1348        &mut self,
1349        images: &mut ImageManager,
1350        layers: &mut LayerManager,
1351        stage: i64,
1352        obj_idx: i64,
1353        r: i64,
1354        g: i64,
1355        b: i64,
1356    ) -> Result<()> {
1357        let stage_i = stage as isize;
1358        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1359            return Ok(());
1360        }
1361        let stage_u = stage_i as usize;
1362        let obj_u = obj_idx as usize;
1363        {
1364            let obj = self.ensure_object_mut(stage_u, obj_u);
1365            obj.color_add_r = r;
1366            obj.color_add_g = g;
1367            obj.color_add_b = b;
1368        }
1369        self.sync_object_sprite(images, layers, stage_u, obj_u)
1370    }
1371
1372    pub fn object_set_color(
1373        &mut self,
1374        images: &mut ImageManager,
1375        layers: &mut LayerManager,
1376        stage: i64,
1377        obj_idx: i64,
1378        r: i64,
1379        g: i64,
1380        b: i64,
1381    ) -> Result<()> {
1382        let stage_i = stage as isize;
1383        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1384            return Ok(());
1385        }
1386        let stage_u = stage_i as usize;
1387        let obj_u = obj_idx as usize;
1388        {
1389            let obj = self.ensure_object_mut(stage_u, obj_u);
1390            obj.color_r = r;
1391            obj.color_g = g;
1392            obj.color_b = b;
1393        }
1394        self.sync_object_sprite(images, layers, stage_u, obj_u)
1395    }
1396
1397    pub fn object_set_blend(
1398        &mut self,
1399        images: &mut ImageManager,
1400        layers: &mut LayerManager,
1401        stage: i64,
1402        obj_idx: i64,
1403        blend: i64,
1404    ) -> Result<()> {
1405        let stage_i = stage as isize;
1406        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1407            return Ok(());
1408        }
1409        let stage_u = stage_i as usize;
1410        let obj_u = obj_idx as usize;
1411        {
1412            let obj = self.ensure_object_mut(stage_u, obj_u);
1413            obj.blend = blend;
1414        }
1415        self.sync_object_sprite(images, layers, stage_u, obj_u)
1416    }
1417
1418    pub fn object_set_light_no(
1419        &mut self,
1420        images: &mut ImageManager,
1421        layers: &mut LayerManager,
1422        stage: i64,
1423        obj_idx: i64,
1424        light_no: i64,
1425    ) -> Result<()> {
1426        let stage_i = stage as isize;
1427        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1428            return Ok(());
1429        }
1430        let stage_u = stage_i as usize;
1431        let obj_u = obj_idx as usize;
1432        {
1433            let obj = self.ensure_object_mut(stage_u, obj_u);
1434            obj.light_no = light_no;
1435        }
1436        self.sync_object_sprite(images, layers, stage_u, obj_u)
1437    }
1438
1439    pub fn object_set_fog_use(
1440        &mut self,
1441        images: &mut ImageManager,
1442        layers: &mut LayerManager,
1443        stage: i64,
1444        obj_idx: i64,
1445        fog_use: i64,
1446    ) -> Result<()> {
1447        let stage_i = stage as isize;
1448        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1449            return Ok(());
1450        }
1451        let stage_u = stage_i as usize;
1452        let obj_u = obj_idx as usize;
1453        {
1454            let obj = self.ensure_object_mut(stage_u, obj_u);
1455            obj.fog_use = fog_use;
1456        }
1457        self.sync_object_sprite(images, layers, stage_u, obj_u)
1458    }
1459
1460    pub fn object_set_z(&mut self, stage: i64, obj_idx: i64, z: i64) -> Result<()> {
1461        let stage_i = stage as isize;
1462        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1463            return Ok(());
1464        }
1465        let stage_u = stage_i as usize;
1466        let obj_u = obj_idx as usize;
1467        let obj = self.ensure_object_mut(stage_u, obj_u);
1468        obj.z = z;
1469        Ok(())
1470    }
1471
1472    pub fn object_clear(
1473        &mut self,
1474        images: &mut ImageManager,
1475        layers: &mut LayerManager,
1476        stage: i64,
1477        obj_idx: i64,
1478    ) -> Result<()> {
1479        let stage_i = stage as isize;
1480        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1481            return Ok(());
1482        }
1483        let stage_u = stage_i as usize;
1484        let obj_u = obj_idx as usize;
1485        let (layer_id, sprite_id) = {
1486            let obj = self.ensure_object_mut(stage_u, obj_u);
1487            obj.file = None;
1488            obj.patno = 0;
1489            obj.bound_file = None;
1490            obj.bound_patno = -1;
1491            obj.disp = false;
1492            obj.alpha = 255;
1493            (obj.layer_id, obj.sprite_id)
1494        };
1495
1496        // free_type()/restruct_pct() in the original engine drops the album on
1497        // failure/free. Merely hiding our sprite is insufficient: retaining an
1498        // old image_id lets a later DISP write resurrect stale pixels even
1499        // though the logical object has no file/album anymore.
1500        if let (Some(lid), Some(sid)) = (layer_id, sprite_id)
1501            && let Some(sprite) = layers
1502                .layer_mut(lid)
1503                .and_then(|layer| layer.sprite_mut(sid))
1504        {
1505            *sprite = Sprite::default();
1506        }
1507
1508        // Kept in the signature because object_clear is paired with the other
1509        // image-backed Gfx operations and callers already pass ImageManager.
1510        let _ = images;
1511        Ok(())
1512    }
1513
1514    pub fn clear_objects_in_layer_no(
1515        &mut self,
1516        images: &mut ImageManager,
1517        layers: &mut LayerManager,
1518        layer_no: i64,
1519    ) -> Result<()> {
1520        for stage in 0..3usize {
1521            let mut indices: Vec<usize> = self.stages[stage].objects.keys().copied().collect();
1522            indices.sort_unstable();
1523            for obj_idx in indices {
1524                let matches = self
1525                    .object(stage, obj_idx)
1526                    .map(|o| o.layer_no == layer_no)
1527                    .unwrap_or(false);
1528                if !matches {
1529                    continue;
1530                }
1531                {
1532                    let obj = self.ensure_object_mut(stage, obj_idx);
1533                    obj.disp = false;
1534                }
1535                let _ = self.sync_object_sprite(images, layers, stage, obj_idx);
1536            }
1537        }
1538        Ok(())
1539    }
1540
1541    pub fn object_get_pos(&self, stage: i64, obj_idx: i64) -> Option<(i64, i64)> {
1542        self.object_peek_pos(stage, obj_idx)
1543    }
1544
1545    pub fn object_get_disp(&self, stage: i64, obj_idx: i64) -> Option<bool> {
1546        self.object_peek_disp(stage, obj_idx).map(|v| v != 0)
1547    }
1548
1549    pub fn object_get_patno(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1550        self.object_peek_patno(stage, obj_idx)
1551    }
1552
1553    pub fn object_get_layer(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1554        self.object_peek_layer(stage, obj_idx)
1555    }
1556
1557    pub fn object_get_order(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1558        self.object_peek_order(stage, obj_idx)
1559    }
1560
1561    pub fn object_get_alpha(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1562        self.object_peek_alpha(stage, obj_idx)
1563    }
1564
1565    pub fn object_set_pat_no(
1566        &mut self,
1567        images: &mut ImageManager,
1568        layers: &mut LayerManager,
1569        stage: i64,
1570        obj_idx: i64,
1571        patno: i64,
1572    ) -> Result<()> {
1573        self.object_set_patno(images, layers, stage, obj_idx, patno)
1574    }
1575
1576    pub fn object_peek_pos(&self, stage: i64, obj_idx: i64) -> Option<(i64, i64)> {
1577        let stage_i = stage as isize;
1578        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1579            return None;
1580        }
1581        let stage_u = stage_i as usize;
1582        let obj_u = obj_idx as usize;
1583        let obj = self.object(stage_u, obj_u)?;
1584        Some((obj.x, obj.y))
1585    }
1586
1587    pub fn object_peek_disp(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1588        let stage_i = stage as isize;
1589        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1590            return None;
1591        }
1592        let stage_u = stage_i as usize;
1593        let obj_u = obj_idx as usize;
1594        let obj = self.object(stage_u, obj_u)?;
1595        Some(if obj.disp { 1 } else { 0 })
1596    }
1597
1598    pub fn object_peek_patno(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1599        let stage_i = stage as isize;
1600        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1601            return None;
1602        }
1603        let stage_u = stage_i as usize;
1604        let obj_u = obj_idx as usize;
1605        let obj = self.object(stage_u, obj_u)?;
1606        Some(obj.patno)
1607    }
1608
1609    pub fn object_peek_layer(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1610        let stage_i = stage as isize;
1611        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1612            return None;
1613        }
1614        let stage_u = stage_i as usize;
1615        let obj_u = obj_idx as usize;
1616        let obj = self.object(stage_u, obj_u)?;
1617        Some(obj.layer_no)
1618    }
1619
1620    pub fn object_peek_order(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1621        let stage_i = stage as isize;
1622        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1623            return None;
1624        }
1625        let stage_u = stage_i as usize;
1626        let obj_u = obj_idx as usize;
1627        let obj = self.object(stage_u, obj_u)?;
1628        Some(obj.order)
1629    }
1630
1631    pub fn object_peek_alpha(&self, stage: i64, obj_idx: i64) -> Option<i64> {
1632        let stage_i = stage as isize;
1633        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1634            return None;
1635        }
1636        let stage_u = stage_i as usize;
1637        let obj_u = obj_idx as usize;
1638        let obj = self.object(stage_u, obj_u)?;
1639        Some(obj.alpha)
1640    }
1641
1642    pub fn object_peek_file(&self, stage: i64, obj_idx: i64) -> Option<String> {
1643        let stage_i = stage as isize;
1644        if !(0..3).contains(&stage_i) || obj_idx < 0 {
1645            return None;
1646        }
1647        let stage_u = stage_i as usize;
1648        let obj_u = obj_idx as usize;
1649        let obj = self.object(stage_u, obj_u)?;
1650        obj.file.clone()
1651    }
1652}
1653
1654fn is_probable_mesh_path(file: &str) -> bool {
1655    let lower = file.to_ascii_lowercase();
1656    lower.ends_with(".x")
1657        || lower.ends_with(".obj")
1658        || lower.ends_with(".fbx")
1659        || lower.ends_with(".gltf")
1660        || lower.ends_with(".glb")
1661}
1662
1663fn set_object_sprite_image(sprite: &mut Sprite, images: &ImageManager, image_id: ImageHandle) {
1664    sprite.image_id = Some(image_id.clone());
1665    if let Some(img) = images.get(&image_id) {
1666        sprite.object_anchor = true;
1667        sprite.texture_center_x = img.center_x as f32;
1668        sprite.texture_center_y = img.center_y as f32;
1669    } else {
1670        sprite.object_anchor = false;
1671        sprite.texture_center_x = 0.0;
1672        sprite.texture_center_y = 0.0;
1673    }
1674}
1675
1676fn clip_rect(use_flag: i64, left: i64, top: i64, right: i64, bottom: i64) -> Option<ClipRect> {
1677    if use_flag == 0 {
1678        return None;
1679    }
1680    Some(ClipRect {
1681        left: left as i32,
1682        top: top as i32,
1683        right: right as i32,
1684        bottom: bottom as i32,
1685    })
1686}
1687
1688#[cfg(test)]
1689mod tests {
1690    use super::GfxRuntime;
1691    use crate::layer::LayerManager;
1692
1693    #[test]
1694    fn front_object_zero_mesh_gets_an_ordinary_sprite_binding() {
1695        let mut gfx = GfxRuntime::new();
1696        let mut layers = LayerManager::new();
1697
1698        gfx.object_create_mesh(&mut layers, 1, 0, "room.x", 1, 10, 20, 0)
1699            .unwrap();
1700
1701        let (layer_id, sprite_id) = gfx.object_sprite_binding(1, 0).unwrap();
1702        let sprite = layers.layer(layer_id).unwrap().sprite(sprite_id).unwrap();
1703        assert_eq!(sprite.mesh_kind, 1);
1704        assert_eq!(sprite.mesh_file_name.as_deref(), Some("room.x"));
1705        assert!(!gfx.has_missing_bound_object_image(&layers));
1706    }
1707
1708    #[test]
1709    fn back_object_zero_pct_gets_an_ordinary_sprite_binding() {
1710        let project_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1711        let mut gfx = GfxRuntime::new();
1712        let mut images = crate::image_manager::ImageManager::new(project_dir);
1713        let mut layers = LayerManager::new();
1714
1715        // The image load is expected to fail for this synthetic path. The
1716        // important invariant is established first: BACK.OBJECT[0] is a normal
1717        // stage object and owns a stable sprite just like every other OBJECT.
1718        let _ = gfx.object_create(&mut images, &mut layers, 0, 0, "missing.g00", 1, 10, 20, 0);
1719
1720        let (layer_id, sprite_id) = gfx.object_sprite_binding(0, 0).unwrap();
1721        let sprite = layers.layer(layer_id).unwrap().sprite(sprite_id).unwrap();
1722        assert!(!gfx.debug_object_snapshot(0, 0).unwrap().is_bg);
1723        assert!(!sprite.billboard);
1724    }
1725
1726    #[test]
1727    fn back_object_zero_billboard_is_not_misclassified_as_background() {
1728        let project_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1729        let mut gfx = GfxRuntime::new();
1730        let mut images = crate::image_manager::ImageManager::new(project_dir);
1731        let mut layers = LayerManager::new();
1732
1733        // The image load is expected to fail for this synthetic path, but the
1734        // classification and binding happen before that error is returned.
1735        let _ = gfx.object_create_billboard(
1736            &mut images,
1737            &mut layers,
1738            0,
1739            0,
1740            "missing.g00",
1741            1,
1742            10,
1743            20,
1744            0,
1745        );
1746
1747        let (layer_id, sprite_id) = gfx.object_sprite_binding(0, 0).unwrap();
1748        let sprite = layers.layer(layer_id).unwrap().sprite(sprite_id).unwrap();
1749        assert!(!gfx.debug_object_snapshot(0, 0).unwrap().is_bg);
1750        assert!(sprite.billboard);
1751    }
1752}