Skip to main content

mmd_anim_format/
pmm.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::Serialize;
4
5use crate::binary::{
6    ByteReader, write_f32_le as push_f32, write_i32_le as push_i32, write_u32_le as push_u32,
7};
8use crate::error::ImportError;
9use crate::pmx::PmxParsedModel;
10use crate::sjis::{decode_sjis, decode_sjis_trim_nul, encode_sjis, encode_sjis_prefix_fit};
11use crate::vmd::{VmdParsedAnimation, VmdParsedBoneFrame, VmdParsedMorphFrame};
12
13#[derive(Debug, Clone, Serialize)]
14#[serde(rename_all = "camelCase")]
15pub struct PmmParsedManifest {
16    pub signature: &'static str,
17    pub version: String,
18    pub parsed_version: Option<u32>,
19    pub byte_length: usize,
20    pub project_settings: PmmProjectSettings,
21    pub timeline: PmmTimeline,
22    pub display_state: PmmDisplayState,
23    pub header_text_entries: Vec<PmmHeaderTextEntry>,
24    pub model_slots: Vec<PmmModelSlot>,
25    pub document_summary: Option<PmmDocumentSummary>,
26    pub document_global_summary: Option<PmmDocumentGlobalSummary>,
27    pub project_graph: Option<PmmProjectGraph>,
28    pub asset_summary: PmmAssetSummary,
29    pub asset_references: Vec<PmmAssetReference>,
30    pub model_assets: Vec<PmmSceneAsset>,
31    pub accessory_assets: Vec<PmmSceneAsset>,
32    pub motion_assets: Vec<PmmSceneAsset>,
33    pub audio_assets: Vec<PmmSceneAsset>,
34    pub image_assets: Vec<PmmSceneAsset>,
35    pub video_assets: Vec<PmmSceneAsset>,
36    pub model_paths: Vec<String>,
37    pub accessory_paths: Vec<String>,
38    pub motion_paths: Vec<String>,
39    pub audio_paths: Vec<String>,
40    pub image_paths: Vec<String>,
41    pub video_paths: Vec<String>,
42    pub diagnostics: Vec<PmmParserDiagnostic>,
43    #[serde(skip)]
44    source_bytes: Vec<u8>,
45}
46
47#[derive(Debug, Clone, Serialize)]
48#[serde(rename_all = "camelCase")]
49pub struct PmmProjectSettings {
50    pub screen_width: Option<u32>,
51    pub screen_height: Option<u32>,
52    pub timeline_frame_count: Option<u32>,
53    pub frame_rate: Option<f32>,
54}
55
56#[derive(Debug, Clone, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct PmmTimeline {
59    pub start_frame: Option<u32>,
60    pub end_frame_exclusive: Option<u32>,
61    pub frame_count: Option<u32>,
62    pub frame_rate: Option<f32>,
63    pub duration_seconds: Option<f32>,
64}
65
66#[derive(Debug, Clone, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct PmmDisplayState {
69    pub layout: &'static str,
70    pub model_slot_flags: Vec<u8>,
71    pub model_slot_flag_entries: Vec<PmmModelSlotFlagEntry>,
72    pub document_expand_flags: Option<PmmDocumentExpandFlags>,
73    pub selected_model_index: Option<u8>,
74    pub document_model_count: Option<u8>,
75    pub declared_model_slot_count: Option<u8>,
76    pub model_slot_count: usize,
77    pub non_zero_model_slot_count: usize,
78    pub accessory_slot_count: Option<u8>,
79    pub active_model_slot_indices: Vec<usize>,
80    pub empty_model_slot_indices: Vec<usize>,
81    pub model_slot_flag_counts: BTreeMap<u8, usize>,
82}
83
84#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct PmmDocumentExpandFlags {
87    pub editing_cla: bool,
88    pub camera_panel: bool,
89    pub light_panel: bool,
90    pub accessory_panel: bool,
91    pub bone_panel: bool,
92    pub morph_panel: bool,
93    pub self_shadow_panel: bool,
94}
95
96#[derive(Debug, Clone, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct PmmModelSlotFlagEntry {
99    pub slot_index: usize,
100    pub flag: u8,
101    pub active: bool,
102}
103
104#[derive(Debug, Clone, Serialize)]
105#[serde(rename_all = "camelCase")]
106pub struct PmmHeaderTextEntry {
107    pub index: usize,
108    pub offset: usize,
109    pub offset_end: usize,
110    pub text: String,
111    pub text_bytes: Vec<u8>,
112}
113
114#[derive(Debug, Clone, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct PmmModelSlot {
117    pub slot_index: usize,
118    pub display_slot_index: Option<usize>,
119    pub offset: usize,
120    pub offset_end: usize,
121    pub model_path_offset: usize,
122    pub trailing_zero_padding_bytes: usize,
123    pub next_non_zero_offset: Option<usize>,
124    pub name: String,
125    pub name_bytes: Vec<u8>,
126    pub english_name: String,
127    pub english_name_bytes: Vec<u8>,
128    pub model_path: String,
129    pub normalized_path: String,
130    pub asset_reference_index: Option<usize>,
131    pub confidence: &'static str,
132}
133
134#[derive(Debug, Clone, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct PmmAssetSummary {
137    pub reference_count: usize,
138    pub high_confidence_count: usize,
139    pub medium_confidence_count: usize,
140    pub low_confidence_count: usize,
141    pub kind_counts: BTreeMap<String, usize>,
142    pub extension_counts: BTreeMap<String, usize>,
143    pub confidence_counts: BTreeMap<String, usize>,
144}
145
146#[derive(Debug, Clone, Serialize)]
147#[serde(rename_all = "camelCase")]
148pub struct PmmDocumentSummary {
149    pub source: &'static str,
150    pub selected_model_index: u8,
151    pub model_count: usize,
152    pub counts: PmmDocumentCounts,
153    pub models: Vec<PmmDocumentModelSummary>,
154}
155
156#[derive(Debug, Clone, Serialize)]
157#[serde(rename_all = "camelCase")]
158pub struct PmmDocumentCounts {
159    pub models: usize,
160    pub bones: usize,
161    pub morphs: usize,
162    pub initial_bone_keyframes: usize,
163    pub bone_keyframes: usize,
164    pub initial_morph_keyframes: usize,
165    pub morph_keyframes: usize,
166    pub initial_model_keyframes: usize,
167    pub model_keyframes: usize,
168}
169
170#[derive(Debug, Clone, Serialize)]
171#[serde(rename_all = "camelCase")]
172pub struct PmmDocumentGlobalSummary {
173    pub source: &'static str,
174    pub offset: usize,
175    pub offset_end: usize,
176    pub camera: PmmDocumentTrackSummary,
177    pub light: PmmDocumentTrackSummary,
178    pub accessories: PmmDocumentAccessoryBlockSummary,
179    pub settings: PmmDocumentSettingsSummary,
180    pub gravity: PmmDocumentTrackSummary,
181    pub self_shadow: PmmDocumentTrackSummary,
182}
183
184#[derive(Debug, Clone, Serialize)]
185#[serde(rename_all = "camelCase")]
186pub struct PmmProjectSceneSettings {
187    pub offset: usize,
188    pub offset_end: usize,
189    pub current_frame_index: i32,
190    pub current_frame_index_in_text_field: i32,
191    pub begin_frame_index_enabled: bool,
192    pub end_frame_index_enabled: bool,
193    pub begin_frame_index: i32,
194    pub end_frame_index: i32,
195    pub preferred_fps: f32,
196    pub loop_enabled: bool,
197    pub audio_enabled: bool,
198    pub audio_path: String,
199    pub audio_asset_reference_index: Option<usize>,
200    pub background_video_enabled: bool,
201    pub background_video_path: String,
202    pub background_video_asset_reference_index: Option<usize>,
203    pub background_video_offset: [i32; 2],
204    pub background_video_scale_factor: f32,
205    pub background_image_enabled: bool,
206    pub background_image_path: String,
207    pub background_image_asset_reference_index: Option<usize>,
208    pub background_image_offset: [i32; 2],
209    pub background_image_scale_factor: f32,
210}
211
212#[derive(Debug, Clone, Serialize)]
213#[serde(rename_all = "camelCase")]
214pub struct PmmProjectAssetBinding {
215    pub scope: &'static str,
216    pub asset_kind: &'static str,
217    pub owner_index: Option<usize>,
218    pub document_index: Option<u8>,
219    pub owner_name: Option<String>,
220    pub path: String,
221    pub path_offset: Option<usize>,
222    pub asset_reference_index: Option<usize>,
223    pub asset_reference_offset: Option<usize>,
224    pub asset_reference_end_offset: Option<usize>,
225    pub asset_reference_confidence: Option<&'static str>,
226}
227
228#[derive(Debug, Clone, Serialize)]
229#[serde(rename_all = "camelCase")]
230pub struct PmmProjectExportBlocker {
231    pub code: &'static str,
232    pub severity: &'static str,
233    pub message: String,
234    pub scope: Option<&'static str>,
235    pub kind: Option<&'static str>,
236    pub count: Option<usize>,
237    pub coverage_ratio: Option<f32>,
238}
239
240#[derive(Debug, Clone, Serialize)]
241#[serde(rename_all = "camelCase")]
242pub struct PmmProjectExportReadiness {
243    pub lossless_parsed_byte_export_supported: bool,
244    pub semantic_graph_export_supported: bool,
245    pub source_byte_preservation_required: bool,
246    pub blocker_count: usize,
247    pub blockers: Vec<PmmProjectExportBlocker>,
248}
249
250#[derive(Debug, Clone, Serialize)]
251#[serde(rename_all = "camelCase")]
252pub struct PmmProjectGraph {
253    pub source: &'static str,
254    pub version: String,
255    pub parsed_version: Option<u32>,
256    pub timeline: PmmTimeline,
257    pub display_state: PmmDisplayState,
258    pub asset_summary: PmmAssetSummary,
259    pub asset_references: Vec<PmmAssetReference>,
260    pub models: Vec<PmmDocumentModelSummary>,
261    pub global: PmmDocumentGlobalSummary,
262    pub model_counts: PmmDocumentCounts,
263    pub accessory_count: usize,
264    pub accessory_keyframes: usize,
265    pub track_references: Vec<PmmProjectTrackReference>,
266    pub keyframe_references: Vec<PmmProjectKeyframeReference>,
267    pub byte_coverage: PmmProjectByteCoverage,
268    pub scene_settings: PmmProjectSceneSettings,
269    pub asset_bindings: Vec<PmmProjectAssetBinding>,
270    pub export_readiness: PmmProjectExportReadiness,
271}
272
273#[derive(Debug, Clone, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct PmmProjectTrackReference {
276    pub scope: &'static str,
277    pub track_kind: &'static str,
278    pub owner_index: Option<usize>,
279    pub document_index: Option<u8>,
280    pub owner_name: Option<String>,
281    pub initial_keyframes: usize,
282    pub keyframes: usize,
283    pub initial_keyframes_offset: Option<usize>,
284    pub keyframe_count_offset: Option<usize>,
285    pub keyframes_offset: usize,
286    pub keyframes_end_offset: usize,
287    pub state_offset: Option<usize>,
288    pub state_end_offset: Option<usize>,
289}
290
291#[derive(Debug, Clone, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct PmmProjectKeyframeReference {
294    pub scope: &'static str,
295    pub track_kind: &'static str,
296    pub owner_index: Option<usize>,
297    pub document_index: Option<u8>,
298    pub owner_name: Option<String>,
299    pub initial: bool,
300    pub keyframe_index: Option<i32>,
301    pub frame_index: i32,
302    pub previous_keyframe_index: i32,
303    pub next_keyframe_index: i32,
304    pub offset: usize,
305    pub byte_length: usize,
306    pub payload_offset: usize,
307    pub payload_byte_length: usize,
308}
309
310#[derive(Debug, Clone, Serialize)]
311#[serde(rename_all = "camelCase")]
312pub struct PmmProjectByteRange {
313    pub scope: &'static str,
314    pub kind: &'static str,
315    pub owner_index: Option<usize>,
316    pub document_index: Option<u8>,
317    pub name: Option<String>,
318    pub offset: usize,
319    pub offset_end: usize,
320    pub byte_length: usize,
321}
322
323#[derive(Debug, Clone, Serialize)]
324#[serde(rename_all = "camelCase")]
325pub struct PmmProjectByteCoverage {
326    pub offset: usize,
327    pub offset_end: usize,
328    pub byte_length: usize,
329    pub covered_byte_length: usize,
330    pub coverage_ratio: f32,
331    pub gap_count: usize,
332    pub gaps: Vec<PmmProjectByteRange>,
333    pub range_count: usize,
334    pub ranges: Vec<PmmProjectByteRange>,
335}
336
337#[derive(Debug, Clone, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct PmmDocumentTrackSummary {
340    pub offset: usize,
341    pub offset_end: usize,
342    pub initial_keyframes: usize,
343    pub keyframes: usize,
344    pub initial_keyframe: Option<PmmDocumentKeyframeSummary>,
345    pub keyframe_summaries: Vec<PmmDocumentKeyframeSummary>,
346    pub keyframe_count_offset: usize,
347    pub keyframes_offset: usize,
348    pub keyframes_end_offset: usize,
349    pub state_offset: Option<usize>,
350    pub state_end_offset: Option<usize>,
351}
352
353#[derive(Debug, Clone, Serialize)]
354#[serde(
355    rename_all = "camelCase",
356    rename_all_fields = "camelCase",
357    tag = "kind"
358)]
359pub enum PmmDocumentKeyframeSummary {
360    Camera {
361        index: Option<i32>,
362        frame_index: i32,
363        previous_keyframe_index: i32,
364        next_keyframe_index: i32,
365        distance: f32,
366        look_at: [f32; 3],
367        angle: [f32; 3],
368        parent_model_index: i32,
369        parent_model_bone_index: i32,
370        interpolation: [u8; 24],
371        perspective_view: bool,
372        fov: i32,
373        selected: bool,
374        distance_offset: usize,
375        look_at_offset: usize,
376        look_at_byte_length: usize,
377        angle_offset: usize,
378        angle_byte_length: usize,
379        parent_model_index_offset: usize,
380        parent_model_bone_index_offset: usize,
381        interpolation_offset: usize,
382        interpolation_byte_length: usize,
383        perspective_view_offset: usize,
384        fov_offset: usize,
385        selected_offset: usize,
386        offset: usize,
387        byte_length: usize,
388        payload_offset: usize,
389        payload_byte_length: usize,
390        payload_bytes: Vec<u8>,
391    },
392    Light {
393        index: Option<i32>,
394        frame_index: i32,
395        previous_keyframe_index: i32,
396        next_keyframe_index: i32,
397        color: [f32; 3],
398        direction: [f32; 3],
399        selected: bool,
400        color_offset: usize,
401        color_byte_length: usize,
402        direction_offset: usize,
403        direction_byte_length: usize,
404        selected_offset: usize,
405        offset: usize,
406        byte_length: usize,
407        payload_offset: usize,
408        payload_byte_length: usize,
409        payload_bytes: Vec<u8>,
410    },
411    Gravity {
412        index: Option<i32>,
413        frame_index: i32,
414        previous_keyframe_index: i32,
415        next_keyframe_index: i32,
416        noise_enabled: bool,
417        noise: i32,
418        acceleration: f32,
419        direction: [f32; 3],
420        selected: bool,
421        noise_enabled_offset: usize,
422        noise_offset: usize,
423        acceleration_offset: usize,
424        direction_offset: usize,
425        direction_byte_length: usize,
426        selected_offset: usize,
427        offset: usize,
428        byte_length: usize,
429        payload_offset: usize,
430        payload_byte_length: usize,
431        payload_bytes: Vec<u8>,
432    },
433    SelfShadow {
434        index: Option<i32>,
435        frame_index: i32,
436        previous_keyframe_index: i32,
437        next_keyframe_index: i32,
438        mode: u8,
439        distance: f32,
440        selected: bool,
441        mode_offset: usize,
442        distance_offset: usize,
443        selected_offset: usize,
444        offset: usize,
445        byte_length: usize,
446        payload_offset: usize,
447        payload_byte_length: usize,
448        payload_bytes: Vec<u8>,
449    },
450}
451
452#[derive(Debug, Clone, Copy)]
453struct PmmDocumentBaseKeyframeSummary {
454    index: Option<i32>,
455    frame_index: i32,
456    previous_keyframe_index: i32,
457    next_keyframe_index: i32,
458}
459
460#[derive(Debug, Clone, Serialize)]
461#[serde(rename_all = "camelCase")]
462pub struct PmmDocumentAccessoryKeyframeSummary {
463    pub index: Option<i32>,
464    pub frame_index: i32,
465    pub previous_keyframe_index: i32,
466    pub next_keyframe_index: i32,
467    pub visible: bool,
468    pub opacity: f32,
469    pub parent_model_index: i32,
470    pub parent_model_bone_index: i32,
471    pub translation: [f32; 3],
472    pub orientation: [f32; 3],
473    pub scale_factor: f32,
474    pub shadow_enabled: bool,
475    pub selected: bool,
476    pub packed_opacity_visible_offset: usize,
477    pub parent_model_index_offset: usize,
478    pub parent_model_bone_index_offset: usize,
479    pub translation_offset: usize,
480    pub translation_byte_length: usize,
481    pub orientation_offset: usize,
482    pub orientation_byte_length: usize,
483    pub scale_factor_offset: usize,
484    pub shadow_enabled_offset: usize,
485    pub selected_offset: usize,
486    pub offset: usize,
487    pub byte_length: usize,
488    pub payload_offset: usize,
489    pub payload_byte_length: usize,
490    pub payload_bytes: Vec<u8>,
491}
492
493#[derive(Debug, Clone, Serialize)]
494#[serde(rename_all = "camelCase")]
495pub struct PmmDocumentBoneKeyframeSummary {
496    pub index: Option<i32>,
497    pub frame_index: i32,
498    pub previous_keyframe_index: i32,
499    pub next_keyframe_index: i32,
500    pub interpolation: [u8; 16],
501    pub translation: [f32; 3],
502    pub orientation: [f32; 4],
503    pub physics_disabled: bool,
504    pub selected: bool,
505    pub offset: usize,
506    pub byte_length: usize,
507    pub payload_offset: usize,
508    pub payload_byte_length: usize,
509    pub payload_bytes: Vec<u8>,
510}
511
512#[derive(Debug, Clone, Serialize)]
513#[serde(rename_all = "camelCase")]
514pub struct PmmDocumentMorphKeyframeSummary {
515    pub index: Option<i32>,
516    pub frame_index: i32,
517    pub previous_keyframe_index: i32,
518    pub next_keyframe_index: i32,
519    pub weight: f32,
520    pub selected: bool,
521    pub offset: usize,
522    pub byte_length: usize,
523    pub payload_offset: usize,
524    pub payload_byte_length: usize,
525    pub payload_bytes: Vec<u8>,
526}
527
528#[derive(Debug, Clone, Serialize)]
529#[serde(rename_all = "camelCase")]
530pub struct PmmDocumentModelKeyframeSummary {
531    pub index: Option<i32>,
532    pub frame_index: i32,
533    pub previous_keyframe_index: i32,
534    pub next_keyframe_index: i32,
535    pub visible: bool,
536    pub constraint_states: Vec<bool>,
537    pub outside_parent_indices: Vec<PmmDocumentOutsideParentIndexSummary>,
538    pub self_shadow_enabled: bool,
539    pub visible_offset: usize,
540    pub constraint_state_count: usize,
541    pub constraint_states_offset: usize,
542    pub constraint_states_byte_length: usize,
543    pub outside_parent_index_count: usize,
544    pub outside_parent_indices_offset: usize,
545    pub outside_parent_indices_byte_length: usize,
546    pub self_shadow_enabled_offset: usize,
547    pub offset: usize,
548    pub byte_length: usize,
549    pub payload_offset: usize,
550    pub payload_byte_length: usize,
551    pub payload_bytes: Vec<u8>,
552}
553
554#[derive(Debug, Clone, Serialize)]
555#[serde(rename_all = "camelCase")]
556pub struct PmmDocumentOutsideParentIndexSummary {
557    pub parent_model_index: i32,
558    pub parent_model_bone_index: i32,
559}
560
561#[derive(Debug, Clone, Serialize)]
562#[serde(rename_all = "camelCase")]
563pub struct PmmDocumentBoneStateSummary {
564    pub translation: [f32; 3],
565    pub orientation: [f32; 4],
566    pub dirty: bool,
567    pub physics_disabled: bool,
568    pub selected: bool,
569}
570
571#[derive(Debug, Clone, Serialize)]
572#[serde(rename_all = "camelCase")]
573pub struct PmmDocumentMorphStateSummary {
574    pub weight: f32,
575}
576
577#[derive(Debug, Clone, Serialize)]
578#[serde(rename_all = "camelCase")]
579pub struct PmmDocumentConstraintStateSummary {
580    pub enabled: bool,
581}
582
583/// Raw 16-byte outside-parent state block. Semantics not fully confirmed;
584/// field names reflect observed byte layout only.
585#[derive(Debug, Clone, Serialize)]
586#[serde(rename_all = "camelCase")]
587pub struct PmmDocumentOutsideParentStateSummary {
588    pub parent_model_index: i32,
589    pub parent_model_bone_index: i32,
590    pub subject_bone_index: i32,
591    pub target_model_index: i32,
592}
593
594#[derive(Debug, Clone, Serialize)]
595#[serde(rename_all = "camelCase")]
596pub struct PmmDocumentAccessoryBlockSummary {
597    pub offset: usize,
598    pub offset_end: usize,
599    pub selected_accessory_index: u8,
600    pub horizontal_scroll: i32,
601    pub accessory_count: usize,
602    pub keyframes: usize,
603    pub accessories: Vec<PmmDocumentAccessorySummary>,
604}
605
606#[derive(Debug, Clone, Serialize)]
607#[serde(rename_all = "camelCase")]
608pub struct PmmDocumentAccessorySummary {
609    pub slot_index: usize,
610    pub document_accessory_index: u8,
611    pub offset: usize,
612    pub offset_end: usize,
613    pub path_offset: usize,
614    pub name: String,
615    pub path: String,
616    pub asset_reference_index: Option<usize>,
617    pub draw_order_index: u8,
618    pub keyframes: usize,
619    pub initial_keyframe: PmmDocumentAccessoryKeyframeSummary,
620    pub keyframe_summaries: Vec<PmmDocumentAccessoryKeyframeSummary>,
621    pub keyframe_count_offset: usize,
622    pub keyframes_offset: usize,
623    pub keyframes_end_offset: usize,
624    pub state_offset: usize,
625    pub state_end_offset: usize,
626    pub visible: bool,
627    pub opacity: f32,
628    pub parent_model_index: i32,
629    pub parent_model_bone_index: i32,
630    pub scale_factor: f32,
631    pub shadow_enabled: bool,
632    pub add_blend_enabled: bool,
633}
634
635#[derive(Debug, Clone, Serialize)]
636#[serde(rename_all = "camelCase")]
637pub struct PmmDocumentSettingsSummary {
638    pub offset: usize,
639    pub offset_end: usize,
640    pub current_frame_index: i32,
641    pub current_frame_index_offset: usize,
642    pub horizontal_scroll: i32,
643    pub horizontal_scroll_thumb: i32,
644    pub editing_mode: i32,
645    pub camera_look_mode: u8,
646    pub loop_enabled: bool,
647    pub begin_frame_index_enabled: bool,
648    pub begin_frame_index_enabled_offset: usize,
649    pub end_frame_index_enabled: bool,
650    pub end_frame_index_enabled_offset: usize,
651    pub begin_frame_index: i32,
652    pub begin_frame_index_offset: usize,
653    pub end_frame_index: i32,
654    pub end_frame_index_offset: usize,
655    pub audio_enabled: bool,
656    pub audio_path: String,
657    pub audio_path_offset: usize,
658    pub background_video_offset: [i32; 2],
659    pub background_video_scale_factor: f32,
660    pub background_video_path: String,
661    pub background_video_path_offset: usize,
662    pub background_video_enabled: bool,
663    pub background_image_offset: [i32; 2],
664    pub background_image_scale_factor: f32,
665    pub background_image_path: String,
666    pub background_image_path_offset: usize,
667    pub background_image_enabled: bool,
668    pub information_shown: bool,
669    pub grid_and_axis_shown: bool,
670    pub ground_shadow_shown: bool,
671    pub preferred_fps: f32,
672    pub screen_capture_mode: i32,
673    pub accessory_index_after_models: i32,
674    pub ground_shadow_brightness: f32,
675    pub translucent_ground_shadow_enabled: bool,
676    pub physics_simulation_mode: u8,
677    pub edge_color: [f32; 3],
678    pub black_background_enabled: bool,
679    pub camera_look_at_model_index: i32,
680    pub camera_look_at_model_bone_index: i32,
681    pub unknown_matrix_offset: usize,
682    pub unknown_matrix_end_offset: usize,
683    pub following_look_at_enabled: bool,
684    pub physics_ground_enabled: bool,
685    pub current_frame_index_in_text_field: i32,
686    pub current_frame_index_in_text_field_offset: usize,
687    pub model_selection_footer_present: bool,
688    pub model_selection_footer_offset: Option<usize>,
689    pub model_selection_footer_end_offset: Option<usize>,
690}
691
692#[derive(Debug, Clone, Serialize)]
693#[serde(rename_all = "camelCase")]
694pub struct PmmDocumentModelSummary {
695    pub slot_index: usize,
696    pub document_model_index: u8,
697    pub offset: usize,
698    pub offset_end: usize,
699    pub path_offset: usize,
700    pub name: String,
701    pub english_name: String,
702    pub path: String,
703    pub asset_reference_index: Option<usize>,
704    pub bone_count: usize,
705    pub bone_names: Vec<String>,
706    pub morph_count: usize,
707    pub morph_names: Vec<String>,
708    pub constraint_bone_count: usize,
709    pub outside_parent_subject_bone_count: usize,
710    pub draw_order_index: u8,
711    pub transform_order_index: u8,
712    pub selected_bone_index: i32,
713    pub selected_morph_indices: [i32; 4],
714    pub vertical_scroll: i32,
715    pub sections: PmmDocumentModelSections,
716    pub initial_bone_keyframes: usize,
717    pub initial_bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
718    pub bone_keyframes: usize,
719    pub bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
720    pub initial_morph_keyframes: usize,
721    pub initial_morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
722    pub morph_keyframes: usize,
723    pub morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
724    pub initial_model_keyframes: usize,
725    pub model_keyframes: usize,
726    pub initial_model_keyframe: PmmDocumentModelKeyframeSummary,
727    pub model_keyframe_summaries: Vec<PmmDocumentModelKeyframeSummary>,
728    pub last_frame_index: i32,
729    pub visible: bool,
730    pub blend_enabled: bool,
731    pub edge_width: f32,
732    pub self_shadow_enabled: bool,
733    pub bone_state_summaries: Vec<PmmDocumentBoneStateSummary>,
734    pub morph_state_summaries: Vec<PmmDocumentMorphStateSummary>,
735    pub constraint_state_summaries: Vec<PmmDocumentConstraintStateSummary>,
736    pub outside_parent_state_summaries: Vec<PmmDocumentOutsideParentStateSummary>,
737}
738
739#[derive(Debug, Clone, Serialize)]
740#[serde(rename_all = "camelCase")]
741pub struct PmmDocumentModelSections {
742    pub initial_bone_keyframes_offset: usize,
743    pub bone_keyframe_count_offset: usize,
744    pub bone_keyframes_offset: usize,
745    pub bone_keyframes_end_offset: usize,
746    pub initial_morph_keyframes_offset: usize,
747    pub morph_keyframe_count_offset: usize,
748    pub morph_keyframes_offset: usize,
749    pub morph_keyframes_end_offset: usize,
750    pub initial_model_keyframe_offset: usize,
751    pub model_keyframe_count_offset: usize,
752    pub model_keyframes_offset: usize,
753    pub model_keyframes_end_offset: usize,
754    pub bone_states_offset: usize,
755    pub morph_states_offset: usize,
756    pub constraint_states_offset: usize,
757    pub outside_parent_states_offset: usize,
758}
759
760#[derive(Debug, Clone, Serialize)]
761#[serde(rename_all = "camelCase")]
762pub struct PmmAssetReference {
763    pub path: String,
764    pub normalized_path: String,
765    pub file_name: String,
766    pub extension: String,
767    pub kind: &'static str,
768    pub offset: usize,
769    pub offset_end: usize,
770    pub confidence: &'static str,
771}
772
773#[derive(Debug, Clone, Serialize)]
774#[serde(rename_all = "camelCase")]
775pub struct PmmSceneAsset {
776    pub reference_index: usize,
777    pub kind_index: usize,
778    pub path: String,
779    pub normalized_path: String,
780    pub file_name: String,
781    pub extension: String,
782    pub offset: usize,
783    pub offset_end: usize,
784    pub confidence: &'static str,
785}
786
787#[derive(Debug, Clone, Serialize)]
788#[serde(rename_all = "camelCase")]
789pub struct PmmParserDiagnostic {
790    pub level: &'static str,
791    pub code: &'static str,
792    pub message: String,
793}
794
795#[allow(clippy::too_many_arguments)]
796fn make_keyframe_reference(
797    scope: &'static str,
798    track_kind: &'static str,
799    owner_index: Option<usize>,
800    document_index: Option<u8>,
801    owner_name: Option<String>,
802    initial: bool,
803    keyframe_index: Option<i32>,
804    frame_index: i32,
805    previous_keyframe_index: i32,
806    next_keyframe_index: i32,
807    offset: usize,
808    byte_length: usize,
809    payload_offset: usize,
810    payload_byte_length: usize,
811) -> PmmProjectKeyframeReference {
812    PmmProjectKeyframeReference {
813        scope,
814        track_kind,
815        owner_index,
816        document_index,
817        owner_name,
818        initial,
819        keyframe_index,
820        frame_index,
821        previous_keyframe_index,
822        next_keyframe_index,
823        offset,
824        byte_length,
825        payload_offset,
826        payload_byte_length,
827    }
828}
829
830fn make_global_track_ref(
831    track: &PmmDocumentTrackSummary,
832    kind: &'static str,
833) -> PmmProjectTrackReference {
834    let initial_kf_offset = track.initial_keyframe.as_ref().map(|kf| match kf {
835        PmmDocumentKeyframeSummary::Camera { offset, .. } => *offset,
836        PmmDocumentKeyframeSummary::Light { offset, .. } => *offset,
837        PmmDocumentKeyframeSummary::Gravity { offset, .. } => *offset,
838        PmmDocumentKeyframeSummary::SelfShadow { offset, .. } => *offset,
839    });
840    PmmProjectTrackReference {
841        scope: "global",
842        track_kind: kind,
843        owner_index: None,
844        document_index: None,
845        owner_name: None,
846        initial_keyframes: track.initial_keyframes,
847        keyframes: track.keyframes,
848        initial_keyframes_offset: initial_kf_offset,
849        keyframe_count_offset: Some(track.keyframe_count_offset),
850        keyframes_offset: track.keyframes_offset,
851        keyframes_end_offset: track.keyframes_end_offset,
852        state_offset: track.state_offset,
853        state_end_offset: track.state_end_offset,
854    }
855}
856
857fn append_global_keyframe_refs(
858    refs: &mut Vec<PmmProjectKeyframeReference>,
859    track: &PmmDocumentTrackSummary,
860    kind: &'static str,
861) {
862    if let Some(kf) = &track.initial_keyframe {
863        let (index, frame, prev, next, off, blen, poff, plen) = match kf {
864            PmmDocumentKeyframeSummary::Camera {
865                index,
866                frame_index,
867                previous_keyframe_index,
868                next_keyframe_index,
869                offset,
870                byte_length,
871                payload_offset,
872                payload_byte_length,
873                ..
874            } => (
875                *index,
876                *frame_index,
877                *previous_keyframe_index,
878                *next_keyframe_index,
879                *offset,
880                *byte_length,
881                *payload_offset,
882                *payload_byte_length,
883            ),
884            PmmDocumentKeyframeSummary::Light {
885                index,
886                frame_index,
887                previous_keyframe_index,
888                next_keyframe_index,
889                offset,
890                byte_length,
891                payload_offset,
892                payload_byte_length,
893                ..
894            } => (
895                *index,
896                *frame_index,
897                *previous_keyframe_index,
898                *next_keyframe_index,
899                *offset,
900                *byte_length,
901                *payload_offset,
902                *payload_byte_length,
903            ),
904            PmmDocumentKeyframeSummary::Gravity {
905                index,
906                frame_index,
907                previous_keyframe_index,
908                next_keyframe_index,
909                offset,
910                byte_length,
911                payload_offset,
912                payload_byte_length,
913                ..
914            } => (
915                *index,
916                *frame_index,
917                *previous_keyframe_index,
918                *next_keyframe_index,
919                *offset,
920                *byte_length,
921                *payload_offset,
922                *payload_byte_length,
923            ),
924            PmmDocumentKeyframeSummary::SelfShadow {
925                index,
926                frame_index,
927                previous_keyframe_index,
928                next_keyframe_index,
929                offset,
930                byte_length,
931                payload_offset,
932                payload_byte_length,
933                ..
934            } => (
935                *index,
936                *frame_index,
937                *previous_keyframe_index,
938                *next_keyframe_index,
939                *offset,
940                *byte_length,
941                *payload_offset,
942                *payload_byte_length,
943            ),
944        };
945        refs.push(make_keyframe_reference(
946            "global", kind, None, None, None, true, index, frame, prev, next, off, blen, poff, plen,
947        ));
948    }
949    for kf in &track.keyframe_summaries {
950        let (index, frame, prev, next, off, blen, poff, plen) = match kf {
951            PmmDocumentKeyframeSummary::Camera {
952                index,
953                frame_index,
954                previous_keyframe_index,
955                next_keyframe_index,
956                offset,
957                byte_length,
958                payload_offset,
959                payload_byte_length,
960                ..
961            } => (
962                *index,
963                *frame_index,
964                *previous_keyframe_index,
965                *next_keyframe_index,
966                *offset,
967                *byte_length,
968                *payload_offset,
969                *payload_byte_length,
970            ),
971            PmmDocumentKeyframeSummary::Light {
972                index,
973                frame_index,
974                previous_keyframe_index,
975                next_keyframe_index,
976                offset,
977                byte_length,
978                payload_offset,
979                payload_byte_length,
980                ..
981            } => (
982                *index,
983                *frame_index,
984                *previous_keyframe_index,
985                *next_keyframe_index,
986                *offset,
987                *byte_length,
988                *payload_offset,
989                *payload_byte_length,
990            ),
991            PmmDocumentKeyframeSummary::Gravity {
992                index,
993                frame_index,
994                previous_keyframe_index,
995                next_keyframe_index,
996                offset,
997                byte_length,
998                payload_offset,
999                payload_byte_length,
1000                ..
1001            } => (
1002                *index,
1003                *frame_index,
1004                *previous_keyframe_index,
1005                *next_keyframe_index,
1006                *offset,
1007                *byte_length,
1008                *payload_offset,
1009                *payload_byte_length,
1010            ),
1011            PmmDocumentKeyframeSummary::SelfShadow {
1012                index,
1013                frame_index,
1014                previous_keyframe_index,
1015                next_keyframe_index,
1016                offset,
1017                byte_length,
1018                payload_offset,
1019                payload_byte_length,
1020                ..
1021            } => (
1022                *index,
1023                *frame_index,
1024                *previous_keyframe_index,
1025                *next_keyframe_index,
1026                *offset,
1027                *byte_length,
1028                *payload_offset,
1029                *payload_byte_length,
1030            ),
1031        };
1032        refs.push(make_keyframe_reference(
1033            "global", kind, None, None, None, false, index, frame, prev, next, off, blen, poff,
1034            plen,
1035        ));
1036    }
1037}
1038
1039fn make_global_track_byte_range(
1040    track: &PmmDocumentTrackSummary,
1041    kind: &'static str,
1042) -> PmmProjectByteRange {
1043    let byte_length = track.offset_end - track.offset;
1044    PmmProjectByteRange {
1045        scope: "global",
1046        kind,
1047        owner_index: None,
1048        document_index: None,
1049        name: None,
1050        offset: track.offset,
1051        offset_end: track.offset_end,
1052        byte_length,
1053    }
1054}
1055
1056fn build_project_track_references(
1057    doc: &PmmDocumentSummary,
1058    glob: &PmmDocumentGlobalSummary,
1059) -> Vec<PmmProjectTrackReference> {
1060    let mut track_references: Vec<PmmProjectTrackReference> = Vec::new();
1061    for model in &doc.models {
1062        let s = &model.sections;
1063        track_references.push(PmmProjectTrackReference {
1064            scope: "model",
1065            track_kind: "bone",
1066            owner_index: Some(model.slot_index),
1067            document_index: Some(model.document_model_index),
1068            owner_name: Some(model.name.clone()),
1069            initial_keyframes: model.initial_bone_keyframes,
1070            keyframes: model.bone_keyframes,
1071            initial_keyframes_offset: Some(s.initial_bone_keyframes_offset),
1072            keyframe_count_offset: Some(s.bone_keyframe_count_offset),
1073            keyframes_offset: s.bone_keyframes_offset,
1074            keyframes_end_offset: s.bone_keyframes_end_offset,
1075            state_offset: None,
1076            state_end_offset: None,
1077        });
1078        track_references.push(PmmProjectTrackReference {
1079            scope: "model",
1080            track_kind: "morph",
1081            owner_index: Some(model.slot_index),
1082            document_index: Some(model.document_model_index),
1083            owner_name: Some(model.name.clone()),
1084            initial_keyframes: model.initial_morph_keyframes,
1085            keyframes: model.morph_keyframes,
1086            initial_keyframes_offset: Some(s.initial_morph_keyframes_offset),
1087            keyframe_count_offset: Some(s.morph_keyframe_count_offset),
1088            keyframes_offset: s.morph_keyframes_offset,
1089            keyframes_end_offset: s.morph_keyframes_end_offset,
1090            state_offset: None,
1091            state_end_offset: None,
1092        });
1093        track_references.push(PmmProjectTrackReference {
1094            scope: "model",
1095            track_kind: "model",
1096            owner_index: Some(model.slot_index),
1097            document_index: Some(model.document_model_index),
1098            owner_name: Some(model.name.clone()),
1099            initial_keyframes: model.initial_model_keyframes,
1100            keyframes: model.model_keyframes,
1101            initial_keyframes_offset: Some(s.initial_model_keyframe_offset),
1102            keyframe_count_offset: Some(s.model_keyframe_count_offset),
1103            keyframes_offset: s.model_keyframes_offset,
1104            keyframes_end_offset: s.model_keyframes_end_offset,
1105            state_offset: None,
1106            state_end_offset: None,
1107        });
1108    }
1109    track_references.push(make_global_track_ref(&glob.camera, "camera"));
1110    track_references.push(make_global_track_ref(&glob.light, "light"));
1111    track_references.push(make_global_track_ref(&glob.gravity, "gravity"));
1112    track_references.push(make_global_track_ref(&glob.self_shadow, "selfShadow"));
1113    for acc in &glob.accessories.accessories {
1114        track_references.push(PmmProjectTrackReference {
1115            scope: "accessory",
1116            track_kind: "accessory",
1117            owner_index: Some(acc.slot_index),
1118            document_index: Some(acc.document_accessory_index),
1119            owner_name: Some(acc.name.clone()),
1120            initial_keyframes: 1,
1121            keyframes: acc.keyframes,
1122            initial_keyframes_offset: Some(acc.initial_keyframe.offset),
1123            keyframe_count_offset: Some(acc.keyframe_count_offset),
1124            keyframes_offset: acc.keyframes_offset,
1125            keyframes_end_offset: acc.keyframes_end_offset,
1126            state_offset: Some(acc.state_offset),
1127            state_end_offset: Some(acc.state_end_offset),
1128        });
1129    }
1130    track_references
1131}
1132
1133fn build_project_keyframe_references(
1134    doc: &PmmDocumentSummary,
1135    glob: &PmmDocumentGlobalSummary,
1136) -> Vec<PmmProjectKeyframeReference> {
1137    // Build keyframeReferences inventory derived only from already-decoded summaries.
1138    // This is a graph index slice; no new parsing, no payload bytes duplication.
1139    let mut keyframe_references: Vec<PmmProjectKeyframeReference> = Vec::new();
1140
1141    // per document model: initial + additional for bone, morph, model
1142    for model in &doc.models {
1143        // bone keyframes (initial + additional)
1144        for kf in &model.initial_bone_keyframe_summaries {
1145            keyframe_references.push(make_keyframe_reference(
1146                "model",
1147                "bone",
1148                Some(model.slot_index),
1149                Some(model.document_model_index),
1150                Some(model.name.clone()),
1151                true,
1152                kf.index,
1153                kf.frame_index,
1154                kf.previous_keyframe_index,
1155                kf.next_keyframe_index,
1156                kf.offset,
1157                kf.byte_length,
1158                kf.payload_offset,
1159                kf.payload_byte_length,
1160            ));
1161        }
1162        for kf in &model.bone_keyframe_summaries {
1163            keyframe_references.push(make_keyframe_reference(
1164                "model",
1165                "bone",
1166                Some(model.slot_index),
1167                Some(model.document_model_index),
1168                Some(model.name.clone()),
1169                false,
1170                kf.index,
1171                kf.frame_index,
1172                kf.previous_keyframe_index,
1173                kf.next_keyframe_index,
1174                kf.offset,
1175                kf.byte_length,
1176                kf.payload_offset,
1177                kf.payload_byte_length,
1178            ));
1179        }
1180        // morph keyframes (initial + additional)
1181        for kf in &model.initial_morph_keyframe_summaries {
1182            keyframe_references.push(make_keyframe_reference(
1183                "model",
1184                "morph",
1185                Some(model.slot_index),
1186                Some(model.document_model_index),
1187                Some(model.name.clone()),
1188                true,
1189                kf.index,
1190                kf.frame_index,
1191                kf.previous_keyframe_index,
1192                kf.next_keyframe_index,
1193                kf.offset,
1194                kf.byte_length,
1195                kf.payload_offset,
1196                kf.payload_byte_length,
1197            ));
1198        }
1199        for kf in &model.morph_keyframe_summaries {
1200            keyframe_references.push(make_keyframe_reference(
1201                "model",
1202                "morph",
1203                Some(model.slot_index),
1204                Some(model.document_model_index),
1205                Some(model.name.clone()),
1206                false,
1207                kf.index,
1208                kf.frame_index,
1209                kf.previous_keyframe_index,
1210                kf.next_keyframe_index,
1211                kf.offset,
1212                kf.byte_length,
1213                kf.payload_offset,
1214                kf.payload_byte_length,
1215            ));
1216        }
1217        // model keyframes: initial is singular, additional in vec
1218        keyframe_references.push(make_keyframe_reference(
1219            "model",
1220            "model",
1221            Some(model.slot_index),
1222            Some(model.document_model_index),
1223            Some(model.name.clone()),
1224            true,
1225            model.initial_model_keyframe.index,
1226            model.initial_model_keyframe.frame_index,
1227            model.initial_model_keyframe.previous_keyframe_index,
1228            model.initial_model_keyframe.next_keyframe_index,
1229            model.initial_model_keyframe.offset,
1230            model.initial_model_keyframe.byte_length,
1231            model.initial_model_keyframe.payload_offset,
1232            model.initial_model_keyframe.payload_byte_length,
1233        ));
1234        for kf in &model.model_keyframe_summaries {
1235            keyframe_references.push(make_keyframe_reference(
1236                "model",
1237                "model",
1238                Some(model.slot_index),
1239                Some(model.document_model_index),
1240                Some(model.name.clone()),
1241                false,
1242                kf.index,
1243                kf.frame_index,
1244                kf.previous_keyframe_index,
1245                kf.next_keyframe_index,
1246                kf.offset,
1247                kf.byte_length,
1248                kf.payload_offset,
1249                kf.payload_byte_length,
1250            ));
1251        }
1252    }
1253
1254    // global: initial + additional for camera, light, gravity, selfShadow
1255    // helper to append from PmmDocumentTrackSummary + variant kind (uses existing summaries only)
1256    append_global_keyframe_refs(&mut keyframe_references, &glob.camera, "camera");
1257    append_global_keyframe_refs(&mut keyframe_references, &glob.light, "light");
1258    append_global_keyframe_refs(&mut keyframe_references, &glob.gravity, "gravity");
1259    append_global_keyframe_refs(&mut keyframe_references, &glob.self_shadow, "selfShadow");
1260
1261    // per decoded accessory: initial + additional accessory keyframes
1262    for acc in &glob.accessories.accessories {
1263        keyframe_references.push(make_keyframe_reference(
1264            "accessory",
1265            "accessory",
1266            Some(acc.slot_index),
1267            Some(acc.document_accessory_index),
1268            Some(acc.name.clone()),
1269            true,
1270            acc.initial_keyframe.index,
1271            acc.initial_keyframe.frame_index,
1272            acc.initial_keyframe.previous_keyframe_index,
1273            acc.initial_keyframe.next_keyframe_index,
1274            acc.initial_keyframe.offset,
1275            acc.initial_keyframe.byte_length,
1276            acc.initial_keyframe.payload_offset,
1277            acc.initial_keyframe.payload_byte_length,
1278        ));
1279        for kf in &acc.keyframe_summaries {
1280            keyframe_references.push(make_keyframe_reference(
1281                "accessory",
1282                "accessory",
1283                Some(acc.slot_index),
1284                Some(acc.document_accessory_index),
1285                Some(acc.name.clone()),
1286                false,
1287                kf.index,
1288                kf.frame_index,
1289                kf.previous_keyframe_index,
1290                kf.next_keyframe_index,
1291                kf.offset,
1292                kf.byte_length,
1293                kf.payload_offset,
1294                kf.payload_byte_length,
1295            ));
1296        }
1297    }
1298
1299    keyframe_references
1300}
1301
1302fn build_project_byte_coverage(
1303    doc: &PmmDocumentSummary,
1304    glob: &PmmDocumentGlobalSummary,
1305) -> PmmProjectByteCoverage {
1306    // Build byteCoverage summary derived only from already-decoded range summaries.
1307    // Diagnostic/exporter-prep slice: which top-level decoded sections cover bytes in the PMM.
1308    // Includes: per decoded model, global root, global camera/light/gravity/selfShadow tracks,
1309    // accessory block, and each decoded accessory. No raw bytes, no keyframe-level ranges
1310    // (those are in keyframeReferences).
1311    let mut bc_ranges: Vec<PmmProjectByteRange> = Vec::new();
1312
1313    // each decoded document model
1314    for model in &doc.models {
1315        let bl = model.offset_end - model.offset;
1316        bc_ranges.push(PmmProjectByteRange {
1317            scope: "model",
1318            kind: "model",
1319            owner_index: Some(model.slot_index),
1320            document_index: Some(model.document_model_index),
1321            name: Some(model.name.clone()),
1322            offset: model.offset,
1323            offset_end: model.offset_end,
1324            byte_length: bl,
1325        });
1326    }
1327
1328    // global root (the document global summary span)
1329    {
1330        let off = glob.offset;
1331        let end = glob.offset_end;
1332        bc_ranges.push(PmmProjectByteRange {
1333            scope: "global",
1334            kind: "root",
1335            owner_index: None,
1336            document_index: None,
1337            name: None,
1338            offset: off,
1339            offset_end: end,
1340            byte_length: end - off,
1341        });
1342    }
1343
1344    // global tracks (camera/light/gravity/selfShadow) - use their track.offset..offset_end
1345    bc_ranges.push(make_global_track_byte_range(&glob.camera, "camera"));
1346    bc_ranges.push(make_global_track_byte_range(&glob.light, "light"));
1347    bc_ranges.push(make_global_track_byte_range(&glob.gravity, "gravity"));
1348    bc_ranges.push(make_global_track_byte_range(
1349        &glob.self_shadow,
1350        "selfShadow",
1351    ));
1352
1353    // accessory block
1354    {
1355        let blk = &glob.accessories;
1356        let bl = blk.offset_end - blk.offset;
1357        bc_ranges.push(PmmProjectByteRange {
1358            scope: "global",
1359            kind: "accessoryBlock",
1360            owner_index: None,
1361            document_index: None,
1362            name: None,
1363            offset: blk.offset,
1364            offset_end: blk.offset_end,
1365            byte_length: bl,
1366        });
1367    }
1368
1369    // each decoded accessory
1370    for acc in &glob.accessories.accessories {
1371        let bl = acc.offset_end - acc.offset;
1372        bc_ranges.push(PmmProjectByteRange {
1373            scope: "accessory",
1374            kind: "accessory",
1375            owner_index: Some(acc.slot_index),
1376            document_index: Some(acc.document_accessory_index),
1377            name: Some(acc.name.clone()),
1378            offset: acc.offset,
1379            offset_end: acc.offset_end,
1380            byte_length: bl,
1381        });
1382    }
1383
1384    // Merge overlapping decoded ranges (e.g. global root overlaps global tracks; models/tracks may abut or overlap)
1385    // before summing for covered_byte_length. Compute unknown gaps within the overall span [bc_offset, bc_offset_end].
1386    let mut merged: Vec<(usize, usize)> =
1387        bc_ranges.iter().map(|r| (r.offset, r.offset_end)).collect();
1388    merged.sort_by_key(|&(s, _)| s);
1389    let mut merged_intervals: Vec<(usize, usize)> = Vec::new();
1390    for (start, end) in merged {
1391        if let Some(last) = merged_intervals.last_mut() {
1392            if start <= last.1 {
1393                last.1 = last.1.max(end);
1394            } else {
1395                merged_intervals.push((start, end));
1396            }
1397        } else {
1398            merged_intervals.push((start, end));
1399        }
1400    }
1401    let covered_byte_length: usize = merged_intervals.iter().map(|&(s, e)| e - s).sum();
1402
1403    let bc_offset = bc_ranges.iter().map(|r| r.offset).min().unwrap_or(0);
1404    let bc_offset_end = bc_ranges.iter().map(|r| r.offset_end).max().unwrap_or(0);
1405    let bc_byte_length = bc_offset_end - bc_offset;
1406
1407    let mut gaps: Vec<PmmProjectByteRange> = Vec::new();
1408    let mut cursor = bc_offset;
1409    for (gstart, gend) in &merged_intervals {
1410        if cursor < *gstart {
1411            let goff = cursor;
1412            let gend_ = *gstart;
1413            let glen = gend_ - goff;
1414            gaps.push(PmmProjectByteRange {
1415                scope: "unknown",
1416                kind: "gap",
1417                owner_index: None,
1418                document_index: None,
1419                name: None,
1420                offset: goff,
1421                offset_end: gend_,
1422                byte_length: glen,
1423            });
1424        }
1425        cursor = cursor.max(*gend);
1426    }
1427    if cursor < bc_offset_end {
1428        let goff = cursor;
1429        let gend_ = bc_offset_end;
1430        let glen = gend_ - goff;
1431        gaps.push(PmmProjectByteRange {
1432            scope: "unknown",
1433            kind: "gap",
1434            owner_index: None,
1435            document_index: None,
1436            name: None,
1437            offset: goff,
1438            offset_end: gend_,
1439            byte_length: glen,
1440        });
1441    }
1442
1443    let coverage_ratio = if bc_byte_length == 0 {
1444        1.0
1445    } else {
1446        covered_byte_length as f32 / bc_byte_length as f32
1447    };
1448
1449    PmmProjectByteCoverage {
1450        offset: bc_offset,
1451        offset_end: bc_offset_end,
1452        byte_length: bc_byte_length,
1453        covered_byte_length,
1454        coverage_ratio,
1455        gap_count: gaps.len(),
1456        gaps,
1457        range_count: bc_ranges.len(),
1458        ranges: bc_ranges,
1459    }
1460}
1461
1462#[allow(clippy::too_many_arguments)]
1463fn make_asset_binding(
1464    scope: &'static str,
1465    asset_kind: &'static str,
1466    owner_index: Option<usize>,
1467    document_index: Option<u8>,
1468    owner_name: Option<String>,
1469    path: String,
1470    path_offset: Option<usize>,
1471    asset_reference_index: Option<usize>,
1472    asset_references: &[PmmAssetReference],
1473) -> PmmProjectAssetBinding {
1474    let (ar_offset, ar_end, ar_conf) = match asset_reference_index {
1475        Some(idx) => asset_references.get(idx).map_or((None, None, None), |ar| {
1476            (Some(ar.offset), Some(ar.offset_end), Some(ar.confidence))
1477        }),
1478        None => (None, None, None),
1479    };
1480    PmmProjectAssetBinding {
1481        scope,
1482        asset_kind,
1483        owner_index,
1484        document_index,
1485        owner_name,
1486        path,
1487        path_offset,
1488        asset_reference_index,
1489        asset_reference_offset: ar_offset,
1490        asset_reference_end_offset: ar_end,
1491        asset_reference_confidence: ar_conf,
1492    }
1493}
1494
1495fn build_project_asset_bindings(
1496    doc: &PmmDocumentSummary,
1497    glob: &PmmDocumentGlobalSummary,
1498    asset_references: &[PmmAssetReference],
1499) -> Vec<PmmProjectAssetBinding> {
1500    let mut asset_bindings: Vec<PmmProjectAssetBinding> = Vec::new();
1501
1502    for model in &doc.models {
1503        asset_bindings.push(make_asset_binding(
1504            "model",
1505            "model",
1506            Some(model.slot_index),
1507            Some(model.document_model_index),
1508            Some(model.name.clone()),
1509            model.path.clone(),
1510            Some(model.path_offset),
1511            model.asset_reference_index,
1512            asset_references,
1513        ));
1514    }
1515    for acc in &glob.accessories.accessories {
1516        asset_bindings.push(make_asset_binding(
1517            "accessory",
1518            "accessory",
1519            Some(acc.slot_index),
1520            Some(acc.document_accessory_index),
1521            Some(acc.name.clone()),
1522            acc.path.clone(),
1523            Some(acc.path_offset),
1524            acc.asset_reference_index,
1525            asset_references,
1526        ));
1527    }
1528
1529    let settings = &glob.settings;
1530    let audio_asset_reference_index = if settings.audio_path.is_empty() {
1531        None
1532    } else {
1533        asset_reference_index_for_path(asset_references, "audio", &settings.audio_path)
1534    };
1535    let background_video_asset_reference_index = if settings.background_video_path.is_empty() {
1536        None
1537    } else {
1538        asset_reference_index_for_path(asset_references, "video", &settings.background_video_path)
1539    };
1540    let background_image_asset_reference_index = if settings.background_image_path.is_empty() {
1541        None
1542    } else {
1543        asset_reference_index_for_path(asset_references, "image", &settings.background_image_path)
1544    };
1545
1546    if !settings.audio_path.is_empty() {
1547        asset_bindings.push(make_asset_binding(
1548            "sceneSettings",
1549            "audio",
1550            None,
1551            None,
1552            None,
1553            settings.audio_path.clone(),
1554            Some(settings.audio_path_offset),
1555            audio_asset_reference_index,
1556            asset_references,
1557        ));
1558    }
1559    if !settings.background_video_path.is_empty() {
1560        asset_bindings.push(make_asset_binding(
1561            "sceneSettings",
1562            "video",
1563            None,
1564            None,
1565            None,
1566            settings.background_video_path.clone(),
1567            Some(settings.background_video_path_offset),
1568            background_video_asset_reference_index,
1569            asset_references,
1570        ));
1571    }
1572    if !settings.background_image_path.is_empty() {
1573        asset_bindings.push(make_asset_binding(
1574            "sceneSettings",
1575            "image",
1576            None,
1577            None,
1578            None,
1579            settings.background_image_path.clone(),
1580            Some(settings.background_image_path_offset),
1581            background_image_asset_reference_index,
1582            asset_references,
1583        ));
1584    }
1585
1586    asset_bindings
1587}
1588
1589fn build_project_scene_settings(
1590    glob: &PmmDocumentGlobalSummary,
1591    asset_references: &[PmmAssetReference],
1592) -> PmmProjectSceneSettings {
1593    let settings = &glob.settings;
1594    let audio_asset_reference_index = if settings.audio_path.is_empty() {
1595        None
1596    } else {
1597        asset_reference_index_for_path(asset_references, "audio", &settings.audio_path)
1598    };
1599    let background_video_asset_reference_index = if settings.background_video_path.is_empty() {
1600        None
1601    } else {
1602        asset_reference_index_for_path(asset_references, "video", &settings.background_video_path)
1603    };
1604    let background_image_asset_reference_index = if settings.background_image_path.is_empty() {
1605        None
1606    } else {
1607        asset_reference_index_for_path(asset_references, "image", &settings.background_image_path)
1608    };
1609
1610    PmmProjectSceneSettings {
1611        offset: settings.offset,
1612        offset_end: settings.offset_end,
1613        current_frame_index: settings.current_frame_index,
1614        current_frame_index_in_text_field: settings.current_frame_index_in_text_field,
1615        begin_frame_index_enabled: settings.begin_frame_index_enabled,
1616        end_frame_index_enabled: settings.end_frame_index_enabled,
1617        begin_frame_index: settings.begin_frame_index,
1618        end_frame_index: settings.end_frame_index,
1619        preferred_fps: settings.preferred_fps,
1620        loop_enabled: settings.loop_enabled,
1621        audio_enabled: settings.audio_enabled,
1622        audio_path: settings.audio_path.clone(),
1623        audio_asset_reference_index,
1624        background_video_enabled: settings.background_video_enabled,
1625        background_video_path: settings.background_video_path.clone(),
1626        background_video_asset_reference_index,
1627        background_video_offset: settings.background_video_offset,
1628        background_video_scale_factor: settings.background_video_scale_factor,
1629        background_image_enabled: settings.background_image_enabled,
1630        background_image_path: settings.background_image_path.clone(),
1631        background_image_asset_reference_index,
1632        background_image_offset: settings.background_image_offset,
1633        background_image_scale_factor: settings.background_image_scale_factor,
1634    }
1635}
1636
1637fn build_project_export_readiness(
1638    byte_coverage: &PmmProjectByteCoverage,
1639    asset_bindings: &[PmmProjectAssetBinding],
1640) -> PmmProjectExportReadiness {
1641    let mut blockers: Vec<PmmProjectExportBlocker> = Vec::new();
1642    blockers.push(PmmProjectExportBlocker {
1643        code: "PMM_SEMANTIC_EXPORTER_UNFINISHED",
1644        severity: "blocker",
1645        message: "full semantic graph export/editing is not yet supported".to_string(),
1646        scope: None,
1647        kind: None,
1648        count: None,
1649        coverage_ratio: None,
1650    });
1651    if byte_coverage.gap_count > 0 {
1652        blockers.push(PmmProjectExportBlocker {
1653            code: "PMM_DECODED_BYTE_GAPS_REMAIN",
1654            severity: "warning",
1655            message: format!(
1656                "{} decoded byte gaps remain (coverage ratio {:.4})",
1657                byte_coverage.gap_count, byte_coverage.coverage_ratio
1658            ),
1659            scope: Some("projectGraph"),
1660            kind: Some("byteCoverage"),
1661            count: Some(byte_coverage.gap_count),
1662            coverage_ratio: Some(byte_coverage.coverage_ratio),
1663        });
1664    }
1665    let unresolved_count = asset_bindings
1666        .iter()
1667        .filter(|binding| binding.asset_reference_index.is_none())
1668        .count();
1669    if unresolved_count > 0 {
1670        blockers.push(PmmProjectExportBlocker {
1671            code: "PMM_UNRESOLVED_ASSET_BINDINGS",
1672            severity: "warning",
1673            message: format!(
1674                "{} asset bindings have unresolved asset references",
1675                unresolved_count
1676            ),
1677            scope: Some("projectGraph"),
1678            kind: Some("assetBindings"),
1679            count: Some(unresolved_count),
1680            coverage_ratio: None,
1681        });
1682    }
1683    let blocker_count = blockers.len();
1684    PmmProjectExportReadiness {
1685        lossless_parsed_byte_export_supported: true,
1686        semantic_graph_export_supported: false,
1687        source_byte_preservation_required: true,
1688        blocker_count,
1689        blockers,
1690    }
1691}
1692
1693const PMM_MANIFEST_PREFIX: &[u8] = b"Polygon Movie maker ";
1694
1695fn parse_pmm_manifest_version(data: &[u8]) -> Result<(String, Option<u32>), ImportError> {
1696    if !data.starts_with(PMM_MANIFEST_PREFIX) {
1697        return Err(ImportError::InvalidMagic { format: "PMM" });
1698    }
1699    let version_bytes = &data[PMM_MANIFEST_PREFIX.len()..data.len().min(32)];
1700    let version_end = version_bytes
1701        .iter()
1702        .position(|&byte| byte == 0)
1703        .unwrap_or(version_bytes.len());
1704    let version = String::from_utf8_lossy(&version_bytes[..version_end])
1705        .trim()
1706        .to_owned();
1707    let parsed_version = version.parse::<u32>().ok();
1708    Ok((version, parsed_version))
1709}
1710
1711pub fn parse_pmm_manifest(data: &[u8]) -> Result<PmmParsedManifest, ImportError> {
1712    let (version, parsed_version) = parse_pmm_manifest_version(data)?;
1713    let project_settings = parse_project_settings(data);
1714    let display_state = parse_display_state(data, parsed_version);
1715    let asset_references = extract_asset_references(data);
1716    let model_slot_scan = parse_model_slots_from_header(data, &asset_references, &display_state);
1717    let document_summary = parse_document_summary(data, parsed_version, &asset_references);
1718    let document_global_summary =
1719        parse_document_global_summary(data, parsed_version, &asset_references);
1720    let header_text_entries =
1721        parse_header_text_entries(data, parsed_version, asset_references.first());
1722    let model_assets = scene_assets_by_kind(&asset_references, "model");
1723    let accessory_assets = scene_assets_by_kind(&asset_references, "accessory");
1724    let motion_assets = scene_assets_by_kind(&asset_references, "motion");
1725    let audio_assets = scene_assets_by_kind(&asset_references, "audio");
1726    let image_assets = scene_assets_by_kind(&asset_references, "image");
1727    let video_assets = scene_assets_by_kind(&asset_references, "video");
1728    let timeline = timeline_from_project_settings(&project_settings);
1729    let asset_summary = asset_summary(&asset_references);
1730    let diagnostics = pmm_diagnostics(
1731        &asset_references,
1732        &model_slot_scan.slots,
1733        document_summary.as_ref(),
1734        document_global_summary.as_ref(),
1735        &display_state,
1736        model_slot_scan.stop.as_ref(),
1737        data,
1738    );
1739    let project_graph = match (&document_summary, &document_global_summary) {
1740        (Some(doc), Some(glob)) => {
1741            let track_references = build_project_track_references(doc, glob);
1742            let keyframe_references = build_project_keyframe_references(doc, glob);
1743            let byte_coverage = build_project_byte_coverage(doc, glob);
1744
1745            // Build assetBindings derived only from already-decoded summaries + asset_references.
1746            // Exporter-prep read-only slice: connects owners (model/accessory/sceneSettings) to their asset paths + resolved asset reference metadata.
1747            // No new parsing.
1748            let asset_bindings = build_project_asset_bindings(doc, glob, &asset_references);
1749            let scene_settings = build_project_scene_settings(glob, &asset_references);
1750
1751            // Build exportReadiness immediately before PmmProjectGraph construction (exporter-prep diagnostics only).
1752            // lossless parsed byte supported; semantic graph export remains false (full exporter unfinished).
1753            let export_readiness = build_project_export_readiness(&byte_coverage, &asset_bindings);
1754
1755            Some(PmmProjectGraph {
1756                source: "mmd-anim-format first PMMv2 document graph DTO slice",
1757                version: version.clone(),
1758                parsed_version,
1759                timeline: timeline.clone(),
1760                display_state: display_state.clone(),
1761                asset_summary: asset_summary.clone(),
1762                asset_references: asset_references.clone(),
1763                models: doc.models.clone(),
1764                global: glob.clone(),
1765                model_counts: doc.counts.clone(),
1766                accessory_count: glob.accessories.accessory_count,
1767                accessory_keyframes: glob.accessories.keyframes,
1768                track_references,
1769                keyframe_references,
1770                byte_coverage,
1771                scene_settings,
1772                asset_bindings,
1773                export_readiness,
1774            })
1775        }
1776        _ => None,
1777    };
1778    Ok(PmmParsedManifest {
1779        signature: "Polygon Movie maker",
1780        version,
1781        parsed_version,
1782        byte_length: data.len(),
1783        timeline,
1784        project_settings,
1785        display_state,
1786        header_text_entries,
1787        model_slots: model_slot_scan.slots,
1788        document_summary,
1789        document_global_summary,
1790        project_graph,
1791        asset_summary,
1792        model_assets,
1793        accessory_assets,
1794        motion_assets,
1795        audio_assets,
1796        image_assets,
1797        video_assets,
1798        model_paths: paths_by_kind(&asset_references, "model"),
1799        accessory_paths: paths_by_kind(&asset_references, "accessory"),
1800        motion_paths: paths_by_kind(&asset_references, "motion"),
1801        audio_paths: paths_by_kind(&asset_references, "audio"),
1802        image_paths: paths_by_kind(&asset_references, "image"),
1803        video_paths: paths_by_kind(&asset_references, "video"),
1804        diagnostics,
1805        asset_references,
1806        source_bytes: data.to_vec(),
1807    })
1808}
1809
1810pub fn export_pmm_manifest(manifest: &PmmParsedManifest) -> Vec<u8> {
1811    if !manifest.source_bytes.is_empty() {
1812        return manifest.source_bytes.clone();
1813    }
1814    let mut out = Vec::new();
1815    out.extend_from_slice(b"Polygon Movie maker 0002");
1816    out.resize(30, 0);
1817
1818    push_u32(
1819        &mut out,
1820        manifest.project_settings.screen_width.unwrap_or(640),
1821    );
1822    push_u32(
1823        &mut out,
1824        manifest.project_settings.screen_height.unwrap_or(480),
1825    );
1826    push_u32(
1827        &mut out,
1828        manifest
1829            .project_settings
1830            .timeline_frame_count
1831            .or(manifest.timeline.frame_count)
1832            .unwrap_or(0),
1833    );
1834    push_f32(
1835        &mut out,
1836        manifest.project_settings.frame_rate.unwrap_or(30.0),
1837    );
1838
1839    let mut flags = [0u8; 8];
1840    for (index, flag) in manifest
1841        .display_state
1842        .model_slot_flags
1843        .iter()
1844        .take(8)
1845        .enumerate()
1846    {
1847        flags[index] = *flag;
1848    }
1849    if flags.iter().all(|flag| *flag == 0) && !manifest.model_slots.is_empty() {
1850        for slot in &manifest.model_slots {
1851            if slot.slot_index < flags.len() {
1852                flags[slot.slot_index] = 1;
1853            }
1854        }
1855    }
1856    out.extend_from_slice(&flags);
1857
1858    let document_model_count = manifest
1859        .display_state
1860        .document_model_count
1861        .unwrap_or_else(|| manifest.model_slots.len().min(u8::MAX as usize) as u8);
1862    out.push(document_model_count);
1863    out.push(manifest.display_state.accessory_slot_count.unwrap_or(0));
1864
1865    let mut emitted_paths = Vec::<String>::new();
1866    for slot in &manifest.model_slots {
1867        push_pmm_len_prefixed_sjis(&mut out, &slot.name, &slot.name_bytes);
1868        push_pmm_len_prefixed_sjis(&mut out, &slot.english_name, &slot.english_name_bytes);
1869        push_pmm_sjis_string(&mut out, &slot.model_path, None);
1870        out.push(0);
1871        emitted_paths.push(slot.normalized_path.to_ascii_lowercase());
1872    }
1873
1874    for reference in &manifest.asset_references {
1875        let normalized = reference.normalized_path.to_ascii_lowercase();
1876        if emitted_paths.iter().any(|path| path == &normalized) {
1877            continue;
1878        }
1879        push_pmm_sjis_string(&mut out, &reference.path, None);
1880        out.push(0);
1881        emitted_paths.push(normalized);
1882    }
1883
1884    out
1885}
1886
1887/// Lossless source-byte patch export for PMMv2 document model paths only.
1888/// Clones the manifest's source_bytes and overwrites only the 256-byte fixed path
1889/// region for each specified document_model_index (from the decoded PmmDocumentModelSummary).
1890/// This is a narrow helper for MMD GUI load-smoke model path policy adjustments.
1891/// It does not implement full semantic PMM export (which remains unfinished).
1892pub fn export_pmm_manifest_with_document_model_path_overrides(
1893    manifest: &PmmParsedManifest,
1894    overrides: &[(u8, &str)],
1895) -> Result<Vec<u8>, String> {
1896    if manifest.source_bytes.is_empty() {
1897        return Err(
1898            "parsed source bytes are required for document model path override export".to_owned(),
1899        );
1900    }
1901    let document = manifest.document_summary.as_ref().ok_or_else(|| {
1902        "PMMv2 document summary is required for document model path override export".to_owned()
1903    })?;
1904
1905    let mut patched = manifest.source_bytes.clone();
1906
1907    for &(document_model_index, new_path) in overrides {
1908        let matching: Vec<_> = document
1909            .models
1910            .iter()
1911            .filter(|m| m.document_model_index == document_model_index)
1912            .collect();
1913        if matching.len() != 1 {
1914            return Err(format!(
1915                "exactly one PmmDocumentModelSummary with document_model_index {} is required (found {})",
1916                document_model_index,
1917                matching.len()
1918            ));
1919        }
1920        let model = matching[0];
1921        let start = model.path_offset;
1922        let end = start + 256;
1923        if end > patched.len() {
1924            return Err("document model path field exceeds source byte length".to_owned());
1925        }
1926        let field = &mut patched[start..end];
1927        write_pmm_fixed_sjis_to_slice(field, new_path)?;
1928    }
1929
1930    Ok(patched)
1931}
1932
1933/// Lossless source-byte patch export for decoded scene frame range fields in PMMv2 document settings.
1934/// Clones the manifest's source_bytes and overwrites only the exact byte positions recorded in
1935/// PmmDocumentSettingsSummary (current_frame_index_offset etc). Only fields with Some(...) are patched.
1936/// i32 values are written little-endian; bool as single byte 0/1. Byte length is preserved.
1937/// Requires source bytes and PMMv2 global/document settings summary. This is a narrow exporter-prep
1938/// step and does not implement full semantic PMM export (which remains unfinished).
1939#[derive(Debug, Clone, Default)]
1940pub struct PmmSceneFrameRangePatch {
1941    pub current_frame_index: Option<i32>,
1942    pub current_frame_index_in_text_field: Option<i32>,
1943    pub begin_frame_index_enabled: Option<bool>,
1944    pub end_frame_index_enabled: Option<bool>,
1945    pub begin_frame_index: Option<i32>,
1946    pub end_frame_index: Option<i32>,
1947}
1948
1949pub fn export_pmm_manifest_with_scene_frame_range_patch(
1950    manifest: &PmmParsedManifest,
1951    patch: &PmmSceneFrameRangePatch,
1952) -> Result<Vec<u8>, String> {
1953    if manifest.source_bytes.is_empty() {
1954        return Err(
1955            "parsed source bytes are required for scene frame range patch export".to_owned(),
1956        );
1957    }
1958    let global = manifest.document_global_summary.as_ref().ok_or_else(|| {
1959        "PMMv2 global/document settings summary is required for scene frame range patch export"
1960            .to_owned()
1961    })?;
1962    let settings = &global.settings;
1963
1964    let mut patched = manifest.source_bytes.clone();
1965
1966    if let Some(v) = patch.current_frame_index {
1967        let off = settings.current_frame_index_offset;
1968        if off + 4 > patched.len() {
1969            return Err("current_frame_index field offset exceeds source byte length".to_owned());
1970        }
1971        patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
1972    }
1973    if let Some(v) = patch.current_frame_index_in_text_field {
1974        let off = settings.current_frame_index_in_text_field_offset;
1975        if off + 4 > patched.len() {
1976            return Err(
1977                "current_frame_index_in_text_field field offset exceeds source byte length"
1978                    .to_owned(),
1979            );
1980        }
1981        patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
1982    }
1983    if let Some(v) = patch.begin_frame_index_enabled {
1984        let off = settings.begin_frame_index_enabled_offset;
1985        if off >= patched.len() {
1986            return Err(
1987                "begin_frame_index_enabled field offset exceeds source byte length".to_owned(),
1988            );
1989        }
1990        patched[off] = if v { 1 } else { 0 };
1991    }
1992    if let Some(v) = patch.end_frame_index_enabled {
1993        let off = settings.end_frame_index_enabled_offset;
1994        if off >= patched.len() {
1995            return Err(
1996                "end_frame_index_enabled field offset exceeds source byte length".to_owned(),
1997            );
1998        }
1999        patched[off] = if v { 1 } else { 0 };
2000    }
2001    if let Some(v) = patch.begin_frame_index {
2002        let off = settings.begin_frame_index_offset;
2003        if off + 4 > patched.len() {
2004            return Err("begin_frame_index field offset exceeds source byte length".to_owned());
2005        }
2006        patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2007    }
2008    if let Some(v) = patch.end_frame_index {
2009        let off = settings.end_frame_index_offset;
2010        if off + 4 > patched.len() {
2011            return Err("end_frame_index field offset exceeds source byte length".to_owned());
2012        }
2013        patched[off..off + 4].copy_from_slice(&v.to_le_bytes());
2014    }
2015
2016    Ok(patched)
2017}
2018
2019/// Lossless source-byte patch export for decoded scene media paths (audio / background video / background image)
2020/// in PMMv2 document settings.
2021/// Clones the manifest's source_bytes and overwrites only the exact 256-byte fixed path regions
2022/// recorded via the new *_path_offset fields in PmmDocumentSettingsSummary. Only fields with Some(...) are patched.
2023/// Uses write_pmm_fixed_sjis_to_slice to preserve 256-byte length and NUL room. Byte length is preserved.
2024/// Requires source bytes and PMMv2 global/document settings summary. This is a narrow exporter-prep
2025/// step and does not implement full semantic PMM export (which remains unfinished).
2026#[derive(Debug, Clone, Default)]
2027pub struct PmmSceneMediaPathPatch {
2028    pub audio_path: Option<String>,
2029    pub background_video_path: Option<String>,
2030    pub background_image_path: Option<String>,
2031}
2032
2033pub fn export_pmm_manifest_with_scene_media_path_patch(
2034    manifest: &PmmParsedManifest,
2035    patch: &PmmSceneMediaPathPatch,
2036) -> Result<Vec<u8>, String> {
2037    if manifest.source_bytes.is_empty() {
2038        return Err(
2039            "parsed source bytes are required for scene media path patch export".to_owned(),
2040        );
2041    }
2042    let global = manifest.document_global_summary.as_ref().ok_or_else(|| {
2043        "PMMv2 global/document settings summary is required for scene media path patch export"
2044            .to_owned()
2045    })?;
2046    let settings = &global.settings;
2047
2048    let mut patched = manifest.source_bytes.clone();
2049
2050    if let Some(ref p) = patch.audio_path {
2051        let off = settings.audio_path_offset;
2052        if off + 256 > patched.len() {
2053            return Err("audio path field offset exceeds source byte length".to_owned());
2054        }
2055        let field = &mut patched[off..off + 256];
2056        write_pmm_fixed_sjis_to_slice(field, p)?;
2057    }
2058    if let Some(ref p) = patch.background_video_path {
2059        let off = settings.background_video_path_offset;
2060        if off + 256 > patched.len() {
2061            return Err("background video path field offset exceeds source byte length".to_owned());
2062        }
2063        let field = &mut patched[off..off + 256];
2064        write_pmm_fixed_sjis_to_slice(field, p)?;
2065    }
2066    if let Some(ref p) = patch.background_image_path {
2067        let off = settings.background_image_path_offset;
2068        if off + 256 > patched.len() {
2069            return Err("background image path field offset exceeds source byte length".to_owned());
2070        }
2071        let field = &mut patched[off..off + 256];
2072        write_pmm_fixed_sjis_to_slice(field, p)?;
2073    }
2074
2075    Ok(patched)
2076}
2077
2078#[derive(Debug, Clone)]
2079pub struct PmmSceneExportOptions {
2080    pub screen_width: u32,
2081    pub screen_height: u32,
2082    pub frame_rate: f32,
2083    pub camera_fov: f32,
2084}
2085
2086impl Default for PmmSceneExportOptions {
2087    fn default() -> Self {
2088        Self {
2089            screen_width: 1024,
2090            screen_height: 1024,
2091            frame_rate: 30.0,
2092            camera_fov: 30.0,
2093        }
2094    }
2095}
2096
2097#[derive(Debug, Clone)]
2098pub struct PmmSceneExportReport {
2099    pub bytes: Vec<u8>,
2100    pub bone_keyframes: usize,
2101    pub morph_keyframes: usize,
2102    pub frame_zero_bone_keyframes: usize,
2103    pub frame_zero_morph_keyframes: usize,
2104    pub skipped_bone_keyframes: usize,
2105    pub skipped_morph_keyframes: usize,
2106    pub max_frame: u32,
2107}
2108
2109#[derive(Debug, Clone)]
2110struct PmmExportBoneKeyframe {
2111    bone_index: usize,
2112    frame: u32,
2113    translation: [f32; 3],
2114    rotation: [f32; 4],
2115    interpolation: [u8; 16],
2116}
2117
2118#[derive(Debug, Clone)]
2119struct PmmExportMorphKeyframe {
2120    morph_index: usize,
2121    frame: u32,
2122    weight: f32,
2123}
2124
2125pub fn export_pmm_scene_from_pmx_vmd(
2126    model: &PmxParsedModel,
2127    motion: &VmdParsedAnimation,
2128    model_path: &str,
2129    options: &PmmSceneExportOptions,
2130) -> PmmSceneExportReport {
2131    let bone_names: Vec<&str> = model
2132        .skeleton
2133        .bones
2134        .iter()
2135        .map(|bone| bone.name.as_str())
2136        .collect();
2137    let morph_names: Vec<&str> = model
2138        .morphs
2139        .iter()
2140        .map(|morph| morph.name.as_str())
2141        .collect();
2142    let bone_indices: HashMap<&str, usize> = bone_names
2143        .iter()
2144        .enumerate()
2145        .map(|(index, name)| (*name, index))
2146        .collect();
2147    let morph_indices: HashMap<&str, usize> = morph_names
2148        .iter()
2149        .enumerate()
2150        .map(|(index, name)| (*name, index))
2151        .collect();
2152
2153    let mut deduped_bones = BTreeMap::<(usize, u32), &VmdParsedBoneFrame>::new();
2154    let mut skipped_bone_keyframes = 0usize;
2155    for frame in &motion.bone_frames {
2156        if let Some(&bone_index) = bone_indices.get(frame.bone_name.as_str()) {
2157            deduped_bones.insert((bone_index, frame.frame), frame);
2158        } else {
2159            skipped_bone_keyframes += 1;
2160        }
2161    }
2162
2163    let mut deduped_morphs = BTreeMap::<(usize, u32), &VmdParsedMorphFrame>::new();
2164    let mut skipped_morph_keyframes = 0usize;
2165    for frame in &motion.morph_frames {
2166        if let Some(&morph_index) = morph_indices.get(frame.morph_name.as_str()) {
2167            deduped_morphs.insert((morph_index, frame.frame), frame);
2168        } else {
2169            skipped_morph_keyframes += 1;
2170        }
2171    }
2172
2173    let mut initial_bones = vec![None::<PmmExportBoneKeyframe>; bone_names.len()];
2174    let mut additional_bones = Vec::<PmmExportBoneKeyframe>::new();
2175    let mut max_frame = 0u32;
2176    for ((bone_index, frame_index), frame) in deduped_bones {
2177        max_frame = max_frame.max(frame_index);
2178        let keyframe = pmm_export_bone_keyframe(bone_index, frame);
2179        if frame_index == 0 {
2180            initial_bones[bone_index] = Some(keyframe);
2181        } else {
2182            additional_bones.push(keyframe);
2183        }
2184    }
2185    additional_bones.sort_by_key(|frame| (frame.bone_index, frame.frame));
2186
2187    let mut initial_morphs = vec![None::<PmmExportMorphKeyframe>; morph_names.len()];
2188    let mut additional_morphs = Vec::<PmmExportMorphKeyframe>::new();
2189    for ((morph_index, frame_index), frame) in deduped_morphs {
2190        max_frame = max_frame.max(frame_index);
2191        let keyframe = PmmExportMorphKeyframe {
2192            morph_index,
2193            frame: frame.frame,
2194            weight: frame.weight,
2195        };
2196        if frame_index == 0 {
2197            initial_morphs[morph_index] = Some(keyframe);
2198        } else {
2199            additional_morphs.push(keyframe);
2200        }
2201    }
2202    additional_morphs.sort_by_key(|frame| (frame.morph_index, frame.frame));
2203
2204    let frame_zero_bone_keyframes = initial_bones.iter().filter(|frame| frame.is_some()).count();
2205    let frame_zero_morph_keyframes = initial_morphs
2206        .iter()
2207        .filter(|frame| frame.is_some())
2208        .count();
2209    let bytes = write_pmm_scene(
2210        model,
2211        model_path,
2212        options,
2213        &bone_names,
2214        &morph_names,
2215        &initial_bones,
2216        &additional_bones,
2217        &initial_morphs,
2218        &additional_morphs,
2219        max_frame,
2220    );
2221
2222    PmmSceneExportReport {
2223        bytes,
2224        bone_keyframes: additional_bones.len(),
2225        morph_keyframes: additional_morphs.len(),
2226        frame_zero_bone_keyframes,
2227        frame_zero_morph_keyframes,
2228        skipped_bone_keyframes,
2229        skipped_morph_keyframes,
2230        max_frame,
2231    }
2232}
2233
2234fn pmm_export_bone_keyframe(
2235    bone_index: usize,
2236    frame: &VmdParsedBoneFrame,
2237) -> PmmExportBoneKeyframe {
2238    PmmExportBoneKeyframe {
2239        bone_index,
2240        frame: frame.frame,
2241        translation: frame.translation,
2242        rotation: frame.rotation,
2243        interpolation: pmm_bone_interpolation_from_vmd(&frame.interpolation),
2244    }
2245}
2246
2247fn pmm_bone_interpolation_from_vmd(interpolation: &[u8]) -> [u8; 16] {
2248    if interpolation.len() < 16 {
2249        return [20; 16];
2250    }
2251    [
2252        interpolation[0],
2253        interpolation[4],
2254        interpolation[8],
2255        interpolation[12],
2256        interpolation[1],
2257        interpolation[5],
2258        interpolation[9],
2259        interpolation[13],
2260        interpolation[2],
2261        interpolation[6],
2262        interpolation[10],
2263        interpolation[14],
2264        interpolation[3],
2265        interpolation[7],
2266        interpolation[11],
2267        interpolation[15],
2268    ]
2269}
2270
2271/// Max byte length MMD keeps for a bone/morph name (20-byte NUL-terminated buffer).
2272const PMM_MODEL_NAME_MAX_BYTES: usize = 19;
2273
2274struct PmmExportModelStructure {
2275    fixed_tracks: u8,
2276    constraint_bone_indices: Vec<i32>,
2277    outside_parent_subject_bone_indices: Vec<i32>,
2278}
2279
2280fn pmm_export_model_structure_from_pmx(model: &PmxParsedModel) -> PmmExportModelStructure {
2281    let constraint_bone_indices = model
2282        .skeleton
2283        .bones
2284        .iter()
2285        .enumerate()
2286        .filter(|(_, bone)| bone.flags.ik)
2287        .map(|(index, _)| index as i32)
2288        .collect();
2289    let outside_parent_subject_bone_indices = std::iter::once(-1)
2290        .chain(
2291            model
2292                .skeleton
2293                .bones
2294                .iter()
2295                .enumerate()
2296                .filter(|(_, bone)| pmm_is_outside_parent_subject_bone(bone))
2297                .map(|(index, _)| index as i32),
2298        )
2299        .collect();
2300    let fixed_tracks = if model.display_frames.is_empty() {
2301        0
2302    } else {
2303        (model.display_frames.len() + 1).min(255) as u8
2304    };
2305    PmmExportModelStructure {
2306        fixed_tracks,
2307        constraint_bone_indices,
2308        outside_parent_subject_bone_indices,
2309    }
2310}
2311
2312fn pmm_is_outside_parent_subject_bone(bone: &crate::pmx::PmxParsedBone) -> bool {
2313    bone.flags.ik || (bone.flags.visible && bone.flags.translatable)
2314}
2315
2316fn push_document_outside_parent_current_state(out: &mut Vec<u8>) {
2317    push_i32(out, 0);
2318    push_i32(out, 0);
2319    push_i32(out, -1);
2320    push_i32(out, 0);
2321}
2322
2323#[allow(clippy::too_many_arguments)]
2324fn write_pmm_scene(
2325    model: &PmxParsedModel,
2326    model_path: &str,
2327    options: &PmmSceneExportOptions,
2328    bone_names: &[&str],
2329    morph_names: &[&str],
2330    initial_bones: &[Option<PmmExportBoneKeyframe>],
2331    additional_bones: &[PmmExportBoneKeyframe],
2332    initial_morphs: &[Option<PmmExportMorphKeyframe>],
2333    additional_morphs: &[PmmExportMorphKeyframe],
2334    max_frame: u32,
2335) -> Vec<u8> {
2336    let model_structure = pmm_export_model_structure_from_pmx(model);
2337    let constraint_count = model_structure.constraint_bone_indices.len();
2338    let outside_parent_count = model_structure.outside_parent_subject_bone_indices.len();
2339    let constraint_states = vec![true; constraint_count];
2340    let outside_parent_keyframe_indices = vec![(-1, 0); outside_parent_count];
2341
2342    let mut out = b"Polygon Movie maker 0002".to_vec();
2343    out.resize(30, 0);
2344    push_u32(&mut out, options.screen_width);
2345    push_u32(&mut out, options.screen_height);
2346    push_u32(&mut out, max_frame);
2347    push_f32(&mut out, options.frame_rate);
2348    out.extend_from_slice(&[0, 1, 1, 1, 1, 1, 1]);
2349    out.push(0);
2350    out.push(1);
2351
2352    out.push(0);
2353    push_pmm_len_prefixed_sjis(&mut out, &model.metadata.name, &[]);
2354    push_pmm_len_prefixed_sjis(&mut out, &model.metadata.english_name, &[]);
2355    push_pmm_fixed_sjis(&mut out, model_path, 256);
2356    out.push(model_structure.fixed_tracks);
2357
2358    // MMD stores bone/morph names in a 20-byte NUL-terminated buffer, so it caps
2359    // each name at 19 content bytes (on a char boundary) when it loads the PMX. The
2360    // PMM must carry the same truncated names or MMD reports "モデル構造が異なります".
2361    push_i32(&mut out, bone_names.len() as i32);
2362    for name in bone_names {
2363        let fitted = encode_sjis_prefix_fit(name, PMM_MODEL_NAME_MAX_BYTES);
2364        push_pmm_len_prefixed_sjis(&mut out, name, &fitted);
2365    }
2366    push_i32(&mut out, morph_names.len() as i32);
2367    for name in morph_names {
2368        let fitted = encode_sjis_prefix_fit(name, PMM_MODEL_NAME_MAX_BYTES);
2369        push_pmm_len_prefixed_sjis(&mut out, name, &fitted);
2370    }
2371
2372    push_i32(&mut out, constraint_count as i32);
2373    for index in &model_structure.constraint_bone_indices {
2374        push_i32(&mut out, *index);
2375    }
2376    push_i32(&mut out, outside_parent_count as i32);
2377    for index in &model_structure.outside_parent_subject_bone_indices {
2378        push_i32(&mut out, *index);
2379    }
2380    out.push(1); // draw order index (1-based; MMD treats 0 as invalid)
2381    out.push(1);
2382    push_i32(&mut out, -1);
2383    for _ in 0..4 {
2384        push_i32(&mut out, -1);
2385    }
2386    out.push(0);
2387    push_i32(&mut out, 0);
2388    push_i32(&mut out, max_frame as i32);
2389
2390    let bone_next =
2391        next_keyframe_indices(bone_names.len(), bone_names.len(), additional_bones, |f| {
2392            f.bone_index
2393        });
2394    for bone_index in 0..bone_names.len() {
2395        let fallback = PmmExportBoneKeyframe {
2396            bone_index,
2397            frame: 0,
2398            translation: [0.0; 3],
2399            rotation: [0.0, 0.0, 0.0, 1.0],
2400            interpolation: [20; 16],
2401        };
2402        let frame = initial_bones[bone_index].as_ref().unwrap_or(&fallback);
2403        push_document_bone_keyframe(
2404            &mut out,
2405            None,
2406            0,
2407            0,
2408            bone_next[bone_index],
2409            frame.interpolation,
2410            frame.translation,
2411            frame.rotation,
2412        );
2413    }
2414    push_i32(&mut out, additional_bones.len() as i32);
2415    for (offset, frame) in additional_bones.iter().enumerate() {
2416        let index = bone_names.len() + offset;
2417        let previous = previous_keyframe_index(
2418            additional_bones,
2419            offset,
2420            frame.bone_index,
2421            bone_names.len(),
2422            |f| f.bone_index,
2423        );
2424        let next = next_keyframe_index(
2425            additional_bones,
2426            offset,
2427            frame.bone_index,
2428            bone_names.len(),
2429            |f| f.bone_index,
2430        );
2431        push_document_bone_keyframe(
2432            &mut out,
2433            Some(index as i32),
2434            frame.frame as i32,
2435            previous,
2436            next,
2437            frame.interpolation,
2438            frame.translation,
2439            frame.rotation,
2440        );
2441    }
2442
2443    let morph_next = next_keyframe_indices(
2444        morph_names.len(),
2445        morph_names.len(),
2446        additional_morphs,
2447        |f| f.morph_index,
2448    );
2449    for morph_index in 0..morph_names.len() {
2450        let weight = initial_morphs[morph_index]
2451            .as_ref()
2452            .map(|frame| frame.weight)
2453            .unwrap_or(0.0);
2454        push_document_morph_keyframe(&mut out, None, 0, 0, morph_next[morph_index], weight);
2455    }
2456    push_i32(&mut out, additional_morphs.len() as i32);
2457    for (offset, frame) in additional_morphs.iter().enumerate() {
2458        let index = morph_names.len() + offset;
2459        let previous = previous_keyframe_index(
2460            additional_morphs,
2461            offset,
2462            frame.morph_index,
2463            morph_names.len(),
2464            |f| f.morph_index,
2465        );
2466        let next = next_keyframe_index(
2467            additional_morphs,
2468            offset,
2469            frame.morph_index,
2470            morph_names.len(),
2471            |f| f.morph_index,
2472        );
2473        push_document_morph_keyframe(
2474            &mut out,
2475            Some(index as i32),
2476            frame.frame as i32,
2477            previous,
2478            next,
2479            frame.weight,
2480        );
2481    }
2482
2483    push_document_model_keyframe(
2484        &mut out,
2485        None,
2486        0,
2487        0,
2488        0,
2489        true,
2490        &constraint_states,
2491        &outside_parent_keyframe_indices,
2492        false,
2493    );
2494    push_i32(&mut out, 0);
2495
2496    for _ in bone_names {
2497        for value in [0.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0] {
2498            push_f32(&mut out, value);
2499        }
2500        out.push(0);
2501        out.push(0);
2502        out.push(0);
2503    }
2504    for _ in morph_names {
2505        push_f32(&mut out, 0.0);
2506    }
2507
2508    // Constraint (IK) current states: all enabled.
2509    out.resize(out.len() + constraint_count, 1);
2510    for _ in 0..outside_parent_count {
2511        push_document_outside_parent_current_state(&mut out);
2512    }
2513
2514    out.push(0);
2515    push_f32(&mut out, 1.0);
2516    out.push(0);
2517    out.push(1); // transform order index (1-based, single model)
2518
2519    write_pmm_global_tail(&mut out, max_frame, options.camera_fov);
2520    push_pmm_sjis_string(&mut out, model_path, None);
2521    out.push(0);
2522    out
2523}
2524
2525fn next_keyframe_indices<T>(
2526    count: usize,
2527    initial_count: usize,
2528    frames: &[T],
2529    object_index: impl Fn(&T) -> usize,
2530) -> Vec<i32> {
2531    let mut next = vec![0; count];
2532    for (offset, frame) in frames.iter().enumerate() {
2533        let target = object_index(frame);
2534        if target < count && next[target] == 0 {
2535            next[target] = (initial_count + offset) as i32;
2536        }
2537    }
2538    next
2539}
2540
2541fn previous_keyframe_index<T>(
2542    frames: &[T],
2543    offset: usize,
2544    object_index: usize,
2545    initial_count: usize,
2546    frame_object_index: impl Fn(&T) -> usize,
2547) -> i32 {
2548    if offset > 0 && frame_object_index(&frames[offset - 1]) == object_index {
2549        (initial_count + offset - 1) as i32
2550    } else {
2551        object_index as i32
2552    }
2553}
2554
2555fn next_keyframe_index<T>(
2556    frames: &[T],
2557    offset: usize,
2558    object_index: usize,
2559    initial_count: usize,
2560    frame_object_index: impl Fn(&T) -> usize,
2561) -> i32 {
2562    if offset + 1 < frames.len() && frame_object_index(&frames[offset + 1]) == object_index {
2563        (initial_count + offset + 1) as i32
2564    } else {
2565        0
2566    }
2567}
2568
2569fn push_pmm_fixed_sjis(out: &mut Vec<u8>, text: &str, length: usize) {
2570    let encoded = encode_sjis(text);
2571    let mut bytes = vec![0u8; length];
2572    let copy_len = encoded.len().min(length);
2573    bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
2574    out.extend_from_slice(&bytes);
2575}
2576
2577/// Write Shift-JIS encoded bytes (with zero padding) into an existing fixed-size slice.
2578/// Leaves room for a trailing NUL if encoded length < slice length.
2579/// Intended for patching document model path fields in source bytes.
2580fn write_pmm_fixed_sjis_to_slice(dest: &mut [u8], text: &str) -> Result<(), String> {
2581    let encoded = encode_sjis(text);
2582    if encoded.len() >= dest.len() {
2583        return Err("encoded Shift-JIS path must leave room for NUL terminator".to_owned());
2584    }
2585    dest.fill(0);
2586    let n = encoded.len();
2587    dest[..n].copy_from_slice(&encoded[..n]);
2588    Ok(())
2589}
2590
2591fn push_empty_pmm_path(out: &mut Vec<u8>) {
2592    out.extend_from_slice(&[0u8; 256]);
2593}
2594
2595#[allow(clippy::too_many_arguments)]
2596fn push_document_bone_keyframe(
2597    out: &mut Vec<u8>,
2598    index: Option<i32>,
2599    frame: i32,
2600    previous: i32,
2601    next: i32,
2602    interpolation: [u8; 16],
2603    translation: [f32; 3],
2604    rotation: [f32; 4],
2605) {
2606    if let Some(index) = index {
2607        push_i32(out, index);
2608    }
2609    push_i32(out, frame);
2610    push_i32(out, previous);
2611    push_i32(out, next);
2612    out.extend_from_slice(&interpolation);
2613    for value in translation {
2614        push_f32(out, value);
2615    }
2616    for value in rotation {
2617        push_f32(out, value);
2618    }
2619    out.push(0);
2620    out.push(0);
2621}
2622
2623fn push_document_morph_keyframe(
2624    out: &mut Vec<u8>,
2625    index: Option<i32>,
2626    frame: i32,
2627    previous: i32,
2628    next: i32,
2629    weight: f32,
2630) {
2631    if let Some(index) = index {
2632        push_i32(out, index);
2633    }
2634    push_i32(out, frame);
2635    push_i32(out, previous);
2636    push_i32(out, next);
2637    push_f32(out, weight);
2638    out.push(0);
2639}
2640
2641#[allow(clippy::too_many_arguments)]
2642fn push_document_model_keyframe(
2643    out: &mut Vec<u8>,
2644    index: Option<i32>,
2645    frame: i32,
2646    previous: i32,
2647    next: i32,
2648    visible: bool,
2649    constraint_states: &[bool],
2650    outside_parent_indices: &[(i32, i32)],
2651    self_shadow_enabled: bool,
2652) {
2653    if let Some(index) = index {
2654        push_i32(out, index);
2655    }
2656    push_i32(out, frame);
2657    push_i32(out, previous);
2658    push_i32(out, next);
2659    out.push(u8::from(visible));
2660    for &enabled in constraint_states {
2661        out.push(u8::from(enabled));
2662    }
2663    for &(parent_model_index, parent_model_bone_index) in outside_parent_indices {
2664        push_i32(out, parent_model_index);
2665        push_i32(out, parent_model_bone_index);
2666    }
2667    out.push(u8::from(self_shadow_enabled));
2668}
2669
2670fn push_document_base_keyframe(
2671    out: &mut Vec<u8>,
2672    index: Option<i32>,
2673    frame: i32,
2674    previous: i32,
2675    next: i32,
2676) {
2677    if let Some(index) = index {
2678        push_i32(out, index);
2679    }
2680    push_i32(out, frame);
2681    push_i32(out, previous);
2682    push_i32(out, next);
2683}
2684
2685fn push_document_camera_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool, fov: i32) {
2686    push_document_base_keyframe(out, index, 0, 0, 0);
2687    push_f32(out, 45.0);
2688    out.extend_from_slice(&[0u8; 12]);
2689    out.extend_from_slice(&[0u8; 12]);
2690    push_i32(out, -1);
2691    push_i32(out, -1);
2692    out.extend_from_slice(&[20u8; 24]);
2693    out.push(0);
2694    push_i32(out, fov);
2695    out.push(u8::from(selected));
2696}
2697
2698fn push_document_light_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
2699    push_document_base_keyframe(out, index, 0, 0, 0);
2700    for value in [0.602f32, 0.602, 0.602, -0.5, -1.0, 0.5] {
2701        push_f32(out, value);
2702    }
2703    out.push(u8::from(selected));
2704}
2705
2706fn push_document_gravity_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
2707    push_document_base_keyframe(out, index, 0, 0, 0);
2708    out.push(0);
2709    push_i32(out, 10);
2710    push_f32(out, 9.8);
2711    for value in [0.0f32, -1.0, 0.0] {
2712        push_f32(out, value);
2713    }
2714    out.push(u8::from(selected));
2715}
2716
2717fn push_document_self_shadow_keyframe(out: &mut Vec<u8>, index: Option<i32>, selected: bool) {
2718    push_document_base_keyframe(out, index, 0, 0, 0);
2719    out.push(1);
2720    push_f32(out, 0.01125);
2721    out.push(u8::from(selected));
2722}
2723
2724fn write_pmm_global_tail(out: &mut Vec<u8>, max_frame: u32, camera_fov: f32) {
2725    let camera_fov = camera_fov.round().clamp(1.0, i32::MAX as f32) as i32;
2726    push_document_camera_keyframe(out, None, true, camera_fov);
2727    push_i32(out, 0);
2728    out.extend_from_slice(&[0u8; 12 * 3]);
2729    out.push(0);
2730
2731    push_document_light_keyframe(out, None, true);
2732    push_i32(out, 0);
2733    for value in [0.602f32, 0.602, 0.602, -0.5, -1.0, 0.5] {
2734        push_f32(out, value);
2735    }
2736
2737    out.push(0);
2738    push_i32(out, 0);
2739    out.push(0);
2740
2741    push_i32(out, 0);
2742    push_i32(out, 0);
2743    push_i32(out, max_frame as i32);
2744    push_i32(out, 0);
2745    out.push(0);
2746    out.push(0);
2747    out.push(0);
2748    out.push(0);
2749    push_i32(out, 500);
2750    push_i32(out, 0);
2751    out.push(0);
2752    push_empty_pmm_path(out);
2753    push_i32(out, 0);
2754    push_i32(out, 0);
2755    push_f32(out, 0.0);
2756    push_empty_pmm_path(out);
2757    push_i32(out, 0);
2758    push_i32(out, 0);
2759    push_i32(out, 0);
2760    push_f32(out, 1.0);
2761    push_empty_pmm_path(out);
2762    out.push(0);
2763    out.push(0);
2764    out.push(1);
2765    out.push(1);
2766    push_f32(out, 60.0);
2767    push_i32(out, 0);
2768    push_i32(out, 1);
2769    push_f32(out, 1.0);
2770    out.push(1);
2771    out.push(2);
2772
2773    push_f32(out, 9.8);
2774    push_i32(out, 10);
2775    for value in [0.0f32, -1.0, 0.0] {
2776        push_f32(out, value);
2777    }
2778    out.push(0);
2779    push_document_gravity_keyframe(out, None, true);
2780    push_i32(out, 0);
2781
2782    out.push(1);
2783    push_f32(out, 0.01125);
2784    push_document_self_shadow_keyframe(out, None, true);
2785    push_i32(out, 0);
2786
2787    push_i32(out, 0);
2788    push_i32(out, 0);
2789    push_i32(out, 0);
2790    out.push(0);
2791    push_i32(out, -1);
2792    push_i32(out, -1);
2793    for value in [
2794        1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
2795    ] {
2796        push_f32(out, value);
2797    }
2798    out.push(0);
2799    out.push(0);
2800    out.push(1);
2801    push_i32(out, 0);
2802    out.push(1);
2803    out.push(0);
2804    push_i32(out, 0);
2805}
2806
2807fn push_pmm_len_prefixed_sjis(out: &mut Vec<u8>, text: &str, original_bytes: &[u8]) {
2808    let bytes = if !original_bytes.is_empty() {
2809        original_bytes.to_vec()
2810    } else {
2811        encode_sjis(text)
2812    };
2813    let length = bytes.len().min(u8::MAX as usize);
2814    out.push(length as u8);
2815    out.extend_from_slice(&bytes[..length]);
2816}
2817
2818fn push_pmm_sjis_string(out: &mut Vec<u8>, text: &str, original_bytes: Option<&[u8]>) {
2819    if let Some(bytes) = original_bytes
2820        && !bytes.is_empty()
2821    {
2822        out.extend_from_slice(bytes);
2823        return;
2824    }
2825    out.extend_from_slice(&encode_sjis(text));
2826}
2827
2828fn timeline_from_project_settings(settings: &PmmProjectSettings) -> PmmTimeline {
2829    let duration_seconds = settings
2830        .timeline_frame_count
2831        .zip(settings.frame_rate)
2832        .and_then(|(frame_count, frame_rate)| {
2833            if frame_rate > 0.0 {
2834                Some(frame_count as f32 / frame_rate)
2835            } else {
2836                None
2837            }
2838        });
2839    PmmTimeline {
2840        start_frame: settings.timeline_frame_count.map(|_| 0),
2841        end_frame_exclusive: settings.timeline_frame_count,
2842        frame_count: settings.timeline_frame_count,
2843        frame_rate: settings.frame_rate,
2844        duration_seconds,
2845    }
2846}
2847
2848fn parse_project_settings(data: &[u8]) -> PmmProjectSettings {
2849    let screen_width = read_u32_at(data, 30).filter(|value| (1..=16_384).contains(value));
2850    let screen_height = read_u32_at(data, 34).filter(|value| (1..=16_384).contains(value));
2851    let timeline_frame_count = read_u32_at(data, 38).filter(|value| *value <= 10_000_000);
2852    let frame_rate =
2853        read_f32_at(data, 42).filter(|value| value.is_finite() && *value > 0.0 && *value <= 1000.0);
2854
2855    PmmProjectSettings {
2856        screen_width,
2857        screen_height,
2858        timeline_frame_count,
2859        frame_rate,
2860    }
2861}
2862
2863fn active_slot_indices(flags: &[u8]) -> Vec<usize> {
2864    flags
2865        .iter()
2866        .enumerate()
2867        .filter(|&(_, f)| *f != 0)
2868        .map(|(i, _)| i)
2869        .collect()
2870}
2871
2872fn empty_slot_indices(flags: &[u8]) -> Vec<usize> {
2873    flags
2874        .iter()
2875        .enumerate()
2876        .filter(|&(_, f)| *f == 0)
2877        .map(|(i, _)| i)
2878        .collect()
2879}
2880
2881fn slot_flag_counts(flags: &[u8]) -> BTreeMap<u8, usize> {
2882    let mut counts: BTreeMap<u8, usize> = BTreeMap::new();
2883    for &flag in flags {
2884        *counts.entry(flag).or_insert(0) += 1;
2885    }
2886    counts
2887}
2888
2889fn slot_flag_entries(flags: &[u8]) -> Vec<PmmModelSlotFlagEntry> {
2890    flags
2891        .iter()
2892        .enumerate()
2893        .map(|(slot_index, flag)| PmmModelSlotFlagEntry {
2894            slot_index,
2895            flag: *flag,
2896            active: *flag != 0,
2897        })
2898        .collect()
2899}
2900
2901fn document_expand_flags(flags: &[u8]) -> Option<PmmDocumentExpandFlags> {
2902    Some(PmmDocumentExpandFlags {
2903        editing_cla: *flags.first()? != 0,
2904        camera_panel: *flags.get(1)? != 0,
2905        light_panel: *flags.get(2)? != 0,
2906        accessory_panel: *flags.get(3)? != 0,
2907        bone_panel: *flags.get(4)? != 0,
2908        morph_panel: *flags.get(5)? != 0,
2909        self_shadow_panel: *flags.get(6)? != 0,
2910    })
2911}
2912
2913fn parse_display_state(data: &[u8], parsed_version: Option<u32>) -> PmmDisplayState {
2914    if parsed_version == Some(1) {
2915        if let (Some(model_count), Some(accessory_count)) =
2916            (data.get(52).copied(), data.get(53).copied())
2917            && model_count <= 64
2918            && accessory_count <= 64
2919        {
2920            return PmmDisplayState {
2921                layout: "pmm-v1-counts",
2922                model_slot_flags: Vec::new(),
2923                model_slot_flag_entries: Vec::new(),
2924                document_expand_flags: None,
2925                selected_model_index: None,
2926                document_model_count: None,
2927                declared_model_slot_count: Some(model_count),
2928                model_slot_count: model_count as usize,
2929                non_zero_model_slot_count: model_count as usize,
2930                accessory_slot_count: Some(accessory_count),
2931                active_model_slot_indices: Vec::new(),
2932                empty_model_slot_indices: Vec::new(),
2933                model_slot_flag_counts: BTreeMap::new(),
2934            };
2935        }
2936        return unknown_display_state();
2937    }
2938
2939    let model_slot_flags = data
2940        .get(46..54)
2941        .filter(|bytes| bytes.iter().all(|byte| *byte <= 2))
2942        .map(|bytes| bytes.to_vec());
2943    let accessory_slot_count = None;
2944    let document_expand_flags = model_slot_flags
2945        .as_ref()
2946        .and_then(|flags| document_expand_flags(flags));
2947    let selected_model_index = model_slot_flags
2948        .as_ref()
2949        .and_then(|_| data.get(53).copied());
2950    let document_model_count = model_slot_flags
2951        .as_ref()
2952        .and_then(|_| data.get(54).copied());
2953
2954    let model_slot_flags = model_slot_flags.unwrap_or_default();
2955    let model_slot_count = model_slot_flags.len();
2956    let non_zero_model_slot_count = model_slot_flags.iter().filter(|flag| **flag != 0).count();
2957    let active_model_slot_indices = active_slot_indices(&model_slot_flags);
2958    let empty_model_slot_indices = empty_slot_indices(&model_slot_flags);
2959    let model_slot_flag_counts = slot_flag_counts(&model_slot_flags);
2960    let model_slot_flag_entries = slot_flag_entries(&model_slot_flags);
2961    PmmDisplayState {
2962        layout: if model_slot_flags.is_empty() {
2963            "unknown"
2964        } else {
2965            "pmm-v2-flags"
2966        },
2967        declared_model_slot_count: if model_slot_flags.is_empty() {
2968            None
2969        } else {
2970            Some(model_slot_count as u8)
2971        },
2972        model_slot_count,
2973        non_zero_model_slot_count,
2974        model_slot_flags,
2975        model_slot_flag_entries,
2976        document_expand_flags,
2977        selected_model_index,
2978        document_model_count,
2979        accessory_slot_count,
2980        active_model_slot_indices,
2981        empty_model_slot_indices,
2982        model_slot_flag_counts,
2983    }
2984}
2985
2986fn unknown_display_state() -> PmmDisplayState {
2987    PmmDisplayState {
2988        layout: "unknown",
2989        model_slot_flags: Vec::new(),
2990        model_slot_flag_entries: Vec::new(),
2991        document_expand_flags: None,
2992        selected_model_index: None,
2993        document_model_count: None,
2994        declared_model_slot_count: None,
2995        model_slot_count: 0,
2996        non_zero_model_slot_count: 0,
2997        accessory_slot_count: None,
2998        active_model_slot_indices: Vec::new(),
2999        empty_model_slot_indices: Vec::new(),
3000        model_slot_flag_counts: BTreeMap::new(),
3001    }
3002}
3003
3004fn parse_header_text_entries(
3005    data: &[u8],
3006    parsed_version: Option<u32>,
3007    first_reference: Option<&PmmAssetReference>,
3008) -> Vec<PmmHeaderTextEntry> {
3009    if parsed_version != Some(1) {
3010        return Vec::new();
3011    }
3012    let start = 54usize;
3013    let Some(first_reference) = first_reference else {
3014        return Vec::new();
3015    };
3016    let end = first_reference.offset.min(data.len());
3017    if end <= start {
3018        return Vec::new();
3019    }
3020
3021    let mut entries = Vec::new();
3022    let mut chunk_start = start;
3023    for index in start..end {
3024        if data[index] != 0 {
3025            continue;
3026        }
3027        if index > chunk_start {
3028            let bytes = &data[chunk_start..index];
3029            let text = decode_sjis(bytes).trim().to_owned();
3030            if !text.is_empty() {
3031                entries.push(PmmHeaderTextEntry {
3032                    index: entries.len(),
3033                    offset: chunk_start,
3034                    offset_end: index,
3035                    text,
3036                    text_bytes: bytes.to_vec(),
3037                });
3038            }
3039        }
3040        chunk_start = index + 1;
3041    }
3042    entries
3043}
3044
3045fn read_u32_at(data: &[u8], offset: usize) -> Option<u32> {
3046    data.get(offset..offset + 4)
3047        .and_then(|bytes| bytes.try_into().ok())
3048        .map(u32::from_le_bytes)
3049}
3050
3051fn read_f32_at(data: &[u8], offset: usize) -> Option<f32> {
3052    data.get(offset..offset + 4)
3053        .and_then(|bytes| bytes.try_into().ok())
3054        .map(f32::from_le_bytes)
3055}
3056
3057struct PmmDocumentCursor<'a> {
3058    inner: ByteReader<'a>,
3059}
3060
3061impl<'a> PmmDocumentCursor<'a> {
3062    fn new(data: &'a [u8]) -> Self {
3063        Self {
3064            inner: ByteReader::new(data),
3065        }
3066    }
3067
3068    fn offset(&self) -> usize {
3069        self.inner.pos
3070    }
3071
3072    fn data(&self) -> &'a [u8] {
3073        self.inner.data
3074    }
3075
3076    fn read_u8(&mut self) -> Option<u8> {
3077        self.inner.read_u8().ok()
3078    }
3079
3080    fn read_bool(&mut self) -> Option<bool> {
3081        Some(self.read_u8()? != 0)
3082    }
3083
3084    fn read_i32(&mut self) -> Option<i32> {
3085        self.inner.read_i32_le().ok()
3086    }
3087
3088    fn read_f32(&mut self) -> Option<f32> {
3089        self.inner.read_f32_le().ok()
3090    }
3091
3092    fn read_f32x3(&mut self) -> Option<[f32; 3]> {
3093        Some([self.read_f32()?, self.read_f32()?, self.read_f32()?])
3094    }
3095
3096    fn read_f32x4(&mut self) -> Option<[f32; 4]> {
3097        Some([
3098            self.read_f32()?,
3099            self.read_f32()?,
3100            self.read_f32()?,
3101            self.read_f32()?,
3102        ])
3103    }
3104
3105    fn read_bytes16(&mut self) -> Option<[u8; 16]> {
3106        self.inner.read_bytes(16).ok()?.try_into().ok()
3107    }
3108
3109    fn read_bytes24(&mut self) -> Option<[u8; 24]> {
3110        self.inner.read_bytes(24).ok()?.try_into().ok()
3111    }
3112
3113    fn read_variable_string(&mut self) -> Option<String> {
3114        let length = self.read_u8()? as usize;
3115        let bytes = self.inner.read_bytes(length).ok()?;
3116        Some(decode_shift_jis(bytes))
3117    }
3118
3119    fn read_fixed_string(&mut self, length: usize) -> Option<String> {
3120        let bytes = self.inner.read_bytes(length).ok()?;
3121        let end = bytes
3122            .iter()
3123            .position(|byte| *byte == 0)
3124            .unwrap_or(bytes.len());
3125        Some(decode_shift_jis(&bytes[..end]))
3126    }
3127
3128    fn skip(&mut self, length: usize) -> Option<()> {
3129        self.inner.skip(length).ok()
3130    }
3131}
3132
3133fn parse_document_summary(
3134    data: &[u8],
3135    parsed_version: Option<u32>,
3136    references: &[PmmAssetReference],
3137) -> Option<PmmDocumentSummary> {
3138    if parsed_version != Some(2) {
3139        return None;
3140    }
3141    let mut cursor = PmmDocumentCursor::new(data);
3142    cursor.skip(30)?;
3143    cursor.read_i32()?;
3144    cursor.read_i32()?;
3145    cursor.read_i32()?;
3146    cursor.read_f32()?;
3147    cursor.skip(7)?;
3148    let selected_model_index = cursor.read_u8()?;
3149    let model_count = cursor.read_u8()? as usize;
3150    if model_count == 0 || selected_model_index as usize >= model_count {
3151        return None;
3152    }
3153
3154    let mut models = Vec::with_capacity(model_count);
3155    for slot_index in 0..model_count {
3156        models.push(read_document_model_summary(
3157            &mut cursor,
3158            slot_index,
3159            references,
3160        )?);
3161    }
3162    let counts = summarize_document_counts(&models);
3163
3164    Some(PmmDocumentSummary {
3165        source: "nanoem/ext/document.c PMMv2 layout",
3166        selected_model_index,
3167        model_count,
3168        counts,
3169        models,
3170    })
3171}
3172
3173fn parse_document_global_summary(
3174    data: &[u8],
3175    parsed_version: Option<u32>,
3176    references: &[PmmAssetReference],
3177) -> Option<PmmDocumentGlobalSummary> {
3178    if parsed_version != Some(2) {
3179        return None;
3180    }
3181    let mut cursor = PmmDocumentCursor::new(data);
3182    cursor.skip(30)?;
3183    cursor.read_i32()?;
3184    cursor.read_i32()?;
3185    cursor.read_i32()?;
3186    cursor.read_f32()?;
3187    cursor.skip(7)?;
3188    let selected_model_index = cursor.read_u8()?;
3189    let model_count = cursor.read_u8()? as usize;
3190    if model_count == 0 || selected_model_index as usize >= model_count {
3191        return None;
3192    }
3193    for slot_index in 0..model_count {
3194        read_document_model_summary(&mut cursor, slot_index, references)?;
3195    }
3196
3197    let offset = cursor.offset();
3198    let camera = read_document_camera_summary(&mut cursor)?;
3199    let light = read_document_light_summary(&mut cursor)?;
3200    let accessories = read_document_accessory_block_summary(&mut cursor, references)?;
3201    let settings_before_gravity = read_document_settings_before_gravity(&mut cursor)?;
3202    let gravity = read_document_gravity_summary(&mut cursor)?;
3203    let self_shadow = read_document_self_shadow_summary(&mut cursor)?;
3204    let settings =
3205        finish_document_settings_summary(settings_before_gravity, &mut cursor, model_count)?;
3206    let offset_end = cursor.offset();
3207
3208    Some(PmmDocumentGlobalSummary {
3209        source: "nanoem/ext/document.c PMMv2 global layout",
3210        offset,
3211        offset_end,
3212        camera,
3213        light,
3214        accessories,
3215        settings,
3216        gravity,
3217        self_shadow,
3218    })
3219}
3220
3221fn read_document_model_summary(
3222    cursor: &mut PmmDocumentCursor<'_>,
3223    slot_index: usize,
3224    references: &[PmmAssetReference],
3225) -> Option<PmmDocumentModelSummary> {
3226    const PMM_PATH_BYTE_LENGTH: usize = 256;
3227    let offset = cursor.offset();
3228    let document_model_index = cursor.read_u8()?;
3229    let name = cursor.read_variable_string()?;
3230    let english_name = cursor.read_variable_string()?;
3231    let path_offset = cursor.offset();
3232    let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3233    let asset_reference_index = asset_reference_index_for_path(references, "model", &path);
3234    cursor.read_u8()?;
3235    let bone_count = usize_from_i32(cursor.read_i32()?)?;
3236    let mut bone_names = Vec::new();
3237    for _ in 0..bone_count {
3238        bone_names.push(cursor.read_variable_string()?);
3239    }
3240    let morph_count = usize_from_i32(cursor.read_i32()?)?;
3241    let mut morph_names = Vec::new();
3242    for _ in 0..morph_count {
3243        morph_names.push(cursor.read_variable_string()?);
3244    }
3245    let constraint_bone_count = usize_from_i32(cursor.read_i32()?)?;
3246    cursor.skip(constraint_bone_count.checked_mul(4)?)?;
3247    let outside_parent_subject_bone_count = usize_from_i32(cursor.read_i32()?)?;
3248    cursor.skip(outside_parent_subject_bone_count.checked_mul(4)?)?;
3249    let draw_order_index = cursor.read_u8()?;
3250    let visible = cursor.read_bool()?;
3251    let selected_bone_index = cursor.read_i32()?;
3252    let selected_morph_indices = [
3253        cursor.read_i32()?,
3254        cursor.read_i32()?,
3255        cursor.read_i32()?,
3256        cursor.read_i32()?,
3257    ];
3258    let expansion_state_count = cursor.read_u8()? as usize;
3259    cursor.skip(expansion_state_count)?;
3260    let vertical_scroll = cursor.read_i32()?;
3261    let last_frame_index = cursor.read_i32()?;
3262
3263    let initial_bone_keyframes_offset = cursor.offset();
3264    let initial_bone_keyframes = bone_count;
3265    let mut initial_bone_keyframe_summaries = Vec::with_capacity(initial_bone_keyframes);
3266    for _ in 0..initial_bone_keyframes {
3267        initial_bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, false)?);
3268    }
3269    let bone_keyframe_count_offset = cursor.offset();
3270    let bone_keyframes = usize_from_i32(cursor.read_i32()?)?;
3271    let bone_keyframes_offset = cursor.offset();
3272    let mut bone_keyframe_summaries = Vec::with_capacity(bone_keyframes);
3273    for _ in 0..bone_keyframes {
3274        bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, true)?);
3275    }
3276    let bone_keyframes_end_offset = cursor.offset();
3277
3278    let initial_morph_keyframes_offset = cursor.offset();
3279    let initial_morph_keyframes = morph_count;
3280    let mut initial_morph_keyframe_summaries = Vec::with_capacity(initial_morph_keyframes);
3281    for _ in 0..initial_morph_keyframes {
3282        initial_morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, false)?);
3283    }
3284    let morph_keyframe_count_offset = cursor.offset();
3285    let morph_keyframes = usize_from_i32(cursor.read_i32()?)?;
3286    let morph_keyframes_offset = cursor.offset();
3287    let mut morph_keyframe_summaries = Vec::with_capacity(morph_keyframes);
3288    for _ in 0..morph_keyframes {
3289        morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, true)?);
3290    }
3291    let morph_keyframes_end_offset = cursor.offset();
3292
3293    let initial_model_keyframes = 1;
3294    let initial_model_keyframe_offset = cursor.offset();
3295    let initial_model_keyframe = read_document_model_keyframe(
3296        cursor,
3297        false,
3298        constraint_bone_count,
3299        outside_parent_subject_bone_count,
3300    )?;
3301    let model_keyframe_count_offset = cursor.offset();
3302    let model_keyframes = usize_from_i32(cursor.read_i32()?)?;
3303    let model_keyframes_offset = cursor.offset();
3304    let mut model_keyframe_summaries = Vec::with_capacity(model_keyframes);
3305    for _ in 0..model_keyframes {
3306        model_keyframe_summaries.push(read_document_model_keyframe(
3307            cursor,
3308            true,
3309            constraint_bone_count,
3310            outside_parent_subject_bone_count,
3311        )?);
3312    }
3313    let model_keyframes_end_offset = cursor.offset();
3314
3315    let bone_states_offset = cursor.offset();
3316    let mut bone_state_summaries = Vec::with_capacity(bone_count);
3317    for _ in 0..bone_count {
3318        bone_state_summaries.push(read_document_bone_state(cursor)?);
3319    }
3320    let morph_states_offset = cursor.offset();
3321    let mut morph_state_summaries = Vec::with_capacity(morph_count);
3322    for _ in 0..morph_count {
3323        morph_state_summaries.push(read_document_morph_state(cursor)?);
3324    }
3325    let constraint_states_offset = cursor.offset();
3326    let mut constraint_state_summaries = Vec::with_capacity(constraint_bone_count);
3327    for _ in 0..constraint_bone_count {
3328        constraint_state_summaries.push(read_document_constraint_state(cursor)?);
3329    }
3330    let outside_parent_states_offset = cursor.offset();
3331    let mut outside_parent_state_summaries = Vec::with_capacity(outside_parent_subject_bone_count);
3332    for _ in 0..outside_parent_subject_bone_count {
3333        outside_parent_state_summaries.push(read_document_outside_parent_state(cursor)?);
3334    }
3335
3336    let blend_enabled = cursor.read_bool()?;
3337    let edge_width = cursor.read_f32()?;
3338    let self_shadow_enabled = cursor.read_bool()?;
3339    let transform_order_index = cursor.read_u8()?;
3340    let offset_end = cursor.offset();
3341
3342    Some(PmmDocumentModelSummary {
3343        slot_index,
3344        document_model_index,
3345        offset,
3346        offset_end,
3347        path_offset,
3348        name,
3349        english_name,
3350        path,
3351        asset_reference_index,
3352        bone_count,
3353        bone_names,
3354        morph_count,
3355        morph_names,
3356        constraint_bone_count,
3357        outside_parent_subject_bone_count,
3358        draw_order_index,
3359        transform_order_index,
3360        selected_bone_index,
3361        selected_morph_indices,
3362        vertical_scroll,
3363        sections: PmmDocumentModelSections {
3364            initial_bone_keyframes_offset,
3365            bone_keyframe_count_offset,
3366            bone_keyframes_offset,
3367            bone_keyframes_end_offset,
3368            initial_morph_keyframes_offset,
3369            morph_keyframe_count_offset,
3370            morph_keyframes_offset,
3371            morph_keyframes_end_offset,
3372            initial_model_keyframe_offset,
3373            model_keyframe_count_offset,
3374            model_keyframes_offset,
3375            model_keyframes_end_offset,
3376            bone_states_offset,
3377            morph_states_offset,
3378            constraint_states_offset,
3379            outside_parent_states_offset,
3380        },
3381        initial_bone_keyframes,
3382        initial_bone_keyframe_summaries,
3383        bone_keyframes,
3384        bone_keyframe_summaries,
3385        initial_morph_keyframes,
3386        initial_morph_keyframe_summaries,
3387        morph_keyframes,
3388        morph_keyframe_summaries,
3389        initial_model_keyframes,
3390        model_keyframes,
3391        initial_model_keyframe,
3392        model_keyframe_summaries,
3393        last_frame_index,
3394        visible,
3395        blend_enabled,
3396        edge_width,
3397        self_shadow_enabled,
3398        bone_state_summaries,
3399        morph_state_summaries,
3400        constraint_state_summaries,
3401        outside_parent_state_summaries,
3402    })
3403}
3404
3405fn read_document_bone_keyframe(
3406    cursor: &mut PmmDocumentCursor<'_>,
3407    include_index: bool,
3408) -> Option<PmmDocumentBoneKeyframeSummary> {
3409    let offset = cursor.offset();
3410    let base = read_document_base_keyframe(cursor, include_index)?;
3411    let payload_offset = cursor.offset();
3412    let interpolation = cursor.read_bytes16()?;
3413    let translation = cursor.read_f32x3()?;
3414    let orientation = cursor.read_f32x4()?;
3415    let physics_disabled = cursor.read_bool()?;
3416    let selected = cursor.read_bool()?;
3417    let byte_length = cursor.offset() - offset;
3418    let payload_byte_length = cursor.offset() - payload_offset;
3419    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3420    Some(PmmDocumentBoneKeyframeSummary {
3421        index: base.index,
3422        frame_index: base.frame_index,
3423        previous_keyframe_index: base.previous_keyframe_index,
3424        next_keyframe_index: base.next_keyframe_index,
3425        interpolation,
3426        translation,
3427        orientation,
3428        physics_disabled,
3429        selected,
3430        offset,
3431        byte_length,
3432        payload_offset,
3433        payload_byte_length,
3434        payload_bytes,
3435    })
3436}
3437
3438fn read_document_morph_keyframe(
3439    cursor: &mut PmmDocumentCursor<'_>,
3440    include_index: bool,
3441) -> Option<PmmDocumentMorphKeyframeSummary> {
3442    let offset = cursor.offset();
3443    let base = read_document_base_keyframe(cursor, include_index)?;
3444    let payload_offset = cursor.offset();
3445    let weight = cursor.read_f32()?;
3446    let selected = cursor.read_bool()?;
3447    let byte_length = cursor.offset() - offset;
3448    let payload_byte_length = cursor.offset() - payload_offset;
3449    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3450    Some(PmmDocumentMorphKeyframeSummary {
3451        index: base.index,
3452        frame_index: base.frame_index,
3453        previous_keyframe_index: base.previous_keyframe_index,
3454        next_keyframe_index: base.next_keyframe_index,
3455        weight,
3456        selected,
3457        offset,
3458        byte_length,
3459        payload_offset,
3460        payload_byte_length,
3461        payload_bytes,
3462    })
3463}
3464
3465fn read_document_model_keyframe(
3466    cursor: &mut PmmDocumentCursor<'_>,
3467    include_index: bool,
3468    constraint_bone_count: usize,
3469    outside_parent_subject_bone_count: usize,
3470) -> Option<PmmDocumentModelKeyframeSummary> {
3471    let offset = cursor.offset();
3472    let base = read_document_base_keyframe(cursor, include_index)?;
3473    let payload_offset = cursor.offset();
3474    let visible_offset = cursor.offset();
3475    let visible = cursor.read_bool()?;
3476    let constraint_states_offset = cursor.offset();
3477    let mut constraint_states = Vec::with_capacity(constraint_bone_count);
3478    for _ in 0..constraint_bone_count {
3479        constraint_states.push(cursor.read_bool()?);
3480    }
3481    let constraint_states_byte_length = cursor.offset() - constraint_states_offset;
3482    let outside_parent_indices_offset = cursor.offset();
3483    let mut outside_parent_indices = Vec::with_capacity(outside_parent_subject_bone_count);
3484    for _ in 0..outside_parent_subject_bone_count {
3485        outside_parent_indices.push(PmmDocumentOutsideParentIndexSummary {
3486            parent_model_index: cursor.read_i32()?,
3487            parent_model_bone_index: cursor.read_i32()?,
3488        });
3489    }
3490    let outside_parent_indices_byte_length = cursor.offset() - outside_parent_indices_offset;
3491    let self_shadow_enabled_offset = cursor.offset();
3492    let self_shadow_enabled = cursor.read_bool()?;
3493    let byte_length = cursor.offset() - offset;
3494    let payload_byte_length = cursor.offset() - payload_offset;
3495    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3496    let constraint_state_count = constraint_bone_count;
3497    let outside_parent_index_count = outside_parent_subject_bone_count;
3498    Some(PmmDocumentModelKeyframeSummary {
3499        index: base.index,
3500        frame_index: base.frame_index,
3501        previous_keyframe_index: base.previous_keyframe_index,
3502        next_keyframe_index: base.next_keyframe_index,
3503        visible,
3504        constraint_states,
3505        outside_parent_indices,
3506        self_shadow_enabled,
3507        visible_offset,
3508        constraint_state_count,
3509        constraint_states_offset,
3510        constraint_states_byte_length,
3511        outside_parent_index_count,
3512        outside_parent_indices_offset,
3513        outside_parent_indices_byte_length,
3514        self_shadow_enabled_offset,
3515        offset,
3516        byte_length,
3517        payload_offset,
3518        payload_byte_length,
3519        payload_bytes,
3520    })
3521}
3522
3523fn read_document_camera_summary(
3524    cursor: &mut PmmDocumentCursor<'_>,
3525) -> Option<PmmDocumentTrackSummary> {
3526    let offset = cursor.offset();
3527    let initial_keyframe = read_document_camera_keyframe(cursor, false)?;
3528    let keyframe_count_offset = cursor.offset();
3529    let keyframes = usize_from_i32(cursor.read_i32()?)?;
3530    let keyframes_offset = cursor.offset();
3531    let mut keyframe_summaries = Vec::with_capacity(keyframes);
3532    for _ in 0..keyframes {
3533        keyframe_summaries.push(read_document_camera_keyframe(cursor, true)?);
3534    }
3535    let keyframes_end_offset = cursor.offset();
3536    let state_offset = cursor.offset();
3537    cursor.skip(12 * 3)?;
3538    cursor.read_bool()?;
3539    let state_end_offset = cursor.offset();
3540    Some(PmmDocumentTrackSummary {
3541        offset,
3542        offset_end: state_end_offset,
3543        initial_keyframes: 1,
3544        keyframes,
3545        initial_keyframe: Some(initial_keyframe),
3546        keyframe_summaries,
3547        keyframe_count_offset,
3548        keyframes_offset,
3549        keyframes_end_offset,
3550        state_offset: Some(state_offset),
3551        state_end_offset: Some(state_end_offset),
3552    })
3553}
3554
3555fn read_document_camera_keyframe(
3556    cursor: &mut PmmDocumentCursor<'_>,
3557    include_index: bool,
3558) -> Option<PmmDocumentKeyframeSummary> {
3559    let offset = cursor.offset();
3560    let base = read_document_base_keyframe(cursor, include_index)?;
3561    let payload_offset = cursor.offset();
3562    let distance_offset = cursor.offset();
3563    let distance = cursor.read_f32()?;
3564    let look_at_offset = cursor.offset();
3565    let look_at = cursor.read_f32x3()?;
3566    let look_at_byte_length = cursor.offset() - look_at_offset;
3567    let angle_offset = cursor.offset();
3568    let angle = cursor.read_f32x3()?;
3569    let angle_byte_length = cursor.offset() - angle_offset;
3570    let parent_model_index_offset = cursor.offset();
3571    let parent_model_index = cursor.read_i32()?;
3572    let parent_model_bone_index_offset = cursor.offset();
3573    let parent_model_bone_index = cursor.read_i32()?;
3574    let interpolation_offset = cursor.offset();
3575    let interpolation = cursor.read_bytes24()?;
3576    let interpolation_byte_length = cursor.offset() - interpolation_offset;
3577    let perspective_view_offset = cursor.offset();
3578    let perspective_view = !cursor.read_bool()?;
3579    let fov_offset = cursor.offset();
3580    let fov = cursor.read_i32()?;
3581    let selected_offset = cursor.offset();
3582    let selected = cursor.read_bool()?;
3583    let byte_length = cursor.offset() - offset;
3584    let payload_byte_length = cursor.offset() - payload_offset;
3585    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3586    Some(PmmDocumentKeyframeSummary::Camera {
3587        index: base.index,
3588        frame_index: base.frame_index,
3589        previous_keyframe_index: base.previous_keyframe_index,
3590        next_keyframe_index: base.next_keyframe_index,
3591        distance,
3592        look_at,
3593        angle,
3594        parent_model_index,
3595        parent_model_bone_index,
3596        interpolation,
3597        perspective_view,
3598        fov,
3599        selected,
3600        distance_offset,
3601        look_at_offset,
3602        look_at_byte_length,
3603        angle_offset,
3604        angle_byte_length,
3605        parent_model_index_offset,
3606        parent_model_bone_index_offset,
3607        interpolation_offset,
3608        interpolation_byte_length,
3609        perspective_view_offset,
3610        fov_offset,
3611        selected_offset,
3612        offset,
3613        byte_length,
3614        payload_offset,
3615        payload_byte_length,
3616        payload_bytes,
3617    })
3618}
3619
3620fn read_document_light_summary(
3621    cursor: &mut PmmDocumentCursor<'_>,
3622) -> Option<PmmDocumentTrackSummary> {
3623    let offset = cursor.offset();
3624    let initial_keyframe = read_document_light_keyframe(cursor, false)?;
3625    let keyframe_count_offset = cursor.offset();
3626    let keyframes = usize_from_i32(cursor.read_i32()?)?;
3627    let keyframes_offset = cursor.offset();
3628    let mut keyframe_summaries = Vec::with_capacity(keyframes);
3629    for _ in 0..keyframes {
3630        keyframe_summaries.push(read_document_light_keyframe(cursor, true)?);
3631    }
3632    let keyframes_end_offset = cursor.offset();
3633    let state_offset = cursor.offset();
3634    cursor.skip(12)?;
3635    cursor.skip(12)?;
3636    let state_end_offset = cursor.offset();
3637    Some(PmmDocumentTrackSummary {
3638        offset,
3639        offset_end: state_end_offset,
3640        initial_keyframes: 1,
3641        keyframes,
3642        initial_keyframe: Some(initial_keyframe),
3643        keyframe_summaries,
3644        keyframe_count_offset,
3645        keyframes_offset,
3646        keyframes_end_offset,
3647        state_offset: Some(state_offset),
3648        state_end_offset: Some(state_end_offset),
3649    })
3650}
3651
3652fn read_document_light_keyframe(
3653    cursor: &mut PmmDocumentCursor<'_>,
3654    include_index: bool,
3655) -> Option<PmmDocumentKeyframeSummary> {
3656    let offset = cursor.offset();
3657    let base = read_document_base_keyframe(cursor, include_index)?;
3658    let payload_offset = cursor.offset();
3659    let color_offset = cursor.offset();
3660    let color = cursor.read_f32x3()?;
3661    let color_byte_length = cursor.offset() - color_offset;
3662    let direction_offset = cursor.offset();
3663    let direction = cursor.read_f32x3()?;
3664    let direction_byte_length = cursor.offset() - direction_offset;
3665    let selected_offset = cursor.offset();
3666    let selected = cursor.read_bool()?;
3667    let byte_length = cursor.offset() - offset;
3668    let payload_byte_length = cursor.offset() - payload_offset;
3669    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3670    Some(PmmDocumentKeyframeSummary::Light {
3671        index: base.index,
3672        frame_index: base.frame_index,
3673        previous_keyframe_index: base.previous_keyframe_index,
3674        next_keyframe_index: base.next_keyframe_index,
3675        color,
3676        direction,
3677        selected,
3678        color_offset,
3679        color_byte_length,
3680        direction_offset,
3681        direction_byte_length,
3682        selected_offset,
3683        offset,
3684        byte_length,
3685        payload_offset,
3686        payload_byte_length,
3687        payload_bytes,
3688    })
3689}
3690
3691fn read_document_accessory_block_summary(
3692    cursor: &mut PmmDocumentCursor<'_>,
3693    references: &[PmmAssetReference],
3694) -> Option<PmmDocumentAccessoryBlockSummary> {
3695    let offset = cursor.offset();
3696    let selected_accessory_index = cursor.read_u8()?;
3697    let horizontal_scroll = cursor.read_i32()?;
3698    let accessory_count = cursor.read_u8()? as usize;
3699    let mut accessories = Vec::with_capacity(accessory_count);
3700    for slot_index in 0..accessory_count {
3701        accessories.push(read_document_accessory_summary(
3702            cursor, slot_index, references,
3703        )?);
3704    }
3705    let offset_end = cursor.offset();
3706    let keyframes = accessories
3707        .iter()
3708        .map(|accessory| accessory.keyframes)
3709        .sum();
3710    Some(PmmDocumentAccessoryBlockSummary {
3711        offset,
3712        offset_end,
3713        selected_accessory_index,
3714        horizontal_scroll,
3715        accessory_count,
3716        keyframes,
3717        accessories,
3718    })
3719}
3720
3721fn read_document_accessory_summary(
3722    cursor: &mut PmmDocumentCursor<'_>,
3723    slot_index: usize,
3724    references: &[PmmAssetReference],
3725) -> Option<PmmDocumentAccessorySummary> {
3726    const PMM_ACCESSORY_NAME_BYTE_LENGTH: usize = 100;
3727    const PMM_PATH_BYTE_LENGTH: usize = 256;
3728    let offset = cursor.offset();
3729    let document_accessory_index = cursor.read_u8()?;
3730    let name = cursor.read_fixed_string(PMM_ACCESSORY_NAME_BYTE_LENGTH)?;
3731    let path_offset = cursor.offset();
3732    let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3733    let asset_reference_index = asset_reference_index_for_path(references, "accessory", &path);
3734    let draw_order_index = cursor.read_u8()?;
3735    let initial_keyframe = read_document_accessory_keyframe(cursor, false)?;
3736    let keyframe_count_offset = cursor.offset();
3737    let keyframes = usize_from_i32(cursor.read_i32()?)?;
3738    let keyframes_offset = cursor.offset();
3739    let mut keyframe_summaries = Vec::with_capacity(keyframes);
3740    for _ in 0..keyframes {
3741        keyframe_summaries.push(read_document_accessory_keyframe(cursor, true)?);
3742    }
3743    let keyframes_end_offset = cursor.offset();
3744
3745    let state_offset = cursor.offset();
3746    let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
3747    let parent_model_index = cursor.read_i32()?;
3748    let parent_model_bone_index = cursor.read_i32()?;
3749    cursor.skip(12)?;
3750    let scale_factor = cursor.read_f32()?;
3751    cursor.skip(12)?;
3752    let shadow_enabled = cursor.read_bool()?;
3753    let add_blend_enabled = cursor.read_bool()?;
3754    let state_end_offset = cursor.offset();
3755    let offset_end = cursor.offset();
3756
3757    Some(PmmDocumentAccessorySummary {
3758        slot_index,
3759        document_accessory_index,
3760        offset,
3761        offset_end,
3762        path_offset,
3763        name,
3764        path,
3765        asset_reference_index,
3766        draw_order_index,
3767        keyframes,
3768        initial_keyframe,
3769        keyframe_summaries,
3770        keyframe_count_offset,
3771        keyframes_offset,
3772        keyframes_end_offset,
3773        state_offset,
3774        state_end_offset,
3775        visible,
3776        opacity,
3777        parent_model_index,
3778        parent_model_bone_index,
3779        scale_factor,
3780        shadow_enabled,
3781        add_blend_enabled,
3782    })
3783}
3784
3785fn read_document_accessory_keyframe(
3786    cursor: &mut PmmDocumentCursor<'_>,
3787    include_index: bool,
3788) -> Option<PmmDocumentAccessoryKeyframeSummary> {
3789    let offset = cursor.offset();
3790    let base = read_document_base_keyframe(cursor, include_index)?;
3791    let payload_offset = cursor.offset();
3792    let packed_opacity_visible_offset = cursor.offset();
3793    let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
3794    let parent_model_index_offset = cursor.offset();
3795    let parent_model_index = cursor.read_i32()?;
3796    let parent_model_bone_index_offset = cursor.offset();
3797    let parent_model_bone_index = cursor.read_i32()?;
3798    let translation_offset = cursor.offset();
3799    let translation = cursor.read_f32x3()?;
3800    let translation_byte_length = cursor.offset() - translation_offset;
3801    let orientation_offset = cursor.offset();
3802    let orientation = cursor.read_f32x3()?;
3803    let orientation_byte_length = cursor.offset() - orientation_offset;
3804    let scale_factor_offset = cursor.offset();
3805    let scale_factor = cursor.read_f32()?;
3806    let shadow_enabled_offset = cursor.offset();
3807    let shadow_enabled = cursor.read_bool()?;
3808    let selected_offset = cursor.offset();
3809    let selected = cursor.read_bool()?;
3810    let byte_length = cursor.offset() - offset;
3811    let payload_byte_length = cursor.offset() - payload_offset;
3812    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
3813    Some(PmmDocumentAccessoryKeyframeSummary {
3814        index: base.index,
3815        frame_index: base.frame_index,
3816        previous_keyframe_index: base.previous_keyframe_index,
3817        next_keyframe_index: base.next_keyframe_index,
3818        visible,
3819        opacity,
3820        parent_model_index,
3821        parent_model_bone_index,
3822        translation,
3823        orientation,
3824        scale_factor,
3825        shadow_enabled,
3826        selected,
3827        packed_opacity_visible_offset,
3828        parent_model_index_offset,
3829        parent_model_bone_index_offset,
3830        translation_offset,
3831        translation_byte_length,
3832        orientation_offset,
3833        orientation_byte_length,
3834        scale_factor_offset,
3835        shadow_enabled_offset,
3836        selected_offset,
3837        offset,
3838        byte_length,
3839        payload_offset,
3840        payload_byte_length,
3841        payload_bytes,
3842    })
3843}
3844
3845struct PmmDocumentSettingsBeforeGravity {
3846    offset: usize,
3847    current_frame_index: i32,
3848    current_frame_index_offset: usize,
3849    horizontal_scroll: i32,
3850    horizontal_scroll_thumb: i32,
3851    editing_mode: i32,
3852    camera_look_mode: u8,
3853    loop_enabled: bool,
3854    begin_frame_index_enabled: bool,
3855    begin_frame_index_enabled_offset: usize,
3856    end_frame_index_enabled: bool,
3857    end_frame_index_enabled_offset: usize,
3858    begin_frame_index: i32,
3859    begin_frame_index_offset: usize,
3860    end_frame_index: i32,
3861    end_frame_index_offset: usize,
3862    audio_enabled: bool,
3863    audio_path: String,
3864    audio_path_offset: usize,
3865    background_video_offset: [i32; 2],
3866    background_video_scale_factor: f32,
3867    background_video_path: String,
3868    background_video_path_offset: usize,
3869    background_video_enabled: bool,
3870    background_image_offset: [i32; 2],
3871    background_image_scale_factor: f32,
3872    background_image_path: String,
3873    background_image_path_offset: usize,
3874    background_image_enabled: bool,
3875    information_shown: bool,
3876    grid_and_axis_shown: bool,
3877    ground_shadow_shown: bool,
3878    preferred_fps: f32,
3879    screen_capture_mode: i32,
3880    accessory_index_after_models: i32,
3881    ground_shadow_brightness: f32,
3882    translucent_ground_shadow_enabled: bool,
3883    physics_simulation_mode: u8,
3884}
3885
3886fn read_document_settings_before_gravity(
3887    cursor: &mut PmmDocumentCursor<'_>,
3888) -> Option<PmmDocumentSettingsBeforeGravity> {
3889    const PMM_PATH_BYTE_LENGTH: usize = 256;
3890    let offset = cursor.offset();
3891    let current_frame_index_offset = cursor.offset();
3892    let current_frame_index = cursor.read_i32()?;
3893    let horizontal_scroll = cursor.read_i32()?;
3894    let horizontal_scroll_thumb = cursor.read_i32()?;
3895    let editing_mode = cursor.read_i32()?;
3896    let camera_look_mode = cursor.read_u8()?;
3897    let loop_enabled = cursor.read_bool()?;
3898    let begin_frame_index_enabled_offset = cursor.offset();
3899    let begin_frame_index_enabled = cursor.read_bool()?;
3900    let end_frame_index_enabled_offset = cursor.offset();
3901    let end_frame_index_enabled = cursor.read_bool()?;
3902    let begin_frame_index_offset = cursor.offset();
3903    let begin_frame_index = cursor.read_i32()?;
3904    let end_frame_index_offset = cursor.offset();
3905    let end_frame_index = cursor.read_i32()?;
3906    let audio_enabled = cursor.read_bool()?;
3907    let audio_path_offset = cursor.offset();
3908    let audio_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3909    let background_video_offset = [cursor.read_i32()?, cursor.read_i32()?];
3910    let background_video_scale_factor = cursor.read_f32()?;
3911    let background_video_path_offset = cursor.offset();
3912    let background_video_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3913    let background_video_enabled = cursor.read_i32()? != 0;
3914    let background_image_offset = [cursor.read_i32()?, cursor.read_i32()?];
3915    let background_image_scale_factor = cursor.read_f32()?;
3916    let background_image_path_offset = cursor.offset();
3917    let background_image_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
3918    let background_image_enabled = cursor.read_bool()?;
3919    let information_shown = cursor.read_bool()?;
3920    let grid_and_axis_shown = cursor.read_bool()?;
3921    let ground_shadow_shown = cursor.read_bool()?;
3922    let preferred_fps = cursor.read_f32()?;
3923    let screen_capture_mode = cursor.read_i32()?;
3924    let accessory_index_after_models = cursor.read_i32()?;
3925    let ground_shadow_brightness = cursor.read_f32()?;
3926    let translucent_ground_shadow_enabled = cursor.read_bool()?;
3927    let physics_simulation_mode = cursor.read_u8()?;
3928
3929    Some(PmmDocumentSettingsBeforeGravity {
3930        offset,
3931        current_frame_index,
3932        current_frame_index_offset,
3933        horizontal_scroll,
3934        horizontal_scroll_thumb,
3935        editing_mode,
3936        camera_look_mode,
3937        loop_enabled,
3938        begin_frame_index_enabled,
3939        begin_frame_index_enabled_offset,
3940        end_frame_index_enabled,
3941        end_frame_index_enabled_offset,
3942        begin_frame_index,
3943        begin_frame_index_offset,
3944        end_frame_index,
3945        end_frame_index_offset,
3946        audio_enabled,
3947        audio_path,
3948        audio_path_offset,
3949        background_video_offset,
3950        background_video_scale_factor,
3951        background_video_path,
3952        background_video_path_offset,
3953        background_video_enabled,
3954        background_image_offset,
3955        background_image_scale_factor,
3956        background_image_path,
3957        background_image_path_offset,
3958        background_image_enabled,
3959        information_shown,
3960        grid_and_axis_shown,
3961        ground_shadow_shown,
3962        preferred_fps,
3963        screen_capture_mode,
3964        accessory_index_after_models,
3965        ground_shadow_brightness,
3966        translucent_ground_shadow_enabled,
3967        physics_simulation_mode,
3968    })
3969}
3970
3971fn read_document_gravity_summary(
3972    cursor: &mut PmmDocumentCursor<'_>,
3973) -> Option<PmmDocumentTrackSummary> {
3974    let offset = cursor.offset();
3975    let state_offset = cursor.offset();
3976    cursor.read_f32()?;
3977    cursor.read_i32()?;
3978    cursor.skip(12)?;
3979    cursor.read_bool()?;
3980    let state_end_offset = cursor.offset();
3981    let initial_keyframe = read_document_gravity_keyframe(cursor, false)?;
3982    let keyframe_count_offset = cursor.offset();
3983    let keyframes = usize_from_i32(cursor.read_i32()?)?;
3984    let keyframes_offset = cursor.offset();
3985    let mut keyframe_summaries = Vec::with_capacity(keyframes);
3986    for _ in 0..keyframes {
3987        keyframe_summaries.push(read_document_gravity_keyframe(cursor, true)?);
3988    }
3989    let keyframes_end_offset = cursor.offset();
3990    Some(PmmDocumentTrackSummary {
3991        offset,
3992        offset_end: keyframes_end_offset,
3993        initial_keyframes: 1,
3994        keyframes,
3995        initial_keyframe: Some(initial_keyframe),
3996        keyframe_summaries,
3997        keyframe_count_offset,
3998        keyframes_offset,
3999        keyframes_end_offset,
4000        state_offset: Some(state_offset),
4001        state_end_offset: Some(state_end_offset),
4002    })
4003}
4004
4005fn read_document_gravity_keyframe(
4006    cursor: &mut PmmDocumentCursor<'_>,
4007    include_index: bool,
4008) -> Option<PmmDocumentKeyframeSummary> {
4009    let offset = cursor.offset();
4010    let base = read_document_base_keyframe(cursor, include_index)?;
4011    let payload_offset = cursor.offset();
4012    let noise_enabled_offset = cursor.offset();
4013    let noise_enabled = cursor.read_bool()?;
4014    let noise_offset = cursor.offset();
4015    let noise = cursor.read_i32()?;
4016    let acceleration_offset = cursor.offset();
4017    let acceleration = cursor.read_f32()?;
4018    let direction_offset = cursor.offset();
4019    let direction = cursor.read_f32x3()?;
4020    let direction_byte_length = cursor.offset() - direction_offset;
4021    let selected_offset = cursor.offset();
4022    let selected = cursor.read_bool()?;
4023    let byte_length = cursor.offset() - offset;
4024    let payload_byte_length = cursor.offset() - payload_offset;
4025    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4026    Some(PmmDocumentKeyframeSummary::Gravity {
4027        index: base.index,
4028        frame_index: base.frame_index,
4029        previous_keyframe_index: base.previous_keyframe_index,
4030        next_keyframe_index: base.next_keyframe_index,
4031        noise_enabled,
4032        noise,
4033        acceleration,
4034        direction,
4035        selected,
4036        noise_enabled_offset,
4037        noise_offset,
4038        acceleration_offset,
4039        direction_offset,
4040        direction_byte_length,
4041        selected_offset,
4042        offset,
4043        byte_length,
4044        payload_offset,
4045        payload_byte_length,
4046        payload_bytes,
4047    })
4048}
4049
4050fn read_document_self_shadow_summary(
4051    cursor: &mut PmmDocumentCursor<'_>,
4052) -> Option<PmmDocumentTrackSummary> {
4053    let offset = cursor.offset();
4054    let state_offset = cursor.offset();
4055    cursor.read_bool()?;
4056    cursor.read_f32()?;
4057    let state_end_offset = cursor.offset();
4058    let initial_keyframe = read_document_self_shadow_keyframe(cursor, false)?;
4059    let keyframe_count_offset = cursor.offset();
4060    let keyframes = usize_from_i32(cursor.read_i32()?)?;
4061    let keyframes_offset = cursor.offset();
4062    let mut keyframe_summaries = Vec::with_capacity(keyframes);
4063    for _ in 0..keyframes {
4064        keyframe_summaries.push(read_document_self_shadow_keyframe(cursor, true)?);
4065    }
4066    let keyframes_end_offset = cursor.offset();
4067    Some(PmmDocumentTrackSummary {
4068        offset,
4069        offset_end: keyframes_end_offset,
4070        initial_keyframes: 1,
4071        keyframes,
4072        initial_keyframe: Some(initial_keyframe),
4073        keyframe_summaries,
4074        keyframe_count_offset,
4075        keyframes_offset,
4076        keyframes_end_offset,
4077        state_offset: Some(state_offset),
4078        state_end_offset: Some(state_end_offset),
4079    })
4080}
4081
4082fn read_document_self_shadow_keyframe(
4083    cursor: &mut PmmDocumentCursor<'_>,
4084    include_index: bool,
4085) -> Option<PmmDocumentKeyframeSummary> {
4086    let offset = cursor.offset();
4087    let base = read_document_base_keyframe(cursor, include_index)?;
4088    let payload_offset = cursor.offset();
4089    let mode_offset = cursor.offset();
4090    let mode = cursor.read_u8()?;
4091    let distance_offset = cursor.offset();
4092    let distance = cursor.read_f32()?;
4093    let selected_offset = cursor.offset();
4094    let selected = cursor.read_bool()?;
4095    let byte_length = cursor.offset() - offset;
4096    let payload_byte_length = cursor.offset() - payload_offset;
4097    let payload_bytes = cursor.data().get(payload_offset..cursor.offset())?.to_vec();
4098    Some(PmmDocumentKeyframeSummary::SelfShadow {
4099        index: base.index,
4100        frame_index: base.frame_index,
4101        previous_keyframe_index: base.previous_keyframe_index,
4102        next_keyframe_index: base.next_keyframe_index,
4103        mode,
4104        distance,
4105        selected,
4106        mode_offset,
4107        distance_offset,
4108        selected_offset,
4109        offset,
4110        byte_length,
4111        payload_offset,
4112        payload_byte_length,
4113        payload_bytes,
4114    })
4115}
4116
4117fn finish_document_settings_summary(
4118    before_gravity: PmmDocumentSettingsBeforeGravity,
4119    cursor: &mut PmmDocumentCursor<'_>,
4120    model_count: usize,
4121) -> Option<PmmDocumentSettingsSummary> {
4122    let edge_color = [
4123        cursor.read_i32()? as f32 / 255.0,
4124        cursor.read_i32()? as f32 / 255.0,
4125        cursor.read_i32()? as f32 / 255.0,
4126    ];
4127    let black_background_enabled = cursor.read_bool()?;
4128    let camera_look_at_model_index = cursor.read_i32()?;
4129    let camera_look_at_model_bone_index = cursor.read_i32()?;
4130    let unknown_matrix_offset = cursor.offset();
4131    cursor.skip(16 * 4)?;
4132    let unknown_matrix_end_offset = cursor.offset();
4133    let following_look_at_enabled = cursor.read_bool()?;
4134    cursor.read_bool()?;
4135    let physics_ground_enabled = cursor.read_bool()?;
4136    let current_frame_index_in_text_field_offset = cursor.offset();
4137    let current_frame_index_in_text_field = cursor.read_i32()?;
4138
4139    let mut model_selection_footer_present = false;
4140    let mut model_selection_footer_offset = None;
4141    let mut model_selection_footer_end_offset = None;
4142    if cursor.offset() < cursor.data().len() {
4143        let footer_offset = cursor.offset();
4144        if cursor.read_u8()? != 0 {
4145            model_selection_footer_present = true;
4146            model_selection_footer_offset = Some(footer_offset);
4147            for _ in 0..model_count {
4148                cursor.read_u8()?;
4149                cursor.read_i32()?;
4150            }
4151            model_selection_footer_end_offset = Some(cursor.offset());
4152        }
4153    }
4154    let offset_end = cursor.offset();
4155
4156    Some(PmmDocumentSettingsSummary {
4157        offset: before_gravity.offset,
4158        offset_end,
4159        current_frame_index: before_gravity.current_frame_index,
4160        current_frame_index_offset: before_gravity.current_frame_index_offset,
4161        horizontal_scroll: before_gravity.horizontal_scroll,
4162        horizontal_scroll_thumb: before_gravity.horizontal_scroll_thumb,
4163        editing_mode: before_gravity.editing_mode,
4164        camera_look_mode: before_gravity.camera_look_mode,
4165        loop_enabled: before_gravity.loop_enabled,
4166        begin_frame_index_enabled: before_gravity.begin_frame_index_enabled,
4167        begin_frame_index_enabled_offset: before_gravity.begin_frame_index_enabled_offset,
4168        end_frame_index_enabled: before_gravity.end_frame_index_enabled,
4169        end_frame_index_enabled_offset: before_gravity.end_frame_index_enabled_offset,
4170        begin_frame_index: before_gravity.begin_frame_index,
4171        begin_frame_index_offset: before_gravity.begin_frame_index_offset,
4172        end_frame_index: before_gravity.end_frame_index,
4173        end_frame_index_offset: before_gravity.end_frame_index_offset,
4174        audio_enabled: before_gravity.audio_enabled,
4175        audio_path: before_gravity.audio_path,
4176        audio_path_offset: before_gravity.audio_path_offset,
4177        background_video_offset: before_gravity.background_video_offset,
4178        background_video_scale_factor: before_gravity.background_video_scale_factor,
4179        background_video_path: before_gravity.background_video_path,
4180        background_video_path_offset: before_gravity.background_video_path_offset,
4181        background_video_enabled: before_gravity.background_video_enabled,
4182        background_image_offset: before_gravity.background_image_offset,
4183        background_image_scale_factor: before_gravity.background_image_scale_factor,
4184        background_image_path: before_gravity.background_image_path,
4185        background_image_path_offset: before_gravity.background_image_path_offset,
4186        background_image_enabled: before_gravity.background_image_enabled,
4187        information_shown: before_gravity.information_shown,
4188        grid_and_axis_shown: before_gravity.grid_and_axis_shown,
4189        ground_shadow_shown: before_gravity.ground_shadow_shown,
4190        preferred_fps: before_gravity.preferred_fps,
4191        screen_capture_mode: before_gravity.screen_capture_mode,
4192        accessory_index_after_models: before_gravity.accessory_index_after_models,
4193        ground_shadow_brightness: before_gravity.ground_shadow_brightness,
4194        translucent_ground_shadow_enabled: before_gravity.translucent_ground_shadow_enabled,
4195        physics_simulation_mode: before_gravity.physics_simulation_mode,
4196        edge_color,
4197        black_background_enabled,
4198        camera_look_at_model_index,
4199        camera_look_at_model_bone_index,
4200        unknown_matrix_offset,
4201        unknown_matrix_end_offset,
4202        following_look_at_enabled,
4203        physics_ground_enabled,
4204        current_frame_index_in_text_field,
4205        current_frame_index_in_text_field_offset,
4206        model_selection_footer_present,
4207        model_selection_footer_offset,
4208        model_selection_footer_end_offset,
4209    })
4210}
4211
4212fn read_document_bone_state(
4213    cursor: &mut PmmDocumentCursor<'_>,
4214) -> Option<PmmDocumentBoneStateSummary> {
4215    let translation = cursor.read_f32x3()?;
4216    let orientation = cursor.read_f32x4()?;
4217    let dirty = cursor.read_bool()?;
4218    let physics_disabled = cursor.read_bool()?;
4219    let selected = cursor.read_bool()?;
4220    Some(PmmDocumentBoneStateSummary {
4221        translation,
4222        orientation,
4223        dirty,
4224        physics_disabled,
4225        selected,
4226    })
4227}
4228
4229fn read_document_morph_state(
4230    cursor: &mut PmmDocumentCursor<'_>,
4231) -> Option<PmmDocumentMorphStateSummary> {
4232    let weight = cursor.read_f32()?;
4233    Some(PmmDocumentMorphStateSummary { weight })
4234}
4235
4236fn read_document_constraint_state(
4237    cursor: &mut PmmDocumentCursor<'_>,
4238) -> Option<PmmDocumentConstraintStateSummary> {
4239    let enabled = cursor.read_bool()?;
4240    Some(PmmDocumentConstraintStateSummary { enabled })
4241}
4242
4243fn read_document_outside_parent_state(
4244    cursor: &mut PmmDocumentCursor<'_>,
4245) -> Option<PmmDocumentOutsideParentStateSummary> {
4246    let parent_model_index = cursor.read_i32()?;
4247    let parent_model_bone_index = cursor.read_i32()?;
4248    let subject_bone_index = cursor.read_i32()?;
4249    let target_model_index = cursor.read_i32()?;
4250    Some(PmmDocumentOutsideParentStateSummary {
4251        parent_model_index,
4252        parent_model_bone_index,
4253        subject_bone_index,
4254        target_model_index,
4255    })
4256}
4257
4258fn read_document_base_keyframe(
4259    cursor: &mut PmmDocumentCursor<'_>,
4260    include_index: bool,
4261) -> Option<PmmDocumentBaseKeyframeSummary> {
4262    let index = if include_index {
4263        Some(cursor.read_i32()?)
4264    } else {
4265        None
4266    };
4267    let frame_index = cursor.read_i32()?;
4268    let previous_keyframe_index = cursor.read_i32()?;
4269    let next_keyframe_index = cursor.read_i32()?;
4270    Some(PmmDocumentBaseKeyframeSummary {
4271        index,
4272        frame_index,
4273        previous_keyframe_index,
4274        next_keyframe_index,
4275    })
4276}
4277
4278fn unpack_document_accessory_opacity_and_visible(value: u8) -> (f32, bool) {
4279    let visible = value & 1 != 0;
4280    let opacity = 1.0 - ((value >> 1) as f32 / 100.0);
4281    (opacity, visible)
4282}
4283
4284fn summarize_document_counts(models: &[PmmDocumentModelSummary]) -> PmmDocumentCounts {
4285    PmmDocumentCounts {
4286        models: models.len(),
4287        bones: models.iter().map(|model| model.bone_count).sum(),
4288        morphs: models.iter().map(|model| model.morph_count).sum(),
4289        initial_bone_keyframes: models
4290            .iter()
4291            .map(|model| model.initial_bone_keyframes)
4292            .sum(),
4293        bone_keyframes: models.iter().map(|model| model.bone_keyframes).sum(),
4294        initial_morph_keyframes: models
4295            .iter()
4296            .map(|model| model.initial_morph_keyframes)
4297            .sum(),
4298        morph_keyframes: models.iter().map(|model| model.morph_keyframes).sum(),
4299        initial_model_keyframes: models
4300            .iter()
4301            .map(|model| model.initial_model_keyframes)
4302            .sum(),
4303        model_keyframes: models.iter().map(|model| model.model_keyframes).sum(),
4304    }
4305}
4306
4307fn usize_from_i32(value: i32) -> Option<usize> {
4308    usize::try_from(value).ok()
4309}
4310
4311fn decode_shift_jis(bytes: &[u8]) -> String {
4312    decode_sjis_trim_nul(bytes)
4313}
4314
4315#[derive(Debug, Clone)]
4316struct PmmModelSlotScan {
4317    slots: Vec<PmmModelSlot>,
4318    stop: Option<PmmModelSlotScanStop>,
4319}
4320
4321#[derive(Debug, Clone)]
4322struct PmmModelSlotScanStop {
4323    offset: usize,
4324    reason: &'static str,
4325}
4326
4327fn parse_model_slots_from_header(
4328    data: &[u8],
4329    references: &[PmmAssetReference],
4330    display_state: &PmmDisplayState,
4331) -> PmmModelSlotScan {
4332    const INITIAL_MODEL_SLOT_OFFSET: usize = 56;
4333    if display_state.layout != "pmm-v2-flags" {
4334        return PmmModelSlotScan {
4335            slots: Vec::new(),
4336            stop: None,
4337        };
4338    }
4339    let declared_count = display_state.non_zero_model_slot_count.max(1);
4340    let mut slots = Vec::new();
4341    let mut offset = INITIAL_MODEL_SLOT_OFFSET;
4342    let mut stop = None;
4343
4344    for slot_index in 0..declared_count {
4345        let require_reference_at_offset = slot_index > 0;
4346        match parse_model_slot_at(
4347            data,
4348            references,
4349            offset,
4350            slot_index,
4351            display_state
4352                .active_model_slot_indices
4353                .get(slot_index)
4354                .copied(),
4355            require_reference_at_offset,
4356        ) {
4357            Ok((slot, next_offset)) => {
4358                slots.push(slot);
4359                offset = next_offset;
4360            }
4361            Err(reason) => {
4362                if !slots.is_empty() && slot_index < declared_count {
4363                    stop = Some(PmmModelSlotScanStop { offset, reason });
4364                }
4365                break;
4366            }
4367        }
4368    }
4369
4370    PmmModelSlotScan { slots, stop }
4371}
4372
4373fn parse_model_slot_at(
4374    data: &[u8],
4375    references: &[PmmAssetReference],
4376    offset: usize,
4377    slot_index: usize,
4378    display_slot_index: Option<usize>,
4379    require_reference_at_offset: bool,
4380) -> Result<(PmmModelSlot, usize), &'static str> {
4381    let (name, name_bytes, after_name) =
4382        read_pmm_len_prefixed_sjis(data, offset).ok_or("missing_name")?;
4383    let (english_name, english_name_bytes, path_offset) =
4384        read_pmm_len_prefixed_sjis(data, after_name).ok_or("missing_english_name")?;
4385    let path_end = data
4386        .get(path_offset..)
4387        .ok_or("missing_model_path")?
4388        .iter()
4389        .position(|byte| *byte == 0)
4390        .map(|relative| path_offset + relative)
4391        .ok_or("missing_path_terminator")?;
4392    if path_end == path_offset {
4393        return Err("empty_path");
4394    }
4395    let decoded_path = decode_sjis(&data[path_offset..path_end]);
4396    let model_path = find_asset_candidates(&decoded_path)
4397        .into_iter()
4398        .find(|candidate| {
4399            candidate
4400                .rsplit_once('.')
4401                .map(|(_, ext)| matches!(ext.to_ascii_lowercase().as_str(), "pmx" | "pmd"))
4402                .unwrap_or(false)
4403        })
4404        .ok_or("missing_model_path")?;
4405    let normalized_path = normalize_path(&model_path);
4406    let asset_reference_index = references.iter().position(|reference| {
4407        reference.kind == "model"
4408            && reference
4409                .normalized_path
4410                .eq_ignore_ascii_case(&normalized_path)
4411            && (!require_reference_at_offset || reference.offset == offset)
4412    });
4413    if require_reference_at_offset && asset_reference_index.is_none() {
4414        return Err("asset_reference_not_matched");
4415    }
4416    let confidence = asset_reference_index
4417        .and_then(|index| references.get(index).map(|reference| reference.confidence))
4418        .unwrap_or_else(|| asset_reference_confidence(&model_path, &normalized_path));
4419    let padding_start = path_end + 1;
4420    let trailing_zero_padding_bytes = data
4421        .get(padding_start..)
4422        .map(|bytes| bytes.iter().take_while(|byte| **byte == 0).count())
4423        .unwrap_or(0);
4424    let next_non_zero_offset = data
4425        .get(padding_start..)
4426        .and_then(|bytes| bytes.iter().position(|byte| *byte != 0))
4427        .map(|relative| padding_start + relative);
4428
4429    Ok((
4430        PmmModelSlot {
4431            slot_index,
4432            display_slot_index,
4433            offset,
4434            offset_end: path_end,
4435            model_path_offset: path_offset,
4436            trailing_zero_padding_bytes,
4437            next_non_zero_offset,
4438            name,
4439            name_bytes,
4440            english_name,
4441            english_name_bytes,
4442            model_path,
4443            normalized_path,
4444            asset_reference_index,
4445            confidence,
4446        },
4447        path_end + 1,
4448    ))
4449}
4450
4451fn read_pmm_len_prefixed_sjis(data: &[u8], offset: usize) -> Option<(String, Vec<u8>, usize)> {
4452    let length = *data.get(offset)? as usize;
4453    if length == 0 || length > 128 {
4454        return None;
4455    }
4456    let start = offset + 1;
4457    let end = start.checked_add(length)?;
4458    let bytes = data.get(start..end)?;
4459    if bytes.contains(&0) {
4460        return None;
4461    }
4462    Some((decode_sjis_trim_nul(bytes), bytes.to_vec(), end))
4463}
4464
4465fn extract_asset_references(data: &[u8]) -> Vec<PmmAssetReference> {
4466    let mut refs = Vec::new();
4467    let mut chunk_start = 0usize;
4468    for index in 0..=data.len() {
4469        if index < data.len() && data[index] != 0 {
4470            continue;
4471        }
4472        if index > chunk_start {
4473            let decoded = decode_sjis(&data[chunk_start..index]);
4474            for candidate in find_asset_candidates(&decoded) {
4475                let normalized = normalize_path(&candidate);
4476                if refs.iter().any(|existing: &PmmAssetReference| {
4477                    existing.normalized_path.eq_ignore_ascii_case(&normalized)
4478                }) {
4479                    continue;
4480                }
4481                let file_name = normalized
4482                    .rsplit('/')
4483                    .next()
4484                    .unwrap_or(&normalized)
4485                    .to_owned();
4486                let extension = file_name
4487                    .rsplit_once('.')
4488                    .map(|(_, ext)| ext.to_ascii_lowercase())
4489                    .unwrap_or_default();
4490                refs.push(PmmAssetReference {
4491                    confidence: asset_reference_confidence(&candidate, &normalized),
4492                    path: candidate,
4493                    normalized_path: normalized,
4494                    file_name,
4495                    kind: classify_extension(&extension),
4496                    extension,
4497                    offset: chunk_start,
4498                    offset_end: index,
4499                });
4500            }
4501        }
4502        chunk_start = index + 1;
4503    }
4504    refs.sort_by_key(|reference| reference.offset);
4505    refs
4506}
4507
4508fn asset_reference_confidence(path: &str, normalized_path: &str) -> &'static str {
4509    if path.contains('\u{FFFD}')
4510        || normalized_path.starts_with('/')
4511        || normalized_path.starts_with(":/")
4512    {
4513        return "low";
4514    }
4515    if normalized_path.contains('/') || last_windows_drive_path_start(path).is_some() {
4516        return "high";
4517    }
4518    "medium"
4519}
4520
4521fn find_asset_candidates(text: &str) -> Vec<String> {
4522    let mut candidates = Vec::new();
4523    for segment in text.split(|c: char| c.is_control() || c == '"' || c == '\'') {
4524        let lower = segment.to_ascii_lowercase();
4525        let mut search_from = 0usize;
4526        while let Some((relative_end, extension)) = next_asset_extension(&lower[search_from..]) {
4527            let end = search_from + relative_end + extension.len();
4528            let start = asset_candidate_start(segment, end);
4529            let candidate = segment[start..end].trim();
4530            if has_asset_file_stem(candidate)
4531                && !candidates
4532                    .iter()
4533                    .any(|existing: &String| existing.eq_ignore_ascii_case(candidate))
4534            {
4535                candidates.push(candidate.to_owned());
4536            }
4537            search_from = end;
4538        }
4539    }
4540    candidates
4541}
4542
4543fn next_asset_extension(text: &str) -> Option<(usize, &'static str)> {
4544    [
4545        ".pmd", ".pmx", ".vmd", ".vac", ".x", ".wav", ".mp3", ".ogg", ".bmp", ".tga", ".png",
4546        ".jpg", ".jpeg", ".dds", ".avi", ".avs", ".mp4", ".wmv",
4547    ]
4548    .iter()
4549    .filter_map(|extension| {
4550        find_extension_with_boundary(text, extension).map(|index| (index, *extension))
4551    })
4552    .min_by_key(|(index, _)| *index)
4553}
4554
4555fn find_extension_with_boundary(text: &str, extension: &str) -> Option<usize> {
4556    let mut search_from = 0usize;
4557    while let Some(relative_index) = text[search_from..].find(extension) {
4558        let index = search_from + relative_index;
4559        let after = index + extension.len();
4560        if text[after..]
4561            .chars()
4562            .next()
4563            .is_none_or(|ch| ch.is_whitespace())
4564        {
4565            return Some(index);
4566        }
4567        search_from = after;
4568    }
4569    None
4570}
4571
4572fn asset_candidate_start(segment: &str, end: usize) -> usize {
4573    let prefix = &segment[..end];
4574    let lower = prefix.to_ascii_lowercase();
4575    if let Some(index) = last_userfile_path_start(&lower) {
4576        return index;
4577    }
4578    if let Some(index) = last_windows_drive_path_start(prefix) {
4579        return index;
4580    }
4581    prefix
4582        .rfind(char::is_whitespace)
4583        .map(|index| index + 1)
4584        .unwrap_or(0)
4585}
4586
4587fn last_userfile_path_start(value: &str) -> Option<usize> {
4588    ["userfile\\", "userfile/"]
4589        .iter()
4590        .flat_map(|needle| value.match_indices(needle).map(|(index, _)| index))
4591        .filter(|index| {
4592            *index == 0
4593                || value.as_bytes()[index - 1] == b'\\'
4594                || value.as_bytes()[index - 1] == b'/'
4595        })
4596        .last()
4597}
4598
4599fn last_windows_drive_path_start(value: &str) -> Option<usize> {
4600    let bytes = value.as_bytes();
4601    if bytes.len() < 3 {
4602        return None;
4603    }
4604    let mut last = None;
4605    for index in 0..=bytes.len() - 3 {
4606        if bytes[index].is_ascii_alphabetic()
4607            && bytes[index + 1] == b':'
4608            && (bytes[index + 2] == b'\\' || bytes[index + 2] == b'/')
4609        {
4610            last = Some(index);
4611        }
4612    }
4613    last
4614}
4615
4616fn has_asset_file_stem(value: &str) -> bool {
4617    let Some((stem, _)) = value
4618        .rsplit(['\\', '/'])
4619        .next()
4620        .and_then(|file_name| file_name.rsplit_once('.'))
4621    else {
4622        return false;
4623    };
4624    if stem.is_empty() {
4625        return false;
4626    }
4627    if value.contains(['\\', '/']) || last_windows_drive_path_start(value).is_some() {
4628        return true;
4629    }
4630    !stem
4631        .chars()
4632        .any(|c| c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*' | '='))
4633}
4634
4635fn normalize_path(value: &str) -> String {
4636    let normalized = value.replace('\\', "/").replace("//", "/");
4637    let lower = normalized.to_ascii_lowercase();
4638    match last_userfile_path_start(&lower) {
4639        Some(index) => normalized[index..].to_owned(),
4640        None => normalized,
4641    }
4642}
4643
4644fn classify_extension(extension: &str) -> &'static str {
4645    match extension {
4646        "pmd" | "pmx" => "model",
4647        "x" | "vac" => "accessory",
4648        "vmd" => "motion",
4649        "wav" | "mp3" | "ogg" => "audio",
4650        "bmp" | "tga" | "png" | "jpg" | "jpeg" | "dds" => "image",
4651        "avi" | "avs" | "mp4" | "wmv" => "video",
4652        _ => "unknown",
4653    }
4654}
4655
4656fn paths_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<String> {
4657    references
4658        .iter()
4659        .filter(|reference| reference.kind == kind)
4660        .map(|reference| reference.normalized_path.clone())
4661        .collect()
4662}
4663
4664fn asset_reference_index_for_path(
4665    references: &[PmmAssetReference],
4666    kind: &str,
4667    path: &str,
4668) -> Option<usize> {
4669    let normalized = normalize_path(path);
4670    references.iter().position(|reference| {
4671        reference.kind == kind && reference.normalized_path.eq_ignore_ascii_case(&normalized)
4672    })
4673}
4674
4675fn scene_assets_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<PmmSceneAsset> {
4676    references
4677        .iter()
4678        .enumerate()
4679        .filter(|(_, reference)| reference.kind == kind)
4680        .enumerate()
4681        .map(|(kind_index, (reference_index, reference))| PmmSceneAsset {
4682            reference_index,
4683            kind_index,
4684            path: reference.path.clone(),
4685            normalized_path: reference.normalized_path.clone(),
4686            file_name: reference.file_name.clone(),
4687            extension: reference.extension.clone(),
4688            offset: reference.offset,
4689            offset_end: reference.offset_end,
4690            confidence: reference.confidence,
4691        })
4692        .collect()
4693}
4694
4695fn asset_summary(references: &[PmmAssetReference]) -> PmmAssetSummary {
4696    let mut kind_counts = BTreeMap::new();
4697    let mut extension_counts = BTreeMap::new();
4698    let mut confidence_counts = BTreeMap::new();
4699    for reference in references {
4700        *kind_counts.entry(reference.kind.to_owned()).or_insert(0) += 1;
4701        *extension_counts
4702            .entry(reference.extension.to_owned())
4703            .or_insert(0) += 1;
4704        *confidence_counts
4705            .entry(reference.confidence.to_owned())
4706            .or_insert(0) += 1;
4707    }
4708
4709    PmmAssetSummary {
4710        reference_count: references.len(),
4711        high_confidence_count: *confidence_counts.get("high").unwrap_or(&0),
4712        medium_confidence_count: *confidence_counts.get("medium").unwrap_or(&0),
4713        low_confidence_count: *confidence_counts.get("low").unwrap_or(&0),
4714        kind_counts,
4715        extension_counts,
4716        confidence_counts,
4717    }
4718}
4719
4720fn pmm_diagnostics(
4721    references: &[PmmAssetReference],
4722    model_slots: &[PmmModelSlot],
4723    document_summary: Option<&PmmDocumentSummary>,
4724    document_global_summary: Option<&PmmDocumentGlobalSummary>,
4725    display_state: &PmmDisplayState,
4726    model_slot_scan_stop: Option<&PmmModelSlotScanStop>,
4727    data: &[u8],
4728) -> Vec<PmmParserDiagnostic> {
4729    let mut diagnostics = vec![
4730        PmmParserDiagnostic {
4731            level: "warning",
4732            code: "PMM_PROJECT_GRAPH_PARTIAL",
4733            message: "PMM parser currently exposes header-derived project settings, timeline, display state, and manifest-derived asset references; binary project graph tracks are not fully decoded yet.".to_owned(),
4734        },
4735        PmmParserDiagnostic {
4736            level: "info",
4737            code: "PMM_ASSET_REFERENCES_SCAN",
4738            message: format!(
4739                "PMM asset references are extracted from decoded null-terminated text chunks and grouped by extension; references={}.",
4740                references.len()
4741            ),
4742        },
4743    ];
4744    if !model_slots.is_empty() {
4745        diagnostics.push(PmmParserDiagnostic {
4746            level: "info",
4747            code: "PMM_MODEL_SLOT_INITIAL_SLICE",
4748            message: format!(
4749                "PMM parser decoded model slot records from the initial fixed-header slice; modelSlots={}.",
4750                model_slots.len()
4751            ),
4752        });
4753    }
4754    if let Some(stop) = model_slot_scan_stop {
4755        diagnostics.push(PmmParserDiagnostic {
4756            level: "info",
4757            code: "PMM_MODEL_SLOT_SCAN_STOPPED",
4758            message: format!(
4759                "PMM model slot header scan stopped at offset {}: {}; decodedSlots={} declaredModelSlots={}.",
4760                stop.offset,
4761                stop.reason,
4762                model_slots.len(),
4763                display_state.non_zero_model_slot_count
4764            ),
4765        });
4766    }
4767    if let Some(summary) = document_summary {
4768        let total_bone_keyframes =
4769            summary.counts.initial_bone_keyframes + summary.counts.bone_keyframes;
4770        let total_morph_keyframes =
4771            summary.counts.initial_morph_keyframes + summary.counts.morph_keyframes;
4772        diagnostics.push(PmmParserDiagnostic {
4773            level: "info",
4774            code: "PMM_DOCUMENT_SUMMARY_PARTIAL",
4775            message: format!(
4776                "PMM parser decoded a PMMv2 document inventory slice with {} model(s), {} total bone keyframe(s) ({} additional), and {} total morph keyframe(s) ({} additional); full keyframe payloads and camera/light/self-shadow/property tracks are not exposed yet.",
4777                summary.model_count,
4778                total_bone_keyframes,
4779                summary.counts.bone_keyframes,
4780                total_morph_keyframes,
4781                summary.counts.morph_keyframes
4782            ),
4783        });
4784    }
4785    if let Some(summary) = document_global_summary {
4786        diagnostics.push(PmmParserDiagnostic {
4787            level: "info",
4788            code: "PMM_DOCUMENT_GLOBAL_SUMMARY_PARTIAL",
4789            message: format!(
4790                "PMM parser decoded a PMMv2 global document inventory slice with {} camera keyframe(s), {} light keyframe(s), {} accessory object(s), {} gravity keyframe(s), and {} self-shadow keyframe(s); full payloads are not exposed yet.",
4791                summary.camera.initial_keyframes + summary.camera.keyframes,
4792                summary.light.initial_keyframes + summary.light.keyframes,
4793                summary.accessories.accessory_count,
4794                summary.gravity.initial_keyframes + summary.gravity.keyframes,
4795                summary.self_shadow.initial_keyframes + summary.self_shadow.keyframes
4796            ),
4797        });
4798    }
4799    if display_state.layout == "pmm-v2-flags"
4800        && display_state.non_zero_model_slot_count > model_slots.len()
4801    {
4802        diagnostics.push(PmmParserDiagnostic {
4803            level: "warning",
4804            code: "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE",
4805            message: format!(
4806                "PMM displayState reports {} non-zero model slot flags, but only {} initial model slot(s) are decoded.",
4807                display_state.non_zero_model_slot_count,
4808                model_slots.len()
4809            ),
4810        });
4811    }
4812    diagnostics.extend(asset_count_mismatch_diagnostics(references, display_state));
4813    if display_state.layout == "unknown"
4814        && let Some(bytes) = data.get(46..54)
4815        && bytes.iter().any(|byte| *byte > 2)
4816    {
4817        diagnostics.push(PmmParserDiagnostic {
4818            level: "warning",
4819            code: "PMM_DISPLAY_STATE_UNPLAUSIBLE",
4820            message: format!(
4821                "PMM displayState.modelSlotFlags header bytes were outside the observed 0..=2 range and were omitted: {:?}.",
4822                bytes
4823            ),
4824        });
4825    }
4826    diagnostics.extend(duplicate_asset_reference_diagnostics(references));
4827    if references.iter().any(|reference| reference.kind == "model")
4828        && !references
4829            .iter()
4830            .any(|reference| reference.kind == "motion")
4831    {
4832        diagnostics.push(PmmParserDiagnostic {
4833            level: "warning",
4834            code: "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN",
4835            message: "PMM manifest-derived asset scan found model references but no motion references; motion paths may live in unparsed binary project graph sections.".to_owned(),
4836        });
4837    }
4838    diagnostics
4839}
4840
4841fn asset_count_mismatch_diagnostics(
4842    references: &[PmmAssetReference],
4843    display_state: &PmmDisplayState,
4844) -> Vec<PmmParserDiagnostic> {
4845    let mut diagnostics = Vec::new();
4846    let scanned_models = references
4847        .iter()
4848        .filter(|reference| reference.kind == "model")
4849        .count();
4850    if display_state.model_slot_count > 0 && scanned_models != display_state.model_slot_count {
4851        diagnostics.push(PmmParserDiagnostic {
4852            level: "info",
4853            code: "PMM_ASSET_COUNT_MISMATCH",
4854            message: format!(
4855                "PMM displayState declares {} model slot(s), but manifest-derived asset scan found {} model reference(s).",
4856                display_state.model_slot_count,
4857                scanned_models
4858            ),
4859        });
4860    }
4861
4862    if let Some(accessory_count) = display_state.accessory_slot_count {
4863        let scanned_accessories = references
4864            .iter()
4865            .filter(|reference| reference.kind == "accessory")
4866            .count();
4867        if scanned_accessories != accessory_count as usize {
4868            diagnostics.push(PmmParserDiagnostic {
4869                level: "info",
4870                code: "PMM_ASSET_COUNT_MISMATCH",
4871                message: format!(
4872                    "PMM displayState declares {} accessory slot(s), but manifest-derived asset scan found {} accessory reference(s).",
4873                    accessory_count,
4874                    scanned_accessories
4875                ),
4876            });
4877        }
4878    }
4879    diagnostics
4880}
4881
4882fn duplicate_asset_reference_diagnostics(
4883    references: &[PmmAssetReference],
4884) -> Vec<PmmParserDiagnostic> {
4885    let mut by_file = HashMap::<(String, String), Vec<(usize, &PmmAssetReference)>>::new();
4886    for (index, reference) in references.iter().enumerate() {
4887        by_file
4888            .entry((reference.file_name.clone(), reference.extension.clone()))
4889            .or_default()
4890            .push((index, reference));
4891    }
4892
4893    let mut diagnostics = Vec::new();
4894    for ((file_name, extension), entries) in by_file {
4895        if entries.len() < 2 {
4896            continue;
4897        }
4898        let mut unique_paths = entries
4899            .iter()
4900            .map(|(_, reference)| reference.normalized_path.to_ascii_lowercase())
4901            .collect::<Vec<_>>();
4902        unique_paths.sort();
4903        unique_paths.dedup();
4904        if unique_paths.len() < 2 {
4905            continue;
4906        }
4907        let indices = entries
4908            .iter()
4909            .map(|(index, _)| index.to_string())
4910            .collect::<Vec<_>>()
4911            .join(",");
4912        diagnostics.push(PmmParserDiagnostic {
4913            level: "warning",
4914            code: "PMM_ASSET_REFERENCE_DUPLICATE",
4915            message: format!(
4916                "PMM manifest-derived asset scan found duplicate file names with different paths: fileName={file_name:?} extension={extension:?} referenceIndices={indices}."
4917            ),
4918        });
4919    }
4920    diagnostics.sort_by(|a, b| a.message.cmp(&b.message));
4921    diagnostics
4922}
4923
4924#[cfg(test)]
4925mod tests {
4926    use super::*;
4927
4928    fn pmm_with_project_settings() -> Vec<u8> {
4929        let mut data = b"Polygon Movie maker 0002".to_vec();
4930        data.resize(30, 0);
4931        data.extend_from_slice(&1920u32.to_le_bytes());
4932        data.extend_from_slice(&1080u32.to_le_bytes());
4933        data.extend_from_slice(&424u32.to_le_bytes());
4934        data.extend_from_slice(&30.0f32.to_le_bytes());
4935        data.extend_from_slice(&[1, 1, 2, 0, 0, 0, 0, 0]);
4936        data.push(3);
4937        data.push(0);
4938        data.extend_from_slice(
4939            b"UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0UserFile\\Audio\\song.wav\0",
4940        );
4941        data
4942    }
4943
4944    fn pmm_with_initial_model_slot() -> Vec<u8> {
4945        let mut data = b"Polygon Movie maker 0002".to_vec();
4946        data.resize(30, 0);
4947        data.extend_from_slice(&640u32.to_le_bytes());
4948        data.extend_from_slice(&360u32.to_le_bytes());
4949        data.extend_from_slice(&250u32.to_le_bytes());
4950        data.extend_from_slice(&30.0f32.to_le_bytes());
4951        data.extend_from_slice(&[0, 1, 1, 1, 1, 1, 1, 0]);
4952        data.push(1);
4953        data.push(0);
4954        data.push(9);
4955        data.extend_from_slice(b"TestModel");
4956        data.push(4);
4957        data.extend_from_slice(b"Base");
4958        data.extend_from_slice(b"C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx");
4959        data.push(0);
4960        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
4961        data
4962    }
4963
4964    #[test]
4965    fn exports_pmm_project_settings_and_asset_references() {
4966        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
4967        let exported = export_pmm_manifest(&parsed);
4968        let reparsed = parse_pmm_manifest(&exported).unwrap();
4969
4970        assert_eq!(reparsed.version, "0002");
4971        assert_eq!(reparsed.project_settings.screen_width, Some(1920));
4972        assert_eq!(reparsed.project_settings.screen_height, Some(1080));
4973        assert_eq!(reparsed.project_settings.timeline_frame_count, Some(424));
4974        assert_eq!(reparsed.project_settings.frame_rate, Some(30.0));
4975        assert_eq!(reparsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
4976        assert_eq!(reparsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
4977        assert_eq!(reparsed.audio_paths, vec!["UserFile/Audio/song.wav"]);
4978    }
4979
4980    #[test]
4981    fn exports_pmm_manifest_preserves_v1_input_bytes() {
4982        let mut data = pmm_with_project_settings();
4983        data[20..24].copy_from_slice(b"0001");
4984        let parsed = parse_pmm_manifest(&data).unwrap();
4985        assert_eq!(parsed.version, "0001");
4986
4987        let exported = export_pmm_manifest(&parsed);
4988        assert_eq!(
4989            exported, data,
4990            "PMMv1 input must be preserved byte-for-byte"
4991        );
4992        let reparsed = parse_pmm_manifest(&exported).unwrap();
4993
4994        assert_eq!(reparsed.version, "0001");
4995        assert_eq!(reparsed.project_settings.timeline_frame_count, Some(424));
4996        assert_eq!(reparsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
4997    }
4998
4999    #[test]
5000    fn exports_pmm_initial_model_slot() {
5001        let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
5002        let exported = export_pmm_manifest(&parsed);
5003        let reparsed = parse_pmm_manifest(&exported).unwrap();
5004
5005        assert_eq!(reparsed.project_settings.screen_width, Some(640));
5006        assert_eq!(reparsed.project_settings.screen_height, Some(360));
5007        assert_eq!(reparsed.project_settings.timeline_frame_count, Some(250));
5008        assert_eq!(reparsed.model_slots.len(), 1);
5009        let slot = &reparsed.model_slots[0];
5010        assert_eq!(slot.name, "TestModel");
5011        assert_eq!(slot.english_name, "Base");
5012        assert_eq!(
5013            slot.normalized_path,
5014            "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
5015        );
5016        assert_eq!(reparsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
5017    }
5018
5019    #[test]
5020    fn pmm_export_roundtrip_bytes_v2_document_global() {
5021        let input = pmm_with_document_global_summary();
5022        let parsed = parse_pmm_manifest(&input).unwrap();
5023        let exported = export_pmm_manifest(&parsed);
5024        assert_eq!(
5025            exported, input,
5026            "parse->export must be byte-for-byte for PMMv2 document/global synthetic"
5027        );
5028        assert!(parsed.document_summary.is_some());
5029        assert!(parsed.document_global_summary.is_some());
5030    }
5031
5032    #[test]
5033    fn pmm_export_roundtrip_bytes_v2_document_summary() {
5034        let input = pmm_with_document_summary();
5035        let parsed = parse_pmm_manifest(&input).unwrap();
5036        let exported = export_pmm_manifest(&parsed);
5037        assert_eq!(
5038            exported, input,
5039            "parse->export must be byte-for-byte for PMMv2 document synthetic"
5040        );
5041    }
5042
5043    #[test]
5044    fn pmm_document_model_path_override_basic() {
5045        // Uses only repo-local synthetic fixture (pmm_with_document_summary) -- no MMDDumper.
5046        let input = pmm_with_document_summary();
5047        let parsed = parse_pmm_manifest(&input).unwrap();
5048        let document = parsed
5049            .document_summary
5050            .as_ref()
5051            .expect("document_summary required");
5052        let model0 = &document.models[0];
5053        assert_eq!(model0.document_model_index, 0);
5054        let original_path = model0.path.clone();
5055        let path_offset = model0.path_offset;
5056
5057        let new_path = "UserFile\\Model\\override_for_gui_smoke.pmx";
5058        let patched =
5059            export_pmm_manifest_with_document_model_path_overrides(&parsed, &[(0u8, new_path)])
5060                .expect("override patch must succeed for valid short path");
5061
5062        assert_eq!(
5063            patched.len(),
5064            input.len(),
5065            "patched bytes must keep identical length"
5066        );
5067
5068        // Only the target 256-byte document model path field may differ.
5069        let mut changed_positions = Vec::new();
5070        for (i, (a, b)) in input.iter().zip(patched.iter()).enumerate() {
5071            if a != b {
5072                changed_positions.push(i);
5073            }
5074        }
5075        assert!(
5076            !changed_positions.is_empty(),
5077            "expected at least the path bytes to change"
5078        );
5079        let field_start = path_offset;
5080        let field_end = path_offset + 256;
5081        for &pos in &changed_positions {
5082            assert!(
5083                pos >= field_start && pos < field_end,
5084                "changed byte at {} must be inside the document model path field [{}, {})",
5085                pos,
5086                field_start,
5087                field_end
5088            );
5089        }
5090
5091        let reparsed = parse_pmm_manifest(&patched).unwrap();
5092        let reparsed_doc = reparsed
5093            .document_summary
5094            .as_ref()
5095            .expect("reparsed must have document_summary");
5096        assert_eq!(
5097            reparsed_doc.models[0].path, new_path,
5098            "reparsed document model path must reflect the override"
5099        );
5100        assert_ne!(
5101            reparsed_doc.models[0].path, original_path,
5102            "reparsed path must differ from original"
5103        );
5104    }
5105
5106    #[test]
5107    fn pmm_document_model_path_override_too_long_returns_err() {
5108        let input = pmm_with_document_summary();
5109        let parsed = parse_pmm_manifest(&input).unwrap();
5110        // 256 ASCII chars -> SJIS 256 bytes >=256 -> err (must leave room for NUL)
5111        let too_long = "X".repeat(256);
5112        let result =
5113            export_pmm_manifest_with_document_model_path_overrides(&parsed, &[(0u8, &too_long)]);
5114        assert!(result.is_err(), "too-long path must error");
5115        let msg = result.unwrap_err();
5116        assert!(
5117            msg.contains("NUL") || msg.contains("too long") || msg.contains("leave room"),
5118            "error message should indicate length/NUL room issue: {}",
5119            msg
5120        );
5121    }
5122
5123    #[test]
5124    fn pmm_document_model_path_override_missing_index_returns_err() {
5125        let input = pmm_with_document_summary();
5126        let parsed = parse_pmm_manifest(&input).unwrap();
5127        let result = export_pmm_manifest_with_document_model_path_overrides(
5128            &parsed,
5129            &[(99u8, "some/path.pmx")],
5130        );
5131        assert!(result.is_err(), "missing document_model_index must error");
5132        let msg = result.unwrap_err();
5133        assert!(
5134            msg.contains("exactly one") && msg.contains("99"),
5135            "error should report missing index: {}",
5136            msg
5137        );
5138    }
5139
5140    #[test]
5141    fn pmm_document_model_path_override_requires_source_bytes() {
5142        let input = pmm_with_document_summary();
5143        let mut parsed = parse_pmm_manifest(&input).unwrap();
5144        parsed.source_bytes.clear();
5145
5146        let result = export_pmm_manifest_with_document_model_path_overrides(
5147            &parsed,
5148            &[(0u8, "UserFile\\Model\\override_for_gui_smoke.pmx")],
5149        );
5150        assert!(result.is_err(), "missing source bytes must error");
5151        let msg = result.unwrap_err();
5152        assert!(
5153            msg.contains("source bytes"),
5154            "error should explain source bytes are required: {}",
5155            msg
5156        );
5157    }
5158
5159    #[test]
5160    fn pmm_scene_frame_range_patch_updates_only_expected_bytes() {
5161        let input = pmm_with_document_global_summary();
5162        let parsed = parse_pmm_manifest(&input).unwrap();
5163        let settings = parsed
5164            .document_global_summary
5165            .as_ref()
5166            .expect("fixture must have global summary")
5167            .settings
5168            .clone();
5169
5170        let patch = PmmSceneFrameRangePatch {
5171            current_frame_index: Some(99),
5172            current_frame_index_in_text_field: Some(77),
5173            begin_frame_index_enabled: Some(true),
5174            end_frame_index_enabled: Some(false),
5175            begin_frame_index: Some(10),
5176            end_frame_index: Some(240),
5177        };
5178        let patched = export_pmm_manifest_with_scene_frame_range_patch(&parsed, &patch)
5179            .expect("scene frame range patch must succeed");
5180
5181        assert_eq!(
5182            patched.len(),
5183            input.len(),
5184            "patch must preserve byte length"
5185        );
5186
5187        let reparsed = parse_pmm_manifest(&patched).unwrap();
5188        let updated = &reparsed
5189            .document_global_summary
5190            .as_ref()
5191            .expect("patched PMM must keep global summary")
5192            .settings;
5193        assert_eq!(updated.current_frame_index, 99);
5194        assert_eq!(updated.current_frame_index_in_text_field, 77);
5195        assert!(updated.begin_frame_index_enabled);
5196        assert!(!updated.end_frame_index_enabled);
5197        assert_eq!(updated.begin_frame_index, 10);
5198        assert_eq!(updated.end_frame_index, 240);
5199
5200        let mut allowed_changed = Vec::new();
5201        allowed_changed
5202            .extend(settings.current_frame_index_offset..settings.current_frame_index_offset + 4);
5203        allowed_changed.push(settings.begin_frame_index_enabled_offset);
5204        allowed_changed.push(settings.end_frame_index_enabled_offset);
5205        allowed_changed
5206            .extend(settings.begin_frame_index_offset..settings.begin_frame_index_offset + 4);
5207        allowed_changed
5208            .extend(settings.end_frame_index_offset..settings.end_frame_index_offset + 4);
5209        allowed_changed.extend(
5210            settings.current_frame_index_in_text_field_offset
5211                ..settings.current_frame_index_in_text_field_offset + 4,
5212        );
5213
5214        let changed: Vec<usize> = input
5215            .iter()
5216            .zip(patched.iter())
5217            .enumerate()
5218            .filter_map(|(index, (before, after))| (before != after).then_some(index))
5219            .collect();
5220
5221        assert!(
5222            changed.iter().all(|index| allowed_changed.contains(index)),
5223            "scene frame range patch changed bytes outside decoded frame range fields: {:?}",
5224            changed
5225                .iter()
5226                .filter(|index| !allowed_changed.contains(index))
5227                .collect::<Vec<_>>()
5228        );
5229        assert_eq!(
5230            &patched[settings.current_frame_index_offset..settings.current_frame_index_offset + 4],
5231            &99i32.to_le_bytes()
5232        );
5233        assert_eq!(patched[settings.begin_frame_index_enabled_offset], 1);
5234        assert_eq!(patched[settings.end_frame_index_enabled_offset], 0);
5235        assert_eq!(
5236            &patched[settings.begin_frame_index_offset..settings.begin_frame_index_offset + 4],
5237            &10i32.to_le_bytes()
5238        );
5239        assert_eq!(
5240            &patched[settings.end_frame_index_offset..settings.end_frame_index_offset + 4],
5241            &240i32.to_le_bytes()
5242        );
5243        assert_eq!(
5244            &patched[settings.current_frame_index_in_text_field_offset
5245                ..settings.current_frame_index_in_text_field_offset + 4],
5246            &77i32.to_le_bytes()
5247        );
5248    }
5249
5250    #[test]
5251    fn pmm_scene_frame_range_patch_noop_preserves_source_bytes() {
5252        let input = pmm_with_document_global_summary();
5253        let parsed = parse_pmm_manifest(&input).unwrap();
5254        let patched = export_pmm_manifest_with_scene_frame_range_patch(
5255            &parsed,
5256            &PmmSceneFrameRangePatch::default(),
5257        )
5258        .expect("no-op scene frame range patch must succeed");
5259
5260        assert_eq!(patched, input, "no-op patch must be byte-identical");
5261    }
5262
5263    #[test]
5264    fn pmm_scene_frame_range_patch_requires_source_bytes() {
5265        let input = pmm_with_document_global_summary();
5266        let mut parsed = parse_pmm_manifest(&input).unwrap();
5267        parsed.source_bytes.clear();
5268
5269        let result = export_pmm_manifest_with_scene_frame_range_patch(
5270            &parsed,
5271            &PmmSceneFrameRangePatch {
5272                begin_frame_index: Some(1),
5273                ..PmmSceneFrameRangePatch::default()
5274            },
5275        );
5276        assert!(result.is_err(), "missing source bytes must error");
5277        let msg = result.unwrap_err();
5278        assert!(
5279            msg.contains("source bytes"),
5280            "error should explain source bytes are required: {}",
5281            msg
5282        );
5283    }
5284
5285    #[test]
5286    fn pmm_scene_media_path_patch_updates_only_expected_bytes() {
5287        let input = pmm_with_document_global_summary();
5288        let parsed = parse_pmm_manifest(&input).unwrap();
5289        let settings = parsed
5290            .document_global_summary
5291            .as_ref()
5292            .expect("fixture must have global summary")
5293            .settings
5294            .clone();
5295
5296        let audio_path = "UserFile\\Audio\\song.wav";
5297        let background_video_path = "UserFile\\Movie\\bg.avi";
5298        let background_image_path = "UserFile\\Image\\bg.png";
5299        let patch = PmmSceneMediaPathPatch {
5300            audio_path: Some(audio_path.to_owned()),
5301            background_video_path: Some(background_video_path.to_owned()),
5302            background_image_path: Some(background_image_path.to_owned()),
5303        };
5304        let patched = export_pmm_manifest_with_scene_media_path_patch(&parsed, &patch)
5305            .expect("scene media path patch must succeed");
5306
5307        assert_eq!(
5308            patched.len(),
5309            input.len(),
5310            "patch must preserve byte length"
5311        );
5312
5313        let reparsed = parse_pmm_manifest(&patched).unwrap();
5314        let updated = &reparsed
5315            .document_global_summary
5316            .as_ref()
5317            .expect("patched PMM must keep global summary")
5318            .settings;
5319        assert_eq!(updated.audio_path, audio_path);
5320        assert_eq!(updated.background_video_path, background_video_path);
5321        assert_eq!(updated.background_image_path, background_image_path);
5322
5323        let bindings = &reparsed
5324            .project_graph
5325            .as_ref()
5326            .expect("patched PMM must keep project graph")
5327            .asset_bindings;
5328        let scene_binding_offset = |kind: &str| {
5329            bindings
5330                .iter()
5331                .find(|binding| binding.scope == "sceneSettings" && binding.asset_kind == kind)
5332                .and_then(|binding| binding.path_offset)
5333        };
5334        assert_eq!(
5335            scene_binding_offset("audio"),
5336            Some(settings.audio_path_offset)
5337        );
5338        assert_eq!(
5339            scene_binding_offset("video"),
5340            Some(settings.background_video_path_offset)
5341        );
5342        assert_eq!(
5343            scene_binding_offset("image"),
5344            Some(settings.background_image_path_offset)
5345        );
5346
5347        let allowed_ranges = [
5348            settings.audio_path_offset..settings.audio_path_offset + 256,
5349            settings.background_video_path_offset..settings.background_video_path_offset + 256,
5350            settings.background_image_path_offset..settings.background_image_path_offset + 256,
5351        ];
5352        let changed: Vec<usize> = input
5353            .iter()
5354            .zip(patched.iter())
5355            .enumerate()
5356            .filter_map(|(index, (before, after))| (before != after).then_some(index))
5357            .collect();
5358
5359        assert!(
5360            changed
5361                .iter()
5362                .all(|index| allowed_ranges.iter().any(|range| range.contains(index))),
5363            "scene media path patch changed bytes outside decoded media path fields: {:?}",
5364            changed
5365                .iter()
5366                .filter(|index| !allowed_ranges.iter().any(|range| range.contains(index)))
5367                .collect::<Vec<_>>()
5368        );
5369
5370        let assert_path_field = |offset: usize, path: &str| {
5371            let field = &patched[offset..offset + 256];
5372            assert_eq!(&field[..path.len()], path.as_bytes());
5373            assert_eq!(field[path.len()], 0);
5374        };
5375        assert_path_field(settings.audio_path_offset, audio_path);
5376        assert_path_field(settings.background_video_path_offset, background_video_path);
5377        assert_path_field(settings.background_image_path_offset, background_image_path);
5378    }
5379
5380    #[test]
5381    fn pmm_scene_media_path_patch_noop_preserves_source_bytes() {
5382        let input = pmm_with_document_global_summary();
5383        let parsed = parse_pmm_manifest(&input).unwrap();
5384        let patched = export_pmm_manifest_with_scene_media_path_patch(
5385            &parsed,
5386            &PmmSceneMediaPathPatch::default(),
5387        )
5388        .expect("no-op scene media path patch must succeed");
5389
5390        assert_eq!(patched, input, "no-op patch must be byte-identical");
5391    }
5392
5393    #[test]
5394    fn pmm_scene_media_path_patch_requires_source_bytes() {
5395        let input = pmm_with_document_global_summary();
5396        let mut parsed = parse_pmm_manifest(&input).unwrap();
5397        parsed.source_bytes.clear();
5398
5399        let result = export_pmm_manifest_with_scene_media_path_patch(
5400            &parsed,
5401            &PmmSceneMediaPathPatch {
5402                audio_path: Some("UserFile\\Audio\\song.wav".to_owned()),
5403                ..PmmSceneMediaPathPatch::default()
5404            },
5405        );
5406        assert!(result.is_err(), "missing source bytes must error");
5407        let msg = result.unwrap_err();
5408        assert!(
5409            msg.contains("source bytes"),
5410            "error should explain source bytes are required: {}",
5411            msg
5412        );
5413    }
5414
5415    #[test]
5416    fn pmm_scene_media_path_patch_too_long_returns_err() {
5417        let input = pmm_with_document_global_summary();
5418        let parsed = parse_pmm_manifest(&input).unwrap();
5419
5420        let result = export_pmm_manifest_with_scene_media_path_patch(
5421            &parsed,
5422            &PmmSceneMediaPathPatch {
5423                audio_path: Some("X".repeat(256)),
5424                ..PmmSceneMediaPathPatch::default()
5425            },
5426        );
5427        assert!(result.is_err(), "too-long media path must error");
5428        let msg = result.unwrap_err();
5429        assert!(
5430            msg.contains("NUL") || msg.contains("leave room"),
5431            "error should explain fixed string length/NUL room: {}",
5432            msg
5433        );
5434    }
5435
5436    #[test]
5437    fn pmm_export_json_top_level_does_not_include_source_bytes() {
5438        // Explicit proof per spec: serde_json output must not expose the internal preservation field.
5439        let data = pmm_with_document_global_summary();
5440        let parsed = parse_pmm_manifest(&data).unwrap();
5441        let value = serde_json::to_value(&parsed).unwrap();
5442        let keys = json_keys(&value);
5443        assert!(
5444            !keys.contains(&"sourceBytes".to_string())
5445                && !keys.contains(&"source_bytes".to_string()),
5446            "top-level JSON keys must not include source bytes field: {:?}",
5447            keys
5448        );
5449    }
5450
5451    #[test]
5452    fn exports_pmm_scene_header_timeline_fps_and_camera_fov() {
5453        let model =
5454            crate::pmx::parse_pmx_model(include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx"))
5455                .unwrap();
5456        let motion = VmdParsedAnimation {
5457            kind: "vmd",
5458            metadata: crate::vmd::VmdParsedMetadata {
5459                format: "vmd",
5460                model_name: "ik_multi_axis_limit_fixture".to_owned(),
5461                model_name_bytes: Vec::new(),
5462                counts: crate::vmd::VmdParsedCounts {
5463                    bones: 1,
5464                    morphs: 0,
5465                    cameras: 0,
5466                    lights: 0,
5467                    self_shadows: 0,
5468                    properties: 0,
5469                },
5470                max_frame: 600,
5471            },
5472            bone_frames: vec![VmdParsedBoneFrame {
5473                bone_name: "link_root".to_owned(),
5474                bone_name_bytes: Vec::new(),
5475                frame: 600,
5476                translation: [0.0, 1.0, 0.0],
5477                rotation: [0.0, 0.0, 0.0, 1.0],
5478                interpolation: vec![20; 64],
5479            }],
5480            morph_frames: Vec::new(),
5481            camera_frames: Vec::new(),
5482            light_frames: Vec::new(),
5483            self_shadow_frames: Vec::new(),
5484            property_frames: Vec::new(),
5485        };
5486
5487        let report = export_pmm_scene_from_pmx_vmd(
5488            &model,
5489            &motion,
5490            "UserFile/Model/ik_multi_axis_limit.pmx",
5491            &PmmSceneExportOptions {
5492                screen_width: 800,
5493                screen_height: 600,
5494                frame_rate: 60.0,
5495                camera_fov: 42.0,
5496            },
5497        );
5498        let reparsed = parse_pmm_manifest(&report.bytes).unwrap();
5499
5500        assert_eq!(report.max_frame, 600);
5501        assert_eq!(reparsed.project_settings.screen_width, Some(800));
5502        assert_eq!(reparsed.project_settings.screen_height, Some(600));
5503        assert_eq!(reparsed.project_settings.timeline_frame_count, Some(600));
5504        assert_eq!(reparsed.project_settings.frame_rate, Some(60.0));
5505        let global = reparsed.document_global_summary.as_ref().unwrap();
5506        match global.camera.initial_keyframe.as_ref().unwrap() {
5507            PmmDocumentKeyframeSummary::Camera { fov, .. } => assert_eq!(*fov, 42),
5508            other => panic!("unexpected camera keyframe summary: {other:?}"),
5509        }
5510    }
5511
5512    fn append_pmm_model_slot(data: &mut Vec<u8>, name: &[u8], english_name: &[u8], path: &[u8]) {
5513        data.push(name.len() as u8);
5514        data.extend_from_slice(name);
5515        data.push(english_name.len() as u8);
5516        data.extend_from_slice(english_name);
5517        data.extend_from_slice(path);
5518        data.push(0);
5519    }
5520
5521    fn push_i32(data: &mut Vec<u8>, value: i32) {
5522        data.extend_from_slice(&value.to_le_bytes());
5523    }
5524
5525    fn push_f32(data: &mut Vec<u8>, value: f32) {
5526        data.extend_from_slice(&value.to_le_bytes());
5527    }
5528
5529    fn push_pmm_variable_string(data: &mut Vec<u8>, value: &[u8]) {
5530        data.push(value.len() as u8);
5531        data.extend_from_slice(value);
5532    }
5533
5534    fn push_pmm_fixed_path(data: &mut Vec<u8>, value: &[u8]) {
5535        let mut path = [0u8; 256];
5536        path[..value.len()].copy_from_slice(value);
5537        data.extend_from_slice(&path);
5538    }
5539
5540    fn push_pmm_fixed_bytes<const N: usize>(data: &mut Vec<u8>, value: &[u8]) {
5541        let mut bytes = [0u8; N];
5542        bytes[..value.len()].copy_from_slice(value);
5543        data.extend_from_slice(&bytes);
5544    }
5545
5546    fn push_document_bone_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5547        if include_index {
5548            push_i32(data, 0);
5549        }
5550        push_i32(data, frame);
5551        push_i32(data, 0);
5552        push_i32(data, 0);
5553        data.extend_from_slice(&[20u8; 16]);
5554        data.extend_from_slice(&[0.0f32.to_le_bytes(); 3].concat());
5555        push_f32(data, 0.0);
5556        push_f32(data, 0.0);
5557        push_f32(data, 0.0);
5558        push_f32(data, 1.0);
5559        data.push(0);
5560        data.push(0);
5561    }
5562
5563    fn push_document_morph_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5564        if include_index {
5565            push_i32(data, 0);
5566        }
5567        push_i32(data, frame);
5568        push_i32(data, 0);
5569        push_i32(data, 0);
5570        push_f32(data, 0.5);
5571        data.push(0);
5572    }
5573
5574    fn push_document_model_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5575        if include_index {
5576            push_i32(data, frame);
5577        }
5578        push_i32(data, frame);
5579        push_i32(data, 0);
5580        push_i32(data, 0);
5581        data.push(1);
5582        data.push(1);
5583        push_i32(data, 2);
5584        push_i32(data, 3);
5585        data.push(1);
5586    }
5587
5588    fn assert_model_keyframe_payload_layout(kf: &PmmDocumentModelKeyframeSummary) {
5589        assert_eq!(kf.visible_offset, kf.payload_offset);
5590        assert_eq!(kf.constraint_state_count, kf.constraint_states.len());
5591        assert_eq!(kf.constraint_state_count, 1);
5592        assert_eq!(kf.constraint_states_offset, kf.payload_offset + 1);
5593        assert_eq!(kf.constraint_states_byte_length, kf.constraint_state_count);
5594        assert_eq!(
5595            kf.outside_parent_index_count,
5596            kf.outside_parent_indices.len()
5597        );
5598        assert_eq!(kf.outside_parent_index_count, 1);
5599        assert_eq!(kf.outside_parent_indices_offset, kf.payload_offset + 2);
5600        assert_eq!(
5601            kf.outside_parent_indices_byte_length,
5602            kf.outside_parent_index_count * 8
5603        );
5604        assert_eq!(
5605            kf.self_shadow_enabled_offset,
5606            kf.payload_offset + kf.payload_byte_length - 1
5607        );
5608        assert_eq!(
5609            kf.payload_byte_length,
5610            1 + kf.constraint_states_byte_length + kf.outside_parent_indices_byte_length + 1
5611        );
5612        assert_eq!(kf.payload_bytes[0], kf.visible as u8);
5613        assert_eq!(kf.payload_bytes[1], kf.constraint_states[0] as u8);
5614        assert_eq!(
5615            i32::from_le_bytes(kf.payload_bytes[2..6].try_into().unwrap()),
5616            kf.outside_parent_indices[0].parent_model_index
5617        );
5618        assert_eq!(
5619            i32::from_le_bytes(kf.payload_bytes[6..10].try_into().unwrap()),
5620            kf.outside_parent_indices[0].parent_model_bone_index
5621        );
5622        assert_eq!(
5623            kf.payload_bytes[kf.payload_bytes.len() - 1],
5624            kf.self_shadow_enabled as u8
5625        );
5626    }
5627
5628    fn push_document_base_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5629        if include_index {
5630            push_i32(data, frame);
5631        }
5632        push_i32(data, frame);
5633        push_i32(data, 0);
5634        push_i32(data, 0);
5635    }
5636
5637    fn push_document_camera_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5638        push_document_base_keyframe(data, include_index, frame);
5639        push_f32(data, 45.0);
5640        data.extend_from_slice(&[0u8; 12]);
5641        data.extend_from_slice(&[0u8; 12]);
5642        push_i32(data, -1);
5643        push_i32(data, -1);
5644        data.extend_from_slice(&[20u8; 24]);
5645        data.push(0);
5646        push_i32(data, 30);
5647        data.push(0);
5648    }
5649
5650    fn push_document_light_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5651        push_document_base_keyframe(data, include_index, frame);
5652        for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
5653            push_f32(data, value);
5654        }
5655        data.push(0);
5656    }
5657
5658    fn push_document_gravity_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5659        push_document_base_keyframe(data, include_index, frame);
5660        data.push(0);
5661        push_i32(data, 10);
5662        push_f32(data, 9.8);
5663        for value in [0.0f32, -1.0, 0.0] {
5664            push_f32(data, value);
5665        }
5666        data.push(0);
5667    }
5668
5669    fn push_document_self_shadow_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5670        push_document_base_keyframe(data, include_index, frame);
5671        data.push(1);
5672        push_f32(data, 8875.0);
5673        data.push(0);
5674    }
5675
5676    fn push_document_accessory_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
5677        push_document_base_keyframe(data, include_index, frame);
5678        data.push(1);
5679        push_i32(data, -1);
5680        push_i32(data, -1);
5681        for value in [1.0f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
5682            push_f32(data, value);
5683        }
5684        push_f32(data, 10.0);
5685        data.push(1);
5686        data.push(0);
5687    }
5688
5689    fn push_empty_pmm_path(data: &mut Vec<u8>) {
5690        data.extend_from_slice(&[0u8; 256]);
5691    }
5692
5693    fn pmm_with_document_summary() -> Vec<u8> {
5694        let mut data = b"Polygon Movie maker 0002".to_vec();
5695        data.resize(30, 0);
5696        push_i32(&mut data, 640);
5697        push_i32(&mut data, 360);
5698        push_i32(&mut data, 120);
5699        push_f32(&mut data, 30.0);
5700        data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0]);
5701        data.push(0);
5702        data.push(1);
5703        data.push(0);
5704        push_pmm_variable_string(&mut data, b"Miku");
5705        push_pmm_variable_string(&mut data, b"MikuEn");
5706        push_pmm_fixed_path(&mut data, b"UserFile\\Model\\miku.pmx");
5707        data.push(0);
5708        push_i32(&mut data, 1);
5709        push_pmm_variable_string(&mut data, b"center");
5710        push_i32(&mut data, 1);
5711        push_pmm_variable_string(&mut data, b"smile");
5712        push_i32(&mut data, 1);
5713        push_i32(&mut data, 7);
5714        push_i32(&mut data, 1);
5715        push_i32(&mut data, 8);
5716        data.push(0);
5717        data.push(1);
5718        push_i32(&mut data, 0);
5719        for _ in 0..4 {
5720            push_i32(&mut data, -1);
5721        }
5722        data.push(0);
5723        push_i32(&mut data, 0);
5724        push_i32(&mut data, 30);
5725        push_document_bone_keyframe(&mut data, false, 0);
5726        push_i32(&mut data, 1);
5727        push_document_bone_keyframe(&mut data, true, 30);
5728        push_document_morph_keyframe(&mut data, false, 0);
5729        push_i32(&mut data, 1);
5730        push_document_morph_keyframe(&mut data, true, 15);
5731        push_document_model_keyframe(&mut data, false, 0);
5732        push_i32(&mut data, 1);
5733        push_document_model_keyframe(&mut data, true, 30);
5734        // bone state: translation [1.0, 2.0, 3.0], orientation [0.0, 0.0, 0.0, 1.0], dirty=true, physicsDisabled=false, selected=true
5735        push_f32(&mut data, 1.0);
5736        push_f32(&mut data, 2.0);
5737        push_f32(&mut data, 3.0);
5738        push_f32(&mut data, 0.0);
5739        push_f32(&mut data, 0.0);
5740        push_f32(&mut data, 0.0);
5741        push_f32(&mut data, 1.0);
5742        data.push(1); // dirty
5743        data.push(0); // physicsDisabled
5744        data.push(1); // selected
5745        // morph state: weight 0.75
5746        push_f32(&mut data, 0.75);
5747        // constraint state: enabled=true
5748        data.push(1);
5749        // outside parent state: parentModelIndex=0, parentModelBoneIndex=5, subjectBoneIndex=7, targetModelIndex=1
5750        push_i32(&mut data, 0);
5751        push_i32(&mut data, 5);
5752        push_i32(&mut data, 7);
5753        push_i32(&mut data, 1);
5754        data.push(1);
5755        push_f32(&mut data, 1.0);
5756        data.push(0);
5757        data.push(0);
5758        data.extend_from_slice(b"UserFile\\Model\\miku.pmx\0");
5759        data
5760    }
5761
5762    fn pmm_with_document_global_summary() -> Vec<u8> {
5763        let mut data = pmm_with_document_summary();
5764        let trailing_reference = b"UserFile\\Model\\miku.pmx\0";
5765        data.truncate(data.len() - trailing_reference.len());
5766
5767        push_document_camera_keyframe(&mut data, false, 0);
5768        push_i32(&mut data, 1);
5769        push_document_camera_keyframe(&mut data, true, 42);
5770        data.extend_from_slice(&[0u8; 12 * 3]);
5771        data.push(0);
5772
5773        push_document_light_keyframe(&mut data, false, 0);
5774        push_i32(&mut data, 1);
5775        push_document_light_keyframe(&mut data, true, 43);
5776        for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
5777            push_f32(&mut data, value);
5778        }
5779
5780        data.push(0);
5781        push_i32(&mut data, 0);
5782        data.push(1);
5783        data.push(0);
5784        push_pmm_fixed_bytes::<100>(&mut data, b"Stage");
5785        push_pmm_fixed_path(&mut data, b"UserFile\\Accessory\\stage.x");
5786        data.push(0);
5787        push_document_accessory_keyframe(&mut data, false, 0);
5788        push_i32(&mut data, 1);
5789        push_document_accessory_keyframe(&mut data, true, 46);
5790        data.push(1);
5791        push_i32(&mut data, -1);
5792        push_i32(&mut data, -1);
5793        for value in [1.0f32, 2.0, 3.0] {
5794            push_f32(&mut data, value);
5795        }
5796        push_f32(&mut data, 10.0);
5797        for value in [0.1f32, 0.2, 0.3] {
5798            push_f32(&mut data, value);
5799        }
5800        data.push(1);
5801        data.push(0);
5802
5803        push_i32(&mut data, 12);
5804        push_i32(&mut data, 34);
5805        push_i32(&mut data, 56);
5806        push_i32(&mut data, 2);
5807        data.push(0);
5808        data.push(1);
5809        data.push(0);
5810        data.push(1);
5811        push_i32(&mut data, 3);
5812        push_i32(&mut data, 120);
5813        data.push(1);
5814        push_empty_pmm_path(&mut data);
5815        push_i32(&mut data, 0);
5816        push_i32(&mut data, 0);
5817        push_f32(&mut data, 1.0);
5818        push_empty_pmm_path(&mut data);
5819        push_i32(&mut data, 0);
5820        push_i32(&mut data, 0);
5821        push_i32(&mut data, 0);
5822        push_f32(&mut data, 1.0);
5823        push_empty_pmm_path(&mut data);
5824        data.push(0);
5825        data.push(1);
5826        data.push(1);
5827        data.push(1);
5828        push_f32(&mut data, 60.0);
5829        push_i32(&mut data, 2);
5830        push_i32(&mut data, -1);
5831        push_f32(&mut data, 1.0);
5832        data.push(1);
5833        data.push(2);
5834
5835        push_f32(&mut data, 9.8);
5836        push_i32(&mut data, 10);
5837        for value in [0.0f32, -1.0, 0.0] {
5838            push_f32(&mut data, value);
5839        }
5840        data.push(0);
5841        push_document_gravity_keyframe(&mut data, false, 0);
5842        push_i32(&mut data, 1);
5843        push_document_gravity_keyframe(&mut data, true, 44);
5844
5845        data.push(1);
5846        push_f32(&mut data, 8875.0);
5847        push_document_self_shadow_keyframe(&mut data, false, 0);
5848        push_i32(&mut data, 1);
5849        push_document_self_shadow_keyframe(&mut data, true, 45);
5850
5851        push_i32(&mut data, 0);
5852        push_i32(&mut data, 0);
5853        push_i32(&mut data, 0);
5854        data.push(0);
5855        push_i32(&mut data, -1);
5856        push_i32(&mut data, -1);
5857        data.extend_from_slice(&[0u8; 16 * 4]);
5858        data.push(0);
5859        data.push(0);
5860        data.push(1);
5861        push_i32(&mut data, 12);
5862        data.push(1);
5863        data.push(0);
5864        push_i32(&mut data, 0);
5865
5866        data
5867    }
5868
5869    fn pmm_with_two_initial_model_slots() -> Vec<u8> {
5870        let mut data = b"Polygon Movie maker 0002".to_vec();
5871        data.resize(30, 0);
5872        data.extend_from_slice(&640u32.to_le_bytes());
5873        data.extend_from_slice(&360u32.to_le_bytes());
5874        data.extend_from_slice(&250u32.to_le_bytes());
5875        data.extend_from_slice(&30.0f32.to_le_bytes());
5876        data.extend_from_slice(&[1, 1, 0, 0, 0, 0, 0, 0]);
5877        data.push(1);
5878        data.push(0);
5879        append_pmm_model_slot(
5880            &mut data,
5881            b"TestModel",
5882            b"Base",
5883            b"C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx",
5884        );
5885        append_pmm_model_slot(
5886            &mut data,
5887            b"Sour",
5888            b"Sour",
5889            b"G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx",
5890        );
5891        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
5892        data
5893    }
5894
5895    fn pmm_v1_with_display_counts() -> Vec<u8> {
5896        let mut data = b"Polygon Movie maker 0001".to_vec();
5897        data.resize(30, 0);
5898        data.extend_from_slice(&512u32.to_le_bytes());
5899        data.extend_from_slice(&288u32.to_le_bytes());
5900        data.extend_from_slice(&435u32.to_le_bytes());
5901        data.extend_from_slice(&35.0f32.to_le_bytes());
5902        data.extend_from_slice(&[1, 1, 1, 1, 1, 1]);
5903        data.push(8);
5904        data.push(9);
5905        data.extend_from_slice(b"Dummy\0 maker Dummy\0");
5906        data.extend_from_slice(b"UserFile\\Model\\miku.pmd\0UserFile\\Accessory\\stage01.x\0");
5907        data
5908    }
5909
5910    fn json_keys(value: &serde_json::Value) -> Vec<String> {
5911        let mut keys = value
5912            .as_object()
5913            .unwrap()
5914            .keys()
5915            .map(ToOwned::to_owned)
5916            .collect::<Vec<_>>();
5917        keys.sort();
5918        keys
5919    }
5920
5921    #[test]
5922    fn pmm_manifest_json_top_level_schema_is_stable() {
5923        let data =
5924            b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
5925        let parsed = parse_pmm_manifest(data).unwrap();
5926        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
5927
5928        assert_eq!(
5929            keys,
5930            vec![
5931                "accessoryAssets",
5932                "accessoryPaths",
5933                "assetReferences",
5934                "assetSummary",
5935                "audioAssets",
5936                "audioPaths",
5937                "byteLength",
5938                "diagnostics",
5939                "displayState",
5940                "documentGlobalSummary",
5941                "documentSummary",
5942                "headerTextEntries",
5943                "imageAssets",
5944                "imagePaths",
5945                "modelAssets",
5946                "modelPaths",
5947                "modelSlots",
5948                "motionAssets",
5949                "motionPaths",
5950                "parsedVersion",
5951                "projectGraph",
5952                "projectSettings",
5953                "signature",
5954                "timeline",
5955                "version",
5956                "videoAssets",
5957                "videoPaths",
5958            ]
5959        );
5960    }
5961
5962    #[test]
5963    fn pmm_project_graph_json_schema_is_stable() {
5964        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
5965        let graph = parsed.project_graph.unwrap();
5966        let keys = json_keys(&serde_json::to_value(&graph).unwrap());
5967
5968        assert_eq!(
5969            keys,
5970            vec![
5971                "accessoryCount",
5972                "accessoryKeyframes",
5973                "assetBindings",
5974                "assetReferences",
5975                "assetSummary",
5976                "byteCoverage",
5977                "displayState",
5978                "exportReadiness",
5979                "global",
5980                "keyframeReferences",
5981                "modelCounts",
5982                "models",
5983                "parsedVersion",
5984                "sceneSettings",
5985                "source",
5986                "timeline",
5987                "trackReferences",
5988                "version",
5989            ]
5990        );
5991    }
5992
5993    #[test]
5994    fn pmm_project_export_readiness_json_schema_is_stable() {
5995        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
5996        let graph = parsed.project_graph.unwrap();
5997        let readiness = &graph.export_readiness;
5998        let keys = json_keys(&serde_json::to_value(readiness).unwrap());
5999
6000        assert_eq!(
6001            keys,
6002            vec![
6003                "blockerCount",
6004                "blockers",
6005                "losslessParsedByteExportSupported",
6006                "semanticGraphExportSupported",
6007                "sourceBytePreservationRequired",
6008            ]
6009        );
6010    }
6011
6012    #[test]
6013    fn pmm_project_export_blocker_json_schema_is_stable() {
6014        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6015        let graph = parsed.project_graph.unwrap();
6016        // the always-present semantic exporter unfinished blocker (first one)
6017        let blocker = graph
6018            .export_readiness
6019            .blockers
6020            .iter()
6021            .find(|b| b.code == "PMM_SEMANTIC_EXPORTER_UNFINISHED")
6022            .expect("PMM_SEMANTIC_EXPORTER_UNFINISHED blocker must be present for schema test");
6023        let keys = json_keys(&serde_json::to_value(blocker).unwrap());
6024
6025        assert_eq!(
6026            keys,
6027            vec![
6028                "code",
6029                "count",
6030                "coverageRatio",
6031                "kind",
6032                "message",
6033                "scope",
6034                "severity",
6035            ]
6036        );
6037    }
6038
6039    #[test]
6040    fn pmm_project_asset_binding_json_schema_is_stable() {
6041        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6042        let graph = parsed.project_graph.unwrap();
6043        // use first (model) binding for schema
6044        let binding = &graph.asset_bindings[0];
6045        let keys = json_keys(&serde_json::to_value(binding).unwrap());
6046
6047        assert_eq!(
6048            keys,
6049            vec![
6050                "assetKind",
6051                "assetReferenceConfidence",
6052                "assetReferenceEndOffset",
6053                "assetReferenceIndex",
6054                "assetReferenceOffset",
6055                "documentIndex",
6056                "ownerIndex",
6057                "ownerName",
6058                "path",
6059                "pathOffset",
6060                "scope",
6061            ]
6062        );
6063    }
6064
6065    #[test]
6066    fn pmm_project_scene_settings_json_schema_is_stable() {
6067        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6068        let graph = parsed.project_graph.unwrap();
6069        let keys = json_keys(&serde_json::to_value(&graph.scene_settings).unwrap());
6070
6071        assert_eq!(
6072            keys,
6073            vec![
6074                "audioAssetReferenceIndex",
6075                "audioEnabled",
6076                "audioPath",
6077                "backgroundImageAssetReferenceIndex",
6078                "backgroundImageEnabled",
6079                "backgroundImageOffset",
6080                "backgroundImagePath",
6081                "backgroundImageScaleFactor",
6082                "backgroundVideoAssetReferenceIndex",
6083                "backgroundVideoEnabled",
6084                "backgroundVideoOffset",
6085                "backgroundVideoPath",
6086                "backgroundVideoScaleFactor",
6087                "beginFrameIndex",
6088                "beginFrameIndexEnabled",
6089                "currentFrameIndex",
6090                "currentFrameIndexInTextField",
6091                "endFrameIndex",
6092                "endFrameIndexEnabled",
6093                "loopEnabled",
6094                "offset",
6095                "offsetEnd",
6096                "preferredFps",
6097            ]
6098        );
6099    }
6100
6101    #[test]
6102    fn pmm_project_track_reference_json_schema_is_stable() {
6103        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6104        let graph = parsed.project_graph.unwrap();
6105        let reference = &graph.track_references[0];
6106        let keys = json_keys(&serde_json::to_value(reference).unwrap());
6107
6108        assert_eq!(
6109            keys,
6110            vec![
6111                "documentIndex",
6112                "initialKeyframes",
6113                "initialKeyframesOffset",
6114                "keyframeCountOffset",
6115                "keyframes",
6116                "keyframesEndOffset",
6117                "keyframesOffset",
6118                "ownerIndex",
6119                "ownerName",
6120                "scope",
6121                "stateEndOffset",
6122                "stateOffset",
6123                "trackKind",
6124            ]
6125        );
6126    }
6127
6128    #[test]
6129    fn pmm_project_keyframe_reference_json_schema_is_stable() {
6130        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6131        let graph = parsed.project_graph.unwrap();
6132        let reference = &graph.keyframe_references[0];
6133        let keys = json_keys(&serde_json::to_value(reference).unwrap());
6134
6135        assert_eq!(
6136            keys,
6137            vec![
6138                "byteLength",
6139                "documentIndex",
6140                "frameIndex",
6141                "initial",
6142                "keyframeIndex",
6143                "nextKeyframeIndex",
6144                "offset",
6145                "ownerIndex",
6146                "ownerName",
6147                "payloadByteLength",
6148                "payloadOffset",
6149                "previousKeyframeIndex",
6150                "scope",
6151                "trackKind",
6152            ]
6153        );
6154    }
6155
6156    #[test]
6157    fn pmm_project_byte_coverage_json_schema_is_stable() {
6158        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6159        let graph = parsed.project_graph.unwrap();
6160        let coverage = &graph.byte_coverage;
6161        let keys = json_keys(&serde_json::to_value(coverage).unwrap());
6162
6163        assert_eq!(
6164            keys,
6165            vec![
6166                "byteLength",
6167                "coverageRatio",
6168                "coveredByteLength",
6169                "gapCount",
6170                "gaps",
6171                "offset",
6172                "offsetEnd",
6173                "rangeCount",
6174                "ranges",
6175            ]
6176        );
6177    }
6178
6179    #[test]
6180    fn pmm_project_byte_range_json_schema_is_stable() {
6181        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6182        let graph = parsed.project_graph.unwrap();
6183        let range0 = &graph.byte_coverage.ranges[0];
6184        let keys = json_keys(&serde_json::to_value(range0).unwrap());
6185
6186        assert_eq!(
6187            keys,
6188            vec![
6189                "byteLength",
6190                "documentIndex",
6191                "kind",
6192                "name",
6193                "offset",
6194                "offsetEnd",
6195                "ownerIndex",
6196                "scope",
6197            ]
6198        );
6199    }
6200
6201    #[test]
6202    fn pmm_header_text_entry_json_schema_is_stable() {
6203        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
6204        let keys = json_keys(&serde_json::to_value(&parsed.header_text_entries[0]).unwrap());
6205
6206        assert_eq!(
6207            keys,
6208            vec!["index", "offset", "offsetEnd", "text", "textBytes"]
6209        );
6210    }
6211
6212    #[test]
6213    fn pmm_model_slot_json_schema_is_stable() {
6214        let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
6215        let keys = json_keys(&serde_json::to_value(&parsed.model_slots[0]).unwrap());
6216
6217        assert_eq!(
6218            keys,
6219            vec![
6220                "assetReferenceIndex",
6221                "confidence",
6222                "displaySlotIndex",
6223                "englishName",
6224                "englishNameBytes",
6225                "modelPath",
6226                "modelPathOffset",
6227                "name",
6228                "nameBytes",
6229                "nextNonZeroOffset",
6230                "normalizedPath",
6231                "offset",
6232                "offsetEnd",
6233                "slotIndex",
6234                "trailingZeroPaddingBytes"
6235            ]
6236        );
6237    }
6238
6239    #[test]
6240    fn pmm_project_settings_json_schema_is_stable() {
6241        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6242        let keys = json_keys(&serde_json::to_value(&parsed.project_settings).unwrap());
6243
6244        assert_eq!(
6245            keys,
6246            vec![
6247                "frameRate",
6248                "screenHeight",
6249                "screenWidth",
6250                "timelineFrameCount",
6251            ]
6252        );
6253    }
6254
6255    #[test]
6256    fn pmm_display_state_json_schema_is_stable() {
6257        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6258        let keys = json_keys(&serde_json::to_value(&parsed.display_state).unwrap());
6259
6260        assert_eq!(
6261            keys,
6262            vec![
6263                "accessorySlotCount",
6264                "activeModelSlotIndices",
6265                "declaredModelSlotCount",
6266                "documentExpandFlags",
6267                "documentModelCount",
6268                "emptyModelSlotIndices",
6269                "layout",
6270                "modelSlotCount",
6271                "modelSlotFlagCounts",
6272                "modelSlotFlagEntries",
6273                "modelSlotFlags",
6274                "nonZeroModelSlotCount",
6275                "selectedModelIndex"
6276            ]
6277        );
6278    }
6279
6280    #[test]
6281    fn pmm_model_slot_flag_entry_json_schema_is_stable() {
6282        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6283        let keys = json_keys(
6284            &serde_json::to_value(&parsed.display_state.model_slot_flag_entries[0]).unwrap(),
6285        );
6286
6287        assert_eq!(keys, vec!["active", "flag", "slotIndex"]);
6288    }
6289
6290    #[test]
6291    fn pmm_document_expand_flags_json_schema_is_stable() {
6292        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6293        let keys = json_keys(
6294            &serde_json::to_value(parsed.display_state.document_expand_flags.unwrap()).unwrap(),
6295        );
6296
6297        assert_eq!(
6298            keys,
6299            vec![
6300                "accessoryPanel",
6301                "bonePanel",
6302                "cameraPanel",
6303                "editingCla",
6304                "lightPanel",
6305                "morphPanel",
6306                "selfShadowPanel"
6307            ]
6308        );
6309    }
6310
6311    #[test]
6312    fn pmm_timeline_json_schema_is_stable() {
6313        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6314        let keys = json_keys(&serde_json::to_value(&parsed.timeline).unwrap());
6315
6316        assert_eq!(
6317            keys,
6318            vec![
6319                "durationSeconds",
6320                "endFrameExclusive",
6321                "frameCount",
6322                "frameRate",
6323                "startFrame"
6324            ]
6325        );
6326    }
6327
6328    #[test]
6329    fn pmm_scene_asset_json_schema_is_stable() {
6330        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6331        let keys = json_keys(&serde_json::to_value(&parsed.model_assets[0]).unwrap());
6332
6333        assert_eq!(
6334            keys,
6335            vec![
6336                "confidence",
6337                "extension",
6338                "fileName",
6339                "kindIndex",
6340                "normalizedPath",
6341                "offset",
6342                "offsetEnd",
6343                "path",
6344                "referenceIndex"
6345            ]
6346        );
6347    }
6348
6349    #[test]
6350    fn pmm_asset_reference_json_schema_is_stable() {
6351        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6352        let keys = json_keys(&serde_json::to_value(&parsed.asset_references[0]).unwrap());
6353
6354        assert_eq!(
6355            keys,
6356            vec![
6357                "confidence",
6358                "extension",
6359                "fileName",
6360                "kind",
6361                "normalizedPath",
6362                "offset",
6363                "offsetEnd",
6364                "path"
6365            ]
6366        );
6367    }
6368
6369    #[test]
6370    fn pmm_asset_summary_json_schema_is_stable() {
6371        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
6372        let keys = json_keys(&serde_json::to_value(&parsed.asset_summary).unwrap());
6373
6374        assert_eq!(
6375            keys,
6376            vec![
6377                "confidenceCounts",
6378                "extensionCounts",
6379                "highConfidenceCount",
6380                "kindCounts",
6381                "lowConfidenceCount",
6382                "mediumConfidenceCount",
6383                "referenceCount"
6384            ]
6385        );
6386    }
6387
6388    #[test]
6389    fn pmm_document_summary_json_schema_is_stable() {
6390        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
6391        let keys = json_keys(&serde_json::to_value(parsed.document_summary.unwrap()).unwrap());
6392
6393        assert_eq!(
6394            keys,
6395            vec![
6396                "counts",
6397                "modelCount",
6398                "models",
6399                "selectedModelIndex",
6400                "source"
6401            ]
6402        );
6403    }
6404
6405    #[test]
6406    fn pmm_document_counts_json_schema_is_stable() {
6407        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
6408        let document = parsed.document_summary.unwrap();
6409        let keys = json_keys(&serde_json::to_value(document.counts).unwrap());
6410
6411        assert_eq!(
6412            keys,
6413            vec![
6414                "boneKeyframes",
6415                "bones",
6416                "initialBoneKeyframes",
6417                "initialModelKeyframes",
6418                "initialMorphKeyframes",
6419                "modelKeyframes",
6420                "models",
6421                "morphKeyframes",
6422                "morphs"
6423            ]
6424        );
6425    }
6426
6427    #[test]
6428    fn pmm_document_global_summary_json_schema_is_stable() {
6429        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6430        let keys =
6431            json_keys(&serde_json::to_value(parsed.document_global_summary.unwrap()).unwrap());
6432
6433        assert_eq!(
6434            keys,
6435            vec![
6436                "accessories",
6437                "camera",
6438                "gravity",
6439                "light",
6440                "offset",
6441                "offsetEnd",
6442                "selfShadow",
6443                "settings",
6444                "source"
6445            ]
6446        );
6447    }
6448
6449    #[test]
6450    fn pmm_initial_camera_keyframe_json_schema_is_stable() {
6451        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6452        let global = parsed.document_global_summary.unwrap();
6453        let keys =
6454            json_keys(&serde_json::to_value(global.camera.initial_keyframe.unwrap()).unwrap());
6455
6456        assert_eq!(
6457            keys,
6458            vec![
6459                "angle",
6460                "angleByteLength",
6461                "angleOffset",
6462                "byteLength",
6463                "distance",
6464                "distanceOffset",
6465                "fov",
6466                "fovOffset",
6467                "frameIndex",
6468                "index",
6469                "interpolation",
6470                "interpolationByteLength",
6471                "interpolationOffset",
6472                "kind",
6473                "lookAt",
6474                "lookAtByteLength",
6475                "lookAtOffset",
6476                "nextKeyframeIndex",
6477                "offset",
6478                "parentModelBoneIndex",
6479                "parentModelBoneIndexOffset",
6480                "parentModelIndex",
6481                "parentModelIndexOffset",
6482                "payloadByteLength",
6483                "payloadBytes",
6484                "payloadOffset",
6485                "perspectiveView",
6486                "perspectiveViewOffset",
6487                "previousKeyframeIndex",
6488                "selected",
6489                "selectedOffset"
6490            ]
6491        );
6492    }
6493
6494    #[test]
6495    fn pmm_initial_light_keyframe_json_schema_is_stable() {
6496        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6497        let global = parsed.document_global_summary.unwrap();
6498        let keys =
6499            json_keys(&serde_json::to_value(global.light.initial_keyframe.unwrap()).unwrap());
6500
6501        assert_eq!(
6502            keys,
6503            vec![
6504                "byteLength",
6505                "color",
6506                "colorByteLength",
6507                "colorOffset",
6508                "direction",
6509                "directionByteLength",
6510                "directionOffset",
6511                "frameIndex",
6512                "index",
6513                "kind",
6514                "nextKeyframeIndex",
6515                "offset",
6516                "payloadByteLength",
6517                "payloadBytes",
6518                "payloadOffset",
6519                "previousKeyframeIndex",
6520                "selected",
6521                "selectedOffset"
6522            ]
6523        );
6524    }
6525
6526    #[test]
6527    fn pmm_initial_gravity_keyframe_json_schema_is_stable() {
6528        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6529        let global = parsed.document_global_summary.unwrap();
6530        let keys =
6531            json_keys(&serde_json::to_value(global.gravity.initial_keyframe.unwrap()).unwrap());
6532
6533        assert_eq!(
6534            keys,
6535            vec![
6536                "acceleration",
6537                "accelerationOffset",
6538                "byteLength",
6539                "direction",
6540                "directionByteLength",
6541                "directionOffset",
6542                "frameIndex",
6543                "index",
6544                "kind",
6545                "nextKeyframeIndex",
6546                "noise",
6547                "noiseEnabled",
6548                "noiseEnabledOffset",
6549                "noiseOffset",
6550                "offset",
6551                "payloadByteLength",
6552                "payloadBytes",
6553                "payloadOffset",
6554                "previousKeyframeIndex",
6555                "selected",
6556                "selectedOffset"
6557            ]
6558        );
6559    }
6560
6561    #[test]
6562    fn pmm_initial_self_shadow_keyframe_json_schema_is_stable() {
6563        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6564        let global = parsed.document_global_summary.unwrap();
6565        let keys =
6566            json_keys(&serde_json::to_value(global.self_shadow.initial_keyframe.unwrap()).unwrap());
6567
6568        assert_eq!(
6569            keys,
6570            vec![
6571                "byteLength",
6572                "distance",
6573                "distanceOffset",
6574                "frameIndex",
6575                "index",
6576                "kind",
6577                "mode",
6578                "modeOffset",
6579                "nextKeyframeIndex",
6580                "offset",
6581                "payloadByteLength",
6582                "payloadBytes",
6583                "payloadOffset",
6584                "previousKeyframeIndex",
6585                "selected",
6586                "selectedOffset"
6587            ]
6588        );
6589    }
6590
6591    #[test]
6592    fn pmm_document_track_summary_json_schema_is_stable() {
6593        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6594        let global = parsed.document_global_summary.unwrap();
6595        let keys = json_keys(&serde_json::to_value(global.camera).unwrap());
6596
6597        assert_eq!(
6598            keys,
6599            vec![
6600                "initialKeyframe",
6601                "initialKeyframes",
6602                "keyframeCountOffset",
6603                "keyframeSummaries",
6604                "keyframes",
6605                "keyframesEndOffset",
6606                "keyframesOffset",
6607                "offset",
6608                "offsetEnd",
6609                "stateEndOffset",
6610                "stateOffset"
6611            ]
6612        );
6613    }
6614
6615    #[test]
6616    fn pmm_document_settings_summary_json_schema_is_stable() {
6617        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6618        let global = parsed.document_global_summary.unwrap();
6619        let keys = json_keys(&serde_json::to_value(global.settings).unwrap());
6620
6621        assert_eq!(
6622            keys,
6623            vec![
6624                "accessoryIndexAfterModels",
6625                "audioEnabled",
6626                "audioPath",
6627                "audioPathOffset",
6628                "backgroundImageEnabled",
6629                "backgroundImageOffset",
6630                "backgroundImagePath",
6631                "backgroundImagePathOffset",
6632                "backgroundImageScaleFactor",
6633                "backgroundVideoEnabled",
6634                "backgroundVideoOffset",
6635                "backgroundVideoPath",
6636                "backgroundVideoPathOffset",
6637                "backgroundVideoScaleFactor",
6638                "beginFrameIndex",
6639                "beginFrameIndexEnabled",
6640                "beginFrameIndexEnabledOffset",
6641                "beginFrameIndexOffset",
6642                "blackBackgroundEnabled",
6643                "cameraLookAtModelBoneIndex",
6644                "cameraLookAtModelIndex",
6645                "cameraLookMode",
6646                "currentFrameIndex",
6647                "currentFrameIndexInTextField",
6648                "currentFrameIndexInTextFieldOffset",
6649                "currentFrameIndexOffset",
6650                "edgeColor",
6651                "editingMode",
6652                "endFrameIndex",
6653                "endFrameIndexEnabled",
6654                "endFrameIndexEnabledOffset",
6655                "endFrameIndexOffset",
6656                "followingLookAtEnabled",
6657                "gridAndAxisShown",
6658                "groundShadowBrightness",
6659                "groundShadowShown",
6660                "horizontalScroll",
6661                "horizontalScrollThumb",
6662                "informationShown",
6663                "loopEnabled",
6664                "modelSelectionFooterEndOffset",
6665                "modelSelectionFooterOffset",
6666                "modelSelectionFooterPresent",
6667                "offset",
6668                "offsetEnd",
6669                "physicsGroundEnabled",
6670                "physicsSimulationMode",
6671                "preferredFps",
6672                "screenCaptureMode",
6673                "translucentGroundShadowEnabled",
6674                "unknownMatrixEndOffset",
6675                "unknownMatrixOffset"
6676            ]
6677        );
6678    }
6679
6680    #[test]
6681    fn pmm_document_accessory_summary_json_schema_is_stable() {
6682        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6683        let global = parsed.document_global_summary.unwrap();
6684        let keys = json_keys(&serde_json::to_value(&global.accessories.accessories[0]).unwrap());
6685
6686        assert_eq!(
6687            keys,
6688            vec![
6689                "addBlendEnabled",
6690                "assetReferenceIndex",
6691                "documentAccessoryIndex",
6692                "drawOrderIndex",
6693                "initialKeyframe",
6694                "keyframeCountOffset",
6695                "keyframeSummaries",
6696                "keyframes",
6697                "keyframesEndOffset",
6698                "keyframesOffset",
6699                "name",
6700                "offset",
6701                "offsetEnd",
6702                "opacity",
6703                "parentModelBoneIndex",
6704                "parentModelIndex",
6705                "path",
6706                "pathOffset",
6707                "scaleFactor",
6708                "shadowEnabled",
6709                "slotIndex",
6710                "stateEndOffset",
6711                "stateOffset",
6712                "visible"
6713            ]
6714        );
6715    }
6716
6717    #[test]
6718    fn pmm_document_accessory_keyframe_summary_json_schema_is_stable() {
6719        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6720        let global = parsed.document_global_summary.unwrap();
6721        let keys = json_keys(
6722            &serde_json::to_value(&global.accessories.accessories[0].initial_keyframe).unwrap(),
6723        );
6724
6725        assert_eq!(
6726            keys,
6727            vec![
6728                "byteLength",
6729                "frameIndex",
6730                "index",
6731                "nextKeyframeIndex",
6732                "offset",
6733                "opacity",
6734                "orientation",
6735                "orientationByteLength",
6736                "orientationOffset",
6737                "packedOpacityVisibleOffset",
6738                "parentModelBoneIndex",
6739                "parentModelBoneIndexOffset",
6740                "parentModelIndex",
6741                "parentModelIndexOffset",
6742                "payloadByteLength",
6743                "payloadBytes",
6744                "payloadOffset",
6745                "previousKeyframeIndex",
6746                "scaleFactor",
6747                "scaleFactorOffset",
6748                "selected",
6749                "selectedOffset",
6750                "shadowEnabled",
6751                "shadowEnabledOffset",
6752                "translation",
6753                "translationByteLength",
6754                "translationOffset",
6755                "visible"
6756            ]
6757        );
6758    }
6759
6760    #[test]
6761    fn parses_pmm_v2_document_global_summary() {
6762        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
6763        let global = parsed.document_global_summary.as_ref().unwrap();
6764
6765        assert_eq!(global.source, "nanoem/ext/document.c PMMv2 global layout");
6766        assert_eq!(global.camera.initial_keyframes, 1);
6767        assert_eq!(global.camera.keyframes, 1);
6768        match global.camera.initial_keyframe.as_ref().unwrap() {
6769            PmmDocumentKeyframeSummary::Camera {
6770                index,
6771                frame_index,
6772                distance,
6773                look_at,
6774                angle,
6775                parent_model_index,
6776                parent_model_bone_index,
6777                interpolation,
6778                perspective_view,
6779                fov,
6780                selected,
6781                offset,
6782                byte_length,
6783                payload_offset,
6784                payload_byte_length,
6785                payload_bytes,
6786                distance_offset,
6787                look_at_offset,
6788                look_at_byte_length,
6789                angle_offset,
6790                angle_byte_length,
6791                parent_model_index_offset,
6792                parent_model_bone_index_offset,
6793                interpolation_offset,
6794                interpolation_byte_length,
6795                perspective_view_offset,
6796                fov_offset,
6797                selected_offset,
6798                ..
6799            } => {
6800                assert_eq!(*index, None);
6801                assert_eq!(*frame_index, 0);
6802                assert_eq!(*offset, global.camera.offset);
6803                assert_eq!(*byte_length, 78);
6804                assert_eq!(*payload_offset, *offset + 12);
6805                assert_eq!(*payload_byte_length, *byte_length - 12);
6806                assert_eq!(payload_bytes.len(), *payload_byte_length);
6807                assert_eq!(&payload_bytes[..4], &(*distance).to_le_bytes()[..]);
6808                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
6809                assert_eq!(*distance, 45.0);
6810                assert_eq!(*look_at, [0.0, 0.0, 0.0]);
6811                assert_eq!(*angle, [0.0, 0.0, 0.0]);
6812                assert_eq!(*parent_model_index, -1);
6813                assert_eq!(*parent_model_bone_index, -1);
6814                // 24-byte raw interpolation block exposed (after dist/look/ang/parents in payload)
6815                assert_eq!(interpolation.len(), 24);
6816                assert_eq!(*interpolation, [20u8; 24]);
6817                let interp_offset_in_payload = 4 + 12 + 12 + 4 + 4; // distance + lookAt + angle + parentModelIndex + parentModelBoneIndex
6818                assert_eq!(
6819                    &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
6820                    &interpolation[..]
6821                );
6822                assert_eq!(
6823                    &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
6824                    &[20u8; 24]
6825                );
6826                assert!(*perspective_view);
6827                assert_eq!(*fov, 30);
6828                assert!(!*selected);
6829
6830                // layout metadata for camera keyframe payload (exporter-prep slice).
6831                // offsets are absolute; prove they address correct bytes inside payload_bytes for initial keyframe.
6832                assert_eq!(*distance_offset, *payload_offset);
6833                assert_eq!(*look_at_offset, *payload_offset + 4);
6834                assert_eq!(*look_at_byte_length, 12);
6835                assert_eq!(*angle_offset, *payload_offset + 16);
6836                assert_eq!(*angle_byte_length, 12);
6837                assert_eq!(*parent_model_index_offset, *payload_offset + 28);
6838                assert_eq!(*parent_model_bone_index_offset, *payload_offset + 32);
6839                assert_eq!(*interpolation_offset, *payload_offset + 36);
6840                assert_eq!(*interpolation_byte_length, 24);
6841                assert_eq!(*perspective_view_offset, *payload_offset + 60);
6842                assert_eq!(*fov_offset, *payload_offset + 61);
6843                assert_eq!(*selected_offset, *payload_offset + 65);
6844                let p0 = &payload_bytes;
6845                assert_eq!(
6846                    &p0[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
6847                    &45.0f32.to_le_bytes()
6848                );
6849                let la0 = *look_at_offset - *payload_offset;
6850                assert_eq!(&p0[la0..la0 + 12], &[0u8; 12]);
6851                let ang0 = *angle_offset - *payload_offset;
6852                assert_eq!(&p0[ang0..ang0 + 12], &[0u8; 12]);
6853                assert_eq!(
6854                    &p0[*parent_model_index_offset - *payload_offset
6855                        ..*parent_model_index_offset - *payload_offset + 4],
6856                    &(-1i32).to_le_bytes()
6857                );
6858                assert_eq!(
6859                    &p0[*parent_model_bone_index_offset - *payload_offset
6860                        ..*parent_model_bone_index_offset - *payload_offset + 4],
6861                    &(-1i32).to_le_bytes()
6862                );
6863                let ip0 = *interpolation_offset - *payload_offset;
6864                assert_eq!(&p0[ip0..ip0 + 24], &[20u8; 24]);
6865                assert_eq!(p0[*perspective_view_offset - *payload_offset], 0u8);
6866                assert_eq!(
6867                    &p0[*fov_offset - *payload_offset..*fov_offset - *payload_offset + 4],
6868                    &30i32.to_le_bytes()
6869                );
6870                assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
6871            }
6872            other => panic!("unexpected camera keyframe summary: {other:?}"),
6873        }
6874        match &global.camera.keyframe_summaries[0] {
6875            PmmDocumentKeyframeSummary::Camera {
6876                index,
6877                frame_index,
6878                distance,
6879                parent_model_index,
6880                parent_model_bone_index,
6881                interpolation,
6882                selected,
6883                offset,
6884                byte_length,
6885                payload_offset,
6886                payload_byte_length,
6887                payload_bytes,
6888                distance_offset,
6889                look_at_offset,
6890                look_at_byte_length,
6891                angle_offset,
6892                angle_byte_length,
6893                parent_model_index_offset,
6894                parent_model_bone_index_offset,
6895                interpolation_offset,
6896                interpolation_byte_length,
6897                perspective_view_offset,
6898                fov_offset,
6899                selected_offset,
6900                ..
6901            } => {
6902                assert_eq!(*index, Some(42));
6903                assert_eq!(*frame_index, 42);
6904                assert_eq!(*offset, global.camera.keyframes_offset);
6905                assert_eq!(*byte_length, 82);
6906                assert_eq!(*payload_offset, *offset + 16);
6907                assert_eq!(*payload_byte_length, *byte_length - 16);
6908                assert_eq!(payload_bytes.len(), *payload_byte_length);
6909                assert_eq!(&payload_bytes[..4], &(*distance).to_le_bytes()[..]);
6910                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
6911                // additional camera keyframe also exposes the 24 raw interp bytes at same payload offset
6912                assert_eq!(interpolation.len(), 24);
6913                assert_eq!(*interpolation, [20u8; 24]);
6914                let interp_offset_in_payload = 4 + 12 + 12 + 4 + 4; // distance + lookAt + angle + parentModelIndex + parentModelBoneIndex
6915                assert_eq!(
6916                    &payload_bytes[interp_offset_in_payload..interp_offset_in_payload + 24],
6917                    &interpolation[..]
6918                );
6919                assert_eq!(*parent_model_index, -1);
6920                assert_eq!(*parent_model_bone_index, -1);
6921
6922                // same layout assertions for indexed (non-initial) camera keyframe; relative positions inside payload identical.
6923                assert_eq!(*distance_offset, *payload_offset);
6924                assert_eq!(*look_at_offset, *payload_offset + 4);
6925                assert_eq!(*look_at_byte_length, 12);
6926                assert_eq!(*angle_offset, *payload_offset + 16);
6927                assert_eq!(*angle_byte_length, 12);
6928                assert_eq!(*parent_model_index_offset, *payload_offset + 28);
6929                assert_eq!(*parent_model_bone_index_offset, *payload_offset + 32);
6930                assert_eq!(*interpolation_offset, *payload_offset + 36);
6931                assert_eq!(*interpolation_byte_length, 24);
6932                assert_eq!(*perspective_view_offset, *payload_offset + 60);
6933                assert_eq!(*fov_offset, *payload_offset + 61);
6934                assert_eq!(*selected_offset, *payload_offset + 65);
6935                let p1 = &payload_bytes;
6936                assert_eq!(
6937                    &p1[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
6938                    &(*distance).to_le_bytes()[..]
6939                );
6940                let la1 = *look_at_offset - *payload_offset;
6941                assert_eq!(&p1[la1..la1 + 12], &[0u8; 12]);
6942                let ang1 = *angle_offset - *payload_offset;
6943                assert_eq!(&p1[ang1..ang1 + 12], &[0u8; 12]);
6944                assert_eq!(
6945                    &p1[*parent_model_index_offset - *payload_offset
6946                        ..*parent_model_index_offset - *payload_offset + 4],
6947                    &(-1i32).to_le_bytes()
6948                );
6949                assert_eq!(
6950                    &p1[*parent_model_bone_index_offset - *payload_offset
6951                        ..*parent_model_bone_index_offset - *payload_offset + 4],
6952                    &(-1i32).to_le_bytes()
6953                );
6954                let ip1 = *interpolation_offset - *payload_offset;
6955                assert_eq!(&p1[ip1..ip1 + 24], &[20u8; 24]);
6956                assert_eq!(p1[*perspective_view_offset - *payload_offset], 0u8);
6957                assert_eq!(
6958                    &p1[*fov_offset - *payload_offset..*fov_offset - *payload_offset + 4],
6959                    &30i32.to_le_bytes()
6960                );
6961                assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
6962            }
6963            other => panic!("unexpected additional camera keyframe summary: {other:?}"),
6964        }
6965        assert_eq!(global.light.initial_keyframes, 1);
6966        assert_eq!(global.light.keyframes, 1);
6967        match global.light.initial_keyframe.as_ref().unwrap() {
6968            PmmDocumentKeyframeSummary::Light {
6969                color,
6970                direction,
6971                selected,
6972                offset,
6973                byte_length,
6974                payload_offset,
6975                payload_byte_length,
6976                payload_bytes,
6977                color_offset,
6978                color_byte_length,
6979                direction_offset,
6980                direction_byte_length,
6981                selected_offset,
6982                ..
6983            } => {
6984                assert_eq!(*offset, global.light.offset);
6985                assert_eq!(*byte_length, 37);
6986                assert_eq!(*payload_offset, *offset + 12);
6987                assert_eq!(*payload_byte_length, *byte_length - 12);
6988                assert_eq!(payload_bytes.len(), *payload_byte_length);
6989                let mut color_bytes = Vec::with_capacity(12);
6990                for c in color {
6991                    color_bytes.extend_from_slice(&c.to_le_bytes());
6992                }
6993                assert_eq!(&payload_bytes[..12], &color_bytes[..]);
6994                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
6995                assert_eq!(*color, [0.6, 0.6, 0.6]);
6996                assert_eq!(*direction, [-0.5, -1.0, 0.5]);
6997                assert!(!*selected);
6998
6999                // layout metadata for light keyframe payload (exporter-prep slice).
7000                assert_eq!(*color_offset, *payload_offset);
7001                assert_eq!(*color_byte_length, 12);
7002                assert_eq!(*direction_offset, *payload_offset + 12);
7003                assert_eq!(*direction_byte_length, 12);
7004                assert_eq!(*selected_offset, *payload_offset + 24);
7005                let p0 = &payload_bytes;
7006                let mut color_bytes = Vec::with_capacity(12);
7007                for c in color {
7008                    color_bytes.extend_from_slice(&c.to_le_bytes());
7009                }
7010                assert_eq!(
7011                    &p0[*color_offset - *payload_offset..*color_offset - *payload_offset + 12],
7012                    &color_bytes[..]
7013                );
7014                let d0 = *direction_offset - *payload_offset;
7015                assert_eq!(
7016                    &p0[d0..d0 + 12],
7017                    &[-0.5f32, -1.0, 0.5]
7018                        .iter()
7019                        .flat_map(|f| f.to_le_bytes())
7020                        .collect::<Vec<_>>()[..]
7021                );
7022                assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7023            }
7024            other => panic!("unexpected light keyframe summary: {other:?}"),
7025        }
7026        match &global.light.keyframe_summaries[0] {
7027            PmmDocumentKeyframeSummary::Light {
7028                index,
7029                frame_index,
7030                color,
7031                selected,
7032                offset,
7033                byte_length,
7034                payload_offset,
7035                payload_byte_length,
7036                payload_bytes,
7037                color_offset,
7038                color_byte_length,
7039                direction_offset,
7040                direction_byte_length,
7041                selected_offset,
7042                ..
7043            } => {
7044                assert_eq!(*index, Some(43));
7045                assert_eq!(*frame_index, 43);
7046                assert_eq!(*offset, global.light.keyframes_offset);
7047                assert_eq!(*byte_length, 41);
7048                assert_eq!(*payload_offset, *offset + 16);
7049                assert_eq!(*payload_byte_length, *byte_length - 16);
7050                assert_eq!(payload_bytes.len(), *payload_byte_length);
7051                let mut color_bytes = Vec::with_capacity(12);
7052                for c in color {
7053                    color_bytes.extend_from_slice(&c.to_le_bytes());
7054                }
7055                assert_eq!(&payload_bytes[..12], &color_bytes[..]);
7056                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7057
7058                // same layout for indexed light keyframe
7059                assert_eq!(*color_offset, *payload_offset);
7060                assert_eq!(*color_byte_length, 12);
7061                assert_eq!(*direction_offset, *payload_offset + 12);
7062                assert_eq!(*direction_byte_length, 12);
7063                assert_eq!(*selected_offset, *payload_offset + 24);
7064                let p1 = &payload_bytes;
7065                let mut color_bytes2 = Vec::with_capacity(12);
7066                for c in color {
7067                    color_bytes2.extend_from_slice(&c.to_le_bytes());
7068                }
7069                assert_eq!(
7070                    &p1[*color_offset - *payload_offset..*color_offset - *payload_offset + 12],
7071                    &color_bytes2[..]
7072                );
7073                let d1 = *direction_offset - *payload_offset;
7074                assert_eq!(
7075                    &p1[d1..d1 + 12],
7076                    &[-0.5f32, -1.0, 0.5]
7077                        .iter()
7078                        .flat_map(|f| f.to_le_bytes())
7079                        .collect::<Vec<_>>()[..]
7080                );
7081                assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7082            }
7083            other => panic!("unexpected additional light keyframe summary: {other:?}"),
7084        }
7085        assert_eq!(global.accessories.accessory_count, 1);
7086        assert_eq!(global.accessories.keyframes, 1);
7087        let accessory = &global.accessories.accessories[0];
7088        assert_eq!(accessory.slot_index, 0);
7089        assert_eq!(accessory.document_accessory_index, 0);
7090        assert_eq!(accessory.name, "Stage");
7091        assert_eq!(accessory.path, "UserFile\\Accessory\\stage.x");
7092        assert_eq!(accessory.keyframes, 1);
7093        assert!(accessory.visible);
7094        assert_eq!(accessory.opacity, 1.0);
7095        assert_eq!(accessory.scale_factor, 10.0);
7096        assert!(accessory.shadow_enabled);
7097        assert!(!accessory.add_blend_enabled);
7098        assert_eq!(accessory.initial_keyframe.index, None);
7099        assert_eq!(accessory.initial_keyframe.frame_index, 0);
7100        assert_eq!(
7101            accessory.initial_keyframe.offset,
7102            accessory.offset + 1 + 100 + 256 + 1
7103        );
7104        assert_eq!(accessory.initial_keyframe.byte_length, 51);
7105        assert_eq!(
7106            accessory.initial_keyframe.payload_offset,
7107            accessory.initial_keyframe.offset + 12
7108        );
7109        assert_eq!(accessory.initial_keyframe.payload_byte_length, 39);
7110        assert_eq!(accessory.initial_keyframe.payload_bytes.len(), 39);
7111        assert_eq!(
7112            accessory.initial_keyframe.payload_bytes.len(),
7113            accessory.initial_keyframe.payload_byte_length
7114        );
7115        assert_eq!(accessory.initial_keyframe.payload_bytes[0], 1);
7116        assert_eq!(
7117            &accessory.initial_keyframe.payload_bytes
7118                [accessory.initial_keyframe.payload_bytes.len() - 2..],
7119            &[1, 0]
7120        );
7121        assert_eq!(accessory.initial_keyframe.translation, [1.0, 2.0, 3.0]);
7122        assert_eq!(accessory.initial_keyframe.orientation, [0.1, 0.2, 0.3]);
7123        // explicit payload layout offsets/ranges on initial accessory keyframe (from global summary path)
7124        assert_eq!(
7125            accessory.initial_keyframe.packed_opacity_visible_offset,
7126            accessory.initial_keyframe.payload_offset
7127        );
7128        assert_eq!(
7129            accessory.initial_keyframe.selected_offset,
7130            accessory.initial_keyframe.payload_offset + 38
7131        );
7132        assert_eq!(accessory.initial_keyframe.translation_byte_length, 12);
7133        assert_eq!(accessory.initial_keyframe.orientation_byte_length, 12);
7134        assert_eq!(accessory.keyframe_summaries[0].index, Some(46));
7135        assert_eq!(accessory.keyframe_summaries[0].frame_index, 46);
7136        assert_eq!(
7137            accessory.keyframe_summaries[0].offset,
7138            accessory.keyframes_offset
7139        );
7140        assert_eq!(accessory.keyframe_summaries[0].byte_length, 55);
7141        assert_eq!(
7142            accessory.keyframe_summaries[0].payload_offset,
7143            accessory.keyframe_summaries[0].offset + 16
7144        );
7145        assert_eq!(accessory.keyframe_summaries[0].payload_byte_length, 39);
7146        assert_eq!(accessory.keyframe_summaries[0].payload_bytes.len(), 39);
7147        assert_eq!(
7148            accessory.keyframe_summaries[0].payload_bytes.len(),
7149            accessory.keyframe_summaries[0].payload_byte_length
7150        );
7151        assert_eq!(accessory.keyframe_summaries[0].payload_bytes[0], 1);
7152        assert_eq!(
7153            &accessory.keyframe_summaries[0].payload_bytes
7154                [accessory.keyframe_summaries[0].payload_bytes.len() - 2..],
7155            &[1, 0]
7156        );
7157        // layout on indexed accessory keyframe (global summary test path)
7158        assert_eq!(
7159            accessory.keyframe_summaries[0].packed_opacity_visible_offset,
7160            accessory.keyframe_summaries[0].payload_offset
7161        );
7162        assert_eq!(
7163            accessory.keyframe_summaries[0].scale_factor_offset,
7164            accessory.keyframe_summaries[0].payload_offset + 33
7165        );
7166        assert_eq!(
7167            accessory.keyframe_summaries[0].shadow_enabled_offset,
7168            accessory.keyframe_summaries[0].payload_offset + 37
7169        );
7170        assert_eq!(global.gravity.initial_keyframes, 1);
7171        assert_eq!(global.gravity.keyframes, 1);
7172        match global.gravity.initial_keyframe.as_ref().unwrap() {
7173            PmmDocumentKeyframeSummary::Gravity {
7174                noise_enabled,
7175                noise,
7176                acceleration,
7177                direction,
7178                selected,
7179                offset,
7180                byte_length,
7181                payload_offset,
7182                payload_byte_length,
7183                payload_bytes,
7184                noise_enabled_offset,
7185                noise_offset,
7186                acceleration_offset,
7187                direction_offset,
7188                direction_byte_length,
7189                selected_offset,
7190                ..
7191            } => {
7192                assert_eq!(*offset, global.gravity.state_end_offset.unwrap());
7193                assert_eq!(*byte_length, 34);
7194                assert_eq!(*payload_offset, *offset + 12);
7195                assert_eq!(*payload_byte_length, *byte_length - 12);
7196                assert_eq!(payload_bytes.len(), *payload_byte_length);
7197                assert_eq!(payload_bytes[0], *noise_enabled as u8);
7198                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7199                assert!(!*noise_enabled);
7200                assert_eq!(*noise, 10);
7201                assert_eq!(*acceleration, 9.8);
7202                assert_eq!(*direction, [0.0, -1.0, 0.0]);
7203                assert!(!*selected);
7204
7205                // layout metadata for gravity keyframe payload (exporter-prep slice).
7206                assert_eq!(*noise_enabled_offset, *payload_offset);
7207                assert_eq!(*noise_offset, *payload_offset + 1);
7208                assert_eq!(*acceleration_offset, *payload_offset + 5);
7209                assert_eq!(*direction_offset, *payload_offset + 9);
7210                assert_eq!(*direction_byte_length, 12);
7211                assert_eq!(*selected_offset, *payload_offset + 21);
7212                let p0 = &payload_bytes;
7213                assert_eq!(
7214                    p0[*noise_enabled_offset - *payload_offset],
7215                    *noise_enabled as u8
7216                );
7217                assert_eq!(
7218                    &p0[*noise_offset - *payload_offset..*noise_offset - *payload_offset + 4],
7219                    &10i32.to_le_bytes()
7220                );
7221                assert_eq!(
7222                    &p0[*acceleration_offset - *payload_offset
7223                        ..*acceleration_offset - *payload_offset + 4],
7224                    &9.8f32.to_le_bytes()
7225                );
7226                let dir0 = *direction_offset - *payload_offset;
7227                assert_eq!(
7228                    &p0[dir0..dir0 + 12],
7229                    &[0.0f32, -1.0, 0.0]
7230                        .iter()
7231                        .flat_map(|f| f.to_le_bytes())
7232                        .collect::<Vec<_>>()[..]
7233                );
7234                assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7235            }
7236            other => panic!("unexpected gravity keyframe summary: {other:?}"),
7237        }
7238        match &global.gravity.keyframe_summaries[0] {
7239            PmmDocumentKeyframeSummary::Gravity {
7240                index,
7241                frame_index,
7242                noise_enabled,
7243                selected,
7244                offset,
7245                byte_length,
7246                payload_offset,
7247                payload_byte_length,
7248                payload_bytes,
7249                noise_enabled_offset,
7250                noise_offset,
7251                acceleration_offset,
7252                direction_offset,
7253                direction_byte_length,
7254                selected_offset,
7255                ..
7256            } => {
7257                assert_eq!(*index, Some(44));
7258                assert_eq!(*frame_index, 44);
7259                assert_eq!(*offset, global.gravity.keyframes_offset);
7260                assert_eq!(*byte_length, 38);
7261                assert_eq!(*payload_offset, *offset + 16);
7262                assert_eq!(*payload_byte_length, *byte_length - 16);
7263                assert_eq!(payload_bytes.len(), *payload_byte_length);
7264                assert_eq!(payload_bytes[0], *noise_enabled as u8);
7265                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7266
7267                // same layout for indexed gravity keyframe
7268                assert_eq!(*noise_enabled_offset, *payload_offset);
7269                assert_eq!(*noise_offset, *payload_offset + 1);
7270                assert_eq!(*acceleration_offset, *payload_offset + 5);
7271                assert_eq!(*direction_offset, *payload_offset + 9);
7272                assert_eq!(*direction_byte_length, 12);
7273                assert_eq!(*selected_offset, *payload_offset + 21);
7274                let p1 = &payload_bytes;
7275                assert_eq!(
7276                    p1[*noise_enabled_offset - *payload_offset],
7277                    *noise_enabled as u8
7278                );
7279                assert_eq!(
7280                    &p1[*noise_offset - *payload_offset..*noise_offset - *payload_offset + 4],
7281                    &10i32.to_le_bytes()
7282                );
7283                assert_eq!(
7284                    &p1[*acceleration_offset - *payload_offset
7285                        ..*acceleration_offset - *payload_offset + 4],
7286                    &9.8f32.to_le_bytes()
7287                );
7288                let dir1 = *direction_offset - *payload_offset;
7289                assert_eq!(
7290                    &p1[dir1..dir1 + 12],
7291                    &[0.0f32, -1.0, 0.0]
7292                        .iter()
7293                        .flat_map(|f| f.to_le_bytes())
7294                        .collect::<Vec<_>>()[..]
7295                );
7296                assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7297            }
7298            other => panic!("unexpected additional gravity keyframe summary: {other:?}"),
7299        }
7300        assert_eq!(global.self_shadow.initial_keyframes, 1);
7301        assert_eq!(global.self_shadow.keyframes, 1);
7302        match global.self_shadow.initial_keyframe.as_ref().unwrap() {
7303            PmmDocumentKeyframeSummary::SelfShadow {
7304                mode,
7305                distance,
7306                selected,
7307                offset,
7308                byte_length,
7309                payload_offset,
7310                payload_byte_length,
7311                payload_bytes,
7312                mode_offset,
7313                distance_offset,
7314                selected_offset,
7315                ..
7316            } => {
7317                assert_eq!(*offset, global.self_shadow.state_end_offset.unwrap());
7318                assert_eq!(*byte_length, 18);
7319                assert_eq!(*payload_offset, *offset + 12);
7320                assert_eq!(*payload_byte_length, *byte_length - 12);
7321                assert_eq!(payload_bytes.len(), *payload_byte_length);
7322                assert_eq!(payload_bytes[0], *mode);
7323                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7324                assert_eq!(*mode, 1);
7325                assert_eq!(*distance, 8875.0);
7326                assert!(!*selected);
7327
7328                // layout metadata for self-shadow keyframe payload (exporter-prep slice).
7329                assert_eq!(*mode_offset, *payload_offset);
7330                assert_eq!(*distance_offset, *payload_offset + 1);
7331                assert_eq!(*selected_offset, *payload_offset + 5);
7332                let p0 = &payload_bytes;
7333                assert_eq!(p0[*mode_offset - *payload_offset], *mode);
7334                assert_eq!(
7335                    &p0[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7336                    &8875.0f32.to_le_bytes()
7337                );
7338                assert_eq!(p0[*selected_offset - *payload_offset], 0u8);
7339            }
7340            other => panic!("unexpected self-shadow keyframe summary: {other:?}"),
7341        }
7342        match &global.self_shadow.keyframe_summaries[0] {
7343            PmmDocumentKeyframeSummary::SelfShadow {
7344                index,
7345                frame_index,
7346                mode,
7347                selected,
7348                offset,
7349                byte_length,
7350                payload_offset,
7351                payload_byte_length,
7352                payload_bytes,
7353                mode_offset,
7354                distance_offset,
7355                selected_offset,
7356                ..
7357            } => {
7358                assert_eq!(*index, Some(45));
7359                assert_eq!(*frame_index, 45);
7360                assert_eq!(*offset, global.self_shadow.keyframes_offset);
7361                assert_eq!(*byte_length, 22);
7362                assert_eq!(*payload_offset, *offset + 16);
7363                assert_eq!(*payload_byte_length, *byte_length - 16);
7364                assert_eq!(payload_bytes.len(), *payload_byte_length);
7365                assert_eq!(payload_bytes[0], *mode);
7366                assert_eq!(payload_bytes.last().copied(), Some(*selected as u8));
7367
7368                // same layout for indexed self-shadow keyframe
7369                assert_eq!(*mode_offset, *payload_offset);
7370                assert_eq!(*distance_offset, *payload_offset + 1);
7371                assert_eq!(*selected_offset, *payload_offset + 5);
7372                let p1 = &payload_bytes;
7373                assert_eq!(p1[*mode_offset - *payload_offset], *mode);
7374                assert_eq!(
7375                    &p1[*distance_offset - *payload_offset..*distance_offset - *payload_offset + 4],
7376                    &8875.0f32.to_le_bytes()
7377                );
7378                assert_eq!(p1[*selected_offset - *payload_offset], 0u8);
7379            }
7380            other => panic!("unexpected additional self-shadow keyframe summary: {other:?}"),
7381        }
7382        assert_eq!(global.settings.current_frame_index, 12);
7383        assert_eq!(global.settings.horizontal_scroll, 34);
7384        assert_eq!(global.settings.horizontal_scroll_thumb, 56);
7385        assert_eq!(global.settings.begin_frame_index, 3);
7386        assert_eq!(global.settings.end_frame_index, 120);
7387        assert!(global.settings.audio_enabled);
7388        assert!(global.settings.grid_and_axis_shown);
7389        assert!(global.settings.physics_ground_enabled);
7390        assert!(global.settings.model_selection_footer_present);
7391        assert!(global.offset_end > global.offset);
7392        assert!(global.camera.offset < global.light.offset);
7393        assert!(global.light.offset < global.accessories.offset);
7394        assert!(global.gravity.offset < global.self_shadow.offset);
7395    }
7396
7397    #[test]
7398    fn parses_pmm_project_graph_only_when_full_document_and_global_summary_present() {
7399        // PMM data without full document/global summary has project_graph == None
7400        let parsed_minimal = parse_pmm_manifest(
7401            b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0",
7402        )
7403        .unwrap();
7404        assert!(parsed_minimal.project_graph.is_none());
7405
7406        let parsed_settings = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
7407        assert!(parsed_settings.project_graph.is_none());
7408
7409        let parsed_doc_only = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
7410        assert!(parsed_doc_only.project_graph.is_none());
7411        assert!(parsed_doc_only.document_summary.is_some());
7412        assert!(parsed_doc_only.document_global_summary.is_none());
7413
7414        // pmm_with_document_global_summary() has project_graph == Some(...)
7415        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7416        let graph = parsed
7417            .project_graph
7418            .as_ref()
7419            .expect("project_graph must be Some for pmm_with_document_global_summary");
7420        assert_eq!(
7421            graph.source,
7422            "mmd-anim-format first PMMv2 document graph DTO slice"
7423        );
7424        assert_eq!(graph.version, "0002");
7425        assert_eq!(graph.parsed_version, Some(2));
7426
7427        // the graph model/global counts match document_summary and document_global_summary
7428        let doc = parsed.document_summary.as_ref().unwrap();
7429        let glob = parsed.document_global_summary.as_ref().unwrap();
7430        assert_eq!(graph.model_counts.models, doc.counts.models);
7431        assert_eq!(graph.model_counts.bones, doc.counts.bones);
7432        assert_eq!(graph.model_counts.morphs, doc.counts.morphs);
7433        assert_eq!(graph.model_counts.bone_keyframes, doc.counts.bone_keyframes);
7434        assert_eq!(
7435            graph.model_counts.morph_keyframes,
7436            doc.counts.morph_keyframes
7437        );
7438        assert_eq!(graph.accessory_count, glob.accessories.accessory_count);
7439        assert_eq!(graph.accessory_keyframes, glob.accessories.keyframes);
7440        assert_eq!(graph.models.len(), doc.models.len());
7441        assert_eq!(graph.global.camera.keyframes, glob.camera.keyframes);
7442    }
7443
7444    #[test]
7445    fn pmm_project_scene_settings_synthetic_pmmv2_full_fixture() {
7446        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7447        let graph = parsed
7448            .project_graph
7449            .as_ref()
7450            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7451        let ss = &graph.scene_settings;
7452        let glob = parsed.document_global_summary.as_ref().unwrap();
7453        let settings = &glob.settings;
7454
7455        // offset/offsetEnd match global.settings
7456        assert_eq!(ss.offset, settings.offset);
7457        assert_eq!(ss.offset_end, settings.offset_end);
7458
7459        // current/begin/end/preferredFps/loop fields match global.settings
7460        assert_eq!(ss.current_frame_index, settings.current_frame_index);
7461        assert_eq!(
7462            ss.current_frame_index_in_text_field,
7463            settings.current_frame_index_in_text_field
7464        );
7465        assert_eq!(
7466            ss.begin_frame_index_enabled,
7467            settings.begin_frame_index_enabled
7468        );
7469        assert_eq!(ss.end_frame_index_enabled, settings.end_frame_index_enabled);
7470        assert_eq!(ss.begin_frame_index, settings.begin_frame_index);
7471        assert_eq!(ss.end_frame_index, settings.end_frame_index);
7472        assert_eq!(ss.preferred_fps, settings.preferred_fps);
7473        assert_eq!(ss.loop_enabled, settings.loop_enabled);
7474
7475        // audio/background paths and enabled flags match global.settings
7476        assert_eq!(ss.audio_enabled, settings.audio_enabled);
7477        assert_eq!(ss.audio_path, settings.audio_path);
7478        assert_eq!(
7479            ss.background_video_enabled,
7480            settings.background_video_enabled
7481        );
7482        assert_eq!(ss.background_video_path, settings.background_video_path);
7483        assert_eq!(
7484            ss.background_image_enabled,
7485            settings.background_image_enabled
7486        );
7487        assert_eq!(ss.background_image_path, settings.background_image_path);
7488
7489        // empty synthetic paths (using push_empty_pmm_path in fixture) resolve to None
7490        assert!(
7491            ss.audio_path.is_empty(),
7492            "synthetic fixture uses empty audio path"
7493        );
7494        assert!(
7495            ss.background_video_path.is_empty(),
7496            "synthetic fixture uses empty background video path"
7497        );
7498        assert!(
7499            ss.background_image_path.is_empty(),
7500            "synthetic fixture uses empty background image path"
7501        );
7502        assert!(
7503            ss.audio_asset_reference_index.is_none(),
7504            "empty audio path must resolve to None asset reference index"
7505        );
7506        assert!(
7507            ss.background_video_asset_reference_index.is_none(),
7508            "empty background video path must resolve to None asset reference index"
7509        );
7510        assert!(
7511            ss.background_image_asset_reference_index.is_none(),
7512            "empty background image path must resolve to None asset reference index"
7513        );
7514    }
7515
7516    #[test]
7517    fn pmm_project_asset_bindings_synthetic_pmmv2_full_fixture() {
7518        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7519        let graph = parsed
7520            .project_graph
7521            .as_ref()
7522            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7523        let doc = parsed.document_summary.as_ref().unwrap();
7524        let glob = parsed.document_global_summary.as_ref().unwrap();
7525
7526        // exactly one model binding + one accessory binding (sceneSettings omitted for empty paths)
7527        assert_eq!(
7528            graph.asset_bindings.len(),
7529            2,
7530            "synthetic PMMv2 full fixture must have exactly 2 asset bindings (1 model + 1 accessory)"
7531        );
7532        let model_bindings: Vec<_> = graph
7533            .asset_bindings
7534            .iter()
7535            .filter(|b| b.scope == "model")
7536            .collect();
7537        let acc_bindings: Vec<_> = graph
7538            .asset_bindings
7539            .iter()
7540            .filter(|b| b.scope == "accessory")
7541            .collect();
7542        let scene_bindings: Vec<_> = graph
7543            .asset_bindings
7544            .iter()
7545            .filter(|b| b.scope == "sceneSettings")
7546            .collect();
7547        assert_eq!(model_bindings.len(), 1);
7548        assert_eq!(acc_bindings.len(), 1);
7549        assert_eq!(
7550            scene_bindings.len(),
7551            0,
7552            "no sceneSettings binding exists because the synthetic fixture paths are empty"
7553        );
7554
7555        // model binding matches decoded owner path, path_offset, owner indexes, asset_reference_index
7556        let model0 = &doc.models[0];
7557        let mb = model_bindings[0];
7558        assert_eq!(mb.scope, "model");
7559        assert_eq!(mb.asset_kind, "model");
7560        assert_eq!(mb.owner_index, Some(model0.slot_index));
7561        assert_eq!(mb.document_index, Some(model0.document_model_index));
7562        assert_eq!(mb.owner_name.as_deref(), Some(model0.name.as_str()));
7563        assert_eq!(mb.path, model0.path);
7564        assert_eq!(mb.path_offset, Some(model0.path_offset));
7565        assert_eq!(mb.asset_reference_index, model0.asset_reference_index);
7566        if let Some(idx) = mb.asset_reference_index {
7567            let ar = &graph.asset_references[idx];
7568            assert_eq!(mb.asset_reference_offset, Some(ar.offset));
7569            assert_eq!(mb.asset_reference_end_offset, Some(ar.offset_end));
7570            assert_eq!(mb.asset_reference_confidence, Some(ar.confidence));
7571        }
7572
7573        // accessory binding matches decoded owner path, path_offset, owner indexes, asset_reference_index
7574        let acc0 = &glob.accessories.accessories[0];
7575        let ab = acc_bindings[0];
7576        assert_eq!(ab.scope, "accessory");
7577        assert_eq!(ab.asset_kind, "accessory");
7578        assert_eq!(ab.owner_index, Some(acc0.slot_index));
7579        assert_eq!(ab.document_index, Some(acc0.document_accessory_index));
7580        assert_eq!(ab.owner_name.as_deref(), Some(acc0.name.as_str()));
7581        assert_eq!(ab.path, acc0.path);
7582        assert_eq!(ab.path_offset, Some(acc0.path_offset));
7583        assert_eq!(ab.asset_reference_index, acc0.asset_reference_index);
7584        if let Some(idx) = ab.asset_reference_index {
7585            let ar = &graph.asset_references[idx];
7586            assert_eq!(ab.asset_reference_offset, Some(ar.offset));
7587            assert_eq!(ab.asset_reference_end_offset, Some(ar.offset_end));
7588            assert_eq!(ab.asset_reference_confidence, Some(ar.confidence));
7589        }
7590    }
7591
7592    #[test]
7593    fn pmm_project_export_readiness_synthetic_pmmv2_full_fixture() {
7594        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7595        let graph = parsed
7596            .project_graph
7597            .as_ref()
7598            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7599        let cov = &graph.byte_coverage;
7600        let r = &graph.export_readiness;
7601
7602        assert!(
7603            r.lossless_parsed_byte_export_supported,
7604            "lossless parsed-byte export supported"
7605        );
7606        assert!(
7607            !r.semantic_graph_export_supported,
7608            "semantic graph export must be false (full exporter unfinished)"
7609        );
7610        assert!(
7611            r.source_byte_preservation_required,
7612            "source byte preservation required"
7613        );
7614        assert_eq!(
7615            r.blocker_count,
7616            r.blockers.len(),
7617            "blocker_count must equal blockers.len()"
7618        );
7619
7620        // unfinished semantic exporter blocker is always present with exact shape
7621        let semantic_blocker = r
7622            .blockers
7623            .iter()
7624            .find(|b| b.code == "PMM_SEMANTIC_EXPORTER_UNFINISHED")
7625            .expect("PMM_SEMANTIC_EXPORTER_UNFINISHED blocker must be present");
7626        assert_eq!(semantic_blocker.severity, "blocker");
7627        assert!(semantic_blocker.scope.is_none());
7628        assert!(semantic_blocker.kind.is_none());
7629        assert!(
7630            semantic_blocker
7631                .message
7632                .contains("semantic graph export/editing is not yet supported"),
7633            "message must state full semantic graph export/editing is not yet supported"
7634        );
7635
7636        // gap blocker presence matches byte_coverage.gap_count > 0 (synthetic full fixture has 0)
7637        let has_gap_blocker = r
7638            .blockers
7639            .iter()
7640            .any(|b| b.code == "PMM_DECODED_BYTE_GAPS_REMAIN");
7641        assert_eq!(
7642            has_gap_blocker,
7643            cov.gap_count > 0,
7644            "PMM_DECODED_BYTE_GAPS_REMAIN presence must match gap_count > 0"
7645        );
7646    }
7647
7648    #[test]
7649    fn pmm_project_track_reference_synthetic_pmmv2_full_fixture() {
7650        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7651        let graph = parsed
7652            .project_graph
7653            .as_ref()
7654            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7655
7656        // exactly 8 track references: 3 model (bone/morph/model) + 4 global + 1 accessory
7657        assert_eq!(
7658            graph.track_references.len(),
7659            8,
7660            "synthetic PMMv2 full fixture must have exactly 8 track references"
7661        );
7662        let model_tracks: Vec<_> = graph
7663            .track_references
7664            .iter()
7665            .filter(|r| r.scope == "model")
7666            .collect();
7667        let global_tracks: Vec<_> = graph
7668            .track_references
7669            .iter()
7670            .filter(|r| r.scope == "global")
7671            .collect();
7672        let accessory_tracks: Vec<_> = graph
7673            .track_references
7674            .iter()
7675            .filter(|r| r.scope == "accessory")
7676            .collect();
7677        assert_eq!(
7678            model_tracks.len(),
7679            3,
7680            "per document model: bone + morph + model track groups"
7681        );
7682        assert_eq!(
7683            global_tracks.len(),
7684            4,
7685            "global: camera + light + gravity + selfShadow"
7686        );
7687        assert_eq!(
7688            accessory_tracks.len(),
7689            1,
7690            "each decoded accessory contributes one accessory track group"
7691        );
7692
7693        let doc = parsed.document_summary.as_ref().unwrap();
7694        let glob = parsed.document_global_summary.as_ref().unwrap();
7695
7696        // model bone track counts/offsets must match PmmDocumentModelSummary + sections
7697        let bone_ref = graph
7698            .track_references
7699            .iter()
7700            .find(|r| r.scope == "model" && r.track_kind == "bone")
7701            .expect("model bone track ref");
7702        let model0 = &doc.models[0];
7703        assert_eq!(bone_ref.owner_index, Some(model0.slot_index));
7704        assert_eq!(bone_ref.document_index, Some(model0.document_model_index));
7705        assert_eq!(bone_ref.owner_name.as_deref(), Some(model0.name.as_str()));
7706        assert_eq!(bone_ref.initial_keyframes, model0.initial_bone_keyframes);
7707        assert_eq!(bone_ref.keyframes, model0.bone_keyframes);
7708        assert_eq!(
7709            bone_ref.initial_keyframes_offset,
7710            Some(model0.sections.initial_bone_keyframes_offset)
7711        );
7712        assert_eq!(
7713            bone_ref.keyframe_count_offset,
7714            Some(model0.sections.bone_keyframe_count_offset)
7715        );
7716        assert_eq!(
7717            bone_ref.keyframes_offset,
7718            model0.sections.bone_keyframes_offset
7719        );
7720        assert_eq!(
7721            bone_ref.keyframes_end_offset,
7722            model0.sections.bone_keyframes_end_offset
7723        );
7724
7725        // global camera track counts/offsets must match PmmDocumentTrackSummary
7726        let cam_ref = graph
7727            .track_references
7728            .iter()
7729            .find(|r| r.scope == "global" && r.track_kind == "camera")
7730            .expect("global camera track ref");
7731        assert_eq!(cam_ref.initial_keyframes, glob.camera.initial_keyframes);
7732        assert_eq!(cam_ref.keyframes, glob.camera.keyframes);
7733        assert_eq!(cam_ref.keyframes_offset, glob.camera.keyframes_offset);
7734        assert_eq!(
7735            cam_ref.keyframes_end_offset,
7736            glob.camera.keyframes_end_offset
7737        );
7738        // for camera, initial kf starts at the track offset
7739        assert_eq!(cam_ref.initial_keyframes_offset, Some(glob.camera.offset));
7740        assert_eq!(
7741            cam_ref.keyframe_count_offset,
7742            Some(glob.camera.keyframe_count_offset)
7743        );
7744
7745        // accessory track counts/offsets must match PmmDocumentAccessorySummary
7746        let acc_ref = graph
7747            .track_references
7748            .iter()
7749            .find(|r| r.scope == "accessory")
7750            .expect("accessory track ref");
7751        let acc0 = &glob.accessories.accessories[0];
7752        assert_eq!(acc_ref.owner_index, Some(acc0.slot_index));
7753        assert_eq!(acc_ref.document_index, Some(acc0.document_accessory_index));
7754        assert_eq!(acc_ref.owner_name.as_deref(), Some(acc0.name.as_str()));
7755        assert_eq!(acc_ref.initial_keyframes, 1);
7756        assert_eq!(acc_ref.keyframes, acc0.keyframes);
7757        assert_eq!(
7758            acc_ref.initial_keyframes_offset,
7759            Some(acc0.initial_keyframe.offset)
7760        );
7761        assert_eq!(
7762            acc_ref.keyframe_count_offset,
7763            Some(acc0.keyframe_count_offset)
7764        );
7765        assert_eq!(acc_ref.keyframes_offset, acc0.keyframes_offset);
7766        assert_eq!(acc_ref.keyframes_end_offset, acc0.keyframes_end_offset);
7767    }
7768
7769    #[test]
7770    fn pmm_project_keyframe_reference_synthetic_pmmv2_full_fixture() {
7771        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7772        let graph = parsed
7773            .project_graph
7774            .as_ref()
7775            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7776
7777        // Inspect synthetic fixture: 1 model (6 kfs) + 4 globals (8 kfs) + 1 accessory (2 kfs) = 16
7778        // (model: 1 init bone +1 add bone +1 init morph +1 add morph +1 init model +1 add model =6;
7779        //  global: camera/light/gravity/selfShadow each 1+1=8; accessory:1 init +1 add=2)
7780        // Do not hardcode 15; use actual derived count from current summaries (16 here).
7781        assert_eq!(
7782            graph.keyframe_references.len(),
7783            16,
7784            "synthetic PMMv2 full fixture must have exactly 16 keyframe references (derived from decoded summaries: 6 model + 8 global + 2 accessory)"
7785        );
7786
7787        let model_kfs: Vec<_> = graph
7788            .keyframe_references
7789            .iter()
7790            .filter(|r| r.scope == "model")
7791            .collect();
7792        let global_kfs: Vec<_> = graph
7793            .keyframe_references
7794            .iter()
7795            .filter(|r| r.scope == "global")
7796            .collect();
7797        let accessory_kfs: Vec<_> = graph
7798            .keyframe_references
7799            .iter()
7800            .filter(|r| r.scope == "accessory")
7801            .collect();
7802        assert_eq!(
7803            model_kfs.len(),
7804            6,
7805            "per document model: bone/morph/model each initial+additional"
7806        );
7807        assert_eq!(
7808            global_kfs.len(),
7809            8,
7810            "global: camera/light/gravity/selfShadow each initial+additional"
7811        );
7812        assert_eq!(
7813            accessory_kfs.len(),
7814            2,
7815            "each decoded accessory contributes initial + additional accessory keyframes"
7816        );
7817
7818        let doc = parsed.document_summary.as_ref().unwrap();
7819        let glob = parsed.document_global_summary.as_ref().unwrap();
7820
7821        // initial model bone keyframe reference must match source summary (no payloadBytes)
7822        let init_model_bone_kf = graph
7823            .keyframe_references
7824            .iter()
7825            .find(|r| r.scope == "model" && r.track_kind == "bone" && r.initial)
7826            .expect("initial model bone keyframe ref");
7827        let model0 = &doc.models[0];
7828        let src_init_bone = &model0.initial_bone_keyframe_summaries[0];
7829        assert_eq!(init_model_bone_kf.owner_index, Some(model0.slot_index));
7830        assert_eq!(
7831            init_model_bone_kf.document_index,
7832            Some(model0.document_model_index)
7833        );
7834        assert_eq!(
7835            init_model_bone_kf.owner_name.as_deref(),
7836            Some(model0.name.as_str())
7837        );
7838        assert!(init_model_bone_kf.initial);
7839        assert_eq!(init_model_bone_kf.keyframe_index, src_init_bone.index);
7840        assert_eq!(init_model_bone_kf.frame_index, src_init_bone.frame_index);
7841        assert_eq!(
7842            init_model_bone_kf.previous_keyframe_index,
7843            src_init_bone.previous_keyframe_index
7844        );
7845        assert_eq!(
7846            init_model_bone_kf.next_keyframe_index,
7847            src_init_bone.next_keyframe_index
7848        );
7849        assert_eq!(init_model_bone_kf.offset, src_init_bone.offset);
7850        assert_eq!(init_model_bone_kf.byte_length, src_init_bone.byte_length);
7851        assert_eq!(
7852            init_model_bone_kf.payload_offset,
7853            src_init_bone.payload_offset
7854        );
7855        assert_eq!(
7856            init_model_bone_kf.payload_byte_length,
7857            src_init_bone.payload_byte_length
7858        );
7859
7860        // additional global camera keyframe reference must match source summary
7861        let add_global_cam_kf = graph
7862            .keyframe_references
7863            .iter()
7864            .find(|r| r.scope == "global" && r.track_kind == "camera" && !r.initial)
7865            .expect("additional global camera keyframe ref");
7866        let src_add_cam = match &glob.camera.keyframe_summaries[0] {
7867            PmmDocumentKeyframeSummary::Camera {
7868                index,
7869                frame_index,
7870                previous_keyframe_index,
7871                next_keyframe_index,
7872                offset,
7873                byte_length,
7874                payload_offset,
7875                payload_byte_length,
7876                ..
7877            } => (
7878                *index,
7879                *frame_index,
7880                *previous_keyframe_index,
7881                *next_keyframe_index,
7882                *offset,
7883                *byte_length,
7884                *payload_offset,
7885                *payload_byte_length,
7886            ),
7887            other => panic!("unexpected add cam summary: {other:?}"),
7888        };
7889        assert!(!add_global_cam_kf.initial);
7890        assert_eq!(add_global_cam_kf.keyframe_index, src_add_cam.0);
7891        assert_eq!(add_global_cam_kf.frame_index, src_add_cam.1);
7892        assert_eq!(add_global_cam_kf.previous_keyframe_index, src_add_cam.2);
7893        assert_eq!(add_global_cam_kf.next_keyframe_index, src_add_cam.3);
7894        assert_eq!(add_global_cam_kf.offset, src_add_cam.4);
7895        assert_eq!(add_global_cam_kf.byte_length, src_add_cam.5);
7896        assert_eq!(add_global_cam_kf.payload_offset, src_add_cam.6);
7897        assert_eq!(add_global_cam_kf.payload_byte_length, src_add_cam.7);
7898
7899        // initial accessory keyframe reference must match source summary
7900        let init_acc_kf = graph
7901            .keyframe_references
7902            .iter()
7903            .find(|r| r.scope == "accessory" && r.initial)
7904            .expect("initial accessory keyframe ref");
7905        let acc0 = &glob.accessories.accessories[0];
7906        let src_init_acc = &acc0.initial_keyframe;
7907        assert_eq!(init_acc_kf.owner_index, Some(acc0.slot_index));
7908        assert_eq!(
7909            init_acc_kf.document_index,
7910            Some(acc0.document_accessory_index)
7911        );
7912        assert_eq!(init_acc_kf.owner_name.as_deref(), Some(acc0.name.as_str()));
7913        assert!(init_acc_kf.initial);
7914        assert_eq!(init_acc_kf.keyframe_index, src_init_acc.index);
7915        assert_eq!(init_acc_kf.frame_index, src_init_acc.frame_index);
7916        assert_eq!(
7917            init_acc_kf.previous_keyframe_index,
7918            src_init_acc.previous_keyframe_index
7919        );
7920        assert_eq!(
7921            init_acc_kf.next_keyframe_index,
7922            src_init_acc.next_keyframe_index
7923        );
7924        assert_eq!(init_acc_kf.offset, src_init_acc.offset);
7925        assert_eq!(init_acc_kf.byte_length, src_init_acc.byte_length);
7926        assert_eq!(init_acc_kf.payload_offset, src_init_acc.payload_offset);
7927        assert_eq!(
7928            init_acc_kf.payload_byte_length,
7929            src_init_acc.payload_byte_length
7930        );
7931    }
7932
7933    #[test]
7934    fn pmm_project_byte_coverage_synthetic_pmmv2_full_fixture() {
7935        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
7936        let graph = parsed
7937            .project_graph
7938            .as_ref()
7939            .expect("project_graph must be Some for synthetic PMMv2 full fixture");
7940
7941        let cov = &graph.byte_coverage;
7942        let doc = parsed.document_summary.as_ref().unwrap();
7943        let glob = parsed.document_global_summary.as_ref().unwrap();
7944
7945        // 1 model + 1 global root + 4 global tracks + 1 accessory block + 1 accessory = 8
7946        assert_eq!(
7947            cov.range_count, 8,
7948            "synthetic PMMv2 full fixture must have exactly 8 byte coverage ranges (1 model + 1 global root + 4 global tracks + 1 accessory block + 1 accessory)"
7949        );
7950        assert_eq!(cov.ranges.len(), 8);
7951
7952        // offset equals the first decoded model offset
7953        let first_model = &doc.models[0];
7954        assert_eq!(
7955            cov.offset, first_model.offset,
7956            "coverage offset must equal first model offset"
7957        );
7958
7959        // offsetEnd equals global.offset_end
7960        assert_eq!(
7961            cov.offset_end, glob.offset_end,
7962            "coverage offsetEnd must equal global offset_end"
7963        );
7964
7965        // byteLength == offsetEnd - offset
7966        assert_eq!(cov.byte_length, cov.offset_end - cov.offset);
7967
7968        // a camera track range matches global.camera
7969        let cam_range = cov
7970            .ranges
7971            .iter()
7972            .find(|r| r.scope == "global" && r.kind == "camera")
7973            .expect("camera track byte range in coverage");
7974        assert_eq!(cam_range.offset, glob.camera.offset);
7975        assert_eq!(cam_range.offset_end, glob.camera.offset_end);
7976        assert_eq!(
7977            cam_range.byte_length,
7978            glob.camera.offset_end - glob.camera.offset
7979        );
7980        assert_eq!(cam_range.scope, "global");
7981        assert_eq!(cam_range.kind, "camera");
7982        assert!(cam_range.owner_index.is_none());
7983        assert!(cam_range.document_index.is_none());
7984
7985        // an accessory range matches the decoded accessory summary
7986        let acc_range = cov
7987            .ranges
7988            .iter()
7989            .find(|r| r.scope == "accessory")
7990            .expect("accessory byte range in coverage");
7991        let acc0 = &glob.accessories.accessories[0];
7992        assert_eq!(acc_range.owner_index, Some(acc0.slot_index));
7993        assert_eq!(
7994            acc_range.document_index,
7995            Some(acc0.document_accessory_index)
7996        );
7997        assert_eq!(acc_range.name.as_deref(), Some(acc0.name.as_str()));
7998        assert_eq!(acc_range.offset, acc0.offset);
7999        assert_eq!(acc_range.offset_end, acc0.offset_end);
8000        assert_eq!(acc_range.byte_length, acc0.offset_end - acc0.offset);
8001        assert_eq!(acc_range.scope, "accessory");
8002        assert_eq!(acc_range.kind, "accessory");
8003
8004        // New merged coverage metrics + unknown gap ranges (exporter-prep diagnostic, read-only, from bc_ranges only)
8005        assert!(
8006            cov.covered_byte_length <= cov.byte_length,
8007            "covered_byte_length must not exceed byte_length"
8008        );
8009        assert!(
8010            (0.0..=1.0).contains(&cov.coverage_ratio),
8011            "coverage_ratio must be in [0.0, 1.0], got {}",
8012            cov.coverage_ratio
8013        );
8014        assert_eq!(
8015            cov.gap_count,
8016            cov.gaps.len(),
8017            "gap_count must equal gaps.len()"
8018        );
8019        let cov_span_start = cov.offset;
8020        let cov_span_end = cov.offset_end;
8021        for (i, g) in cov.gaps.iter().enumerate() {
8022            assert_eq!(g.scope, "unknown", "gap[{}] scope must be unknown", i);
8023            assert_eq!(g.kind, "gap", "gap[{}] kind must be gap", i);
8024            assert!(
8025                g.byte_length > 0,
8026                "gap[{}] must have nonzero byte_length",
8027                i
8028            );
8029            assert!(
8030                g.offset >= cov_span_start && g.offset_end <= cov_span_end,
8031                "gap[{}] must be inside coverage span [{}, {})",
8032                i,
8033                cov_span_start,
8034                cov_span_end
8035            );
8036            assert_eq!(g.offset_end - g.offset, g.byte_length);
8037            assert!(g.owner_index.is_none());
8038            assert!(g.document_index.is_none());
8039            assert!(g.name.is_none());
8040        }
8041
8042        // Decoded ranges (ranges) still exist and populated as before (count + spot checks)
8043        assert_eq!(cov.range_count, 8);
8044        assert_eq!(cov.ranges.len(), 8);
8045        let model_range = cov
8046            .ranges
8047            .iter()
8048            .find(|r| r.scope == "model")
8049            .expect("model byte range still present in ranges after adding coverage metrics");
8050        assert_eq!(model_range.scope, "model");
8051        assert_eq!(model_range.kind, "model");
8052        assert!(model_range.owner_index.is_some());
8053    }
8054
8055    #[test]
8056    fn pmm_document_model_summary_json_schema_is_stable() {
8057        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8058        let document = parsed.document_summary.unwrap();
8059        let keys = json_keys(&serde_json::to_value(&document.models[0]).unwrap());
8060
8061        assert_eq!(
8062            keys,
8063            vec![
8064                "assetReferenceIndex",
8065                "blendEnabled",
8066                "boneCount",
8067                "boneKeyframeSummaries",
8068                "boneKeyframes",
8069                "boneNames",
8070                "boneStateSummaries",
8071                "constraintBoneCount",
8072                "constraintStateSummaries",
8073                "documentModelIndex",
8074                "drawOrderIndex",
8075                "edgeWidth",
8076                "englishName",
8077                "initialBoneKeyframeSummaries",
8078                "initialBoneKeyframes",
8079                "initialModelKeyframe",
8080                "initialModelKeyframes",
8081                "initialMorphKeyframeSummaries",
8082                "initialMorphKeyframes",
8083                "lastFrameIndex",
8084                "modelKeyframeSummaries",
8085                "modelKeyframes",
8086                "morphCount",
8087                "morphKeyframeSummaries",
8088                "morphKeyframes",
8089                "morphNames",
8090                "morphStateSummaries",
8091                "name",
8092                "offset",
8093                "offsetEnd",
8094                "outsideParentStateSummaries",
8095                "outsideParentSubjectBoneCount",
8096                "path",
8097                "pathOffset",
8098                "sections",
8099                "selectedBoneIndex",
8100                "selectedMorphIndices",
8101                "selfShadowEnabled",
8102                "slotIndex",
8103                "transformOrderIndex",
8104                "verticalScroll",
8105                "visible"
8106            ]
8107        );
8108    }
8109
8110    #[test]
8111    fn pmm_document_model_keyframe_summary_json_schema_is_stable() {
8112        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8113        let document = parsed.document_summary.unwrap();
8114        let keys =
8115            json_keys(&serde_json::to_value(&document.models[0].initial_model_keyframe).unwrap());
8116
8117        assert_eq!(
8118            keys,
8119            vec![
8120                "byteLength",
8121                "constraintStateCount",
8122                "constraintStates",
8123                "constraintStatesByteLength",
8124                "constraintStatesOffset",
8125                "frameIndex",
8126                "index",
8127                "nextKeyframeIndex",
8128                "offset",
8129                "outsideParentIndexCount",
8130                "outsideParentIndices",
8131                "outsideParentIndicesByteLength",
8132                "outsideParentIndicesOffset",
8133                "payloadByteLength",
8134                "payloadBytes",
8135                "payloadOffset",
8136                "previousKeyframeIndex",
8137                "selfShadowEnabled",
8138                "selfShadowEnabledOffset",
8139                "visible",
8140                "visibleOffset"
8141            ]
8142        );
8143    }
8144
8145    #[test]
8146    fn pmm_outside_parent_index_summary_json_schema_is_stable() {
8147        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8148        let document = parsed.document_summary.unwrap();
8149        let kf = &document.models[0].initial_model_keyframe;
8150        let keys = json_keys(&serde_json::to_value(&kf.outside_parent_indices[0]).unwrap());
8151
8152        assert_eq!(keys, vec!["parentModelBoneIndex", "parentModelIndex"]);
8153    }
8154
8155    #[test]
8156    fn pmm_document_model_sections_json_schema_is_stable() {
8157        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8158        let document = parsed.document_summary.unwrap();
8159        let keys = json_keys(&serde_json::to_value(&document.models[0].sections).unwrap());
8160
8161        assert_eq!(
8162            keys,
8163            vec![
8164                "boneKeyframeCountOffset",
8165                "boneKeyframesEndOffset",
8166                "boneKeyframesOffset",
8167                "boneStatesOffset",
8168                "constraintStatesOffset",
8169                "initialBoneKeyframesOffset",
8170                "initialModelKeyframeOffset",
8171                "initialMorphKeyframesOffset",
8172                "modelKeyframeCountOffset",
8173                "modelKeyframesEndOffset",
8174                "modelKeyframesOffset",
8175                "morphKeyframeCountOffset",
8176                "morphKeyframesEndOffset",
8177                "morphKeyframesOffset",
8178                "morphStatesOffset",
8179                "outsideParentStatesOffset"
8180            ]
8181        );
8182    }
8183
8184    #[test]
8185    fn pmm_diagnostic_json_schema_is_stable() {
8186        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8187        let keys = json_keys(&serde_json::to_value(&parsed.diagnostics[0]).unwrap());
8188
8189        assert_eq!(keys, vec!["code", "level", "message"]);
8190    }
8191
8192    #[test]
8193    fn parses_pmm_project_settings_header_slice() {
8194        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8195
8196        assert_eq!(parsed.project_settings.screen_width, Some(1920));
8197        assert_eq!(parsed.project_settings.screen_height, Some(1080));
8198        assert_eq!(parsed.project_settings.timeline_frame_count, Some(424));
8199        assert_eq!(parsed.project_settings.frame_rate, Some(30.0));
8200        assert_eq!(parsed.timeline.start_frame, Some(0));
8201        assert_eq!(parsed.timeline.end_frame_exclusive, Some(424));
8202        assert_eq!(parsed.timeline.frame_count, Some(424));
8203        assert_eq!(parsed.timeline.frame_rate, Some(30.0));
8204        assert_eq!(parsed.timeline.duration_seconds, Some(424.0 / 30.0));
8205        assert_eq!(
8206            parsed.display_state.model_slot_flags,
8207            vec![1, 1, 2, 0, 0, 0, 0, 0]
8208        );
8209        assert_eq!(parsed.display_state.layout, "pmm-v2-flags");
8210        assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
8211        assert_eq!(parsed.display_state.model_slot_count, 8);
8212        assert_eq!(parsed.display_state.non_zero_model_slot_count, 3);
8213        assert_eq!(parsed.display_state.accessory_slot_count, None);
8214        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
8215        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
8216        assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.wav"]);
8217        assert_eq!(parsed.model_assets.len(), 1);
8218        assert_eq!(parsed.model_assets[0].reference_index, 0);
8219        assert_eq!(parsed.model_assets[0].kind_index, 0);
8220        assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
8221        assert_eq!(parsed.model_assets[0].confidence, "high");
8222        assert!(parsed.model_assets[0].offset_end > parsed.model_assets[0].offset);
8223        assert_eq!(parsed.motion_assets[0].reference_index, 1);
8224        assert_eq!(parsed.motion_assets[0].kind_index, 0);
8225        assert_eq!(parsed.audio_assets[0].reference_index, 2);
8226        assert_eq!(parsed.audio_assets[0].extension, "wav");
8227        assert_eq!(parsed.diagnostics.len(), 4);
8228        assert_eq!(parsed.diagnostics[0].code, "PMM_PROJECT_GRAPH_PARTIAL");
8229        assert_eq!(parsed.diagnostics[1].code, "PMM_ASSET_REFERENCES_SCAN");
8230        assert!(parsed.diagnostics[1].message.contains("references=3"));
8231        assert_eq!(
8232            parsed.diagnostics[2].code,
8233            "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE"
8234        );
8235        assert_eq!(parsed.diagnostics[3].code, "PMM_ASSET_COUNT_MISMATCH");
8236    }
8237
8238    #[test]
8239    fn pmm_asset_summary_groups_manifest_derived_references() {
8240        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8241        let summary = &parsed.asset_summary;
8242
8243        assert_eq!(summary.reference_count, 3);
8244        assert_eq!(summary.high_confidence_count, 3);
8245        assert_eq!(summary.medium_confidence_count, 0);
8246        assert_eq!(summary.low_confidence_count, 0);
8247        assert_eq!(summary.kind_counts.get("model"), Some(&1));
8248        assert_eq!(summary.kind_counts.get("motion"), Some(&1));
8249        assert_eq!(summary.kind_counts.get("audio"), Some(&1));
8250        assert_eq!(summary.extension_counts.get("pmx"), Some(&1));
8251        assert_eq!(summary.extension_counts.get("vmd"), Some(&1));
8252        assert_eq!(summary.extension_counts.get("wav"), Some(&1));
8253        assert_eq!(summary.confidence_counts.get("high"), Some(&3));
8254    }
8255
8256    #[test]
8257    fn parses_pmm_v2_document_model_keyframe_summary() {
8258        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8259        let document = parsed.document_summary.as_ref().unwrap();
8260
8261        assert_eq!(document.source, "nanoem/ext/document.c PMMv2 layout");
8262        assert_eq!(document.selected_model_index, 0);
8263        assert_eq!(document.model_count, 1);
8264        assert_eq!(document.counts.models, 1);
8265        assert_eq!(document.counts.bones, 1);
8266        assert_eq!(document.counts.morphs, 1);
8267        assert_eq!(document.counts.initial_bone_keyframes, 1);
8268        assert_eq!(document.counts.bone_keyframes, 1);
8269        assert_eq!(document.counts.initial_morph_keyframes, 1);
8270        assert_eq!(document.counts.morph_keyframes, 1);
8271        assert_eq!(document.counts.initial_model_keyframes, 1);
8272        assert_eq!(document.counts.model_keyframes, 1);
8273
8274        let model = &document.models[0];
8275        assert_eq!(model.slot_index, 0);
8276        assert_eq!(model.document_model_index, 0);
8277        assert_eq!(model.name, "Miku");
8278        assert_eq!(model.english_name, "MikuEn");
8279        assert_eq!(model.path, "UserFile\\Model\\miku.pmx");
8280        assert_eq!(
8281            model.path_offset,
8282            model.offset + 1 + 1 + "Miku".len() + 1 + "MikuEn".len()
8283        );
8284        let asset_reference_index = model.asset_reference_index.unwrap();
8285        assert_eq!(
8286            parsed.asset_references[asset_reference_index].normalized_path,
8287            "UserFile/Model/miku.pmx"
8288        );
8289        assert_eq!(model.bone_count, 1);
8290        assert_eq!(model.bone_names, vec!["center".to_owned()]);
8291        assert_eq!(model.morph_count, 1);
8292        assert_eq!(model.morph_names, vec!["smile".to_owned()]);
8293        assert_eq!(model.constraint_bone_count, 1);
8294        assert_eq!(model.outside_parent_subject_bone_count, 1);
8295        assert_eq!(model.bone_keyframes, 1);
8296        assert_eq!(model.morph_keyframes, 1);
8297        assert_eq!(model.model_keyframes, 1);
8298        assert_eq!(model.initial_model_keyframe.index, None);
8299        assert_eq!(model.initial_model_keyframe.frame_index, 0);
8300        assert_eq!(model.initial_model_keyframe.previous_keyframe_index, 0);
8301        assert_eq!(model.initial_model_keyframe.next_keyframe_index, 0);
8302        assert_eq!(
8303            model.initial_model_keyframe.offset,
8304            model.sections.initial_model_keyframe_offset
8305        );
8306        assert_eq!(model.initial_model_keyframe.byte_length, 23);
8307        assert_eq!(
8308            model.initial_model_keyframe.payload_offset,
8309            model.initial_model_keyframe.offset + 12
8310        );
8311        assert_eq!(model.initial_model_keyframe.payload_byte_length, 11);
8312        assert_eq!(model.initial_model_keyframe.payload_bytes.len(), 11);
8313        assert_eq!(
8314            model.initial_model_keyframe.payload_bytes.len(),
8315            model.initial_model_keyframe.payload_byte_length
8316        );
8317        assert_model_keyframe_payload_layout(&model.initial_model_keyframe);
8318        assert_eq!(
8319            model.initial_model_keyframe.payload_bytes[0],
8320            model.initial_model_keyframe.visible as u8
8321        );
8322        assert_eq!(
8323            model.initial_model_keyframe.payload_bytes[10],
8324            model.initial_model_keyframe.self_shadow_enabled as u8
8325        );
8326        assert!(model.initial_model_keyframe.visible);
8327        assert_eq!(model.initial_model_keyframe.constraint_states, vec![true]);
8328        assert_eq!(
8329            model.initial_model_keyframe.outside_parent_indices[0].parent_model_index,
8330            2
8331        );
8332        assert_eq!(
8333            model.initial_model_keyframe.outside_parent_indices[0].parent_model_bone_index,
8334            3
8335        );
8336        assert!(model.initial_model_keyframe.self_shadow_enabled);
8337        assert_eq!(model.model_keyframe_summaries.len(), 1);
8338        assert_eq!(model.model_keyframe_summaries[0].index, Some(30));
8339        assert_eq!(model.model_keyframe_summaries[0].frame_index, 30);
8340        assert_eq!(
8341            model.model_keyframe_summaries[0].offset,
8342            model.sections.model_keyframes_offset
8343        );
8344        assert_eq!(model.model_keyframe_summaries[0].byte_length, 27);
8345        assert_eq!(
8346            model.model_keyframe_summaries[0].payload_offset,
8347            model.model_keyframe_summaries[0].offset + 16
8348        );
8349        assert_eq!(model.model_keyframe_summaries[0].payload_byte_length, 11);
8350        assert_eq!(model.model_keyframe_summaries[0].payload_bytes.len(), 11);
8351        assert_eq!(
8352            model.model_keyframe_summaries[0].payload_bytes.len(),
8353            model.model_keyframe_summaries[0].payload_byte_length
8354        );
8355        assert_model_keyframe_payload_layout(&model.model_keyframe_summaries[0]);
8356        assert_eq!(
8357            model.model_keyframe_summaries[0].payload_bytes[0],
8358            model.model_keyframe_summaries[0].visible as u8
8359        );
8360        assert_eq!(
8361            model.model_keyframe_summaries[0].payload_bytes[10],
8362            model.model_keyframe_summaries[0].self_shadow_enabled as u8
8363        );
8364        assert!(model.model_keyframe_summaries[0].visible);
8365        assert_eq!(
8366            model.model_keyframe_summaries[0].constraint_states,
8367            vec![true]
8368        );
8369        assert_eq!(
8370            model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_index,
8371            2
8372        );
8373        assert_eq!(
8374            model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_bone_index,
8375            3
8376        );
8377        assert!(model.model_keyframe_summaries[0].self_shadow_enabled);
8378        assert_eq!(model.draw_order_index, 0);
8379        assert_eq!(model.transform_order_index, 0);
8380        assert_eq!(model.selected_bone_index, 0);
8381        assert_eq!(model.selected_morph_indices, [-1, -1, -1, -1]);
8382        assert_eq!(model.vertical_scroll, 0);
8383        assert_eq!(model.last_frame_index, 30);
8384        assert!(model.visible);
8385        assert!(model.blend_enabled);
8386        assert_eq!(model.edge_width, 1.0);
8387        assert!(!model.self_shadow_enabled);
8388        assert!(model.offset_end > model.offset);
8389        assert!(model.path_offset > model.offset);
8390        assert!(model.sections.initial_bone_keyframes_offset > model.path_offset);
8391        assert!(model.sections.bone_keyframes_offset > model.sections.bone_keyframe_count_offset);
8392        assert!(model.sections.bone_keyframes_end_offset > model.sections.bone_keyframes_offset);
8393        assert!(model.sections.morph_keyframes_offset > model.sections.morph_keyframe_count_offset);
8394        assert!(model.sections.model_keyframes_offset > model.sections.model_keyframe_count_offset);
8395        assert!(
8396            model.sections.outside_parent_states_offset >= model.sections.constraint_states_offset
8397        );
8398        let document_diagnostic = parsed
8399            .diagnostics
8400            .iter()
8401            .find(|diagnostic| diagnostic.code == "PMM_DOCUMENT_SUMMARY_PARTIAL")
8402            .unwrap();
8403        assert!(
8404            document_diagnostic
8405                .message
8406                .contains("2 total bone keyframe(s) (1 additional)")
8407        );
8408        assert!(
8409            document_diagnostic
8410                .message
8411                .contains("2 total morph keyframe(s) (1 additional)")
8412        );
8413    }
8414
8415    #[test]
8416    fn pmm_document_bone_keyframe_summary_json_schema_is_stable() {
8417        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8418        let document = parsed.document_summary.unwrap();
8419        let model = &document.models[0];
8420        let keys =
8421            json_keys(&serde_json::to_value(&model.initial_bone_keyframe_summaries[0]).unwrap());
8422
8423        assert_eq!(
8424            keys,
8425            vec![
8426                "byteLength",
8427                "frameIndex",
8428                "index",
8429                "interpolation",
8430                "nextKeyframeIndex",
8431                "offset",
8432                "orientation",
8433                "payloadByteLength",
8434                "payloadBytes",
8435                "payloadOffset",
8436                "physicsDisabled",
8437                "previousKeyframeIndex",
8438                "selected",
8439                "translation"
8440            ]
8441        );
8442    }
8443
8444    #[test]
8445    fn pmm_document_morph_keyframe_summary_json_schema_is_stable() {
8446        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8447        let document = parsed.document_summary.unwrap();
8448        let model = &document.models[0];
8449        let keys =
8450            json_keys(&serde_json::to_value(&model.initial_morph_keyframe_summaries[0]).unwrap());
8451
8452        assert_eq!(
8453            keys,
8454            vec![
8455                "byteLength",
8456                "frameIndex",
8457                "index",
8458                "nextKeyframeIndex",
8459                "offset",
8460                "payloadByteLength",
8461                "payloadBytes",
8462                "payloadOffset",
8463                "previousKeyframeIndex",
8464                "selected",
8465                "weight"
8466            ]
8467        );
8468    }
8469
8470    #[test]
8471    fn parses_pmm_v2_document_bone_morph_keyframe_summaries() {
8472        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
8473        let document = parsed.document_summary.as_ref().unwrap();
8474        let model = &document.models[0];
8475
8476        assert_eq!(model.initial_bone_keyframe_summaries.len(), 1);
8477        let ibk = &model.initial_bone_keyframe_summaries[0];
8478        assert_eq!(ibk.index, None);
8479        assert_eq!(ibk.frame_index, 0);
8480        assert_eq!(ibk.previous_keyframe_index, 0);
8481        assert_eq!(ibk.next_keyframe_index, 0);
8482        assert_eq!(ibk.interpolation, [20u8; 16]);
8483        assert_eq!(ibk.translation, [0.0, 0.0, 0.0]);
8484        assert_eq!(ibk.orientation, [0.0, 0.0, 0.0, 1.0]);
8485        assert!(!ibk.physics_disabled);
8486        assert!(!ibk.selected);
8487        assert_eq!(ibk.byte_length, 58);
8488        assert_eq!(ibk.offset, model.sections.initial_bone_keyframes_offset);
8489        assert_eq!(ibk.payload_offset, ibk.offset + 12);
8490        assert_eq!(ibk.payload_byte_length, 46);
8491        assert_eq!(ibk.payload_bytes.len(), 46);
8492        assert_eq!(ibk.payload_bytes.len(), ibk.payload_byte_length);
8493        assert_eq!(&ibk.payload_bytes[..16], &ibk.interpolation[..]);
8494
8495        assert_eq!(model.bone_keyframe_summaries.len(), 1);
8496        let bk = &model.bone_keyframe_summaries[0];
8497        assert_eq!(bk.index, Some(0));
8498        assert_eq!(bk.frame_index, 30);
8499        assert_eq!(bk.previous_keyframe_index, 0);
8500        assert_eq!(bk.next_keyframe_index, 0);
8501        assert_eq!(bk.interpolation, [20u8; 16]);
8502        assert_eq!(bk.translation, [0.0, 0.0, 0.0]);
8503        assert_eq!(bk.orientation, [0.0, 0.0, 0.0, 1.0]);
8504        assert!(!bk.physics_disabled);
8505        assert!(!bk.selected);
8506        assert_eq!(bk.byte_length, 62);
8507        assert_eq!(bk.offset, model.sections.bone_keyframes_offset);
8508        assert_eq!(bk.payload_offset, bk.offset + 16);
8509        assert_eq!(bk.payload_byte_length, 46);
8510        assert_eq!(bk.payload_bytes.len(), 46);
8511        assert_eq!(bk.payload_bytes.len(), bk.payload_byte_length);
8512        assert_eq!(&bk.payload_bytes[..16], &bk.interpolation[..]);
8513
8514        assert_eq!(model.initial_morph_keyframe_summaries.len(), 1);
8515        let imk = &model.initial_morph_keyframe_summaries[0];
8516        assert_eq!(imk.index, None);
8517        assert_eq!(imk.frame_index, 0);
8518        assert_eq!(imk.previous_keyframe_index, 0);
8519        assert_eq!(imk.next_keyframe_index, 0);
8520        assert!((imk.weight - 0.5).abs() < 1e-6);
8521        assert!(!imk.selected);
8522        assert_eq!(imk.byte_length, 17);
8523        assert_eq!(imk.offset, model.sections.initial_morph_keyframes_offset);
8524        assert_eq!(imk.payload_offset, imk.offset + 12);
8525        assert_eq!(imk.payload_byte_length, 5);
8526        assert_eq!(imk.payload_bytes.len(), 5);
8527        assert_eq!(imk.payload_bytes.len(), imk.payload_byte_length);
8528        let imk_weight_bytes = f32::from_le_bytes([
8529            imk.payload_bytes[0],
8530            imk.payload_bytes[1],
8531            imk.payload_bytes[2],
8532            imk.payload_bytes[3],
8533        ]);
8534        assert!((imk_weight_bytes - imk.weight).abs() < 1e-6);
8535        assert_eq!(imk.payload_bytes[4], imk.selected as u8);
8536
8537        assert_eq!(model.morph_keyframe_summaries.len(), 1);
8538        let mk = &model.morph_keyframe_summaries[0];
8539        assert_eq!(mk.index, Some(0));
8540        assert_eq!(mk.frame_index, 15);
8541        assert_eq!(mk.previous_keyframe_index, 0);
8542        assert_eq!(mk.next_keyframe_index, 0);
8543        assert!((mk.weight - 0.5).abs() < 1e-6);
8544        assert!(!mk.selected);
8545        assert_eq!(mk.byte_length, 21);
8546        assert_eq!(mk.offset, model.sections.morph_keyframes_offset);
8547        assert_eq!(mk.payload_offset, mk.offset + 16);
8548        assert_eq!(mk.payload_byte_length, 5);
8549        assert_eq!(mk.payload_bytes.len(), 5);
8550        assert_eq!(mk.payload_bytes.len(), mk.payload_byte_length);
8551        let mk_weight_bytes = f32::from_le_bytes([
8552            mk.payload_bytes[0],
8553            mk.payload_bytes[1],
8554            mk.payload_bytes[2],
8555            mk.payload_bytes[3],
8556        ]);
8557        assert!((mk_weight_bytes - mk.weight).abs() < 1e-6);
8558        assert_eq!(mk.payload_bytes[4], mk.selected as u8);
8559    }
8560
8561    #[test]
8562    fn pmm_v2_document_summary_rejects_selected_model_index_out_of_range() {
8563        let mut data = pmm_with_document_summary();
8564        data[53] = 1;
8565
8566        let parsed = parse_pmm_manifest(&data).unwrap();
8567
8568        assert!(parsed.document_summary.is_none());
8569    }
8570
8571    #[test]
8572    fn parses_initial_pmm_model_slot_slice() {
8573        let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
8574
8575        assert_eq!(parsed.model_slots.len(), 1);
8576        let slot = &parsed.model_slots[0];
8577        assert_eq!(slot.slot_index, 0);
8578        assert_eq!(slot.display_slot_index, Some(1));
8579        assert_eq!(slot.offset, 56);
8580        assert_eq!(slot.model_path_offset, 71);
8581        assert_eq!(slot.trailing_zero_padding_bytes, 0);
8582        assert_eq!(slot.next_non_zero_offset, Some(slot.offset_end + 1));
8583        assert_eq!(slot.name, "TestModel");
8584        assert_eq!(slot.name_bytes, b"TestModel");
8585        assert_eq!(slot.english_name, "Base");
8586        assert_eq!(slot.english_name_bytes, b"Base");
8587        assert_eq!(
8588            slot.model_path,
8589            "C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx"
8590        );
8591        assert_eq!(
8592            slot.normalized_path,
8593            "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
8594        );
8595        assert_eq!(slot.asset_reference_index, Some(0));
8596        assert_eq!(slot.confidence, "high");
8597        assert_eq!(parsed.asset_references[0].offset, 56);
8598        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
8599        assert!(
8600            parsed
8601                .diagnostics
8602                .iter()
8603                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_INITIAL_SLICE")
8604        );
8605        assert!(
8606            parsed
8607                .diagnostics
8608                .iter()
8609                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
8610        );
8611        assert!(
8612            parsed
8613                .diagnostics
8614                .iter()
8615                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
8616        );
8617    }
8618
8619    #[test]
8620    fn parses_sequential_pmm_model_slot_header_slice() {
8621        let parsed = parse_pmm_manifest(&pmm_with_two_initial_model_slots()).unwrap();
8622
8623        assert_eq!(parsed.model_slots.len(), 2);
8624        assert_eq!(parsed.model_slots[0].slot_index, 0);
8625        assert_eq!(parsed.model_slots[1].slot_index, 1);
8626        assert_eq!(parsed.model_slots[0].display_slot_index, Some(0));
8627        assert_eq!(parsed.model_slots[1].display_slot_index, Some(1));
8628        assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 0);
8629        assert_eq!(
8630            parsed.model_slots[0].next_non_zero_offset,
8631            Some(parsed.model_slots[1].offset)
8632        );
8633        assert_eq!(
8634            parsed.model_slots[1].offset,
8635            parsed.model_slots[0].offset_end + 1
8636        );
8637        assert_eq!(parsed.model_slots[1].name, "Sour");
8638        assert_eq!(
8639            parsed.model_slots[1].model_path,
8640            "G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx"
8641        );
8642        assert_eq!(
8643            parsed.model_slots[1].normalized_path,
8644            "G:/MikuMikuDance/MMD Models/Sour/sour.pmx"
8645        );
8646        assert_eq!(parsed.model_slots[1].asset_reference_index, Some(1));
8647        assert_eq!(
8648            parsed.asset_references[1].offset,
8649            parsed.model_slots[1].offset
8650        );
8651        assert!(
8652            !parsed
8653                .diagnostics
8654                .iter()
8655                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
8656        );
8657    }
8658
8659    #[test]
8660    fn pmm_model_slot_scan_stops_when_next_asset_reference_is_not_matched() {
8661        let data = pmm_with_two_initial_model_slots();
8662        let display_state = parse_display_state(&data, Some(2));
8663        let references = extract_asset_references(&data);
8664        let scan = parse_model_slots_from_header(&data, &references[..1], &display_state);
8665
8666        assert_eq!(scan.slots.len(), 1);
8667        let stop = scan.stop.unwrap();
8668        assert_eq!(stop.offset, scan.slots[0].offset_end + 1);
8669        assert_eq!(stop.reason, "asset_reference_not_matched");
8670    }
8671
8672    #[test]
8673    fn pmm_model_slot_scan_stops_at_implausible_next_name_length() {
8674        let mut data = pmm_with_initial_model_slot();
8675        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
8676        data.truncate(slot_end + 1);
8677        data.push(255);
8678        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
8679        let parsed = parse_pmm_manifest(&data).unwrap();
8680
8681        assert_eq!(parsed.model_slots.len(), 1);
8682        let diagnostic = parsed
8683            .diagnostics
8684            .iter()
8685            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
8686            .unwrap();
8687        assert!(diagnostic.message.contains("missing_name"));
8688    }
8689
8690    #[test]
8691    fn pmm_model_slot_scan_stops_at_null_byte_in_next_name() {
8692        let mut data = pmm_with_initial_model_slot();
8693        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
8694        data.truncate(slot_end + 1);
8695        data.push(4);
8696        data.extend_from_slice(b"A\0BC");
8697        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
8698        let parsed = parse_pmm_manifest(&data).unwrap();
8699
8700        assert_eq!(parsed.model_slots.len(), 1);
8701        let diagnostic = parsed
8702            .diagnostics
8703            .iter()
8704            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
8705            .unwrap();
8706        assert!(diagnostic.message.contains("missing_name"));
8707    }
8708
8709    #[test]
8710    fn pmm_model_slot_scan_records_zero_padding_before_binary_graph() {
8711        let mut data = pmm_with_initial_model_slot();
8712        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
8713        data.truncate(slot_end + 1);
8714        data.extend_from_slice(&[0; 12]);
8715        data.extend_from_slice(&[15, 34, 3, 0, 0, 8, 145, 128]);
8716        let parsed = parse_pmm_manifest(&data).unwrap();
8717
8718        assert_eq!(parsed.model_slots.len(), 1);
8719        assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 12);
8720        assert_eq!(
8721            parsed.model_slots[0].next_non_zero_offset,
8722            Some(slot_end + 13)
8723        );
8724        let diagnostic = parsed
8725            .diagnostics
8726            .iter()
8727            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
8728            .unwrap();
8729        assert!(diagnostic.message.contains("missing_name"));
8730    }
8731
8732    #[test]
8733    fn parses_pmm_numeric_version() {
8734        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
8735
8736        assert_eq!(parsed.version, "0002");
8737        assert_eq!(parsed.parsed_version, Some(2));
8738    }
8739
8740    #[test]
8741    fn rejects_invalid_pmm_magic() {
8742        let err = parse_pmm_manifest(b"not a PMM project").unwrap_err();
8743
8744        assert_eq!(err, ImportError::InvalidMagic { format: "PMM" });
8745    }
8746
8747    #[test]
8748    fn parses_pmm_v1_display_count_layout() {
8749        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
8750
8751        assert_eq!(parsed.version, "0001");
8752        assert_eq!(parsed.parsed_version, Some(1));
8753        assert_eq!(parsed.project_settings.screen_width, Some(512));
8754        assert_eq!(parsed.project_settings.screen_height, Some(288));
8755        assert_eq!(parsed.project_settings.timeline_frame_count, Some(435));
8756        assert_eq!(parsed.project_settings.frame_rate, Some(35.0));
8757        assert_eq!(parsed.display_state.layout, "pmm-v1-counts");
8758        assert!(parsed.display_state.model_slot_flags.is_empty());
8759        assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
8760        assert_eq!(parsed.display_state.model_slot_count, 8);
8761        assert_eq!(parsed.display_state.non_zero_model_slot_count, 8);
8762        assert_eq!(parsed.display_state.accessory_slot_count, Some(9));
8763        assert_eq!(parsed.header_text_entries.len(), 2);
8764        assert_eq!(parsed.header_text_entries[0].index, 0);
8765        assert_eq!(parsed.header_text_entries[0].offset, 54);
8766        assert_eq!(parsed.header_text_entries[0].text, "Dummy");
8767        assert_eq!(parsed.header_text_entries[0].text_bytes, b"Dummy");
8768        assert_eq!(parsed.header_text_entries[1].text, "maker Dummy");
8769        assert!(
8770            !parsed
8771                .diagnostics
8772                .iter()
8773                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
8774        );
8775        assert!(parsed.model_slots.is_empty());
8776        assert!(
8777            parsed
8778                .diagnostics
8779                .iter()
8780                .any(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
8781        );
8782        assert!(
8783            !parsed
8784                .diagnostics
8785                .iter()
8786                .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
8787        );
8788    }
8789
8790    #[test]
8791    fn pmm_v1_invalid_display_counts_do_not_fall_through_to_v2_flags() {
8792        let mut data = pmm_v1_with_display_counts();
8793        data[52] = 255;
8794        data[53] = 255;
8795        data[54] = 1;
8796        data[55] = 1;
8797        let parsed = parse_pmm_manifest(&data).unwrap();
8798
8799        assert_eq!(parsed.display_state.layout, "unknown");
8800        assert!(parsed.display_state.model_slot_flags.is_empty());
8801        assert_eq!(parsed.display_state.declared_model_slot_count, None);
8802        assert_eq!(parsed.display_state.model_slot_count, 0);
8803        assert_eq!(parsed.display_state.accessory_slot_count, None);
8804        assert!(
8805            parsed
8806                .diagnostics
8807                .iter()
8808                .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
8809        );
8810    }
8811
8812    #[test]
8813    fn pmm_v1_header_text_entries_require_asset_boundary() {
8814        let mut data = b"Polygon Movie maker 0001".to_vec();
8815        data.resize(54, 0);
8816        data.extend_from_slice(b"Header\0Without asset boundary\0");
8817        let parsed = parse_pmm_manifest(&data).unwrap();
8818
8819        assert!(parsed.asset_references.is_empty());
8820        assert!(parsed.header_text_entries.is_empty());
8821    }
8822
8823    #[test]
8824    fn pmm_v1_header_text_entries_allow_empty_region_before_asset() {
8825        let mut data = b"Polygon Movie maker 0001".to_vec();
8826        data.resize(54, 0);
8827        data.extend_from_slice(b"\0\0\0UserFile\\Model\\miku.pmd\0");
8828        let parsed = parse_pmm_manifest(&data).unwrap();
8829
8830        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
8831        assert!(parsed.header_text_entries.is_empty());
8832    }
8833
8834    #[test]
8835    fn pmm_v1_header_text_entries_ignore_unterminated_chunk_before_asset() {
8836        let mut data = b"Polygon Movie maker 0001".to_vec();
8837        data.resize(54, 0);
8838        data.extend_from_slice(b"Unterminated header UserFile\\Model\\miku.pmd\0");
8839        let parsed = parse_pmm_manifest(&data).unwrap();
8840
8841        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
8842        assert!(parsed.header_text_entries.is_empty());
8843    }
8844
8845    #[test]
8846    fn parses_pmm_bare_asset_file_names() {
8847        let data = b"Polygon Movie maker 0002\0miku.pmx\0walk.vmd\0song.wav\0stage.avi\0.X\0";
8848        let parsed = parse_pmm_manifest(data).unwrap();
8849
8850        assert_eq!(parsed.model_paths, vec!["miku.pmx"]);
8851        assert_eq!(parsed.motion_paths, vec!["walk.vmd"]);
8852        assert_eq!(parsed.audio_paths, vec!["song.wav"]);
8853        assert_eq!(parsed.video_paths, vec!["stage.avi"]);
8854        assert!(parsed.accessory_paths.is_empty());
8855        assert_eq!(parsed.model_assets[0].confidence, "medium");
8856        assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
8857        assert_eq!(parsed.motion_assets[0].kind_index, 0);
8858        assert_eq!(parsed.audio_assets[0].reference_index, 2);
8859        assert_eq!(parsed.video_assets[0].reference_index, 3);
8860        assert_eq!(parsed.video_assets[0].kind_index, 0);
8861    }
8862
8863    #[test]
8864    fn parses_pmm_background_video_asset_paths() {
8865        let data =
8866            b"Polygon Movie maker 0002\0UserFile\\Background\\stage.avs\0C:\\Video\\movie.avi\0";
8867        let parsed = parse_pmm_manifest(data).unwrap();
8868
8869        assert_eq!(
8870            parsed.video_paths,
8871            vec!["UserFile/Background/stage.avs", "C:/Video/movie.avi"]
8872        );
8873        assert_eq!(parsed.video_assets.len(), 2);
8874        assert_eq!(parsed.video_assets[0].extension, "avs");
8875        assert_eq!(parsed.video_assets[0].confidence, "high");
8876        assert_eq!(parsed.video_assets[1].extension, "avi");
8877        assert_eq!(parsed.video_assets[1].file_name, "movie.avi");
8878    }
8879
8880    #[test]
8881    fn parses_pmm_common_audio_image_and_video_asset_extensions() {
8882        let data = b"Polygon Movie maker 0002\0UserFile\\Audio\\song.mp3\0UserFile\\Audio\\loop.ogg\0UserFile\\Texture\\toon.png\0UserFile\\Texture\\photo.jpg\0UserFile\\Texture\\poster.jpeg\0UserFile\\Texture\\normal.dds\0UserFile\\Movie\\clip.mp4\0UserFile\\Movie\\stage.wmv\0";
8883        let parsed = parse_pmm_manifest(data).unwrap();
8884
8885        assert_eq!(
8886            parsed.audio_paths,
8887            vec!["UserFile/Audio/song.mp3", "UserFile/Audio/loop.ogg"]
8888        );
8889        assert_eq!(
8890            parsed.image_paths,
8891            vec![
8892                "UserFile/Texture/toon.png",
8893                "UserFile/Texture/photo.jpg",
8894                "UserFile/Texture/poster.jpeg",
8895                "UserFile/Texture/normal.dds"
8896            ]
8897        );
8898        assert_eq!(
8899            parsed.video_paths,
8900            vec!["UserFile/Movie/clip.mp4", "UserFile/Movie/stage.wmv"]
8901        );
8902        assert_eq!(parsed.audio_assets[0].extension, "mp3");
8903        assert_eq!(parsed.audio_assets[1].extension, "ogg");
8904        assert_eq!(parsed.image_assets[0].extension, "png");
8905        assert_eq!(parsed.image_assets[1].extension, "jpg");
8906        assert_eq!(parsed.image_assets[2].extension, "jpeg");
8907        assert_eq!(parsed.image_assets[3].extension, "dds");
8908        assert_eq!(parsed.video_assets[0].extension, "mp4");
8909        assert_eq!(parsed.video_assets[1].extension, "wmv");
8910    }
8911
8912    #[test]
8913    fn pmm_asset_scanner_rejects_partial_common_media_extensions() {
8914        let data = b"Polygon Movie maker 0002\0song.mp3extra\0toon.png2\0poster.jpeg_backup\0movie.mp4.tmp\0UserFile\\Audio\\song.mp3\0";
8915        let parsed = parse_pmm_manifest(data).unwrap();
8916
8917        assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.mp3"]);
8918        assert!(parsed.image_paths.is_empty());
8919        assert!(parsed.video_paths.is_empty());
8920    }
8921
8922    #[test]
8923    fn parses_pmm_asset_paths_with_spaces() {
8924        let data = b"Polygon Movie maker 0002\0Label C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx\0UserFile\\Motion\\walk cycle.vmd\0";
8925        let parsed = parse_pmm_manifest(data).unwrap();
8926
8927        assert_eq!(
8928            parsed.model_paths,
8929            vec!["C:/MikuMikuDance/MMD Models/Append Miku.pmx"]
8930        );
8931        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk cycle.vmd"]);
8932        assert_eq!(
8933            parsed.model_assets[0].path,
8934            "C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx"
8935        );
8936        assert_eq!(parsed.model_assets[0].file_name, "Append Miku.pmx");
8937        assert_eq!(parsed.motion_assets[0].file_name, "walk cycle.vmd");
8938    }
8939
8940    #[test]
8941    fn pmm_asset_reference_marks_replacement_char_as_low_confidence() {
8942        let data = "Polygon Movie maker 0002\0C:\\Models\\bad�name.pmx\0"
8943            .as_bytes()
8944            .to_vec();
8945        let parsed = parse_pmm_manifest(&data).unwrap();
8946
8947        assert_eq!(parsed.model_assets[0].confidence, "low");
8948    }
8949
8950    #[test]
8951    fn pmm_asset_reference_marks_root_relative_fragment_as_low_confidence() {
8952        let data = b"Polygon Movie maker 0002\0\\Model\\miku.pmx\0";
8953        let parsed = parse_pmm_manifest(data).unwrap();
8954
8955        assert_eq!(parsed.model_assets[0].normalized_path, "/Model/miku.pmx");
8956        assert_eq!(parsed.model_assets[0].confidence, "low");
8957    }
8958
8959    #[test]
8960    fn pmm_asset_reference_marks_missing_drive_letter_fragment_as_low_confidence() {
8961        let data =
8962            b"Polygon Movie maker 0002\0:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
8963        let parsed = parse_pmm_manifest(data).unwrap();
8964
8965        assert_eq!(
8966            parsed.model_assets[0].normalized_path,
8967            ":/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
8968        );
8969        assert_eq!(parsed.model_assets[0].confidence, "low");
8970    }
8971
8972    #[test]
8973    fn pmm_asset_scan_keeps_complete_and_fragmented_duplicate_paths_separate() {
8974        let data = b"Polygon Movie maker 0002\0C:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0:\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
8975        let parsed = parse_pmm_manifest(data).unwrap();
8976
8977        assert_eq!(parsed.model_assets.len(), 2);
8978        assert_eq!(
8979            parsed.model_assets[0].normalized_path,
8980            "C:/MMDDev/data/unittest/test_1bone_cube.pmx"
8981        );
8982        assert_eq!(parsed.model_assets[0].confidence, "high");
8983        assert_eq!(
8984            parsed.model_assets[1].normalized_path,
8985            ":/MMDDev/data/unittest/test_1bone_cube.pmx"
8986        );
8987        assert_eq!(parsed.model_assets[1].confidence, "low");
8988        assert!(
8989            parsed
8990                .diagnostics
8991                .iter()
8992                .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
8993        );
8994    }
8995
8996    #[test]
8997    fn pmm_asset_reference_keeps_offset_range() {
8998        let data = b"Polygon Movie maker 0002\0Label UserFile\\Model\\miku.pmx\0";
8999        let parsed = parse_pmm_manifest(data).unwrap();
9000
9001        let reference = &parsed.asset_references[0];
9002        assert_eq!(reference.offset, 25);
9003        assert_eq!(reference.offset_end, data.len() - 1);
9004        assert_eq!(&data[reference.offset_end..reference.offset_end + 1], b"\0");
9005    }
9006
9007    #[test]
9008    fn pmm_asset_reference_offset_end_allows_truncated_final_chunk() {
9009        let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx";
9010        let parsed = parse_pmm_manifest(data).unwrap();
9011
9012        assert_eq!(parsed.asset_references[0].offset_end, data.len());
9013    }
9014
9015    #[test]
9016    fn pmm_diagnostics_flags_duplicate_asset_by_filename() {
9017        let data = b"Polygon Movie maker 0002\0C:\\Models\\miku.pmx\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
9018        let parsed = parse_pmm_manifest(data).unwrap();
9019
9020        assert!(
9021            parsed
9022                .diagnostics
9023                .iter()
9024                .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
9025        );
9026    }
9027
9028    #[test]
9029    fn pmm_diagnostics_flags_missing_motion_when_models_present() {
9030        let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0";
9031        let parsed = parse_pmm_manifest(data).unwrap();
9032
9033        assert!(
9034            parsed
9035                .diagnostics
9036                .iter()
9037                .any(|diagnostic| diagnostic.code == "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN")
9038        );
9039    }
9040
9041    #[test]
9042    fn pmm_diagnostics_flags_declared_asset_count_mismatch() {
9043        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
9044        let mismatches = parsed
9045            .diagnostics
9046            .iter()
9047            .filter(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
9048            .collect::<Vec<_>>();
9049
9050        assert_eq!(mismatches.len(), 2);
9051        assert_eq!(mismatches[0].level, "info");
9052        assert_eq!(mismatches[1].level, "info");
9053        assert!(mismatches[0].message.contains("8 model slot"));
9054        assert!(mismatches[0].message.contains("1 model reference"));
9055        assert!(mismatches[1].message.contains("9 accessory slot"));
9056        assert!(mismatches[1].message.contains("1 accessory reference"));
9057    }
9058
9059    #[test]
9060    fn pmm_display_state_derives_active_and_empty_slot_indices_from_flags() {
9061        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
9062        let ds = &parsed.display_state;
9063
9064        assert_eq!(ds.layout, "pmm-v2-flags");
9065        assert_eq!(ds.model_slot_flags, vec![1, 1, 2, 0, 0, 0, 0, 0]);
9066        assert_eq!(ds.selected_model_index, Some(0));
9067        assert_eq!(ds.document_model_count, Some(3));
9068        let expand_flags = ds.document_expand_flags.as_ref().unwrap();
9069        assert!(expand_flags.editing_cla);
9070        assert!(expand_flags.camera_panel);
9071        assert!(expand_flags.light_panel);
9072        assert!(!expand_flags.accessory_panel);
9073        assert!(!expand_flags.self_shadow_panel);
9074        assert_eq!(ds.model_slot_flag_entries.len(), 8);
9075        assert_eq!(ds.model_slot_flag_entries[0].slot_index, 0);
9076        assert_eq!(ds.model_slot_flag_entries[0].flag, 1);
9077        assert!(ds.model_slot_flag_entries[0].active);
9078        assert_eq!(ds.model_slot_flag_entries[3].slot_index, 3);
9079        assert_eq!(ds.model_slot_flag_entries[3].flag, 0);
9080        assert!(!ds.model_slot_flag_entries[3].active);
9081        assert_eq!(ds.active_model_slot_indices, vec![0, 1, 2]);
9082        assert_eq!(ds.empty_model_slot_indices, vec![3, 4, 5, 6, 7]);
9083    }
9084
9085    #[test]
9086    fn pmm_display_state_flag_counts_are_grouped_and_stable() {
9087        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
9088        let counts = &parsed.display_state.model_slot_flag_counts;
9089
9090        assert_eq!(counts.get(&0), Some(&5));
9091        assert_eq!(counts.get(&1), Some(&2));
9092        assert_eq!(counts.get(&2), Some(&1));
9093        assert_eq!(counts.len(), 3);
9094
9095        let json = serde_json::to_value(counts).unwrap();
9096        let keys: Vec<&str> = json
9097            .as_object()
9098            .unwrap()
9099            .keys()
9100            .map(|k| k.as_str())
9101            .collect();
9102        assert_eq!(keys, vec!["0", "1", "2"]);
9103    }
9104
9105    #[test]
9106    fn pmm_v1_display_state_derived_arrays_are_empty_when_no_raw_flags() {
9107        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
9108        let ds = &parsed.display_state;
9109
9110        assert_eq!(ds.layout, "pmm-v1-counts");
9111        assert!(ds.model_slot_flags.is_empty());
9112        assert!(ds.model_slot_flag_entries.is_empty());
9113        assert!(ds.document_expand_flags.is_none());
9114        assert_eq!(ds.selected_model_index, None);
9115        assert_eq!(ds.document_model_count, None);
9116        assert!(ds.active_model_slot_indices.is_empty());
9117        assert!(ds.empty_model_slot_indices.is_empty());
9118        assert!(ds.model_slot_flag_counts.is_empty());
9119    }
9120
9121    #[test]
9122    fn pmm_unknown_display_state_derived_arrays_are_empty() {
9123        let mut data = pmm_with_project_settings();
9124        data[46] = 7;
9125        let parsed = parse_pmm_manifest(&data).unwrap();
9126        let ds = &parsed.display_state;
9127
9128        assert_eq!(ds.layout, "unknown");
9129        assert!(ds.active_model_slot_indices.is_empty());
9130        assert!(ds.empty_model_slot_indices.is_empty());
9131        assert!(ds.model_slot_flag_counts.is_empty());
9132        assert!(ds.model_slot_flag_entries.is_empty());
9133    }
9134
9135    #[test]
9136    fn pmm_display_state_all_active_slots() {
9137        let flags: &[u8] = &[1, 1, 1, 1, 1, 1, 1, 1];
9138        let active = active_slot_indices(flags);
9139        let empty = empty_slot_indices(flags);
9140        let counts = slot_flag_counts(flags);
9141
9142        assert_eq!(active, vec![0, 1, 2, 3, 4, 5, 6, 7]);
9143        assert!(empty.is_empty());
9144        assert_eq!(counts.get(&1), Some(&8));
9145        assert_eq!(counts.len(), 1);
9146    }
9147
9148    #[test]
9149    fn pmm_display_state_all_empty_slots() {
9150        let flags: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
9151        let active = active_slot_indices(flags);
9152        let empty = empty_slot_indices(flags);
9153        let counts = slot_flag_counts(flags);
9154
9155        assert!(active.is_empty());
9156        assert_eq!(empty, vec![0, 1, 2, 3, 4, 5, 6, 7]);
9157        assert_eq!(counts.get(&0), Some(&8));
9158        assert_eq!(counts.len(), 1);
9159    }
9160
9161    #[test]
9162    fn pmm_document_bone_state_summary_json_schema_is_stable() {
9163        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9164        let document = parsed.document_summary.unwrap();
9165        let model = &document.models[0];
9166        let keys = json_keys(&serde_json::to_value(&model.bone_state_summaries[0]).unwrap());
9167
9168        assert_eq!(
9169            keys,
9170            vec![
9171                "dirty",
9172                "orientation",
9173                "physicsDisabled",
9174                "selected",
9175                "translation"
9176            ]
9177        );
9178    }
9179
9180    #[test]
9181    fn pmm_document_morph_state_summary_json_schema_is_stable() {
9182        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9183        let document = parsed.document_summary.unwrap();
9184        let model = &document.models[0];
9185        let keys = json_keys(&serde_json::to_value(&model.morph_state_summaries[0]).unwrap());
9186
9187        assert_eq!(keys, vec!["weight"]);
9188    }
9189
9190    #[test]
9191    fn pmm_document_constraint_state_summary_json_schema_is_stable() {
9192        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9193        let document = parsed.document_summary.unwrap();
9194        let model = &document.models[0];
9195        let keys = json_keys(&serde_json::to_value(&model.constraint_state_summaries[0]).unwrap());
9196
9197        assert_eq!(keys, vec!["enabled"]);
9198    }
9199
9200    #[test]
9201    fn pmm_document_outside_parent_state_summary_json_schema_is_stable() {
9202        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9203        let document = parsed.document_summary.unwrap();
9204        let model = &document.models[0];
9205        let keys =
9206            json_keys(&serde_json::to_value(&model.outside_parent_state_summaries[0]).unwrap());
9207
9208        assert_eq!(
9209            keys,
9210            vec![
9211                "parentModelBoneIndex",
9212                "parentModelIndex",
9213                "subjectBoneIndex",
9214                "targetModelIndex"
9215            ]
9216        );
9217    }
9218
9219    #[test]
9220    fn parses_pmm_v2_document_model_state_block_summaries() {
9221        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
9222        let document = parsed.document_summary.as_ref().unwrap();
9223        let model = &document.models[0];
9224
9225        assert_eq!(model.bone_state_summaries.len(), 1);
9226        let bs = &model.bone_state_summaries[0];
9227        assert_eq!(bs.translation, [1.0, 2.0, 3.0]);
9228        assert_eq!(bs.orientation, [0.0, 0.0, 0.0, 1.0]);
9229        assert!(bs.dirty);
9230        assert!(!bs.physics_disabled);
9231        assert!(bs.selected);
9232
9233        assert_eq!(model.morph_state_summaries.len(), 1);
9234        let ms = &model.morph_state_summaries[0];
9235        assert!((ms.weight - 0.75).abs() < 1e-6);
9236
9237        assert_eq!(model.constraint_state_summaries.len(), 1);
9238        let cs = &model.constraint_state_summaries[0];
9239        assert!(cs.enabled);
9240
9241        assert_eq!(model.outside_parent_state_summaries.len(), 1);
9242        let ops = &model.outside_parent_state_summaries[0];
9243        assert_eq!(ops.parent_model_index, 0);
9244        assert_eq!(ops.parent_model_bone_index, 5);
9245        assert_eq!(ops.subject_bone_index, 7);
9246        assert_eq!(ops.target_model_index, 1);
9247    }
9248
9249    #[test]
9250    fn pmm_asset_scanner_requires_extension_and_userfile_boundaries() {
9251        let data = b"Polygon Movie maker 0002\0C:\\notuserfile\\Model\\fake.pmx\0dummy.x_v2\0model.x_v2.pmd\0";
9252        let parsed = parse_pmm_manifest(data).unwrap();
9253
9254        assert_eq!(
9255            parsed.model_paths,
9256            vec!["C:/notuserfile/Model/fake.pmx", "model.x_v2.pmd"]
9257        );
9258        assert!(parsed.accessory_paths.is_empty());
9259    }
9260
9261    #[test]
9262    fn ignores_implausible_pmm_model_slot_flags() {
9263        let mut data = pmm_with_project_settings();
9264        data[46] = 7;
9265        let parsed = parse_pmm_manifest(&data).unwrap();
9266
9267        assert!(parsed.display_state.model_slot_flags.is_empty());
9268        assert_eq!(parsed.display_state.layout, "unknown");
9269        assert_eq!(parsed.display_state.declared_model_slot_count, None);
9270        assert_eq!(parsed.display_state.model_slot_count, 0);
9271        assert_eq!(parsed.display_state.non_zero_model_slot_count, 0);
9272        assert_eq!(parsed.display_state.accessory_slot_count, None);
9273        assert_eq!(parsed.diagnostics.len(), 3);
9274        assert_eq!(parsed.diagnostics[2].code, "PMM_DISPLAY_STATE_UNPLAUSIBLE");
9275    }
9276
9277    #[test]
9278    fn parses_pmm_v2_accessory_keyframe_scalar_summaries() {
9279        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
9280        let global = parsed.document_global_summary.as_ref().unwrap();
9281        let accessory = &global.accessories.accessories[0];
9282
9283        assert_eq!(accessory.keyframe_summaries.len(), 1);
9284
9285        let initial = &accessory.initial_keyframe;
9286        assert_eq!(initial.index, None);
9287        assert_eq!(initial.frame_index, 0);
9288        assert!(initial.visible);
9289        assert_eq!(initial.opacity, 1.0);
9290        assert_eq!(initial.parent_model_index, -1);
9291        assert_eq!(initial.parent_model_bone_index, -1);
9292        assert_eq!(initial.translation, [1.0, 2.0, 3.0]);
9293        assert_eq!(initial.orientation, [0.1, 0.2, 0.3]);
9294        assert_eq!(initial.scale_factor, 10.0);
9295        assert!(initial.shadow_enabled);
9296        assert!(!initial.selected);
9297        assert_eq!(initial.payload_bytes.len(), 39);
9298        assert_eq!(initial.payload_bytes.len(), initial.payload_byte_length);
9299        assert_eq!(initial.payload_bytes[0], 1);
9300        assert_eq!(
9301            &initial.payload_bytes[initial.payload_bytes.len() - 2..],
9302            &[1, 0]
9303        );
9304
9305        // layout metadata for accessory keyframe payload (exporter-prep slice).
9306        // offsets are absolute; prove they address correct bytes inside payload_bytes for initial keyframe.
9307        assert_eq!(
9308            initial.packed_opacity_visible_offset,
9309            initial.payload_offset
9310        );
9311        assert_eq!(
9312            initial.parent_model_index_offset,
9313            initial.payload_offset + 1
9314        );
9315        assert_eq!(
9316            initial.parent_model_bone_index_offset,
9317            initial.payload_offset + 5
9318        );
9319        assert_eq!(initial.translation_offset, initial.payload_offset + 9);
9320        assert_eq!(initial.translation_byte_length, 12);
9321        assert_eq!(initial.orientation_offset, initial.payload_offset + 21);
9322        assert_eq!(initial.orientation_byte_length, 12);
9323        assert_eq!(initial.scale_factor_offset, initial.payload_offset + 33);
9324        assert_eq!(initial.shadow_enabled_offset, initial.payload_offset + 37);
9325        assert_eq!(initial.selected_offset, initial.payload_offset + 38);
9326        let p0 = &initial.payload_bytes;
9327        assert_eq!(
9328            p0[initial.packed_opacity_visible_offset - initial.payload_offset],
9329            1u8
9330        );
9331        let pmi0 = initial.parent_model_index_offset - initial.payload_offset;
9332        assert_eq!(&p0[pmi0..pmi0 + 4], &(-1i32).to_le_bytes());
9333        let pbi0 = initial.parent_model_bone_index_offset - initial.payload_offset;
9334        assert_eq!(&p0[pbi0..pbi0 + 4], &(-1i32).to_le_bytes());
9335        let t0 = initial.translation_offset - initial.payload_offset;
9336        assert_eq!(&p0[t0..t0 + 4], &1.0f32.to_le_bytes());
9337        let o0 = initial.orientation_offset - initial.payload_offset;
9338        assert_eq!(&p0[o0 + 8..o0 + 12], &0.3f32.to_le_bytes());
9339        let sf0 = initial.scale_factor_offset - initial.payload_offset;
9340        assert_eq!(&p0[sf0..sf0 + 4], &10.0f32.to_le_bytes());
9341        assert_eq!(
9342            p0[initial.shadow_enabled_offset - initial.payload_offset],
9343            1
9344        );
9345        assert_eq!(p0[initial.selected_offset - initial.payload_offset], 0);
9346
9347        let kf = &accessory.keyframe_summaries[0];
9348        assert_eq!(kf.index, Some(46));
9349        assert_eq!(kf.frame_index, 46);
9350        assert!(kf.visible);
9351        assert_eq!(kf.opacity, 1.0);
9352        assert_eq!(kf.translation, [1.0, 2.0, 3.0]);
9353        assert_eq!(kf.orientation, [0.1, 0.2, 0.3]);
9354        assert_eq!(kf.scale_factor, 10.0);
9355        assert!(kf.shadow_enabled);
9356        assert!(!kf.selected);
9357        assert_eq!(kf.payload_bytes.len(), 39);
9358        assert_eq!(kf.payload_bytes.len(), kf.payload_byte_length);
9359        assert_eq!(kf.payload_bytes[0], 1);
9360        assert_eq!(&kf.payload_bytes[kf.payload_bytes.len() - 2..], &[1, 0]);
9361
9362        // same layout assertions for indexed (non-initial) accessory keyframe; relative positions inside payload identical.
9363        assert_eq!(kf.packed_opacity_visible_offset, kf.payload_offset);
9364        assert_eq!(kf.parent_model_index_offset, kf.payload_offset + 1);
9365        assert_eq!(kf.parent_model_bone_index_offset, kf.payload_offset + 5);
9366        assert_eq!(kf.translation_offset, kf.payload_offset + 9);
9367        assert_eq!(kf.translation_byte_length, 12);
9368        assert_eq!(kf.orientation_offset, kf.payload_offset + 21);
9369        assert_eq!(kf.orientation_byte_length, 12);
9370        assert_eq!(kf.scale_factor_offset, kf.payload_offset + 33);
9371        assert_eq!(kf.shadow_enabled_offset, kf.payload_offset + 37);
9372        assert_eq!(kf.selected_offset, kf.payload_offset + 38);
9373        let p1 = &kf.payload_bytes;
9374        assert_eq!(
9375            p1[kf.packed_opacity_visible_offset - kf.payload_offset],
9376            1u8
9377        );
9378        let pmi1 = kf.parent_model_index_offset - kf.payload_offset;
9379        assert_eq!(&p1[pmi1..pmi1 + 4], &(-1i32).to_le_bytes());
9380        let pbi1 = kf.parent_model_bone_index_offset - kf.payload_offset;
9381        assert_eq!(&p1[pbi1..pbi1 + 4], &(-1i32).to_le_bytes());
9382        let t1 = kf.translation_offset - kf.payload_offset;
9383        assert_eq!(&p1[t1..t1 + 4], &1.0f32.to_le_bytes());
9384        let o1 = kf.orientation_offset - kf.payload_offset;
9385        assert_eq!(&p1[o1 + 8..o1 + 12], &0.3f32.to_le_bytes());
9386        let sf1 = kf.scale_factor_offset - kf.payload_offset;
9387        assert_eq!(&p1[sf1..sf1 + 4], &10.0f32.to_le_bytes());
9388        assert_eq!(p1[kf.shadow_enabled_offset - kf.payload_offset], 1);
9389        assert_eq!(p1[kf.selected_offset - kf.payload_offset], 0);
9390    }
9391
9392    #[test]
9393    fn parses_ik_multi_bone_pmm_fixture_document_summary_and_keyframes() {
9394        let data = include_bytes!("../fixtures/pmm/ik_multi_bone_from_pmx_vmd.pmm");
9395        let parsed = parse_pmm_manifest(data).unwrap();
9396
9397        let document = parsed
9398            .document_summary
9399            .as_ref()
9400            .expect("document summary must exist for this fixture");
9401        assert_eq!(document.model_count, 1);
9402        assert_eq!(document.counts.models, 1);
9403        assert_eq!(document.counts.bones, 3);
9404        assert_eq!(document.counts.morphs, 0);
9405        assert_eq!(document.counts.initial_bone_keyframes, 3);
9406        assert_eq!(document.counts.bone_keyframes, 2);
9407        assert_eq!(document.counts.initial_morph_keyframes, 0);
9408        assert_eq!(document.counts.morph_keyframes, 0);
9409        assert_eq!(document.counts.initial_model_keyframes, 1);
9410        assert_eq!(document.counts.model_keyframes, 0);
9411
9412        assert_eq!(document.models.len(), 1);
9413        let model = &document.models[0];
9414        assert_eq!(model.bone_count, 3);
9415        assert_eq!(model.morph_count, 0);
9416        assert_eq!(model.initial_bone_keyframes, 3);
9417        assert_eq!(model.bone_keyframes, 2);
9418        assert_eq!(model.initial_morph_keyframes, 0);
9419        assert_eq!(model.morph_keyframes, 0);
9420        assert_eq!(model.initial_model_keyframes, 1);
9421        assert_eq!(model.model_keyframes, 0);
9422
9423        // Parser fixture only: direct MMD GUI load can fail on this relative model path.
9424        // GUI smoke PMMs should be generated by the CLI, which embeds an absolute path.
9425        let model_path = &model.path;
9426        assert_eq!(
9427            model_path, "..\\pmx\\ik_multi_axis_limit.pmx",
9428            "model path must be the corrected resolvable relative path"
9429        );
9430
9431        // non-default additional bone keyframes are present inside the additional keyframe block.
9432        assert_eq!(model.bone_keyframe_summaries.len(), 2);
9433
9434        let link15 = model
9435            .bone_keyframe_summaries
9436            .iter()
9437            .find(|kf| kf.frame_index == 15)
9438            .expect("link_root frame 15 must be present as additional keyframe");
9439        assert_eq!(link15.previous_keyframe_index, 0);
9440        assert_eq!(link15.next_keyframe_index, 0);
9441        assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
9442        assert_eq!(link15.orientation, [0.0, 0.0, 0.0, 1.0]);
9443        assert_eq!(
9444            link15.interpolation,
9445            [
9446                10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160
9447            ]
9448        );
9449        assert!(!link15.physics_disabled);
9450
9451        let eff30 = model
9452            .bone_keyframe_summaries
9453            .iter()
9454            .find(|kf| kf.frame_index == 30)
9455            .expect("effector frame 30 must be present as additional keyframe");
9456        assert_eq!(eff30.previous_keyframe_index, 1);
9457        assert_eq!(eff30.next_keyframe_index, 0);
9458        assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
9459        assert_eq!(eff30.orientation, [0.0, 0.0, 0.0, 1.0]);
9460        assert!(!eff30.physics_disabled);
9461
9462        // all asserted physics_disabled flags are false
9463        for kf in &model.initial_bone_keyframe_summaries {
9464            assert!(!kf.physics_disabled);
9465        }
9466        for kf in &model.bone_keyframe_summaries {
9467            assert!(!kf.physics_disabled);
9468        }
9469
9470        // relevant section offsets are monotonic (and model offsets)
9471        let s = &model.sections;
9472        assert!(s.initial_bone_keyframes_offset < s.bone_keyframe_count_offset);
9473        assert!(s.bone_keyframe_count_offset < s.bone_keyframes_offset);
9474        assert!(s.bone_keyframes_offset < s.bone_keyframes_end_offset);
9475        assert!(s.initial_morph_keyframes_offset <= s.morph_keyframe_count_offset);
9476        assert!(s.morph_keyframes_offset <= s.morph_keyframes_end_offset);
9477        assert!(model.offset_end > model.offset);
9478        assert!(s.bone_keyframes_end_offset > s.bone_keyframes_offset);
9479    }
9480
9481    #[test]
9482    fn pmm_ik_multi_bone_fixture_has_expected_document_summary() {
9483        let data = include_bytes!("../fixtures/pmm/ik_multi_bone_from_pmx_vmd.pmm");
9484        let parsed = parse_pmm_manifest(data).unwrap();
9485
9486        assert_eq!(
9487            parsed.byte_length, 2125,
9488            "byte_length must match the checked-in PMM fixture"
9489        );
9490
9491        let document = parsed
9492            .document_summary
9493            .as_ref()
9494            .expect("document summary must exist for this fixture");
9495        assert_eq!(document.selected_model_index, 0);
9496
9497        let c = &document.counts;
9498        assert_eq!(c.models, 1);
9499        assert_eq!(c.bones, 3);
9500        assert_eq!(c.morphs, 0);
9501        assert_eq!(c.bone_keyframes, 2);
9502        assert_eq!(c.morph_keyframes, 0);
9503
9504        assert_eq!(document.models.len(), 1);
9505        let model = &document.models[0];
9506        assert_eq!(model.document_model_index, 0);
9507        assert_eq!(model.name, "ik_multi_axis_limit_fixture");
9508        assert_eq!(model.english_name, "ik_multi_axis_limit_fixture");
9509        assert_eq!(model.path, "..\\pmx\\ik_multi_axis_limit.pmx");
9510        assert_eq!(model.path_offset, 112);
9511        assert_eq!(model.bone_count, 3);
9512        assert_eq!(model.morph_count, 0);
9513        assert_eq!(model.last_frame_index, 30);
9514
9515        let s = &model.sections;
9516        assert_eq!(s.initial_bone_keyframes_offset, 449);
9517        assert_eq!(s.bone_keyframe_count_offset, 623);
9518        assert_eq!(s.bone_keyframes_offset, 627);
9519        assert_eq!(s.bone_keyframes_end_offset, 751);
9520
9521        assert_eq!(model.initial_bone_keyframe_summaries.len(), 3);
9522        for ibk in model.initial_bone_keyframe_summaries.iter() {
9523            assert_eq!(ibk.byte_length, 58);
9524            assert_eq!(ibk.payload_offset, ibk.offset + 12);
9525            assert_eq!(ibk.payload_byte_length, 46);
9526            assert_eq!(ibk.payload_bytes.len(), ibk.payload_byte_length);
9527            assert_eq!(&ibk.payload_bytes[..16], &ibk.interpolation[..]);
9528        }
9529        assert_eq!(model.initial_bone_keyframe_summaries[0].offset, 449);
9530        assert_eq!(model.initial_bone_keyframe_summaries[1].offset, 507);
9531        assert_eq!(model.initial_bone_keyframe_summaries[2].offset, 565);
9532
9533        let imkf = &model.initial_model_keyframe;
9534        assert_eq!(imkf.offset, 755);
9535        assert_eq!(imkf.byte_length, 14);
9536
9537        let link15 = model
9538            .bone_keyframe_summaries
9539            .iter()
9540            .find(|kf| kf.frame_index == 15)
9541            .expect("link_root frame 15 must be present");
9542        assert_eq!(link15.offset, 627);
9543        assert_eq!(link15.byte_length, 62);
9544        assert_eq!(link15.payload_offset, link15.offset + 16);
9545        assert_eq!(link15.payload_byte_length, 46);
9546        assert_eq!(link15.payload_bytes.len(), link15.payload_byte_length);
9547        assert_eq!(&link15.payload_bytes[..16], &link15.interpolation[..]);
9548        assert_eq!(link15.previous_keyframe_index, 0);
9549        assert_eq!(link15.next_keyframe_index, 0);
9550        assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
9551
9552        let eff30 = model
9553            .bone_keyframe_summaries
9554            .iter()
9555            .find(|kf| kf.frame_index == 30)
9556            .expect("effector frame 30 must be present");
9557        assert_eq!(eff30.offset, 689);
9558        assert_eq!(eff30.byte_length, 62);
9559        assert_eq!(eff30.payload_offset, eff30.offset + 16);
9560        assert_eq!(eff30.payload_byte_length, 46);
9561        assert_eq!(eff30.payload_bytes.len(), eff30.payload_byte_length);
9562        assert_eq!(&eff30.payload_bytes[..16], &eff30.interpolation[..]);
9563        assert_eq!(eff30.previous_keyframe_index, 1);
9564        assert_eq!(eff30.next_keyframe_index, 0);
9565        assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
9566    }
9567
9568    #[test]
9569    fn exports_pmm_scene_from_pmx_vmd_roundtrips_ik_multi_bone_nondefault() {
9570        let pmx = include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx");
9571        let model = crate::pmx::parse_pmx_model(pmx).unwrap();
9572
9573        let vmd = include_bytes!("../fixtures/vmd/ik_multi_bone_nondefault.vmd");
9574        let motion = crate::vmd::parse_vmd_animation(vmd).unwrap();
9575
9576        let report = export_pmm_scene_from_pmx_vmd(
9577            &model,
9578            &motion,
9579            // Low-level writer preserves the supplied path; CLI GUI smoke canonicalizes it.
9580            "..\\pmx\\ik_multi_axis_limit.pmx",
9581            &PmmSceneExportOptions::default(),
9582        );
9583
9584        let reparsed = parse_pmm_manifest(&report.bytes).unwrap();
9585        let document = reparsed
9586            .document_summary
9587            .as_ref()
9588            .expect("exported PMM must contain document summary");
9589        assert_eq!(document.model_count, 1);
9590        assert_eq!(document.counts.models, 1);
9591        assert_eq!(document.counts.bones, 3);
9592        assert_eq!(document.counts.morphs, 0);
9593
9594        let m = &document.models[0];
9595        assert_eq!(m.bone_count, 3);
9596        assert_eq!(m.morph_count, 0);
9597        assert_eq!(m.path, "..\\pmx\\ik_multi_axis_limit.pmx");
9598
9599        // semantic parity for the non-default additional frames from VMD
9600        let link15 = m
9601            .bone_keyframe_summaries
9602            .iter()
9603            .find(|kf| kf.frame_index == 15)
9604            .expect("exported PMM must contain link_root frame 15");
9605        assert_eq!(link15.translation, [0.05, 0.0, 0.0]);
9606        assert_eq!(link15.orientation, [0.0, 0.0, 0.0, 1.0]);
9607        assert_eq!(
9608            link15.interpolation,
9609            [
9610                10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160
9611            ]
9612        );
9613        assert!(!link15.physics_disabled);
9614
9615        let eff30 = m
9616            .bone_keyframe_summaries
9617            .iter()
9618            .find(|kf| kf.frame_index == 30)
9619            .expect("exported PMM must contain effector frame 30");
9620        assert_eq!(eff30.translation, [0.0, 0.1, 0.0]);
9621        assert_eq!(eff30.orientation, [0.0, 0.0, 0.0, 1.0]);
9622        assert!(!eff30.physics_disabled);
9623
9624        // exporter always emits per-bone initials (fallback identity when absent in VMD) + additionals
9625        assert_eq!(m.initial_bone_keyframes, 3);
9626        assert_eq!(m.bone_keyframes, 2);
9627
9628        assert_eq!(m.constraint_bone_count, 1);
9629        assert_eq!(m.outside_parent_subject_bone_count, 2);
9630        assert_eq!(m.initial_model_keyframe.constraint_states, vec![true]);
9631        for outside_parent in &m.initial_model_keyframe.outside_parent_indices {
9632            assert_eq!(outside_parent.parent_model_index, -1);
9633            assert_eq!(outside_parent.parent_model_bone_index, 0);
9634        }
9635        assert_eq!(m.constraint_state_summaries.len(), 1);
9636        assert!(m.constraint_state_summaries[0].enabled);
9637        assert_eq!(m.outside_parent_state_summaries.len(), 2);
9638        assert_eq!(m.outside_parent_state_summaries[0].parent_model_index, 0);
9639        assert_eq!(
9640            m.outside_parent_state_summaries[0].parent_model_bone_index,
9641            0
9642        );
9643        assert_eq!(m.outside_parent_state_summaries[0].subject_bone_index, -1);
9644        assert_eq!(m.outside_parent_state_summaries[0].target_model_index, 0);
9645
9646        let fixed_tracks_offset = m.path_offset + 256;
9647        assert_eq!(reparsed.source_bytes[fixed_tracks_offset], 2);
9648    }
9649
9650    #[test]
9651    fn pmm_export_model_structure_matches_pmx_bone_flags_and_display_frames() {
9652        let pmx = include_bytes!("../fixtures/pmx/ik_multi_axis_limit.pmx");
9653        let model = crate::pmx::parse_pmx_model(pmx).unwrap();
9654        let structure = pmm_export_model_structure_from_pmx(&model);
9655
9656        assert_eq!(structure.fixed_tracks, 2);
9657        assert_eq!(structure.constraint_bone_indices, vec![2]);
9658        assert_eq!(structure.outside_parent_subject_bone_indices, vec![-1, 2]);
9659        assert!(pmm_is_outside_parent_subject_bone(&model.skeleton.bones[2]));
9660        assert!(!pmm_is_outside_parent_subject_bone(
9661            &model.skeleton.bones[0]
9662        ));
9663        assert!(!pmm_is_outside_parent_subject_bone(
9664            &model.skeleton.bones[1]
9665        ));
9666    }
9667}