Skip to main content

mmd_anim_format/
pmm.rs

1use std::collections::{BTreeMap, HashMap};
2
3use encoding_rs::SHIFT_JIS;
4use serde::Serialize;
5
6use crate::error::ImportError;
7
8#[derive(Debug, Clone, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PmmParsedManifest {
11    pub signature: &'static str,
12    pub version: String,
13    pub parsed_version: Option<u32>,
14    pub byte_length: usize,
15    pub project_settings: PmmProjectSettings,
16    pub timeline: PmmTimeline,
17    pub display_state: PmmDisplayState,
18    pub header_text_entries: Vec<PmmHeaderTextEntry>,
19    pub model_slots: Vec<PmmModelSlot>,
20    pub document_summary: Option<PmmDocumentSummary>,
21    pub document_global_summary: Option<PmmDocumentGlobalSummary>,
22    pub asset_summary: PmmAssetSummary,
23    pub asset_references: Vec<PmmAssetReference>,
24    pub model_assets: Vec<PmmSceneAsset>,
25    pub accessory_assets: Vec<PmmSceneAsset>,
26    pub motion_assets: Vec<PmmSceneAsset>,
27    pub audio_assets: Vec<PmmSceneAsset>,
28    pub image_assets: Vec<PmmSceneAsset>,
29    pub video_assets: Vec<PmmSceneAsset>,
30    pub model_paths: Vec<String>,
31    pub accessory_paths: Vec<String>,
32    pub motion_paths: Vec<String>,
33    pub audio_paths: Vec<String>,
34    pub image_paths: Vec<String>,
35    pub video_paths: Vec<String>,
36    pub diagnostics: Vec<PmmParserDiagnostic>,
37}
38
39#[derive(Debug, Clone, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct PmmProjectSettings {
42    pub screen_width: Option<u32>,
43    pub screen_height: Option<u32>,
44    pub timeline_frame_count: Option<u32>,
45    pub frame_rate: Option<f32>,
46}
47
48#[derive(Debug, Clone, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct PmmTimeline {
51    pub start_frame: Option<u32>,
52    pub end_frame_exclusive: Option<u32>,
53    pub frame_count: Option<u32>,
54    pub frame_rate: Option<f32>,
55    pub duration_seconds: Option<f32>,
56}
57
58#[derive(Debug, Clone, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PmmDisplayState {
61    pub layout: &'static str,
62    pub model_slot_flags: Vec<u8>,
63    pub model_slot_flag_entries: Vec<PmmModelSlotFlagEntry>,
64    pub document_expand_flags: Option<PmmDocumentExpandFlags>,
65    pub selected_model_index: Option<u8>,
66    pub document_model_count: Option<u8>,
67    pub declared_model_slot_count: Option<u8>,
68    pub model_slot_count: usize,
69    pub non_zero_model_slot_count: usize,
70    pub accessory_slot_count: Option<u8>,
71    pub active_model_slot_indices: Vec<usize>,
72    pub empty_model_slot_indices: Vec<usize>,
73    pub model_slot_flag_counts: BTreeMap<u8, usize>,
74}
75
76#[derive(Debug, Clone, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct PmmDocumentExpandFlags {
79    pub editing_cla: bool,
80    pub camera_panel: bool,
81    pub light_panel: bool,
82    pub accessory_panel: bool,
83    pub bone_panel: bool,
84    pub morph_panel: bool,
85    pub self_shadow_panel: bool,
86}
87
88#[derive(Debug, Clone, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct PmmModelSlotFlagEntry {
91    pub slot_index: usize,
92    pub flag: u8,
93    pub active: bool,
94}
95
96#[derive(Debug, Clone, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct PmmHeaderTextEntry {
99    pub index: usize,
100    pub offset: usize,
101    pub offset_end: usize,
102    pub text: String,
103    pub text_bytes: Vec<u8>,
104}
105
106#[derive(Debug, Clone, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct PmmModelSlot {
109    pub slot_index: usize,
110    pub display_slot_index: Option<usize>,
111    pub offset: usize,
112    pub offset_end: usize,
113    pub model_path_offset: usize,
114    pub trailing_zero_padding_bytes: usize,
115    pub next_non_zero_offset: Option<usize>,
116    pub name: String,
117    pub name_bytes: Vec<u8>,
118    pub english_name: String,
119    pub english_name_bytes: Vec<u8>,
120    pub model_path: String,
121    pub normalized_path: String,
122    pub asset_reference_index: Option<usize>,
123    pub confidence: &'static str,
124}
125
126#[derive(Debug, Clone, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct PmmAssetSummary {
129    pub reference_count: usize,
130    pub high_confidence_count: usize,
131    pub medium_confidence_count: usize,
132    pub low_confidence_count: usize,
133    pub kind_counts: BTreeMap<String, usize>,
134    pub extension_counts: BTreeMap<String, usize>,
135    pub confidence_counts: BTreeMap<String, usize>,
136}
137
138#[derive(Debug, Clone, Serialize)]
139#[serde(rename_all = "camelCase")]
140pub struct PmmDocumentSummary {
141    pub source: &'static str,
142    pub selected_model_index: u8,
143    pub model_count: usize,
144    pub counts: PmmDocumentCounts,
145    pub models: Vec<PmmDocumentModelSummary>,
146}
147
148#[derive(Debug, Clone, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct PmmDocumentCounts {
151    pub models: usize,
152    pub bones: usize,
153    pub morphs: usize,
154    pub initial_bone_keyframes: usize,
155    pub bone_keyframes: usize,
156    pub initial_morph_keyframes: usize,
157    pub morph_keyframes: usize,
158    pub initial_model_keyframes: usize,
159    pub model_keyframes: usize,
160}
161
162#[derive(Debug, Clone, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct PmmDocumentGlobalSummary {
165    pub source: &'static str,
166    pub offset: usize,
167    pub offset_end: usize,
168    pub camera: PmmDocumentTrackSummary,
169    pub light: PmmDocumentTrackSummary,
170    pub accessories: PmmDocumentAccessoryBlockSummary,
171    pub settings: PmmDocumentSettingsSummary,
172    pub gravity: PmmDocumentTrackSummary,
173    pub self_shadow: PmmDocumentTrackSummary,
174}
175
176#[derive(Debug, Clone, Serialize)]
177#[serde(rename_all = "camelCase")]
178pub struct PmmDocumentTrackSummary {
179    pub offset: usize,
180    pub offset_end: usize,
181    pub initial_keyframes: usize,
182    pub keyframes: usize,
183    pub initial_keyframe: Option<PmmDocumentKeyframeSummary>,
184    pub keyframe_summaries: Vec<PmmDocumentKeyframeSummary>,
185    pub keyframe_count_offset: usize,
186    pub keyframes_offset: usize,
187    pub keyframes_end_offset: usize,
188    pub state_offset: Option<usize>,
189    pub state_end_offset: Option<usize>,
190}
191
192#[derive(Debug, Clone, Serialize)]
193#[serde(
194    rename_all = "camelCase",
195    rename_all_fields = "camelCase",
196    tag = "kind"
197)]
198pub enum PmmDocumentKeyframeSummary {
199    Camera {
200        index: Option<i32>,
201        frame_index: i32,
202        previous_keyframe_index: i32,
203        next_keyframe_index: i32,
204        distance: f32,
205        look_at: [f32; 3],
206        angle: [f32; 3],
207        parent_model_index: i32,
208        parent_model_bone_index: i32,
209        perspective_view: bool,
210        fov: i32,
211        selected: bool,
212    },
213    Light {
214        index: Option<i32>,
215        frame_index: i32,
216        previous_keyframe_index: i32,
217        next_keyframe_index: i32,
218        color: [f32; 3],
219        direction: [f32; 3],
220        selected: bool,
221    },
222    Gravity {
223        index: Option<i32>,
224        frame_index: i32,
225        previous_keyframe_index: i32,
226        next_keyframe_index: i32,
227        noise_enabled: bool,
228        noise: i32,
229        acceleration: f32,
230        direction: [f32; 3],
231        selected: bool,
232    },
233    SelfShadow {
234        index: Option<i32>,
235        frame_index: i32,
236        previous_keyframe_index: i32,
237        next_keyframe_index: i32,
238        mode: u8,
239        distance: f32,
240        selected: bool,
241    },
242}
243
244#[derive(Debug, Clone, Copy)]
245struct PmmDocumentBaseKeyframeSummary {
246    index: Option<i32>,
247    frame_index: i32,
248    previous_keyframe_index: i32,
249    next_keyframe_index: i32,
250}
251
252#[derive(Debug, Clone, Serialize)]
253#[serde(rename_all = "camelCase")]
254pub struct PmmDocumentAccessoryKeyframeSummary {
255    pub index: Option<i32>,
256    pub frame_index: i32,
257    pub previous_keyframe_index: i32,
258    pub next_keyframe_index: i32,
259    pub visible: bool,
260    pub opacity: f32,
261    pub parent_model_index: i32,
262    pub parent_model_bone_index: i32,
263    pub translation: [f32; 3],
264    pub orientation: [f32; 3],
265    pub scale_factor: f32,
266    pub shadow_enabled: bool,
267    pub selected: bool,
268}
269
270#[derive(Debug, Clone, Serialize)]
271#[serde(rename_all = "camelCase")]
272pub struct PmmDocumentBoneKeyframeSummary {
273    pub index: Option<i32>,
274    pub frame_index: i32,
275    pub previous_keyframe_index: i32,
276    pub next_keyframe_index: i32,
277    pub interpolation: [u8; 16],
278    pub translation: [f32; 3],
279    pub orientation: [f32; 4],
280    pub physics_disabled: bool,
281    pub selected: bool,
282}
283
284#[derive(Debug, Clone, Serialize)]
285#[serde(rename_all = "camelCase")]
286pub struct PmmDocumentMorphKeyframeSummary {
287    pub index: Option<i32>,
288    pub frame_index: i32,
289    pub previous_keyframe_index: i32,
290    pub next_keyframe_index: i32,
291    pub weight: f32,
292    pub selected: bool,
293}
294
295#[derive(Debug, Clone, Serialize)]
296#[serde(rename_all = "camelCase")]
297pub struct PmmDocumentModelKeyframeSummary {
298    pub index: Option<i32>,
299    pub frame_index: i32,
300    pub previous_keyframe_index: i32,
301    pub next_keyframe_index: i32,
302    pub visible: bool,
303    pub constraint_states: Vec<bool>,
304    pub outside_parent_indices: Vec<PmmDocumentOutsideParentIndexSummary>,
305    pub self_shadow_enabled: bool,
306}
307
308#[derive(Debug, Clone, Serialize)]
309#[serde(rename_all = "camelCase")]
310pub struct PmmDocumentOutsideParentIndexSummary {
311    pub parent_model_index: i32,
312    pub parent_model_bone_index: i32,
313}
314
315#[derive(Debug, Clone, Serialize)]
316#[serde(rename_all = "camelCase")]
317pub struct PmmDocumentBoneStateSummary {
318    pub translation: [f32; 3],
319    pub orientation: [f32; 4],
320    pub dirty: bool,
321    pub physics_disabled: bool,
322    pub selected: bool,
323}
324
325#[derive(Debug, Clone, Serialize)]
326#[serde(rename_all = "camelCase")]
327pub struct PmmDocumentMorphStateSummary {
328    pub weight: f32,
329}
330
331#[derive(Debug, Clone, Serialize)]
332#[serde(rename_all = "camelCase")]
333pub struct PmmDocumentConstraintStateSummary {
334    pub enabled: bool,
335}
336
337/// Raw 16-byte outside-parent state block. Semantics not fully confirmed;
338/// field names reflect observed byte layout only.
339#[derive(Debug, Clone, Serialize)]
340#[serde(rename_all = "camelCase")]
341pub struct PmmDocumentOutsideParentStateSummary {
342    pub parent_model_index: i32,
343    pub parent_model_bone_index: i32,
344    pub subject_bone_index: i32,
345    pub target_model_index: i32,
346}
347
348#[derive(Debug, Clone, Serialize)]
349#[serde(rename_all = "camelCase")]
350pub struct PmmDocumentAccessoryBlockSummary {
351    pub offset: usize,
352    pub offset_end: usize,
353    pub selected_accessory_index: u8,
354    pub horizontal_scroll: i32,
355    pub accessory_count: usize,
356    pub keyframes: usize,
357    pub accessories: Vec<PmmDocumentAccessorySummary>,
358}
359
360#[derive(Debug, Clone, Serialize)]
361#[serde(rename_all = "camelCase")]
362pub struct PmmDocumentAccessorySummary {
363    pub slot_index: usize,
364    pub document_accessory_index: u8,
365    pub offset: usize,
366    pub offset_end: usize,
367    pub path_offset: usize,
368    pub name: String,
369    pub path: String,
370    pub asset_reference_index: Option<usize>,
371    pub draw_order_index: u8,
372    pub keyframes: usize,
373    pub initial_keyframe: PmmDocumentAccessoryKeyframeSummary,
374    pub keyframe_summaries: Vec<PmmDocumentAccessoryKeyframeSummary>,
375    pub keyframe_count_offset: usize,
376    pub keyframes_offset: usize,
377    pub keyframes_end_offset: usize,
378    pub state_offset: usize,
379    pub state_end_offset: usize,
380    pub visible: bool,
381    pub opacity: f32,
382    pub parent_model_index: i32,
383    pub parent_model_bone_index: i32,
384    pub scale_factor: f32,
385    pub shadow_enabled: bool,
386    pub add_blend_enabled: bool,
387}
388
389#[derive(Debug, Clone, Serialize)]
390#[serde(rename_all = "camelCase")]
391pub struct PmmDocumentSettingsSummary {
392    pub offset: usize,
393    pub offset_end: usize,
394    pub current_frame_index: i32,
395    pub horizontal_scroll: i32,
396    pub horizontal_scroll_thumb: i32,
397    pub editing_mode: i32,
398    pub camera_look_mode: u8,
399    pub loop_enabled: bool,
400    pub begin_frame_index_enabled: bool,
401    pub end_frame_index_enabled: bool,
402    pub begin_frame_index: i32,
403    pub end_frame_index: i32,
404    pub audio_enabled: bool,
405    pub audio_path: String,
406    pub background_video_offset: [i32; 2],
407    pub background_video_scale_factor: f32,
408    pub background_video_path: String,
409    pub background_video_enabled: bool,
410    pub background_image_offset: [i32; 2],
411    pub background_image_scale_factor: f32,
412    pub background_image_path: String,
413    pub background_image_enabled: bool,
414    pub information_shown: bool,
415    pub grid_and_axis_shown: bool,
416    pub ground_shadow_shown: bool,
417    pub preferred_fps: f32,
418    pub screen_capture_mode: i32,
419    pub accessory_index_after_models: i32,
420    pub ground_shadow_brightness: f32,
421    pub translucent_ground_shadow_enabled: bool,
422    pub physics_simulation_mode: u8,
423    pub edge_color: [f32; 3],
424    pub black_background_enabled: bool,
425    pub camera_look_at_model_index: i32,
426    pub camera_look_at_model_bone_index: i32,
427    pub unknown_matrix_offset: usize,
428    pub unknown_matrix_end_offset: usize,
429    pub following_look_at_enabled: bool,
430    pub physics_ground_enabled: bool,
431    pub current_frame_index_in_text_field: i32,
432    pub model_selection_footer_present: bool,
433    pub model_selection_footer_offset: Option<usize>,
434    pub model_selection_footer_end_offset: Option<usize>,
435}
436
437#[derive(Debug, Clone, Serialize)]
438#[serde(rename_all = "camelCase")]
439pub struct PmmDocumentModelSummary {
440    pub slot_index: usize,
441    pub document_model_index: u8,
442    pub offset: usize,
443    pub offset_end: usize,
444    pub path_offset: usize,
445    pub name: String,
446    pub english_name: String,
447    pub path: String,
448    pub asset_reference_index: Option<usize>,
449    pub bone_count: usize,
450    pub morph_count: usize,
451    pub constraint_bone_count: usize,
452    pub outside_parent_subject_bone_count: usize,
453    pub draw_order_index: u8,
454    pub transform_order_index: u8,
455    pub selected_bone_index: i32,
456    pub selected_morph_indices: [i32; 4],
457    pub vertical_scroll: i32,
458    pub sections: PmmDocumentModelSections,
459    pub initial_bone_keyframes: usize,
460    pub initial_bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
461    pub bone_keyframes: usize,
462    pub bone_keyframe_summaries: Vec<PmmDocumentBoneKeyframeSummary>,
463    pub initial_morph_keyframes: usize,
464    pub initial_morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
465    pub morph_keyframes: usize,
466    pub morph_keyframe_summaries: Vec<PmmDocumentMorphKeyframeSummary>,
467    pub initial_model_keyframes: usize,
468    pub model_keyframes: usize,
469    pub initial_model_keyframe: PmmDocumentModelKeyframeSummary,
470    pub model_keyframe_summaries: Vec<PmmDocumentModelKeyframeSummary>,
471    pub last_frame_index: i32,
472    pub visible: bool,
473    pub blend_enabled: bool,
474    pub edge_width: f32,
475    pub self_shadow_enabled: bool,
476    pub bone_state_summaries: Vec<PmmDocumentBoneStateSummary>,
477    pub morph_state_summaries: Vec<PmmDocumentMorphStateSummary>,
478    pub constraint_state_summaries: Vec<PmmDocumentConstraintStateSummary>,
479    pub outside_parent_state_summaries: Vec<PmmDocumentOutsideParentStateSummary>,
480}
481
482#[derive(Debug, Clone, Serialize)]
483#[serde(rename_all = "camelCase")]
484pub struct PmmDocumentModelSections {
485    pub initial_bone_keyframes_offset: usize,
486    pub bone_keyframe_count_offset: usize,
487    pub bone_keyframes_offset: usize,
488    pub bone_keyframes_end_offset: usize,
489    pub initial_morph_keyframes_offset: usize,
490    pub morph_keyframe_count_offset: usize,
491    pub morph_keyframes_offset: usize,
492    pub morph_keyframes_end_offset: usize,
493    pub initial_model_keyframe_offset: usize,
494    pub model_keyframe_count_offset: usize,
495    pub model_keyframes_offset: usize,
496    pub model_keyframes_end_offset: usize,
497    pub bone_states_offset: usize,
498    pub morph_states_offset: usize,
499    pub constraint_states_offset: usize,
500    pub outside_parent_states_offset: usize,
501}
502
503#[derive(Debug, Clone, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub struct PmmAssetReference {
506    pub path: String,
507    pub normalized_path: String,
508    pub file_name: String,
509    pub extension: String,
510    pub kind: &'static str,
511    pub offset: usize,
512    pub offset_end: usize,
513    pub confidence: &'static str,
514}
515
516#[derive(Debug, Clone, Serialize)]
517#[serde(rename_all = "camelCase")]
518pub struct PmmSceneAsset {
519    pub reference_index: usize,
520    pub kind_index: usize,
521    pub path: String,
522    pub normalized_path: String,
523    pub file_name: String,
524    pub extension: String,
525    pub offset: usize,
526    pub offset_end: usize,
527    pub confidence: &'static str,
528}
529
530#[derive(Debug, Clone, Serialize)]
531#[serde(rename_all = "camelCase")]
532pub struct PmmParserDiagnostic {
533    pub level: &'static str,
534    pub code: &'static str,
535    pub message: String,
536}
537
538pub fn parse_pmm_manifest(data: &[u8]) -> Result<PmmParsedManifest, ImportError> {
539    const PREFIX: &[u8] = b"Polygon Movie maker ";
540    if !data.starts_with(PREFIX) {
541        return Err(ImportError::InvalidMagic { format: "PMM" });
542    }
543    let version_bytes = &data[PREFIX.len()..data.len().min(32)];
544    let version_end = version_bytes
545        .iter()
546        .position(|&byte| byte == 0)
547        .unwrap_or(version_bytes.len());
548    let version = String::from_utf8_lossy(&version_bytes[..version_end])
549        .trim()
550        .to_owned();
551    let parsed_version = version.parse::<u32>().ok();
552    let project_settings = parse_project_settings(data);
553    let display_state = parse_display_state(data, parsed_version);
554    let asset_references = extract_asset_references(data);
555    let model_slot_scan = parse_model_slots_from_header(data, &asset_references, &display_state);
556    let document_summary = parse_document_summary(data, parsed_version, &asset_references);
557    let document_global_summary =
558        parse_document_global_summary(data, parsed_version, &asset_references);
559    let header_text_entries =
560        parse_header_text_entries(data, parsed_version, asset_references.first());
561    let model_assets = scene_assets_by_kind(&asset_references, "model");
562    let accessory_assets = scene_assets_by_kind(&asset_references, "accessory");
563    let motion_assets = scene_assets_by_kind(&asset_references, "motion");
564    let audio_assets = scene_assets_by_kind(&asset_references, "audio");
565    let image_assets = scene_assets_by_kind(&asset_references, "image");
566    let video_assets = scene_assets_by_kind(&asset_references, "video");
567    let diagnostics = pmm_diagnostics(
568        &asset_references,
569        &model_slot_scan.slots,
570        document_summary.as_ref(),
571        document_global_summary.as_ref(),
572        &display_state,
573        model_slot_scan.stop.as_ref(),
574        data,
575    );
576    Ok(PmmParsedManifest {
577        signature: "Polygon Movie maker",
578        version,
579        parsed_version,
580        byte_length: data.len(),
581        timeline: timeline_from_project_settings(&project_settings),
582        project_settings,
583        display_state,
584        header_text_entries,
585        model_slots: model_slot_scan.slots,
586        document_summary,
587        document_global_summary,
588        asset_summary: asset_summary(&asset_references),
589        model_assets,
590        accessory_assets,
591        motion_assets,
592        audio_assets,
593        image_assets,
594        video_assets,
595        model_paths: paths_by_kind(&asset_references, "model"),
596        accessory_paths: paths_by_kind(&asset_references, "accessory"),
597        motion_paths: paths_by_kind(&asset_references, "motion"),
598        audio_paths: paths_by_kind(&asset_references, "audio"),
599        image_paths: paths_by_kind(&asset_references, "image"),
600        video_paths: paths_by_kind(&asset_references, "video"),
601        diagnostics,
602        asset_references,
603    })
604}
605
606fn timeline_from_project_settings(settings: &PmmProjectSettings) -> PmmTimeline {
607    let duration_seconds = settings
608        .timeline_frame_count
609        .zip(settings.frame_rate)
610        .and_then(|(frame_count, frame_rate)| {
611            if frame_rate > 0.0 {
612                Some(frame_count as f32 / frame_rate)
613            } else {
614                None
615            }
616        });
617    PmmTimeline {
618        start_frame: settings.timeline_frame_count.map(|_| 0),
619        end_frame_exclusive: settings.timeline_frame_count,
620        frame_count: settings.timeline_frame_count,
621        frame_rate: settings.frame_rate,
622        duration_seconds,
623    }
624}
625
626fn parse_project_settings(data: &[u8]) -> PmmProjectSettings {
627    let screen_width = read_u32_at(data, 30).filter(|value| (1..=16_384).contains(value));
628    let screen_height = read_u32_at(data, 34).filter(|value| (1..=16_384).contains(value));
629    let timeline_frame_count = read_u32_at(data, 38).filter(|value| *value <= 10_000_000);
630    let frame_rate =
631        read_f32_at(data, 42).filter(|value| value.is_finite() && *value > 0.0 && *value <= 1000.0);
632
633    PmmProjectSettings {
634        screen_width,
635        screen_height,
636        timeline_frame_count,
637        frame_rate,
638    }
639}
640
641fn active_slot_indices(flags: &[u8]) -> Vec<usize> {
642    flags
643        .iter()
644        .enumerate()
645        .filter(|&(_, f)| *f != 0)
646        .map(|(i, _)| i)
647        .collect()
648}
649
650fn empty_slot_indices(flags: &[u8]) -> Vec<usize> {
651    flags
652        .iter()
653        .enumerate()
654        .filter(|&(_, f)| *f == 0)
655        .map(|(i, _)| i)
656        .collect()
657}
658
659fn slot_flag_counts(flags: &[u8]) -> BTreeMap<u8, usize> {
660    let mut counts: BTreeMap<u8, usize> = BTreeMap::new();
661    for &flag in flags {
662        *counts.entry(flag).or_insert(0) += 1;
663    }
664    counts
665}
666
667fn slot_flag_entries(flags: &[u8]) -> Vec<PmmModelSlotFlagEntry> {
668    flags
669        .iter()
670        .enumerate()
671        .map(|(slot_index, flag)| PmmModelSlotFlagEntry {
672            slot_index,
673            flag: *flag,
674            active: *flag != 0,
675        })
676        .collect()
677}
678
679fn document_expand_flags(flags: &[u8]) -> Option<PmmDocumentExpandFlags> {
680    Some(PmmDocumentExpandFlags {
681        editing_cla: *flags.first()? != 0,
682        camera_panel: *flags.get(1)? != 0,
683        light_panel: *flags.get(2)? != 0,
684        accessory_panel: *flags.get(3)? != 0,
685        bone_panel: *flags.get(4)? != 0,
686        morph_panel: *flags.get(5)? != 0,
687        self_shadow_panel: *flags.get(6)? != 0,
688    })
689}
690
691fn parse_display_state(data: &[u8], parsed_version: Option<u32>) -> PmmDisplayState {
692    if parsed_version == Some(1) {
693        if let (Some(model_count), Some(accessory_count)) =
694            (data.get(52).copied(), data.get(53).copied())
695            && model_count <= 64
696            && accessory_count <= 64
697        {
698            return PmmDisplayState {
699                layout: "pmm-v1-counts",
700                model_slot_flags: Vec::new(),
701                model_slot_flag_entries: Vec::new(),
702                document_expand_flags: None,
703                selected_model_index: None,
704                document_model_count: None,
705                declared_model_slot_count: Some(model_count),
706                model_slot_count: model_count as usize,
707                non_zero_model_slot_count: model_count as usize,
708                accessory_slot_count: Some(accessory_count),
709                active_model_slot_indices: Vec::new(),
710                empty_model_slot_indices: Vec::new(),
711                model_slot_flag_counts: BTreeMap::new(),
712            };
713        }
714        return unknown_display_state();
715    }
716
717    let model_slot_flags = data
718        .get(46..54)
719        .filter(|bytes| bytes.iter().all(|byte| *byte <= 2))
720        .map(|bytes| bytes.to_vec());
721    let accessory_slot_count = None;
722    let document_expand_flags = model_slot_flags
723        .as_ref()
724        .and_then(|flags| document_expand_flags(flags));
725    let selected_model_index = model_slot_flags
726        .as_ref()
727        .and_then(|_| data.get(53).copied());
728    let document_model_count = model_slot_flags
729        .as_ref()
730        .and_then(|_| data.get(54).copied());
731
732    let model_slot_flags = model_slot_flags.unwrap_or_default();
733    let model_slot_count = model_slot_flags.len();
734    let non_zero_model_slot_count = model_slot_flags.iter().filter(|flag| **flag != 0).count();
735    let active_model_slot_indices = active_slot_indices(&model_slot_flags);
736    let empty_model_slot_indices = empty_slot_indices(&model_slot_flags);
737    let model_slot_flag_counts = slot_flag_counts(&model_slot_flags);
738    let model_slot_flag_entries = slot_flag_entries(&model_slot_flags);
739    PmmDisplayState {
740        layout: if model_slot_flags.is_empty() {
741            "unknown"
742        } else {
743            "pmm-v2-flags"
744        },
745        declared_model_slot_count: if model_slot_flags.is_empty() {
746            None
747        } else {
748            Some(model_slot_count as u8)
749        },
750        model_slot_count,
751        non_zero_model_slot_count,
752        model_slot_flags,
753        model_slot_flag_entries,
754        document_expand_flags,
755        selected_model_index,
756        document_model_count,
757        accessory_slot_count,
758        active_model_slot_indices,
759        empty_model_slot_indices,
760        model_slot_flag_counts,
761    }
762}
763
764fn unknown_display_state() -> PmmDisplayState {
765    PmmDisplayState {
766        layout: "unknown",
767        model_slot_flags: Vec::new(),
768        model_slot_flag_entries: Vec::new(),
769        document_expand_flags: None,
770        selected_model_index: None,
771        document_model_count: None,
772        declared_model_slot_count: None,
773        model_slot_count: 0,
774        non_zero_model_slot_count: 0,
775        accessory_slot_count: None,
776        active_model_slot_indices: Vec::new(),
777        empty_model_slot_indices: Vec::new(),
778        model_slot_flag_counts: BTreeMap::new(),
779    }
780}
781
782fn parse_header_text_entries(
783    data: &[u8],
784    parsed_version: Option<u32>,
785    first_reference: Option<&PmmAssetReference>,
786) -> Vec<PmmHeaderTextEntry> {
787    if parsed_version != Some(1) {
788        return Vec::new();
789    }
790    let start = 54usize;
791    let Some(first_reference) = first_reference else {
792        return Vec::new();
793    };
794    let end = first_reference.offset.min(data.len());
795    if end <= start {
796        return Vec::new();
797    }
798
799    let mut entries = Vec::new();
800    let mut chunk_start = start;
801    for index in start..end {
802        if data[index] != 0 {
803            continue;
804        }
805        if index > chunk_start {
806            let bytes = &data[chunk_start..index];
807            let (decoded, _, _) = SHIFT_JIS.decode(bytes);
808            let text = decoded.trim().to_owned();
809            if !text.is_empty() {
810                entries.push(PmmHeaderTextEntry {
811                    index: entries.len(),
812                    offset: chunk_start,
813                    offset_end: index,
814                    text,
815                    text_bytes: bytes.to_vec(),
816                });
817            }
818        }
819        chunk_start = index + 1;
820    }
821    entries
822}
823
824fn read_u32_at(data: &[u8], offset: usize) -> Option<u32> {
825    data.get(offset..offset + 4)
826        .and_then(|bytes| bytes.try_into().ok())
827        .map(u32::from_le_bytes)
828}
829
830fn read_f32_at(data: &[u8], offset: usize) -> Option<f32> {
831    data.get(offset..offset + 4)
832        .and_then(|bytes| bytes.try_into().ok())
833        .map(f32::from_le_bytes)
834}
835
836struct PmmDocumentCursor<'a> {
837    data: &'a [u8],
838    offset: usize,
839}
840
841impl<'a> PmmDocumentCursor<'a> {
842    fn new(data: &'a [u8]) -> Self {
843        Self { data, offset: 0 }
844    }
845
846    fn read_u8(&mut self) -> Option<u8> {
847        let value = *self.data.get(self.offset)?;
848        self.offset += 1;
849        Some(value)
850    }
851
852    fn read_bool(&mut self) -> Option<bool> {
853        Some(self.read_u8()? != 0)
854    }
855
856    fn read_i32(&mut self) -> Option<i32> {
857        let bytes: [u8; 4] = self
858            .data
859            .get(self.offset..self.offset + 4)?
860            .try_into()
861            .ok()?;
862        self.offset += 4;
863        Some(i32::from_le_bytes(bytes))
864    }
865
866    fn read_f32(&mut self) -> Option<f32> {
867        let bytes: [u8; 4] = self
868            .data
869            .get(self.offset..self.offset + 4)?
870            .try_into()
871            .ok()?;
872        self.offset += 4;
873        Some(f32::from_le_bytes(bytes))
874    }
875
876    fn read_f32x3(&mut self) -> Option<[f32; 3]> {
877        Some([self.read_f32()?, self.read_f32()?, self.read_f32()?])
878    }
879
880    fn read_f32x4(&mut self) -> Option<[f32; 4]> {
881        Some([
882            self.read_f32()?,
883            self.read_f32()?,
884            self.read_f32()?,
885            self.read_f32()?,
886        ])
887    }
888
889    fn read_bytes16(&mut self) -> Option<[u8; 16]> {
890        let bytes: [u8; 16] = self
891            .data
892            .get(self.offset..self.offset + 16)?
893            .try_into()
894            .ok()?;
895        self.offset += 16;
896        Some(bytes)
897    }
898
899    fn read_variable_string(&mut self) -> Option<String> {
900        let length = self.read_u8()? as usize;
901        let bytes = self.data.get(self.offset..self.offset + length)?;
902        self.offset += length;
903        Some(decode_shift_jis(bytes))
904    }
905
906    fn read_fixed_string(&mut self, length: usize) -> Option<String> {
907        let bytes = self.data.get(self.offset..self.offset + length)?;
908        self.offset += length;
909        let end = bytes
910            .iter()
911            .position(|byte| *byte == 0)
912            .unwrap_or(bytes.len());
913        Some(decode_shift_jis(&bytes[..end]))
914    }
915
916    fn skip(&mut self, length: usize) -> Option<()> {
917        self.data.get(self.offset..self.offset + length)?;
918        self.offset += length;
919        Some(())
920    }
921}
922
923fn parse_document_summary(
924    data: &[u8],
925    parsed_version: Option<u32>,
926    references: &[PmmAssetReference],
927) -> Option<PmmDocumentSummary> {
928    if parsed_version != Some(2) {
929        return None;
930    }
931    let mut cursor = PmmDocumentCursor::new(data);
932    cursor.skip(30)?;
933    cursor.read_i32()?;
934    cursor.read_i32()?;
935    cursor.read_i32()?;
936    cursor.read_f32()?;
937    cursor.skip(7)?;
938    let selected_model_index = cursor.read_u8()?;
939    let model_count = cursor.read_u8()? as usize;
940    if model_count == 0 || selected_model_index as usize >= model_count {
941        return None;
942    }
943
944    let mut models = Vec::with_capacity(model_count);
945    for slot_index in 0..model_count {
946        models.push(read_document_model_summary(
947            &mut cursor,
948            slot_index,
949            references,
950        )?);
951    }
952    let counts = summarize_document_counts(&models);
953
954    Some(PmmDocumentSummary {
955        source: "nanoem/ext/document.c PMMv2 layout",
956        selected_model_index,
957        model_count,
958        counts,
959        models,
960    })
961}
962
963fn parse_document_global_summary(
964    data: &[u8],
965    parsed_version: Option<u32>,
966    references: &[PmmAssetReference],
967) -> Option<PmmDocumentGlobalSummary> {
968    if parsed_version != Some(2) {
969        return None;
970    }
971    let mut cursor = PmmDocumentCursor::new(data);
972    cursor.skip(30)?;
973    cursor.read_i32()?;
974    cursor.read_i32()?;
975    cursor.read_i32()?;
976    cursor.read_f32()?;
977    cursor.skip(7)?;
978    let selected_model_index = cursor.read_u8()?;
979    let model_count = cursor.read_u8()? as usize;
980    if model_count == 0 || selected_model_index as usize >= model_count {
981        return None;
982    }
983    for slot_index in 0..model_count {
984        read_document_model_summary(&mut cursor, slot_index, references)?;
985    }
986
987    let offset = cursor.offset;
988    let camera = read_document_camera_summary(&mut cursor)?;
989    let light = read_document_light_summary(&mut cursor)?;
990    let accessories = read_document_accessory_block_summary(&mut cursor, references)?;
991    let settings_before_gravity = read_document_settings_before_gravity(&mut cursor)?;
992    let gravity = read_document_gravity_summary(&mut cursor)?;
993    let self_shadow = read_document_self_shadow_summary(&mut cursor)?;
994    let settings =
995        finish_document_settings_summary(settings_before_gravity, &mut cursor, model_count)?;
996    let offset_end = cursor.offset;
997
998    Some(PmmDocumentGlobalSummary {
999        source: "nanoem/ext/document.c PMMv2 global layout",
1000        offset,
1001        offset_end,
1002        camera,
1003        light,
1004        accessories,
1005        settings,
1006        gravity,
1007        self_shadow,
1008    })
1009}
1010
1011fn read_document_model_summary(
1012    cursor: &mut PmmDocumentCursor<'_>,
1013    slot_index: usize,
1014    references: &[PmmAssetReference],
1015) -> Option<PmmDocumentModelSummary> {
1016    const PMM_PATH_BYTE_LENGTH: usize = 256;
1017    let offset = cursor.offset;
1018    let document_model_index = cursor.read_u8()?;
1019    let name = cursor.read_variable_string()?;
1020    let english_name = cursor.read_variable_string()?;
1021    let path_offset = cursor.offset;
1022    let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
1023    let asset_reference_index = asset_reference_index_for_path(references, "model", &path);
1024    cursor.read_u8()?;
1025    let bone_count = usize_from_i32(cursor.read_i32()?)?;
1026    for _ in 0..bone_count {
1027        cursor.read_variable_string()?;
1028    }
1029    let morph_count = usize_from_i32(cursor.read_i32()?)?;
1030    for _ in 0..morph_count {
1031        cursor.read_variable_string()?;
1032    }
1033    let constraint_bone_count = usize_from_i32(cursor.read_i32()?)?;
1034    cursor.skip(constraint_bone_count.checked_mul(4)?)?;
1035    let outside_parent_subject_bone_count = usize_from_i32(cursor.read_i32()?)?;
1036    cursor.skip(outside_parent_subject_bone_count.checked_mul(4)?)?;
1037    let draw_order_index = cursor.read_u8()?;
1038    let visible = cursor.read_bool()?;
1039    let selected_bone_index = cursor.read_i32()?;
1040    let selected_morph_indices = [
1041        cursor.read_i32()?,
1042        cursor.read_i32()?,
1043        cursor.read_i32()?,
1044        cursor.read_i32()?,
1045    ];
1046    let expansion_state_count = cursor.read_u8()? as usize;
1047    cursor.skip(expansion_state_count)?;
1048    let vertical_scroll = cursor.read_i32()?;
1049    let last_frame_index = cursor.read_i32()?;
1050
1051    let initial_bone_keyframes_offset = cursor.offset;
1052    let initial_bone_keyframes = bone_count;
1053    let mut initial_bone_keyframe_summaries = Vec::with_capacity(initial_bone_keyframes);
1054    for _ in 0..initial_bone_keyframes {
1055        initial_bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, false)?);
1056    }
1057    let bone_keyframe_count_offset = cursor.offset;
1058    let bone_keyframes = usize_from_i32(cursor.read_i32()?)?;
1059    let bone_keyframes_offset = cursor.offset;
1060    let mut bone_keyframe_summaries = Vec::with_capacity(bone_keyframes);
1061    for _ in 0..bone_keyframes {
1062        bone_keyframe_summaries.push(read_document_bone_keyframe(cursor, true)?);
1063    }
1064    let bone_keyframes_end_offset = cursor.offset;
1065
1066    let initial_morph_keyframes_offset = cursor.offset;
1067    let initial_morph_keyframes = morph_count;
1068    let mut initial_morph_keyframe_summaries = Vec::with_capacity(initial_morph_keyframes);
1069    for _ in 0..initial_morph_keyframes {
1070        initial_morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, false)?);
1071    }
1072    let morph_keyframe_count_offset = cursor.offset;
1073    let morph_keyframes = usize_from_i32(cursor.read_i32()?)?;
1074    let morph_keyframes_offset = cursor.offset;
1075    let mut morph_keyframe_summaries = Vec::with_capacity(morph_keyframes);
1076    for _ in 0..morph_keyframes {
1077        morph_keyframe_summaries.push(read_document_morph_keyframe(cursor, true)?);
1078    }
1079    let morph_keyframes_end_offset = cursor.offset;
1080
1081    let initial_model_keyframes = 1;
1082    let initial_model_keyframe_offset = cursor.offset;
1083    let initial_model_keyframe = read_document_model_keyframe(
1084        cursor,
1085        false,
1086        constraint_bone_count,
1087        outside_parent_subject_bone_count,
1088    )?;
1089    let model_keyframe_count_offset = cursor.offset;
1090    let model_keyframes = usize_from_i32(cursor.read_i32()?)?;
1091    let model_keyframes_offset = cursor.offset;
1092    let mut model_keyframe_summaries = Vec::with_capacity(model_keyframes);
1093    for _ in 0..model_keyframes {
1094        model_keyframe_summaries.push(read_document_model_keyframe(
1095            cursor,
1096            true,
1097            constraint_bone_count,
1098            outside_parent_subject_bone_count,
1099        )?);
1100    }
1101    let model_keyframes_end_offset = cursor.offset;
1102
1103    let bone_states_offset = cursor.offset;
1104    let mut bone_state_summaries = Vec::with_capacity(bone_count);
1105    for _ in 0..bone_count {
1106        bone_state_summaries.push(read_document_bone_state(cursor)?);
1107    }
1108    let morph_states_offset = cursor.offset;
1109    let mut morph_state_summaries = Vec::with_capacity(morph_count);
1110    for _ in 0..morph_count {
1111        morph_state_summaries.push(read_document_morph_state(cursor)?);
1112    }
1113    let constraint_states_offset = cursor.offset;
1114    let mut constraint_state_summaries = Vec::with_capacity(constraint_bone_count);
1115    for _ in 0..constraint_bone_count {
1116        constraint_state_summaries.push(read_document_constraint_state(cursor)?);
1117    }
1118    let outside_parent_states_offset = cursor.offset;
1119    let mut outside_parent_state_summaries = Vec::with_capacity(outside_parent_subject_bone_count);
1120    for _ in 0..outside_parent_subject_bone_count {
1121        outside_parent_state_summaries.push(read_document_outside_parent_state(cursor)?);
1122    }
1123
1124    let blend_enabled = cursor.read_bool()?;
1125    let edge_width = cursor.read_f32()?;
1126    let self_shadow_enabled = cursor.read_bool()?;
1127    let transform_order_index = cursor.read_u8()?;
1128    let offset_end = cursor.offset;
1129
1130    Some(PmmDocumentModelSummary {
1131        slot_index,
1132        document_model_index,
1133        offset,
1134        offset_end,
1135        path_offset,
1136        name,
1137        english_name,
1138        path,
1139        asset_reference_index,
1140        bone_count,
1141        morph_count,
1142        constraint_bone_count,
1143        outside_parent_subject_bone_count,
1144        draw_order_index,
1145        transform_order_index,
1146        selected_bone_index,
1147        selected_morph_indices,
1148        vertical_scroll,
1149        sections: PmmDocumentModelSections {
1150            initial_bone_keyframes_offset,
1151            bone_keyframe_count_offset,
1152            bone_keyframes_offset,
1153            bone_keyframes_end_offset,
1154            initial_morph_keyframes_offset,
1155            morph_keyframe_count_offset,
1156            morph_keyframes_offset,
1157            morph_keyframes_end_offset,
1158            initial_model_keyframe_offset,
1159            model_keyframe_count_offset,
1160            model_keyframes_offset,
1161            model_keyframes_end_offset,
1162            bone_states_offset,
1163            morph_states_offset,
1164            constraint_states_offset,
1165            outside_parent_states_offset,
1166        },
1167        initial_bone_keyframes,
1168        initial_bone_keyframe_summaries,
1169        bone_keyframes,
1170        bone_keyframe_summaries,
1171        initial_morph_keyframes,
1172        initial_morph_keyframe_summaries,
1173        morph_keyframes,
1174        morph_keyframe_summaries,
1175        initial_model_keyframes,
1176        model_keyframes,
1177        initial_model_keyframe,
1178        model_keyframe_summaries,
1179        last_frame_index,
1180        visible,
1181        blend_enabled,
1182        edge_width,
1183        self_shadow_enabled,
1184        bone_state_summaries,
1185        morph_state_summaries,
1186        constraint_state_summaries,
1187        outside_parent_state_summaries,
1188    })
1189}
1190
1191fn read_document_bone_keyframe(
1192    cursor: &mut PmmDocumentCursor<'_>,
1193    include_index: bool,
1194) -> Option<PmmDocumentBoneKeyframeSummary> {
1195    let base = read_document_base_keyframe(cursor, include_index)?;
1196    let interpolation = cursor.read_bytes16()?;
1197    let translation = cursor.read_f32x3()?;
1198    let orientation = cursor.read_f32x4()?;
1199    let physics_disabled = cursor.read_bool()?;
1200    let selected = cursor.read_bool()?;
1201    Some(PmmDocumentBoneKeyframeSummary {
1202        index: base.index,
1203        frame_index: base.frame_index,
1204        previous_keyframe_index: base.previous_keyframe_index,
1205        next_keyframe_index: base.next_keyframe_index,
1206        interpolation,
1207        translation,
1208        orientation,
1209        physics_disabled,
1210        selected,
1211    })
1212}
1213
1214fn read_document_morph_keyframe(
1215    cursor: &mut PmmDocumentCursor<'_>,
1216    include_index: bool,
1217) -> Option<PmmDocumentMorphKeyframeSummary> {
1218    let base = read_document_base_keyframe(cursor, include_index)?;
1219    let weight = cursor.read_f32()?;
1220    let selected = cursor.read_bool()?;
1221    Some(PmmDocumentMorphKeyframeSummary {
1222        index: base.index,
1223        frame_index: base.frame_index,
1224        previous_keyframe_index: base.previous_keyframe_index,
1225        next_keyframe_index: base.next_keyframe_index,
1226        weight,
1227        selected,
1228    })
1229}
1230
1231fn read_document_model_keyframe(
1232    cursor: &mut PmmDocumentCursor<'_>,
1233    include_index: bool,
1234    constraint_bone_count: usize,
1235    outside_parent_subject_bone_count: usize,
1236) -> Option<PmmDocumentModelKeyframeSummary> {
1237    let base = read_document_base_keyframe(cursor, include_index)?;
1238    let visible = cursor.read_bool()?;
1239    let mut constraint_states = Vec::with_capacity(constraint_bone_count);
1240    for _ in 0..constraint_bone_count {
1241        constraint_states.push(cursor.read_bool()?);
1242    }
1243    let mut outside_parent_indices = Vec::with_capacity(outside_parent_subject_bone_count);
1244    for _ in 0..outside_parent_subject_bone_count {
1245        outside_parent_indices.push(PmmDocumentOutsideParentIndexSummary {
1246            parent_model_index: cursor.read_i32()?,
1247            parent_model_bone_index: cursor.read_i32()?,
1248        });
1249    }
1250    let self_shadow_enabled = cursor.read_bool()?;
1251    Some(PmmDocumentModelKeyframeSummary {
1252        index: base.index,
1253        frame_index: base.frame_index,
1254        previous_keyframe_index: base.previous_keyframe_index,
1255        next_keyframe_index: base.next_keyframe_index,
1256        visible,
1257        constraint_states,
1258        outside_parent_indices,
1259        self_shadow_enabled,
1260    })
1261}
1262
1263fn read_document_camera_summary(
1264    cursor: &mut PmmDocumentCursor<'_>,
1265) -> Option<PmmDocumentTrackSummary> {
1266    let offset = cursor.offset;
1267    let initial_keyframe = read_document_camera_keyframe(cursor, false)?;
1268    let keyframe_count_offset = cursor.offset;
1269    let keyframes = usize_from_i32(cursor.read_i32()?)?;
1270    let keyframes_offset = cursor.offset;
1271    let mut keyframe_summaries = Vec::with_capacity(keyframes);
1272    for _ in 0..keyframes {
1273        keyframe_summaries.push(read_document_camera_keyframe(cursor, true)?);
1274    }
1275    let keyframes_end_offset = cursor.offset;
1276    let state_offset = cursor.offset;
1277    cursor.skip(12 * 3)?;
1278    cursor.read_bool()?;
1279    let state_end_offset = cursor.offset;
1280    Some(PmmDocumentTrackSummary {
1281        offset,
1282        offset_end: state_end_offset,
1283        initial_keyframes: 1,
1284        keyframes,
1285        initial_keyframe: Some(initial_keyframe),
1286        keyframe_summaries,
1287        keyframe_count_offset,
1288        keyframes_offset,
1289        keyframes_end_offset,
1290        state_offset: Some(state_offset),
1291        state_end_offset: Some(state_end_offset),
1292    })
1293}
1294
1295fn read_document_camera_keyframe(
1296    cursor: &mut PmmDocumentCursor<'_>,
1297    include_index: bool,
1298) -> Option<PmmDocumentKeyframeSummary> {
1299    let base = read_document_base_keyframe(cursor, include_index)?;
1300    let distance = cursor.read_f32()?;
1301    let look_at = cursor.read_f32x3()?;
1302    let angle = cursor.read_f32x3()?;
1303    let parent_model_index = cursor.read_i32()?;
1304    let parent_model_bone_index = cursor.read_i32()?;
1305    cursor.skip(6 * 4)?;
1306    let perspective_view = !cursor.read_bool()?;
1307    let fov = cursor.read_i32()?;
1308    let selected = cursor.read_bool()?;
1309    Some(PmmDocumentKeyframeSummary::Camera {
1310        index: base.index,
1311        frame_index: base.frame_index,
1312        previous_keyframe_index: base.previous_keyframe_index,
1313        next_keyframe_index: base.next_keyframe_index,
1314        distance,
1315        look_at,
1316        angle,
1317        parent_model_index,
1318        parent_model_bone_index,
1319        perspective_view,
1320        fov,
1321        selected,
1322    })
1323}
1324
1325fn read_document_light_summary(
1326    cursor: &mut PmmDocumentCursor<'_>,
1327) -> Option<PmmDocumentTrackSummary> {
1328    let offset = cursor.offset;
1329    let initial_keyframe = read_document_light_keyframe(cursor, false)?;
1330    let keyframe_count_offset = cursor.offset;
1331    let keyframes = usize_from_i32(cursor.read_i32()?)?;
1332    let keyframes_offset = cursor.offset;
1333    let mut keyframe_summaries = Vec::with_capacity(keyframes);
1334    for _ in 0..keyframes {
1335        keyframe_summaries.push(read_document_light_keyframe(cursor, true)?);
1336    }
1337    let keyframes_end_offset = cursor.offset;
1338    let state_offset = cursor.offset;
1339    cursor.skip(12)?;
1340    cursor.skip(12)?;
1341    let state_end_offset = cursor.offset;
1342    Some(PmmDocumentTrackSummary {
1343        offset,
1344        offset_end: state_end_offset,
1345        initial_keyframes: 1,
1346        keyframes,
1347        initial_keyframe: Some(initial_keyframe),
1348        keyframe_summaries,
1349        keyframe_count_offset,
1350        keyframes_offset,
1351        keyframes_end_offset,
1352        state_offset: Some(state_offset),
1353        state_end_offset: Some(state_end_offset),
1354    })
1355}
1356
1357fn read_document_light_keyframe(
1358    cursor: &mut PmmDocumentCursor<'_>,
1359    include_index: bool,
1360) -> Option<PmmDocumentKeyframeSummary> {
1361    let base = read_document_base_keyframe(cursor, include_index)?;
1362    let color = cursor.read_f32x3()?;
1363    let direction = cursor.read_f32x3()?;
1364    let selected = cursor.read_bool()?;
1365    Some(PmmDocumentKeyframeSummary::Light {
1366        index: base.index,
1367        frame_index: base.frame_index,
1368        previous_keyframe_index: base.previous_keyframe_index,
1369        next_keyframe_index: base.next_keyframe_index,
1370        color,
1371        direction,
1372        selected,
1373    })
1374}
1375
1376fn read_document_accessory_block_summary(
1377    cursor: &mut PmmDocumentCursor<'_>,
1378    references: &[PmmAssetReference],
1379) -> Option<PmmDocumentAccessoryBlockSummary> {
1380    let offset = cursor.offset;
1381    let selected_accessory_index = cursor.read_u8()?;
1382    let horizontal_scroll = cursor.read_i32()?;
1383    let accessory_count = cursor.read_u8()? as usize;
1384    let mut accessories = Vec::with_capacity(accessory_count);
1385    for slot_index in 0..accessory_count {
1386        accessories.push(read_document_accessory_summary(
1387            cursor, slot_index, references,
1388        )?);
1389    }
1390    let offset_end = cursor.offset;
1391    let keyframes = accessories
1392        .iter()
1393        .map(|accessory| accessory.keyframes)
1394        .sum();
1395    Some(PmmDocumentAccessoryBlockSummary {
1396        offset,
1397        offset_end,
1398        selected_accessory_index,
1399        horizontal_scroll,
1400        accessory_count,
1401        keyframes,
1402        accessories,
1403    })
1404}
1405
1406fn read_document_accessory_summary(
1407    cursor: &mut PmmDocumentCursor<'_>,
1408    slot_index: usize,
1409    references: &[PmmAssetReference],
1410) -> Option<PmmDocumentAccessorySummary> {
1411    const PMM_ACCESSORY_NAME_BYTE_LENGTH: usize = 100;
1412    const PMM_PATH_BYTE_LENGTH: usize = 256;
1413    let offset = cursor.offset;
1414    let document_accessory_index = cursor.read_u8()?;
1415    let name = cursor.read_fixed_string(PMM_ACCESSORY_NAME_BYTE_LENGTH)?;
1416    let path_offset = cursor.offset;
1417    let path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
1418    let asset_reference_index = asset_reference_index_for_path(references, "accessory", &path);
1419    let draw_order_index = cursor.read_u8()?;
1420    let initial_keyframe = read_document_accessory_keyframe(cursor, false)?;
1421    let keyframe_count_offset = cursor.offset;
1422    let keyframes = usize_from_i32(cursor.read_i32()?)?;
1423    let keyframes_offset = cursor.offset;
1424    let mut keyframe_summaries = Vec::with_capacity(keyframes);
1425    for _ in 0..keyframes {
1426        keyframe_summaries.push(read_document_accessory_keyframe(cursor, true)?);
1427    }
1428    let keyframes_end_offset = cursor.offset;
1429
1430    let state_offset = cursor.offset;
1431    let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
1432    let parent_model_index = cursor.read_i32()?;
1433    let parent_model_bone_index = cursor.read_i32()?;
1434    cursor.skip(12)?;
1435    let scale_factor = cursor.read_f32()?;
1436    cursor.skip(12)?;
1437    let shadow_enabled = cursor.read_bool()?;
1438    let add_blend_enabled = cursor.read_bool()?;
1439    let state_end_offset = cursor.offset;
1440    let offset_end = cursor.offset;
1441
1442    Some(PmmDocumentAccessorySummary {
1443        slot_index,
1444        document_accessory_index,
1445        offset,
1446        offset_end,
1447        path_offset,
1448        name,
1449        path,
1450        asset_reference_index,
1451        draw_order_index,
1452        keyframes,
1453        initial_keyframe,
1454        keyframe_summaries,
1455        keyframe_count_offset,
1456        keyframes_offset,
1457        keyframes_end_offset,
1458        state_offset,
1459        state_end_offset,
1460        visible,
1461        opacity,
1462        parent_model_index,
1463        parent_model_bone_index,
1464        scale_factor,
1465        shadow_enabled,
1466        add_blend_enabled,
1467    })
1468}
1469
1470fn read_document_accessory_keyframe(
1471    cursor: &mut PmmDocumentCursor<'_>,
1472    include_index: bool,
1473) -> Option<PmmDocumentAccessoryKeyframeSummary> {
1474    let base = read_document_base_keyframe(cursor, include_index)?;
1475    let (opacity, visible) = unpack_document_accessory_opacity_and_visible(cursor.read_u8()?);
1476    let parent_model_index = cursor.read_i32()?;
1477    let parent_model_bone_index = cursor.read_i32()?;
1478    let translation = cursor.read_f32x3()?;
1479    let orientation = cursor.read_f32x3()?;
1480    let scale_factor = cursor.read_f32()?;
1481    let shadow_enabled = cursor.read_bool()?;
1482    let selected = cursor.read_bool()?;
1483    Some(PmmDocumentAccessoryKeyframeSummary {
1484        index: base.index,
1485        frame_index: base.frame_index,
1486        previous_keyframe_index: base.previous_keyframe_index,
1487        next_keyframe_index: base.next_keyframe_index,
1488        visible,
1489        opacity,
1490        parent_model_index,
1491        parent_model_bone_index,
1492        translation,
1493        orientation,
1494        scale_factor,
1495        shadow_enabled,
1496        selected,
1497    })
1498}
1499
1500struct PmmDocumentSettingsBeforeGravity {
1501    offset: usize,
1502    current_frame_index: i32,
1503    horizontal_scroll: i32,
1504    horizontal_scroll_thumb: i32,
1505    editing_mode: i32,
1506    camera_look_mode: u8,
1507    loop_enabled: bool,
1508    begin_frame_index_enabled: bool,
1509    end_frame_index_enabled: bool,
1510    begin_frame_index: i32,
1511    end_frame_index: i32,
1512    audio_enabled: bool,
1513    audio_path: String,
1514    background_video_offset: [i32; 2],
1515    background_video_scale_factor: f32,
1516    background_video_path: String,
1517    background_video_enabled: bool,
1518    background_image_offset: [i32; 2],
1519    background_image_scale_factor: f32,
1520    background_image_path: String,
1521    background_image_enabled: bool,
1522    information_shown: bool,
1523    grid_and_axis_shown: bool,
1524    ground_shadow_shown: bool,
1525    preferred_fps: f32,
1526    screen_capture_mode: i32,
1527    accessory_index_after_models: i32,
1528    ground_shadow_brightness: f32,
1529    translucent_ground_shadow_enabled: bool,
1530    physics_simulation_mode: u8,
1531}
1532
1533fn read_document_settings_before_gravity(
1534    cursor: &mut PmmDocumentCursor<'_>,
1535) -> Option<PmmDocumentSettingsBeforeGravity> {
1536    const PMM_PATH_BYTE_LENGTH: usize = 256;
1537    let offset = cursor.offset;
1538    let current_frame_index = cursor.read_i32()?;
1539    let horizontal_scroll = cursor.read_i32()?;
1540    let horizontal_scroll_thumb = cursor.read_i32()?;
1541    let editing_mode = cursor.read_i32()?;
1542    let camera_look_mode = cursor.read_u8()?;
1543    let loop_enabled = cursor.read_bool()?;
1544    let begin_frame_index_enabled = cursor.read_bool()?;
1545    let end_frame_index_enabled = cursor.read_bool()?;
1546    let begin_frame_index = cursor.read_i32()?;
1547    let end_frame_index = cursor.read_i32()?;
1548    let audio_enabled = cursor.read_bool()?;
1549    let audio_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
1550    let background_video_offset = [cursor.read_i32()?, cursor.read_i32()?];
1551    let background_video_scale_factor = cursor.read_f32()?;
1552    let background_video_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
1553    let background_video_enabled = cursor.read_i32()? != 0;
1554    let background_image_offset = [cursor.read_i32()?, cursor.read_i32()?];
1555    let background_image_scale_factor = cursor.read_f32()?;
1556    let background_image_path = cursor.read_fixed_string(PMM_PATH_BYTE_LENGTH)?;
1557    let background_image_enabled = cursor.read_bool()?;
1558    let information_shown = cursor.read_bool()?;
1559    let grid_and_axis_shown = cursor.read_bool()?;
1560    let ground_shadow_shown = cursor.read_bool()?;
1561    let preferred_fps = cursor.read_f32()?;
1562    let screen_capture_mode = cursor.read_i32()?;
1563    let accessory_index_after_models = cursor.read_i32()?;
1564    let ground_shadow_brightness = cursor.read_f32()?;
1565    let translucent_ground_shadow_enabled = cursor.read_bool()?;
1566    let physics_simulation_mode = cursor.read_u8()?;
1567
1568    Some(PmmDocumentSettingsBeforeGravity {
1569        offset,
1570        current_frame_index,
1571        horizontal_scroll,
1572        horizontal_scroll_thumb,
1573        editing_mode,
1574        camera_look_mode,
1575        loop_enabled,
1576        begin_frame_index_enabled,
1577        end_frame_index_enabled,
1578        begin_frame_index,
1579        end_frame_index,
1580        audio_enabled,
1581        audio_path,
1582        background_video_offset,
1583        background_video_scale_factor,
1584        background_video_path,
1585        background_video_enabled,
1586        background_image_offset,
1587        background_image_scale_factor,
1588        background_image_path,
1589        background_image_enabled,
1590        information_shown,
1591        grid_and_axis_shown,
1592        ground_shadow_shown,
1593        preferred_fps,
1594        screen_capture_mode,
1595        accessory_index_after_models,
1596        ground_shadow_brightness,
1597        translucent_ground_shadow_enabled,
1598        physics_simulation_mode,
1599    })
1600}
1601
1602fn read_document_gravity_summary(
1603    cursor: &mut PmmDocumentCursor<'_>,
1604) -> Option<PmmDocumentTrackSummary> {
1605    let offset = cursor.offset;
1606    let state_offset = cursor.offset;
1607    cursor.read_f32()?;
1608    cursor.read_i32()?;
1609    cursor.skip(12)?;
1610    cursor.read_bool()?;
1611    let state_end_offset = cursor.offset;
1612    let initial_keyframe = read_document_gravity_keyframe(cursor, false)?;
1613    let keyframe_count_offset = cursor.offset;
1614    let keyframes = usize_from_i32(cursor.read_i32()?)?;
1615    let keyframes_offset = cursor.offset;
1616    let mut keyframe_summaries = Vec::with_capacity(keyframes);
1617    for _ in 0..keyframes {
1618        keyframe_summaries.push(read_document_gravity_keyframe(cursor, true)?);
1619    }
1620    let keyframes_end_offset = cursor.offset;
1621    Some(PmmDocumentTrackSummary {
1622        offset,
1623        offset_end: keyframes_end_offset,
1624        initial_keyframes: 1,
1625        keyframes,
1626        initial_keyframe: Some(initial_keyframe),
1627        keyframe_summaries,
1628        keyframe_count_offset,
1629        keyframes_offset,
1630        keyframes_end_offset,
1631        state_offset: Some(state_offset),
1632        state_end_offset: Some(state_end_offset),
1633    })
1634}
1635
1636fn read_document_gravity_keyframe(
1637    cursor: &mut PmmDocumentCursor<'_>,
1638    include_index: bool,
1639) -> Option<PmmDocumentKeyframeSummary> {
1640    let base = read_document_base_keyframe(cursor, include_index)?;
1641    let noise_enabled = cursor.read_bool()?;
1642    let noise = cursor.read_i32()?;
1643    let acceleration = cursor.read_f32()?;
1644    let direction = cursor.read_f32x3()?;
1645    let selected = cursor.read_bool()?;
1646    Some(PmmDocumentKeyframeSummary::Gravity {
1647        index: base.index,
1648        frame_index: base.frame_index,
1649        previous_keyframe_index: base.previous_keyframe_index,
1650        next_keyframe_index: base.next_keyframe_index,
1651        noise_enabled,
1652        noise,
1653        acceleration,
1654        direction,
1655        selected,
1656    })
1657}
1658
1659fn read_document_self_shadow_summary(
1660    cursor: &mut PmmDocumentCursor<'_>,
1661) -> Option<PmmDocumentTrackSummary> {
1662    let offset = cursor.offset;
1663    let state_offset = cursor.offset;
1664    cursor.read_bool()?;
1665    cursor.read_f32()?;
1666    let state_end_offset = cursor.offset;
1667    let initial_keyframe = read_document_self_shadow_keyframe(cursor, false)?;
1668    let keyframe_count_offset = cursor.offset;
1669    let keyframes = usize_from_i32(cursor.read_i32()?)?;
1670    let keyframes_offset = cursor.offset;
1671    let mut keyframe_summaries = Vec::with_capacity(keyframes);
1672    for _ in 0..keyframes {
1673        keyframe_summaries.push(read_document_self_shadow_keyframe(cursor, true)?);
1674    }
1675    let keyframes_end_offset = cursor.offset;
1676    Some(PmmDocumentTrackSummary {
1677        offset,
1678        offset_end: keyframes_end_offset,
1679        initial_keyframes: 1,
1680        keyframes,
1681        initial_keyframe: Some(initial_keyframe),
1682        keyframe_summaries,
1683        keyframe_count_offset,
1684        keyframes_offset,
1685        keyframes_end_offset,
1686        state_offset: Some(state_offset),
1687        state_end_offset: Some(state_end_offset),
1688    })
1689}
1690
1691fn read_document_self_shadow_keyframe(
1692    cursor: &mut PmmDocumentCursor<'_>,
1693    include_index: bool,
1694) -> Option<PmmDocumentKeyframeSummary> {
1695    let base = read_document_base_keyframe(cursor, include_index)?;
1696    let mode = cursor.read_u8()?;
1697    let distance = cursor.read_f32()?;
1698    let selected = cursor.read_bool()?;
1699    Some(PmmDocumentKeyframeSummary::SelfShadow {
1700        index: base.index,
1701        frame_index: base.frame_index,
1702        previous_keyframe_index: base.previous_keyframe_index,
1703        next_keyframe_index: base.next_keyframe_index,
1704        mode,
1705        distance,
1706        selected,
1707    })
1708}
1709
1710fn finish_document_settings_summary(
1711    before_gravity: PmmDocumentSettingsBeforeGravity,
1712    cursor: &mut PmmDocumentCursor<'_>,
1713    model_count: usize,
1714) -> Option<PmmDocumentSettingsSummary> {
1715    let edge_color = [
1716        cursor.read_i32()? as f32 / 255.0,
1717        cursor.read_i32()? as f32 / 255.0,
1718        cursor.read_i32()? as f32 / 255.0,
1719    ];
1720    let black_background_enabled = cursor.read_bool()?;
1721    let camera_look_at_model_index = cursor.read_i32()?;
1722    let camera_look_at_model_bone_index = cursor.read_i32()?;
1723    let unknown_matrix_offset = cursor.offset;
1724    cursor.skip(16 * 4)?;
1725    let unknown_matrix_end_offset = cursor.offset;
1726    let following_look_at_enabled = cursor.read_bool()?;
1727    cursor.read_bool()?;
1728    let physics_ground_enabled = cursor.read_bool()?;
1729    let current_frame_index_in_text_field = cursor.read_i32()?;
1730
1731    let mut model_selection_footer_present = false;
1732    let mut model_selection_footer_offset = None;
1733    let mut model_selection_footer_end_offset = None;
1734    if cursor.offset < cursor.data.len() {
1735        let footer_offset = cursor.offset;
1736        if cursor.read_u8()? != 0 {
1737            model_selection_footer_present = true;
1738            model_selection_footer_offset = Some(footer_offset);
1739            for _ in 0..model_count {
1740                cursor.read_u8()?;
1741                cursor.read_i32()?;
1742            }
1743            model_selection_footer_end_offset = Some(cursor.offset);
1744        }
1745    }
1746    let offset_end = cursor.offset;
1747
1748    Some(PmmDocumentSettingsSummary {
1749        offset: before_gravity.offset,
1750        offset_end,
1751        current_frame_index: before_gravity.current_frame_index,
1752        horizontal_scroll: before_gravity.horizontal_scroll,
1753        horizontal_scroll_thumb: before_gravity.horizontal_scroll_thumb,
1754        editing_mode: before_gravity.editing_mode,
1755        camera_look_mode: before_gravity.camera_look_mode,
1756        loop_enabled: before_gravity.loop_enabled,
1757        begin_frame_index_enabled: before_gravity.begin_frame_index_enabled,
1758        end_frame_index_enabled: before_gravity.end_frame_index_enabled,
1759        begin_frame_index: before_gravity.begin_frame_index,
1760        end_frame_index: before_gravity.end_frame_index,
1761        audio_enabled: before_gravity.audio_enabled,
1762        audio_path: before_gravity.audio_path,
1763        background_video_offset: before_gravity.background_video_offset,
1764        background_video_scale_factor: before_gravity.background_video_scale_factor,
1765        background_video_path: before_gravity.background_video_path,
1766        background_video_enabled: before_gravity.background_video_enabled,
1767        background_image_offset: before_gravity.background_image_offset,
1768        background_image_scale_factor: before_gravity.background_image_scale_factor,
1769        background_image_path: before_gravity.background_image_path,
1770        background_image_enabled: before_gravity.background_image_enabled,
1771        information_shown: before_gravity.information_shown,
1772        grid_and_axis_shown: before_gravity.grid_and_axis_shown,
1773        ground_shadow_shown: before_gravity.ground_shadow_shown,
1774        preferred_fps: before_gravity.preferred_fps,
1775        screen_capture_mode: before_gravity.screen_capture_mode,
1776        accessory_index_after_models: before_gravity.accessory_index_after_models,
1777        ground_shadow_brightness: before_gravity.ground_shadow_brightness,
1778        translucent_ground_shadow_enabled: before_gravity.translucent_ground_shadow_enabled,
1779        physics_simulation_mode: before_gravity.physics_simulation_mode,
1780        edge_color,
1781        black_background_enabled,
1782        camera_look_at_model_index,
1783        camera_look_at_model_bone_index,
1784        unknown_matrix_offset,
1785        unknown_matrix_end_offset,
1786        following_look_at_enabled,
1787        physics_ground_enabled,
1788        current_frame_index_in_text_field,
1789        model_selection_footer_present,
1790        model_selection_footer_offset,
1791        model_selection_footer_end_offset,
1792    })
1793}
1794
1795fn read_document_bone_state(
1796    cursor: &mut PmmDocumentCursor<'_>,
1797) -> Option<PmmDocumentBoneStateSummary> {
1798    let translation = cursor.read_f32x3()?;
1799    let orientation = cursor.read_f32x4()?;
1800    let dirty = cursor.read_bool()?;
1801    let physics_disabled = cursor.read_bool()?;
1802    let selected = cursor.read_bool()?;
1803    Some(PmmDocumentBoneStateSummary {
1804        translation,
1805        orientation,
1806        dirty,
1807        physics_disabled,
1808        selected,
1809    })
1810}
1811
1812fn read_document_morph_state(
1813    cursor: &mut PmmDocumentCursor<'_>,
1814) -> Option<PmmDocumentMorphStateSummary> {
1815    let weight = cursor.read_f32()?;
1816    Some(PmmDocumentMorphStateSummary { weight })
1817}
1818
1819fn read_document_constraint_state(
1820    cursor: &mut PmmDocumentCursor<'_>,
1821) -> Option<PmmDocumentConstraintStateSummary> {
1822    let enabled = cursor.read_bool()?;
1823    Some(PmmDocumentConstraintStateSummary { enabled })
1824}
1825
1826fn read_document_outside_parent_state(
1827    cursor: &mut PmmDocumentCursor<'_>,
1828) -> Option<PmmDocumentOutsideParentStateSummary> {
1829    let parent_model_index = cursor.read_i32()?;
1830    let parent_model_bone_index = cursor.read_i32()?;
1831    let subject_bone_index = cursor.read_i32()?;
1832    let target_model_index = cursor.read_i32()?;
1833    Some(PmmDocumentOutsideParentStateSummary {
1834        parent_model_index,
1835        parent_model_bone_index,
1836        subject_bone_index,
1837        target_model_index,
1838    })
1839}
1840
1841fn read_document_base_keyframe(
1842    cursor: &mut PmmDocumentCursor<'_>,
1843    include_index: bool,
1844) -> Option<PmmDocumentBaseKeyframeSummary> {
1845    let index = if include_index {
1846        Some(cursor.read_i32()?)
1847    } else {
1848        None
1849    };
1850    let frame_index = cursor.read_i32()?;
1851    let previous_keyframe_index = cursor.read_i32()?;
1852    let next_keyframe_index = cursor.read_i32()?;
1853    Some(PmmDocumentBaseKeyframeSummary {
1854        index,
1855        frame_index,
1856        previous_keyframe_index,
1857        next_keyframe_index,
1858    })
1859}
1860
1861fn unpack_document_accessory_opacity_and_visible(value: u8) -> (f32, bool) {
1862    let visible = value & 1 != 0;
1863    let opacity = 1.0 - ((value >> 1) as f32 / 100.0);
1864    (opacity, visible)
1865}
1866
1867fn summarize_document_counts(models: &[PmmDocumentModelSummary]) -> PmmDocumentCounts {
1868    PmmDocumentCounts {
1869        models: models.len(),
1870        bones: models.iter().map(|model| model.bone_count).sum(),
1871        morphs: models.iter().map(|model| model.morph_count).sum(),
1872        initial_bone_keyframes: models
1873            .iter()
1874            .map(|model| model.initial_bone_keyframes)
1875            .sum(),
1876        bone_keyframes: models.iter().map(|model| model.bone_keyframes).sum(),
1877        initial_morph_keyframes: models
1878            .iter()
1879            .map(|model| model.initial_morph_keyframes)
1880            .sum(),
1881        morph_keyframes: models.iter().map(|model| model.morph_keyframes).sum(),
1882        initial_model_keyframes: models
1883            .iter()
1884            .map(|model| model.initial_model_keyframes)
1885            .sum(),
1886        model_keyframes: models.iter().map(|model| model.model_keyframes).sum(),
1887    }
1888}
1889
1890fn usize_from_i32(value: i32) -> Option<usize> {
1891    usize::try_from(value).ok()
1892}
1893
1894fn decode_shift_jis(bytes: &[u8]) -> String {
1895    let (decoded, _, _) = SHIFT_JIS.decode(bytes);
1896    decoded.trim_end_matches('\0').to_owned()
1897}
1898
1899#[derive(Debug, Clone)]
1900struct PmmModelSlotScan {
1901    slots: Vec<PmmModelSlot>,
1902    stop: Option<PmmModelSlotScanStop>,
1903}
1904
1905#[derive(Debug, Clone)]
1906struct PmmModelSlotScanStop {
1907    offset: usize,
1908    reason: &'static str,
1909}
1910
1911fn parse_model_slots_from_header(
1912    data: &[u8],
1913    references: &[PmmAssetReference],
1914    display_state: &PmmDisplayState,
1915) -> PmmModelSlotScan {
1916    const INITIAL_MODEL_SLOT_OFFSET: usize = 56;
1917    if display_state.layout != "pmm-v2-flags" {
1918        return PmmModelSlotScan {
1919            slots: Vec::new(),
1920            stop: None,
1921        };
1922    }
1923    let declared_count = display_state.non_zero_model_slot_count.max(1);
1924    let mut slots = Vec::new();
1925    let mut offset = INITIAL_MODEL_SLOT_OFFSET;
1926    let mut stop = None;
1927
1928    for slot_index in 0..declared_count {
1929        let require_reference_at_offset = slot_index > 0;
1930        match parse_model_slot_at(
1931            data,
1932            references,
1933            offset,
1934            slot_index,
1935            display_state
1936                .active_model_slot_indices
1937                .get(slot_index)
1938                .copied(),
1939            require_reference_at_offset,
1940        ) {
1941            Ok((slot, next_offset)) => {
1942                slots.push(slot);
1943                offset = next_offset;
1944            }
1945            Err(reason) => {
1946                if !slots.is_empty() && slot_index < declared_count {
1947                    stop = Some(PmmModelSlotScanStop { offset, reason });
1948                }
1949                break;
1950            }
1951        }
1952    }
1953
1954    PmmModelSlotScan { slots, stop }
1955}
1956
1957fn parse_model_slot_at(
1958    data: &[u8],
1959    references: &[PmmAssetReference],
1960    offset: usize,
1961    slot_index: usize,
1962    display_slot_index: Option<usize>,
1963    require_reference_at_offset: bool,
1964) -> Result<(PmmModelSlot, usize), &'static str> {
1965    let (name, name_bytes, after_name) =
1966        read_pmm_len_prefixed_sjis(data, offset).ok_or("missing_name")?;
1967    let (english_name, english_name_bytes, path_offset) =
1968        read_pmm_len_prefixed_sjis(data, after_name).ok_or("missing_english_name")?;
1969    let path_end = data
1970        .get(path_offset..)
1971        .ok_or("missing_model_path")?
1972        .iter()
1973        .position(|byte| *byte == 0)
1974        .map(|relative| path_offset + relative)
1975        .ok_or("missing_path_terminator")?;
1976    if path_end == path_offset {
1977        return Err("empty_path");
1978    }
1979    let (decoded_path, _, _) = SHIFT_JIS.decode(&data[path_offset..path_end]);
1980    let model_path = find_asset_candidates(&decoded_path)
1981        .into_iter()
1982        .find(|candidate| {
1983            candidate
1984                .rsplit_once('.')
1985                .map(|(_, ext)| matches!(ext.to_ascii_lowercase().as_str(), "pmx" | "pmd"))
1986                .unwrap_or(false)
1987        })
1988        .ok_or("missing_model_path")?;
1989    let normalized_path = normalize_path(&model_path);
1990    let asset_reference_index = references.iter().position(|reference| {
1991        reference.kind == "model"
1992            && reference
1993                .normalized_path
1994                .eq_ignore_ascii_case(&normalized_path)
1995            && (!require_reference_at_offset || reference.offset == offset)
1996    });
1997    if require_reference_at_offset && asset_reference_index.is_none() {
1998        return Err("asset_reference_not_matched");
1999    }
2000    let confidence = asset_reference_index
2001        .and_then(|index| references.get(index).map(|reference| reference.confidence))
2002        .unwrap_or_else(|| asset_reference_confidence(&model_path, &normalized_path));
2003    let padding_start = path_end + 1;
2004    let trailing_zero_padding_bytes = data
2005        .get(padding_start..)
2006        .map(|bytes| bytes.iter().take_while(|byte| **byte == 0).count())
2007        .unwrap_or(0);
2008    let next_non_zero_offset = data
2009        .get(padding_start..)
2010        .and_then(|bytes| bytes.iter().position(|byte| *byte != 0))
2011        .map(|relative| padding_start + relative);
2012
2013    Ok((
2014        PmmModelSlot {
2015            slot_index,
2016            display_slot_index,
2017            offset,
2018            offset_end: path_end,
2019            model_path_offset: path_offset,
2020            trailing_zero_padding_bytes,
2021            next_non_zero_offset,
2022            name,
2023            name_bytes,
2024            english_name,
2025            english_name_bytes,
2026            model_path,
2027            normalized_path,
2028            asset_reference_index,
2029            confidence,
2030        },
2031        path_end + 1,
2032    ))
2033}
2034
2035fn read_pmm_len_prefixed_sjis(data: &[u8], offset: usize) -> Option<(String, Vec<u8>, usize)> {
2036    let length = *data.get(offset)? as usize;
2037    if length == 0 || length > 128 {
2038        return None;
2039    }
2040    let start = offset + 1;
2041    let end = start.checked_add(length)?;
2042    let bytes = data.get(start..end)?;
2043    if bytes.contains(&0) {
2044        return None;
2045    }
2046    let (decoded, _, _) = SHIFT_JIS.decode(bytes);
2047    Some((
2048        decoded.trim_end_matches('\0').to_owned(),
2049        bytes.to_vec(),
2050        end,
2051    ))
2052}
2053
2054fn extract_asset_references(data: &[u8]) -> Vec<PmmAssetReference> {
2055    let mut refs = Vec::new();
2056    let mut chunk_start = 0usize;
2057    for index in 0..=data.len() {
2058        if index < data.len() && data[index] != 0 {
2059            continue;
2060        }
2061        if index > chunk_start {
2062            let (decoded, _, _) = SHIFT_JIS.decode(&data[chunk_start..index]);
2063            for candidate in find_asset_candidates(&decoded) {
2064                let normalized = normalize_path(&candidate);
2065                if refs.iter().any(|existing: &PmmAssetReference| {
2066                    existing.normalized_path.eq_ignore_ascii_case(&normalized)
2067                }) {
2068                    continue;
2069                }
2070                let file_name = normalized
2071                    .rsplit('/')
2072                    .next()
2073                    .unwrap_or(&normalized)
2074                    .to_owned();
2075                let extension = file_name
2076                    .rsplit_once('.')
2077                    .map(|(_, ext)| ext.to_ascii_lowercase())
2078                    .unwrap_or_default();
2079                refs.push(PmmAssetReference {
2080                    confidence: asset_reference_confidence(&candidate, &normalized),
2081                    path: candidate,
2082                    normalized_path: normalized,
2083                    file_name,
2084                    kind: classify_extension(&extension),
2085                    extension,
2086                    offset: chunk_start,
2087                    offset_end: index,
2088                });
2089            }
2090        }
2091        chunk_start = index + 1;
2092    }
2093    refs.sort_by_key(|reference| reference.offset);
2094    refs
2095}
2096
2097fn asset_reference_confidence(path: &str, normalized_path: &str) -> &'static str {
2098    if path.contains('\u{FFFD}')
2099        || normalized_path.starts_with('/')
2100        || normalized_path.starts_with(":/")
2101    {
2102        return "low";
2103    }
2104    if normalized_path.contains('/') || last_windows_drive_path_start(path).is_some() {
2105        return "high";
2106    }
2107    "medium"
2108}
2109
2110fn find_asset_candidates(text: &str) -> Vec<String> {
2111    let mut candidates = Vec::new();
2112    for segment in text.split(|c: char| c.is_control() || c == '"' || c == '\'') {
2113        let lower = segment.to_ascii_lowercase();
2114        let mut search_from = 0usize;
2115        while let Some((relative_end, extension)) = next_asset_extension(&lower[search_from..]) {
2116            let end = search_from + relative_end + extension.len();
2117            let start = asset_candidate_start(segment, end);
2118            let candidate = segment[start..end].trim();
2119            if has_asset_file_stem(candidate)
2120                && !candidates
2121                    .iter()
2122                    .any(|existing: &String| existing.eq_ignore_ascii_case(candidate))
2123            {
2124                candidates.push(candidate.to_owned());
2125            }
2126            search_from = end;
2127        }
2128    }
2129    candidates
2130}
2131
2132fn next_asset_extension(text: &str) -> Option<(usize, &'static str)> {
2133    [
2134        ".pmd", ".pmx", ".vmd", ".vac", ".x", ".wav", ".mp3", ".ogg", ".bmp", ".tga", ".png",
2135        ".jpg", ".jpeg", ".dds", ".avi", ".avs", ".mp4", ".wmv",
2136    ]
2137    .iter()
2138    .filter_map(|extension| {
2139        find_extension_with_boundary(text, extension).map(|index| (index, *extension))
2140    })
2141    .min_by_key(|(index, _)| *index)
2142}
2143
2144fn find_extension_with_boundary(text: &str, extension: &str) -> Option<usize> {
2145    let mut search_from = 0usize;
2146    while let Some(relative_index) = text[search_from..].find(extension) {
2147        let index = search_from + relative_index;
2148        let after = index + extension.len();
2149        if text[after..]
2150            .chars()
2151            .next()
2152            .is_none_or(|ch| ch.is_whitespace())
2153        {
2154            return Some(index);
2155        }
2156        search_from = after;
2157    }
2158    None
2159}
2160
2161fn asset_candidate_start(segment: &str, end: usize) -> usize {
2162    let prefix = &segment[..end];
2163    let lower = prefix.to_ascii_lowercase();
2164    if let Some(index) = last_userfile_path_start(&lower) {
2165        return index;
2166    }
2167    if let Some(index) = last_windows_drive_path_start(prefix) {
2168        return index;
2169    }
2170    prefix
2171        .rfind(char::is_whitespace)
2172        .map(|index| index + 1)
2173        .unwrap_or(0)
2174}
2175
2176fn last_userfile_path_start(value: &str) -> Option<usize> {
2177    ["userfile\\", "userfile/"]
2178        .iter()
2179        .flat_map(|needle| value.match_indices(needle).map(|(index, _)| index))
2180        .filter(|index| {
2181            *index == 0
2182                || value.as_bytes()[index - 1] == b'\\'
2183                || value.as_bytes()[index - 1] == b'/'
2184        })
2185        .last()
2186}
2187
2188fn last_windows_drive_path_start(value: &str) -> Option<usize> {
2189    let bytes = value.as_bytes();
2190    if bytes.len() < 3 {
2191        return None;
2192    }
2193    let mut last = None;
2194    for index in 0..=bytes.len() - 3 {
2195        if bytes[index].is_ascii_alphabetic()
2196            && bytes[index + 1] == b':'
2197            && (bytes[index + 2] == b'\\' || bytes[index + 2] == b'/')
2198        {
2199            last = Some(index);
2200        }
2201    }
2202    last
2203}
2204
2205fn has_asset_file_stem(value: &str) -> bool {
2206    let Some((stem, _)) = value
2207        .rsplit(['\\', '/'])
2208        .next()
2209        .and_then(|file_name| file_name.rsplit_once('.'))
2210    else {
2211        return false;
2212    };
2213    if stem.is_empty() {
2214        return false;
2215    }
2216    if value.contains(['\\', '/']) || last_windows_drive_path_start(value).is_some() {
2217        return true;
2218    }
2219    !stem
2220        .chars()
2221        .any(|c| c.is_control() || matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*' | '='))
2222}
2223
2224fn normalize_path(value: &str) -> String {
2225    let normalized = value.replace('\\', "/").replace("//", "/");
2226    let lower = normalized.to_ascii_lowercase();
2227    match last_userfile_path_start(&lower) {
2228        Some(index) => normalized[index..].to_owned(),
2229        None => normalized,
2230    }
2231}
2232
2233fn classify_extension(extension: &str) -> &'static str {
2234    match extension {
2235        "pmd" | "pmx" => "model",
2236        "x" | "vac" => "accessory",
2237        "vmd" => "motion",
2238        "wav" | "mp3" | "ogg" => "audio",
2239        "bmp" | "tga" | "png" | "jpg" | "jpeg" | "dds" => "image",
2240        "avi" | "avs" | "mp4" | "wmv" => "video",
2241        _ => "unknown",
2242    }
2243}
2244
2245fn paths_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<String> {
2246    references
2247        .iter()
2248        .filter(|reference| reference.kind == kind)
2249        .map(|reference| reference.normalized_path.clone())
2250        .collect()
2251}
2252
2253fn asset_reference_index_for_path(
2254    references: &[PmmAssetReference],
2255    kind: &str,
2256    path: &str,
2257) -> Option<usize> {
2258    let normalized = normalize_path(path);
2259    references.iter().position(|reference| {
2260        reference.kind == kind && reference.normalized_path.eq_ignore_ascii_case(&normalized)
2261    })
2262}
2263
2264fn scene_assets_by_kind(references: &[PmmAssetReference], kind: &str) -> Vec<PmmSceneAsset> {
2265    references
2266        .iter()
2267        .enumerate()
2268        .filter(|(_, reference)| reference.kind == kind)
2269        .enumerate()
2270        .map(|(kind_index, (reference_index, reference))| PmmSceneAsset {
2271            reference_index,
2272            kind_index,
2273            path: reference.path.clone(),
2274            normalized_path: reference.normalized_path.clone(),
2275            file_name: reference.file_name.clone(),
2276            extension: reference.extension.clone(),
2277            offset: reference.offset,
2278            offset_end: reference.offset_end,
2279            confidence: reference.confidence,
2280        })
2281        .collect()
2282}
2283
2284fn asset_summary(references: &[PmmAssetReference]) -> PmmAssetSummary {
2285    let mut kind_counts = BTreeMap::new();
2286    let mut extension_counts = BTreeMap::new();
2287    let mut confidence_counts = BTreeMap::new();
2288    for reference in references {
2289        *kind_counts.entry(reference.kind.to_owned()).or_insert(0) += 1;
2290        *extension_counts
2291            .entry(reference.extension.to_owned())
2292            .or_insert(0) += 1;
2293        *confidence_counts
2294            .entry(reference.confidence.to_owned())
2295            .or_insert(0) += 1;
2296    }
2297
2298    PmmAssetSummary {
2299        reference_count: references.len(),
2300        high_confidence_count: *confidence_counts.get("high").unwrap_or(&0),
2301        medium_confidence_count: *confidence_counts.get("medium").unwrap_or(&0),
2302        low_confidence_count: *confidence_counts.get("low").unwrap_or(&0),
2303        kind_counts,
2304        extension_counts,
2305        confidence_counts,
2306    }
2307}
2308
2309fn pmm_diagnostics(
2310    references: &[PmmAssetReference],
2311    model_slots: &[PmmModelSlot],
2312    document_summary: Option<&PmmDocumentSummary>,
2313    document_global_summary: Option<&PmmDocumentGlobalSummary>,
2314    display_state: &PmmDisplayState,
2315    model_slot_scan_stop: Option<&PmmModelSlotScanStop>,
2316    data: &[u8],
2317) -> Vec<PmmParserDiagnostic> {
2318    let mut diagnostics = vec![
2319        PmmParserDiagnostic {
2320            level: "warning",
2321            code: "PMM_PROJECT_GRAPH_PARTIAL",
2322            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(),
2323        },
2324        PmmParserDiagnostic {
2325            level: "info",
2326            code: "PMM_ASSET_REFERENCES_SCAN",
2327            message: format!(
2328                "PMM asset references are extracted from decoded null-terminated text chunks and grouped by extension; references={}.",
2329                references.len()
2330            ),
2331        },
2332    ];
2333    if !model_slots.is_empty() {
2334        diagnostics.push(PmmParserDiagnostic {
2335            level: "info",
2336            code: "PMM_MODEL_SLOT_INITIAL_SLICE",
2337            message: format!(
2338                "PMM parser decoded model slot records from the initial fixed-header slice; modelSlots={}.",
2339                model_slots.len()
2340            ),
2341        });
2342    }
2343    if let Some(stop) = model_slot_scan_stop {
2344        diagnostics.push(PmmParserDiagnostic {
2345            level: "info",
2346            code: "PMM_MODEL_SLOT_SCAN_STOPPED",
2347            message: format!(
2348                "PMM model slot header scan stopped at offset {}: {}; decodedSlots={} declaredModelSlots={}.",
2349                stop.offset,
2350                stop.reason,
2351                model_slots.len(),
2352                display_state.non_zero_model_slot_count
2353            ),
2354        });
2355    }
2356    if let Some(summary) = document_summary {
2357        let total_bone_keyframes =
2358            summary.counts.initial_bone_keyframes + summary.counts.bone_keyframes;
2359        let total_morph_keyframes =
2360            summary.counts.initial_morph_keyframes + summary.counts.morph_keyframes;
2361        diagnostics.push(PmmParserDiagnostic {
2362            level: "info",
2363            code: "PMM_DOCUMENT_SUMMARY_PARTIAL",
2364            message: format!(
2365                "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.",
2366                summary.model_count,
2367                total_bone_keyframes,
2368                summary.counts.bone_keyframes,
2369                total_morph_keyframes,
2370                summary.counts.morph_keyframes
2371            ),
2372        });
2373    }
2374    if let Some(summary) = document_global_summary {
2375        diagnostics.push(PmmParserDiagnostic {
2376            level: "info",
2377            code: "PMM_DOCUMENT_GLOBAL_SUMMARY_PARTIAL",
2378            message: format!(
2379                "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.",
2380                summary.camera.initial_keyframes + summary.camera.keyframes,
2381                summary.light.initial_keyframes + summary.light.keyframes,
2382                summary.accessories.accessory_count,
2383                summary.gravity.initial_keyframes + summary.gravity.keyframes,
2384                summary.self_shadow.initial_keyframes + summary.self_shadow.keyframes
2385            ),
2386        });
2387    }
2388    if display_state.layout == "pmm-v2-flags"
2389        && display_state.non_zero_model_slot_count > model_slots.len()
2390    {
2391        diagnostics.push(PmmParserDiagnostic {
2392            level: "warning",
2393            code: "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE",
2394            message: format!(
2395                "PMM displayState reports {} non-zero model slot flags, but only {} initial model slot(s) are decoded.",
2396                display_state.non_zero_model_slot_count,
2397                model_slots.len()
2398            ),
2399        });
2400    }
2401    diagnostics.extend(asset_count_mismatch_diagnostics(references, display_state));
2402    if display_state.layout == "unknown"
2403        && let Some(bytes) = data.get(46..54)
2404        && bytes.iter().any(|byte| *byte > 2)
2405    {
2406        diagnostics.push(PmmParserDiagnostic {
2407            level: "warning",
2408            code: "PMM_DISPLAY_STATE_UNPLAUSIBLE",
2409            message: format!(
2410                "PMM displayState.modelSlotFlags header bytes were outside the observed 0..=2 range and were omitted: {:?}.",
2411                bytes
2412            ),
2413        });
2414    }
2415    diagnostics.extend(duplicate_asset_reference_diagnostics(references));
2416    if references.iter().any(|reference| reference.kind == "model")
2417        && !references
2418            .iter()
2419            .any(|reference| reference.kind == "motion")
2420    {
2421        diagnostics.push(PmmParserDiagnostic {
2422            level: "warning",
2423            code: "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN",
2424            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(),
2425        });
2426    }
2427    diagnostics
2428}
2429
2430fn asset_count_mismatch_diagnostics(
2431    references: &[PmmAssetReference],
2432    display_state: &PmmDisplayState,
2433) -> Vec<PmmParserDiagnostic> {
2434    let mut diagnostics = Vec::new();
2435    let scanned_models = references
2436        .iter()
2437        .filter(|reference| reference.kind == "model")
2438        .count();
2439    if display_state.model_slot_count > 0 && scanned_models != display_state.model_slot_count {
2440        diagnostics.push(PmmParserDiagnostic {
2441            level: "info",
2442            code: "PMM_ASSET_COUNT_MISMATCH",
2443            message: format!(
2444                "PMM displayState declares {} model slot(s), but manifest-derived asset scan found {} model reference(s).",
2445                display_state.model_slot_count,
2446                scanned_models
2447            ),
2448        });
2449    }
2450
2451    if let Some(accessory_count) = display_state.accessory_slot_count {
2452        let scanned_accessories = references
2453            .iter()
2454            .filter(|reference| reference.kind == "accessory")
2455            .count();
2456        if scanned_accessories != accessory_count as usize {
2457            diagnostics.push(PmmParserDiagnostic {
2458                level: "info",
2459                code: "PMM_ASSET_COUNT_MISMATCH",
2460                message: format!(
2461                    "PMM displayState declares {} accessory slot(s), but manifest-derived asset scan found {} accessory reference(s).",
2462                    accessory_count,
2463                    scanned_accessories
2464                ),
2465            });
2466        }
2467    }
2468    diagnostics
2469}
2470
2471fn duplicate_asset_reference_diagnostics(
2472    references: &[PmmAssetReference],
2473) -> Vec<PmmParserDiagnostic> {
2474    let mut by_file = HashMap::<(String, String), Vec<(usize, &PmmAssetReference)>>::new();
2475    for (index, reference) in references.iter().enumerate() {
2476        by_file
2477            .entry((reference.file_name.clone(), reference.extension.clone()))
2478            .or_default()
2479            .push((index, reference));
2480    }
2481
2482    let mut diagnostics = Vec::new();
2483    for ((file_name, extension), entries) in by_file {
2484        if entries.len() < 2 {
2485            continue;
2486        }
2487        let mut unique_paths = entries
2488            .iter()
2489            .map(|(_, reference)| reference.normalized_path.to_ascii_lowercase())
2490            .collect::<Vec<_>>();
2491        unique_paths.sort();
2492        unique_paths.dedup();
2493        if unique_paths.len() < 2 {
2494            continue;
2495        }
2496        let indices = entries
2497            .iter()
2498            .map(|(index, _)| index.to_string())
2499            .collect::<Vec<_>>()
2500            .join(",");
2501        diagnostics.push(PmmParserDiagnostic {
2502            level: "warning",
2503            code: "PMM_ASSET_REFERENCE_DUPLICATE",
2504            message: format!(
2505                "PMM manifest-derived asset scan found duplicate file names with different paths: fileName={file_name:?} extension={extension:?} referenceIndices={indices}."
2506            ),
2507        });
2508    }
2509    diagnostics.sort_by(|a, b| a.message.cmp(&b.message));
2510    diagnostics
2511}
2512
2513#[cfg(test)]
2514mod tests {
2515    use super::*;
2516
2517    fn pmm_with_project_settings() -> Vec<u8> {
2518        let mut data = b"Polygon Movie maker 0002".to_vec();
2519        data.resize(30, 0);
2520        data.extend_from_slice(&1920u32.to_le_bytes());
2521        data.extend_from_slice(&1080u32.to_le_bytes());
2522        data.extend_from_slice(&424u32.to_le_bytes());
2523        data.extend_from_slice(&30.0f32.to_le_bytes());
2524        data.extend_from_slice(&[1, 1, 2, 0, 0, 0, 0, 0]);
2525        data.push(3);
2526        data.push(0);
2527        data.extend_from_slice(
2528            b"UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0UserFile\\Audio\\song.wav\0",
2529        );
2530        data
2531    }
2532
2533    fn pmm_with_initial_model_slot() -> Vec<u8> {
2534        let mut data = b"Polygon Movie maker 0002".to_vec();
2535        data.resize(30, 0);
2536        data.extend_from_slice(&640u32.to_le_bytes());
2537        data.extend_from_slice(&360u32.to_le_bytes());
2538        data.extend_from_slice(&250u32.to_le_bytes());
2539        data.extend_from_slice(&30.0f32.to_le_bytes());
2540        data.extend_from_slice(&[0, 1, 1, 1, 1, 1, 1, 0]);
2541        data.push(1);
2542        data.push(0);
2543        data.push(9);
2544        data.extend_from_slice(b"TestModel");
2545        data.push(4);
2546        data.extend_from_slice(b"Base");
2547        data.extend_from_slice(b"F:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx");
2548        data.push(0);
2549        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
2550        data
2551    }
2552
2553    fn append_pmm_model_slot(data: &mut Vec<u8>, name: &[u8], english_name: &[u8], path: &[u8]) {
2554        data.push(name.len() as u8);
2555        data.extend_from_slice(name);
2556        data.push(english_name.len() as u8);
2557        data.extend_from_slice(english_name);
2558        data.extend_from_slice(path);
2559        data.push(0);
2560    }
2561
2562    fn push_i32(data: &mut Vec<u8>, value: i32) {
2563        data.extend_from_slice(&value.to_le_bytes());
2564    }
2565
2566    fn push_f32(data: &mut Vec<u8>, value: f32) {
2567        data.extend_from_slice(&value.to_le_bytes());
2568    }
2569
2570    fn push_pmm_variable_string(data: &mut Vec<u8>, value: &[u8]) {
2571        data.push(value.len() as u8);
2572        data.extend_from_slice(value);
2573    }
2574
2575    fn push_pmm_fixed_path(data: &mut Vec<u8>, value: &[u8]) {
2576        let mut path = [0u8; 256];
2577        path[..value.len()].copy_from_slice(value);
2578        data.extend_from_slice(&path);
2579    }
2580
2581    fn push_pmm_fixed_bytes<const N: usize>(data: &mut Vec<u8>, value: &[u8]) {
2582        let mut bytes = [0u8; N];
2583        bytes[..value.len()].copy_from_slice(value);
2584        data.extend_from_slice(&bytes);
2585    }
2586
2587    fn push_document_bone_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2588        if include_index {
2589            push_i32(data, 0);
2590        }
2591        push_i32(data, frame);
2592        push_i32(data, 0);
2593        push_i32(data, 0);
2594        data.extend_from_slice(&[20u8; 16]);
2595        data.extend_from_slice(&[0.0f32.to_le_bytes(); 3].concat());
2596        push_f32(data, 0.0);
2597        push_f32(data, 0.0);
2598        push_f32(data, 0.0);
2599        push_f32(data, 1.0);
2600        data.push(0);
2601        data.push(0);
2602    }
2603
2604    fn push_document_morph_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2605        if include_index {
2606            push_i32(data, 0);
2607        }
2608        push_i32(data, frame);
2609        push_i32(data, 0);
2610        push_i32(data, 0);
2611        push_f32(data, 0.5);
2612        data.push(0);
2613    }
2614
2615    fn push_document_model_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2616        if include_index {
2617            push_i32(data, frame);
2618        }
2619        push_i32(data, frame);
2620        push_i32(data, 0);
2621        push_i32(data, 0);
2622        data.push(1);
2623        data.push(1);
2624        push_i32(data, 2);
2625        push_i32(data, 3);
2626        data.push(1);
2627    }
2628
2629    fn push_document_base_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2630        if include_index {
2631            push_i32(data, frame);
2632        }
2633        push_i32(data, frame);
2634        push_i32(data, 0);
2635        push_i32(data, 0);
2636    }
2637
2638    fn push_document_camera_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2639        push_document_base_keyframe(data, include_index, frame);
2640        push_f32(data, 45.0);
2641        data.extend_from_slice(&[0u8; 12]);
2642        data.extend_from_slice(&[0u8; 12]);
2643        push_i32(data, -1);
2644        push_i32(data, -1);
2645        data.extend_from_slice(&[20u8; 24]);
2646        data.push(0);
2647        push_i32(data, 30);
2648        data.push(0);
2649    }
2650
2651    fn push_document_light_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2652        push_document_base_keyframe(data, include_index, frame);
2653        for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
2654            push_f32(data, value);
2655        }
2656        data.push(0);
2657    }
2658
2659    fn push_document_gravity_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2660        push_document_base_keyframe(data, include_index, frame);
2661        data.push(0);
2662        push_i32(data, 10);
2663        push_f32(data, 9.8);
2664        for value in [0.0f32, -1.0, 0.0] {
2665            push_f32(data, value);
2666        }
2667        data.push(0);
2668    }
2669
2670    fn push_document_self_shadow_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2671        push_document_base_keyframe(data, include_index, frame);
2672        data.push(1);
2673        push_f32(data, 8875.0);
2674        data.push(0);
2675    }
2676
2677    fn push_document_accessory_keyframe(data: &mut Vec<u8>, include_index: bool, frame: i32) {
2678        push_document_base_keyframe(data, include_index, frame);
2679        data.push(1);
2680        push_i32(data, -1);
2681        push_i32(data, -1);
2682        for value in [1.0f32, 2.0, 3.0, 0.1, 0.2, 0.3] {
2683            push_f32(data, value);
2684        }
2685        push_f32(data, 10.0);
2686        data.push(1);
2687        data.push(0);
2688    }
2689
2690    fn push_empty_pmm_path(data: &mut Vec<u8>) {
2691        data.extend_from_slice(&[0u8; 256]);
2692    }
2693
2694    fn pmm_with_document_summary() -> Vec<u8> {
2695        let mut data = b"Polygon Movie maker 0002".to_vec();
2696        data.resize(30, 0);
2697        push_i32(&mut data, 640);
2698        push_i32(&mut data, 360);
2699        push_i32(&mut data, 120);
2700        push_f32(&mut data, 30.0);
2701        data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0]);
2702        data.push(0);
2703        data.push(1);
2704        data.push(0);
2705        push_pmm_variable_string(&mut data, b"Miku");
2706        push_pmm_variable_string(&mut data, b"MikuEn");
2707        push_pmm_fixed_path(&mut data, b"UserFile\\Model\\miku.pmx");
2708        data.push(0);
2709        push_i32(&mut data, 1);
2710        push_pmm_variable_string(&mut data, b"center");
2711        push_i32(&mut data, 1);
2712        push_pmm_variable_string(&mut data, b"smile");
2713        push_i32(&mut data, 1);
2714        push_i32(&mut data, 7);
2715        push_i32(&mut data, 1);
2716        push_i32(&mut data, 8);
2717        data.push(0);
2718        data.push(1);
2719        push_i32(&mut data, 0);
2720        for _ in 0..4 {
2721            push_i32(&mut data, -1);
2722        }
2723        data.push(0);
2724        push_i32(&mut data, 0);
2725        push_i32(&mut data, 30);
2726        push_document_bone_keyframe(&mut data, false, 0);
2727        push_i32(&mut data, 1);
2728        push_document_bone_keyframe(&mut data, true, 30);
2729        push_document_morph_keyframe(&mut data, false, 0);
2730        push_i32(&mut data, 1);
2731        push_document_morph_keyframe(&mut data, true, 15);
2732        push_document_model_keyframe(&mut data, false, 0);
2733        push_i32(&mut data, 1);
2734        push_document_model_keyframe(&mut data, true, 30);
2735        // bone state: translation [1.0, 2.0, 3.0], orientation [0.0, 0.0, 0.0, 1.0], dirty=true, physicsDisabled=false, selected=true
2736        push_f32(&mut data, 1.0);
2737        push_f32(&mut data, 2.0);
2738        push_f32(&mut data, 3.0);
2739        push_f32(&mut data, 0.0);
2740        push_f32(&mut data, 0.0);
2741        push_f32(&mut data, 0.0);
2742        push_f32(&mut data, 1.0);
2743        data.push(1); // dirty
2744        data.push(0); // physicsDisabled
2745        data.push(1); // selected
2746        // morph state: weight 0.75
2747        push_f32(&mut data, 0.75);
2748        // constraint state: enabled=true
2749        data.push(1);
2750        // outside parent state: parentModelIndex=0, parentModelBoneIndex=5, subjectBoneIndex=7, targetModelIndex=1
2751        push_i32(&mut data, 0);
2752        push_i32(&mut data, 5);
2753        push_i32(&mut data, 7);
2754        push_i32(&mut data, 1);
2755        data.push(1);
2756        push_f32(&mut data, 1.0);
2757        data.push(0);
2758        data.push(0);
2759        data.extend_from_slice(b"UserFile\\Model\\miku.pmx\0");
2760        data
2761    }
2762
2763    fn pmm_with_document_global_summary() -> Vec<u8> {
2764        let mut data = pmm_with_document_summary();
2765        let trailing_reference = b"UserFile\\Model\\miku.pmx\0";
2766        data.truncate(data.len() - trailing_reference.len());
2767
2768        push_document_camera_keyframe(&mut data, false, 0);
2769        push_i32(&mut data, 1);
2770        push_document_camera_keyframe(&mut data, true, 42);
2771        data.extend_from_slice(&[0u8; 12 * 3]);
2772        data.push(0);
2773
2774        push_document_light_keyframe(&mut data, false, 0);
2775        push_i32(&mut data, 1);
2776        push_document_light_keyframe(&mut data, true, 43);
2777        for value in [0.6f32, 0.6, 0.6, -0.5, -1.0, 0.5] {
2778            push_f32(&mut data, value);
2779        }
2780
2781        data.push(0);
2782        push_i32(&mut data, 0);
2783        data.push(1);
2784        data.push(0);
2785        push_pmm_fixed_bytes::<100>(&mut data, b"Stage");
2786        push_pmm_fixed_path(&mut data, b"UserFile\\Accessory\\stage.x");
2787        data.push(0);
2788        push_document_accessory_keyframe(&mut data, false, 0);
2789        push_i32(&mut data, 1);
2790        push_document_accessory_keyframe(&mut data, true, 46);
2791        data.push(1);
2792        push_i32(&mut data, -1);
2793        push_i32(&mut data, -1);
2794        for value in [1.0f32, 2.0, 3.0] {
2795            push_f32(&mut data, value);
2796        }
2797        push_f32(&mut data, 10.0);
2798        for value in [0.1f32, 0.2, 0.3] {
2799            push_f32(&mut data, value);
2800        }
2801        data.push(1);
2802        data.push(0);
2803
2804        push_i32(&mut data, 12);
2805        push_i32(&mut data, 34);
2806        push_i32(&mut data, 56);
2807        push_i32(&mut data, 2);
2808        data.push(0);
2809        data.push(1);
2810        data.push(0);
2811        data.push(1);
2812        push_i32(&mut data, 3);
2813        push_i32(&mut data, 120);
2814        data.push(1);
2815        push_empty_pmm_path(&mut data);
2816        push_i32(&mut data, 0);
2817        push_i32(&mut data, 0);
2818        push_f32(&mut data, 1.0);
2819        push_empty_pmm_path(&mut data);
2820        push_i32(&mut data, 0);
2821        push_i32(&mut data, 0);
2822        push_i32(&mut data, 0);
2823        push_f32(&mut data, 1.0);
2824        push_empty_pmm_path(&mut data);
2825        data.push(0);
2826        data.push(1);
2827        data.push(1);
2828        data.push(1);
2829        push_f32(&mut data, 60.0);
2830        push_i32(&mut data, 2);
2831        push_i32(&mut data, -1);
2832        push_f32(&mut data, 1.0);
2833        data.push(1);
2834        data.push(2);
2835
2836        push_f32(&mut data, 9.8);
2837        push_i32(&mut data, 10);
2838        for value in [0.0f32, -1.0, 0.0] {
2839            push_f32(&mut data, value);
2840        }
2841        data.push(0);
2842        push_document_gravity_keyframe(&mut data, false, 0);
2843        push_i32(&mut data, 1);
2844        push_document_gravity_keyframe(&mut data, true, 44);
2845
2846        data.push(1);
2847        push_f32(&mut data, 8875.0);
2848        push_document_self_shadow_keyframe(&mut data, false, 0);
2849        push_i32(&mut data, 1);
2850        push_document_self_shadow_keyframe(&mut data, true, 45);
2851
2852        push_i32(&mut data, 0);
2853        push_i32(&mut data, 0);
2854        push_i32(&mut data, 0);
2855        data.push(0);
2856        push_i32(&mut data, -1);
2857        push_i32(&mut data, -1);
2858        data.extend_from_slice(&[0u8; 16 * 4]);
2859        data.push(0);
2860        data.push(0);
2861        data.push(1);
2862        push_i32(&mut data, 12);
2863        data.push(1);
2864        data.push(0);
2865        push_i32(&mut data, 0);
2866
2867        data
2868    }
2869
2870    fn pmm_with_two_initial_model_slots() -> Vec<u8> {
2871        let mut data = b"Polygon Movie maker 0002".to_vec();
2872        data.resize(30, 0);
2873        data.extend_from_slice(&640u32.to_le_bytes());
2874        data.extend_from_slice(&360u32.to_le_bytes());
2875        data.extend_from_slice(&250u32.to_le_bytes());
2876        data.extend_from_slice(&30.0f32.to_le_bytes());
2877        data.extend_from_slice(&[1, 1, 0, 0, 0, 0, 0, 0]);
2878        data.push(1);
2879        data.push(0);
2880        append_pmm_model_slot(
2881            &mut data,
2882            b"TestModel",
2883            b"Base",
2884            b"F:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx",
2885        );
2886        append_pmm_model_slot(
2887            &mut data,
2888            b"Sour",
2889            b"Sour",
2890            b"G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx",
2891        );
2892        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
2893        data
2894    }
2895
2896    fn pmm_v1_with_display_counts() -> Vec<u8> {
2897        let mut data = b"Polygon Movie maker 0001".to_vec();
2898        data.resize(30, 0);
2899        data.extend_from_slice(&512u32.to_le_bytes());
2900        data.extend_from_slice(&288u32.to_le_bytes());
2901        data.extend_from_slice(&435u32.to_le_bytes());
2902        data.extend_from_slice(&35.0f32.to_le_bytes());
2903        data.extend_from_slice(&[1, 1, 1, 1, 1, 1]);
2904        data.push(8);
2905        data.push(9);
2906        data.extend_from_slice(b"Dummy\0 maker Dummy\0");
2907        data.extend_from_slice(b"UserFile\\Model\\miku.pmd\0UserFile\\Accessory\\stage01.x\0");
2908        data
2909    }
2910
2911    fn json_keys(value: &serde_json::Value) -> Vec<String> {
2912        let mut keys = value
2913            .as_object()
2914            .unwrap()
2915            .keys()
2916            .map(ToOwned::to_owned)
2917            .collect::<Vec<_>>();
2918        keys.sort();
2919        keys
2920    }
2921
2922    #[test]
2923    fn pmm_manifest_json_top_level_schema_is_stable() {
2924        let data =
2925            b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
2926        let parsed = parse_pmm_manifest(data).unwrap();
2927        let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
2928
2929        assert_eq!(
2930            keys,
2931            vec![
2932                "accessoryAssets",
2933                "accessoryPaths",
2934                "assetReferences",
2935                "assetSummary",
2936                "audioAssets",
2937                "audioPaths",
2938                "byteLength",
2939                "diagnostics",
2940                "displayState",
2941                "documentGlobalSummary",
2942                "documentSummary",
2943                "headerTextEntries",
2944                "imageAssets",
2945                "imagePaths",
2946                "modelAssets",
2947                "modelPaths",
2948                "modelSlots",
2949                "motionAssets",
2950                "motionPaths",
2951                "parsedVersion",
2952                "projectSettings",
2953                "signature",
2954                "timeline",
2955                "version",
2956                "videoAssets",
2957                "videoPaths",
2958            ]
2959        );
2960    }
2961
2962    #[test]
2963    fn pmm_header_text_entry_json_schema_is_stable() {
2964        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
2965        let keys = json_keys(&serde_json::to_value(&parsed.header_text_entries[0]).unwrap());
2966
2967        assert_eq!(
2968            keys,
2969            vec!["index", "offset", "offsetEnd", "text", "textBytes"]
2970        );
2971    }
2972
2973    #[test]
2974    fn pmm_model_slot_json_schema_is_stable() {
2975        let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
2976        let keys = json_keys(&serde_json::to_value(&parsed.model_slots[0]).unwrap());
2977
2978        assert_eq!(
2979            keys,
2980            vec![
2981                "assetReferenceIndex",
2982                "confidence",
2983                "displaySlotIndex",
2984                "englishName",
2985                "englishNameBytes",
2986                "modelPath",
2987                "modelPathOffset",
2988                "name",
2989                "nameBytes",
2990                "nextNonZeroOffset",
2991                "normalizedPath",
2992                "offset",
2993                "offsetEnd",
2994                "slotIndex",
2995                "trailingZeroPaddingBytes"
2996            ]
2997        );
2998    }
2999
3000    #[test]
3001    fn pmm_project_settings_json_schema_is_stable() {
3002        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3003        let keys = json_keys(&serde_json::to_value(&parsed.project_settings).unwrap());
3004
3005        assert_eq!(
3006            keys,
3007            vec![
3008                "frameRate",
3009                "screenHeight",
3010                "screenWidth",
3011                "timelineFrameCount",
3012            ]
3013        );
3014    }
3015
3016    #[test]
3017    fn pmm_display_state_json_schema_is_stable() {
3018        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3019        let keys = json_keys(&serde_json::to_value(&parsed.display_state).unwrap());
3020
3021        assert_eq!(
3022            keys,
3023            vec![
3024                "accessorySlotCount",
3025                "activeModelSlotIndices",
3026                "declaredModelSlotCount",
3027                "documentExpandFlags",
3028                "documentModelCount",
3029                "emptyModelSlotIndices",
3030                "layout",
3031                "modelSlotCount",
3032                "modelSlotFlagCounts",
3033                "modelSlotFlagEntries",
3034                "modelSlotFlags",
3035                "nonZeroModelSlotCount",
3036                "selectedModelIndex"
3037            ]
3038        );
3039    }
3040
3041    #[test]
3042    fn pmm_model_slot_flag_entry_json_schema_is_stable() {
3043        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3044        let keys = json_keys(
3045            &serde_json::to_value(&parsed.display_state.model_slot_flag_entries[0]).unwrap(),
3046        );
3047
3048        assert_eq!(keys, vec!["active", "flag", "slotIndex"]);
3049    }
3050
3051    #[test]
3052    fn pmm_document_expand_flags_json_schema_is_stable() {
3053        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3054        let keys = json_keys(
3055            &serde_json::to_value(parsed.display_state.document_expand_flags.unwrap()).unwrap(),
3056        );
3057
3058        assert_eq!(
3059            keys,
3060            vec![
3061                "accessoryPanel",
3062                "bonePanel",
3063                "cameraPanel",
3064                "editingCla",
3065                "lightPanel",
3066                "morphPanel",
3067                "selfShadowPanel"
3068            ]
3069        );
3070    }
3071
3072    #[test]
3073    fn pmm_timeline_json_schema_is_stable() {
3074        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3075        let keys = json_keys(&serde_json::to_value(&parsed.timeline).unwrap());
3076
3077        assert_eq!(
3078            keys,
3079            vec![
3080                "durationSeconds",
3081                "endFrameExclusive",
3082                "frameCount",
3083                "frameRate",
3084                "startFrame"
3085            ]
3086        );
3087    }
3088
3089    #[test]
3090    fn pmm_scene_asset_json_schema_is_stable() {
3091        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3092        let keys = json_keys(&serde_json::to_value(&parsed.model_assets[0]).unwrap());
3093
3094        assert_eq!(
3095            keys,
3096            vec![
3097                "confidence",
3098                "extension",
3099                "fileName",
3100                "kindIndex",
3101                "normalizedPath",
3102                "offset",
3103                "offsetEnd",
3104                "path",
3105                "referenceIndex"
3106            ]
3107        );
3108    }
3109
3110    #[test]
3111    fn pmm_asset_reference_json_schema_is_stable() {
3112        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3113        let keys = json_keys(&serde_json::to_value(&parsed.asset_references[0]).unwrap());
3114
3115        assert_eq!(
3116            keys,
3117            vec![
3118                "confidence",
3119                "extension",
3120                "fileName",
3121                "kind",
3122                "normalizedPath",
3123                "offset",
3124                "offsetEnd",
3125                "path"
3126            ]
3127        );
3128    }
3129
3130    #[test]
3131    fn pmm_asset_summary_json_schema_is_stable() {
3132        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3133        let keys = json_keys(&serde_json::to_value(&parsed.asset_summary).unwrap());
3134
3135        assert_eq!(
3136            keys,
3137            vec![
3138                "confidenceCounts",
3139                "extensionCounts",
3140                "highConfidenceCount",
3141                "kindCounts",
3142                "lowConfidenceCount",
3143                "mediumConfidenceCount",
3144                "referenceCount"
3145            ]
3146        );
3147    }
3148
3149    #[test]
3150    fn pmm_document_summary_json_schema_is_stable() {
3151        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3152        let keys = json_keys(&serde_json::to_value(parsed.document_summary.unwrap()).unwrap());
3153
3154        assert_eq!(
3155            keys,
3156            vec![
3157                "counts",
3158                "modelCount",
3159                "models",
3160                "selectedModelIndex",
3161                "source"
3162            ]
3163        );
3164    }
3165
3166    #[test]
3167    fn pmm_document_counts_json_schema_is_stable() {
3168        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3169        let document = parsed.document_summary.unwrap();
3170        let keys = json_keys(&serde_json::to_value(document.counts).unwrap());
3171
3172        assert_eq!(
3173            keys,
3174            vec![
3175                "boneKeyframes",
3176                "bones",
3177                "initialBoneKeyframes",
3178                "initialModelKeyframes",
3179                "initialMorphKeyframes",
3180                "modelKeyframes",
3181                "models",
3182                "morphKeyframes",
3183                "morphs"
3184            ]
3185        );
3186    }
3187
3188    #[test]
3189    fn pmm_document_global_summary_json_schema_is_stable() {
3190        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3191        let keys =
3192            json_keys(&serde_json::to_value(parsed.document_global_summary.unwrap()).unwrap());
3193
3194        assert_eq!(
3195            keys,
3196            vec![
3197                "accessories",
3198                "camera",
3199                "gravity",
3200                "light",
3201                "offset",
3202                "offsetEnd",
3203                "selfShadow",
3204                "settings",
3205                "source"
3206            ]
3207        );
3208    }
3209
3210    #[test]
3211    fn pmm_initial_camera_keyframe_json_schema_is_stable() {
3212        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3213        let global = parsed.document_global_summary.unwrap();
3214        let keys =
3215            json_keys(&serde_json::to_value(global.camera.initial_keyframe.unwrap()).unwrap());
3216
3217        assert_eq!(
3218            keys,
3219            vec![
3220                "angle",
3221                "distance",
3222                "fov",
3223                "frameIndex",
3224                "index",
3225                "kind",
3226                "lookAt",
3227                "nextKeyframeIndex",
3228                "parentModelBoneIndex",
3229                "parentModelIndex",
3230                "perspectiveView",
3231                "previousKeyframeIndex",
3232                "selected"
3233            ]
3234        );
3235    }
3236
3237    #[test]
3238    fn pmm_initial_light_keyframe_json_schema_is_stable() {
3239        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3240        let global = parsed.document_global_summary.unwrap();
3241        let keys =
3242            json_keys(&serde_json::to_value(global.light.initial_keyframe.unwrap()).unwrap());
3243
3244        assert_eq!(
3245            keys,
3246            vec![
3247                "color",
3248                "direction",
3249                "frameIndex",
3250                "index",
3251                "kind",
3252                "nextKeyframeIndex",
3253                "previousKeyframeIndex",
3254                "selected"
3255            ]
3256        );
3257    }
3258
3259    #[test]
3260    fn pmm_initial_gravity_keyframe_json_schema_is_stable() {
3261        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3262        let global = parsed.document_global_summary.unwrap();
3263        let keys =
3264            json_keys(&serde_json::to_value(global.gravity.initial_keyframe.unwrap()).unwrap());
3265
3266        assert_eq!(
3267            keys,
3268            vec![
3269                "acceleration",
3270                "direction",
3271                "frameIndex",
3272                "index",
3273                "kind",
3274                "nextKeyframeIndex",
3275                "noise",
3276                "noiseEnabled",
3277                "previousKeyframeIndex",
3278                "selected"
3279            ]
3280        );
3281    }
3282
3283    #[test]
3284    fn pmm_initial_self_shadow_keyframe_json_schema_is_stable() {
3285        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3286        let global = parsed.document_global_summary.unwrap();
3287        let keys =
3288            json_keys(&serde_json::to_value(global.self_shadow.initial_keyframe.unwrap()).unwrap());
3289
3290        assert_eq!(
3291            keys,
3292            vec![
3293                "distance",
3294                "frameIndex",
3295                "index",
3296                "kind",
3297                "mode",
3298                "nextKeyframeIndex",
3299                "previousKeyframeIndex",
3300                "selected"
3301            ]
3302        );
3303    }
3304
3305    #[test]
3306    fn pmm_document_track_summary_json_schema_is_stable() {
3307        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3308        let global = parsed.document_global_summary.unwrap();
3309        let keys = json_keys(&serde_json::to_value(global.camera).unwrap());
3310
3311        assert_eq!(
3312            keys,
3313            vec![
3314                "initialKeyframe",
3315                "initialKeyframes",
3316                "keyframeCountOffset",
3317                "keyframeSummaries",
3318                "keyframes",
3319                "keyframesEndOffset",
3320                "keyframesOffset",
3321                "offset",
3322                "offsetEnd",
3323                "stateEndOffset",
3324                "stateOffset"
3325            ]
3326        );
3327    }
3328
3329    #[test]
3330    fn pmm_document_settings_summary_json_schema_is_stable() {
3331        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3332        let global = parsed.document_global_summary.unwrap();
3333        let keys = json_keys(&serde_json::to_value(global.settings).unwrap());
3334
3335        assert_eq!(
3336            keys,
3337            vec![
3338                "accessoryIndexAfterModels",
3339                "audioEnabled",
3340                "audioPath",
3341                "backgroundImageEnabled",
3342                "backgroundImageOffset",
3343                "backgroundImagePath",
3344                "backgroundImageScaleFactor",
3345                "backgroundVideoEnabled",
3346                "backgroundVideoOffset",
3347                "backgroundVideoPath",
3348                "backgroundVideoScaleFactor",
3349                "beginFrameIndex",
3350                "beginFrameIndexEnabled",
3351                "blackBackgroundEnabled",
3352                "cameraLookAtModelBoneIndex",
3353                "cameraLookAtModelIndex",
3354                "cameraLookMode",
3355                "currentFrameIndex",
3356                "currentFrameIndexInTextField",
3357                "edgeColor",
3358                "editingMode",
3359                "endFrameIndex",
3360                "endFrameIndexEnabled",
3361                "followingLookAtEnabled",
3362                "gridAndAxisShown",
3363                "groundShadowBrightness",
3364                "groundShadowShown",
3365                "horizontalScroll",
3366                "horizontalScrollThumb",
3367                "informationShown",
3368                "loopEnabled",
3369                "modelSelectionFooterEndOffset",
3370                "modelSelectionFooterOffset",
3371                "modelSelectionFooterPresent",
3372                "offset",
3373                "offsetEnd",
3374                "physicsGroundEnabled",
3375                "physicsSimulationMode",
3376                "preferredFps",
3377                "screenCaptureMode",
3378                "translucentGroundShadowEnabled",
3379                "unknownMatrixEndOffset",
3380                "unknownMatrixOffset"
3381            ]
3382        );
3383    }
3384
3385    #[test]
3386    fn pmm_document_accessory_summary_json_schema_is_stable() {
3387        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3388        let global = parsed.document_global_summary.unwrap();
3389        let keys = json_keys(&serde_json::to_value(&global.accessories.accessories[0]).unwrap());
3390
3391        assert_eq!(
3392            keys,
3393            vec![
3394                "addBlendEnabled",
3395                "assetReferenceIndex",
3396                "documentAccessoryIndex",
3397                "drawOrderIndex",
3398                "initialKeyframe",
3399                "keyframeCountOffset",
3400                "keyframeSummaries",
3401                "keyframes",
3402                "keyframesEndOffset",
3403                "keyframesOffset",
3404                "name",
3405                "offset",
3406                "offsetEnd",
3407                "opacity",
3408                "parentModelBoneIndex",
3409                "parentModelIndex",
3410                "path",
3411                "pathOffset",
3412                "scaleFactor",
3413                "shadowEnabled",
3414                "slotIndex",
3415                "stateEndOffset",
3416                "stateOffset",
3417                "visible"
3418            ]
3419        );
3420    }
3421
3422    #[test]
3423    fn pmm_document_accessory_keyframe_summary_json_schema_is_stable() {
3424        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3425        let global = parsed.document_global_summary.unwrap();
3426        let keys = json_keys(
3427            &serde_json::to_value(&global.accessories.accessories[0].initial_keyframe).unwrap(),
3428        );
3429
3430        assert_eq!(
3431            keys,
3432            vec![
3433                "frameIndex",
3434                "index",
3435                "nextKeyframeIndex",
3436                "opacity",
3437                "orientation",
3438                "parentModelBoneIndex",
3439                "parentModelIndex",
3440                "previousKeyframeIndex",
3441                "scaleFactor",
3442                "selected",
3443                "shadowEnabled",
3444                "translation",
3445                "visible"
3446            ]
3447        );
3448    }
3449
3450    #[test]
3451    fn parses_pmm_v2_document_global_summary() {
3452        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
3453        let global = parsed.document_global_summary.as_ref().unwrap();
3454
3455        assert_eq!(global.source, "nanoem/ext/document.c PMMv2 global layout");
3456        assert_eq!(global.camera.initial_keyframes, 1);
3457        assert_eq!(global.camera.keyframes, 1);
3458        match global.camera.initial_keyframe.as_ref().unwrap() {
3459            PmmDocumentKeyframeSummary::Camera {
3460                index,
3461                frame_index,
3462                distance,
3463                look_at,
3464                angle,
3465                parent_model_index,
3466                parent_model_bone_index,
3467                perspective_view,
3468                fov,
3469                selected,
3470                ..
3471            } => {
3472                assert_eq!(*index, None);
3473                assert_eq!(*frame_index, 0);
3474                assert_eq!(*distance, 45.0);
3475                assert_eq!(*look_at, [0.0, 0.0, 0.0]);
3476                assert_eq!(*angle, [0.0, 0.0, 0.0]);
3477                assert_eq!(*parent_model_index, -1);
3478                assert_eq!(*parent_model_bone_index, -1);
3479                assert!(*perspective_view);
3480                assert_eq!(*fov, 30);
3481                assert!(!*selected);
3482            }
3483            other => panic!("unexpected camera keyframe summary: {other:?}"),
3484        }
3485        match &global.camera.keyframe_summaries[0] {
3486            PmmDocumentKeyframeSummary::Camera {
3487                index, frame_index, ..
3488            } => {
3489                assert_eq!(*index, Some(42));
3490                assert_eq!(*frame_index, 42);
3491            }
3492            other => panic!("unexpected additional camera keyframe summary: {other:?}"),
3493        }
3494        assert_eq!(global.light.initial_keyframes, 1);
3495        assert_eq!(global.light.keyframes, 1);
3496        match global.light.initial_keyframe.as_ref().unwrap() {
3497            PmmDocumentKeyframeSummary::Light {
3498                color,
3499                direction,
3500                selected,
3501                ..
3502            } => {
3503                assert_eq!(*color, [0.6, 0.6, 0.6]);
3504                assert_eq!(*direction, [-0.5, -1.0, 0.5]);
3505                assert!(!*selected);
3506            }
3507            other => panic!("unexpected light keyframe summary: {other:?}"),
3508        }
3509        match &global.light.keyframe_summaries[0] {
3510            PmmDocumentKeyframeSummary::Light {
3511                index, frame_index, ..
3512            } => {
3513                assert_eq!(*index, Some(43));
3514                assert_eq!(*frame_index, 43);
3515            }
3516            other => panic!("unexpected additional light keyframe summary: {other:?}"),
3517        }
3518        assert_eq!(global.accessories.accessory_count, 1);
3519        assert_eq!(global.accessories.keyframes, 1);
3520        let accessory = &global.accessories.accessories[0];
3521        assert_eq!(accessory.slot_index, 0);
3522        assert_eq!(accessory.document_accessory_index, 0);
3523        assert_eq!(accessory.name, "Stage");
3524        assert_eq!(accessory.path, "UserFile\\Accessory\\stage.x");
3525        assert_eq!(accessory.keyframes, 1);
3526        assert!(accessory.visible);
3527        assert_eq!(accessory.opacity, 1.0);
3528        assert_eq!(accessory.scale_factor, 10.0);
3529        assert!(accessory.shadow_enabled);
3530        assert!(!accessory.add_blend_enabled);
3531        assert_eq!(accessory.initial_keyframe.index, None);
3532        assert_eq!(accessory.initial_keyframe.frame_index, 0);
3533        assert_eq!(accessory.initial_keyframe.translation, [1.0, 2.0, 3.0]);
3534        assert_eq!(accessory.initial_keyframe.orientation, [0.1, 0.2, 0.3]);
3535        assert_eq!(accessory.keyframe_summaries[0].index, Some(46));
3536        assert_eq!(accessory.keyframe_summaries[0].frame_index, 46);
3537        assert_eq!(global.gravity.initial_keyframes, 1);
3538        assert_eq!(global.gravity.keyframes, 1);
3539        match global.gravity.initial_keyframe.as_ref().unwrap() {
3540            PmmDocumentKeyframeSummary::Gravity {
3541                noise_enabled,
3542                noise,
3543                acceleration,
3544                direction,
3545                selected,
3546                ..
3547            } => {
3548                assert!(!*noise_enabled);
3549                assert_eq!(*noise, 10);
3550                assert_eq!(*acceleration, 9.8);
3551                assert_eq!(*direction, [0.0, -1.0, 0.0]);
3552                assert!(!*selected);
3553            }
3554            other => panic!("unexpected gravity keyframe summary: {other:?}"),
3555        }
3556        match &global.gravity.keyframe_summaries[0] {
3557            PmmDocumentKeyframeSummary::Gravity {
3558                index, frame_index, ..
3559            } => {
3560                assert_eq!(*index, Some(44));
3561                assert_eq!(*frame_index, 44);
3562            }
3563            other => panic!("unexpected additional gravity keyframe summary: {other:?}"),
3564        }
3565        assert_eq!(global.self_shadow.initial_keyframes, 1);
3566        assert_eq!(global.self_shadow.keyframes, 1);
3567        match global.self_shadow.initial_keyframe.as_ref().unwrap() {
3568            PmmDocumentKeyframeSummary::SelfShadow {
3569                mode,
3570                distance,
3571                selected,
3572                ..
3573            } => {
3574                assert_eq!(*mode, 1);
3575                assert_eq!(*distance, 8875.0);
3576                assert!(!*selected);
3577            }
3578            other => panic!("unexpected self-shadow keyframe summary: {other:?}"),
3579        }
3580        match &global.self_shadow.keyframe_summaries[0] {
3581            PmmDocumentKeyframeSummary::SelfShadow {
3582                index, frame_index, ..
3583            } => {
3584                assert_eq!(*index, Some(45));
3585                assert_eq!(*frame_index, 45);
3586            }
3587            other => panic!("unexpected additional self-shadow keyframe summary: {other:?}"),
3588        }
3589        assert_eq!(global.settings.current_frame_index, 12);
3590        assert_eq!(global.settings.horizontal_scroll, 34);
3591        assert_eq!(global.settings.horizontal_scroll_thumb, 56);
3592        assert_eq!(global.settings.begin_frame_index, 3);
3593        assert_eq!(global.settings.end_frame_index, 120);
3594        assert!(global.settings.audio_enabled);
3595        assert!(global.settings.grid_and_axis_shown);
3596        assert!(global.settings.physics_ground_enabled);
3597        assert!(global.settings.model_selection_footer_present);
3598        assert!(global.offset_end > global.offset);
3599        assert!(global.camera.offset < global.light.offset);
3600        assert!(global.light.offset < global.accessories.offset);
3601        assert!(global.gravity.offset < global.self_shadow.offset);
3602    }
3603
3604    #[test]
3605    fn pmm_document_model_summary_json_schema_is_stable() {
3606        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3607        let document = parsed.document_summary.unwrap();
3608        let keys = json_keys(&serde_json::to_value(&document.models[0]).unwrap());
3609
3610        assert_eq!(
3611            keys,
3612            vec![
3613                "assetReferenceIndex",
3614                "blendEnabled",
3615                "boneCount",
3616                "boneKeyframeSummaries",
3617                "boneKeyframes",
3618                "boneStateSummaries",
3619                "constraintBoneCount",
3620                "constraintStateSummaries",
3621                "documentModelIndex",
3622                "drawOrderIndex",
3623                "edgeWidth",
3624                "englishName",
3625                "initialBoneKeyframeSummaries",
3626                "initialBoneKeyframes",
3627                "initialModelKeyframe",
3628                "initialModelKeyframes",
3629                "initialMorphKeyframeSummaries",
3630                "initialMorphKeyframes",
3631                "lastFrameIndex",
3632                "modelKeyframeSummaries",
3633                "modelKeyframes",
3634                "morphCount",
3635                "morphKeyframeSummaries",
3636                "morphKeyframes",
3637                "morphStateSummaries",
3638                "name",
3639                "offset",
3640                "offsetEnd",
3641                "outsideParentStateSummaries",
3642                "outsideParentSubjectBoneCount",
3643                "path",
3644                "pathOffset",
3645                "sections",
3646                "selectedBoneIndex",
3647                "selectedMorphIndices",
3648                "selfShadowEnabled",
3649                "slotIndex",
3650                "transformOrderIndex",
3651                "verticalScroll",
3652                "visible"
3653            ]
3654        );
3655    }
3656
3657    #[test]
3658    fn pmm_document_model_keyframe_summary_json_schema_is_stable() {
3659        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3660        let document = parsed.document_summary.unwrap();
3661        let keys =
3662            json_keys(&serde_json::to_value(&document.models[0].initial_model_keyframe).unwrap());
3663
3664        assert_eq!(
3665            keys,
3666            vec![
3667                "constraintStates",
3668                "frameIndex",
3669                "index",
3670                "nextKeyframeIndex",
3671                "outsideParentIndices",
3672                "previousKeyframeIndex",
3673                "selfShadowEnabled",
3674                "visible"
3675            ]
3676        );
3677    }
3678
3679    #[test]
3680    fn pmm_outside_parent_index_summary_json_schema_is_stable() {
3681        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3682        let document = parsed.document_summary.unwrap();
3683        let kf = &document.models[0].initial_model_keyframe;
3684        let keys = json_keys(&serde_json::to_value(&kf.outside_parent_indices[0]).unwrap());
3685
3686        assert_eq!(keys, vec!["parentModelBoneIndex", "parentModelIndex"]);
3687    }
3688
3689    #[test]
3690    fn pmm_document_model_sections_json_schema_is_stable() {
3691        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3692        let document = parsed.document_summary.unwrap();
3693        let keys = json_keys(&serde_json::to_value(&document.models[0].sections).unwrap());
3694
3695        assert_eq!(
3696            keys,
3697            vec![
3698                "boneKeyframeCountOffset",
3699                "boneKeyframesEndOffset",
3700                "boneKeyframesOffset",
3701                "boneStatesOffset",
3702                "constraintStatesOffset",
3703                "initialBoneKeyframesOffset",
3704                "initialModelKeyframeOffset",
3705                "initialMorphKeyframesOffset",
3706                "modelKeyframeCountOffset",
3707                "modelKeyframesEndOffset",
3708                "modelKeyframesOffset",
3709                "morphKeyframeCountOffset",
3710                "morphKeyframesEndOffset",
3711                "morphKeyframesOffset",
3712                "morphStatesOffset",
3713                "outsideParentStatesOffset"
3714            ]
3715        );
3716    }
3717
3718    #[test]
3719    fn pmm_diagnostic_json_schema_is_stable() {
3720        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3721        let keys = json_keys(&serde_json::to_value(&parsed.diagnostics[0]).unwrap());
3722
3723        assert_eq!(keys, vec!["code", "level", "message"]);
3724    }
3725
3726    #[test]
3727    fn parses_pmm_project_settings_header_slice() {
3728        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3729
3730        assert_eq!(parsed.project_settings.screen_width, Some(1920));
3731        assert_eq!(parsed.project_settings.screen_height, Some(1080));
3732        assert_eq!(parsed.project_settings.timeline_frame_count, Some(424));
3733        assert_eq!(parsed.project_settings.frame_rate, Some(30.0));
3734        assert_eq!(parsed.timeline.start_frame, Some(0));
3735        assert_eq!(parsed.timeline.end_frame_exclusive, Some(424));
3736        assert_eq!(parsed.timeline.frame_count, Some(424));
3737        assert_eq!(parsed.timeline.frame_rate, Some(30.0));
3738        assert_eq!(parsed.timeline.duration_seconds, Some(424.0 / 30.0));
3739        assert_eq!(
3740            parsed.display_state.model_slot_flags,
3741            vec![1, 1, 2, 0, 0, 0, 0, 0]
3742        );
3743        assert_eq!(parsed.display_state.layout, "pmm-v2-flags");
3744        assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
3745        assert_eq!(parsed.display_state.model_slot_count, 8);
3746        assert_eq!(parsed.display_state.non_zero_model_slot_count, 3);
3747        assert_eq!(parsed.display_state.accessory_slot_count, None);
3748        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmx"]);
3749        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
3750        assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.wav"]);
3751        assert_eq!(parsed.model_assets.len(), 1);
3752        assert_eq!(parsed.model_assets[0].reference_index, 0);
3753        assert_eq!(parsed.model_assets[0].kind_index, 0);
3754        assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
3755        assert_eq!(parsed.model_assets[0].confidence, "high");
3756        assert!(parsed.model_assets[0].offset_end > parsed.model_assets[0].offset);
3757        assert_eq!(parsed.motion_assets[0].reference_index, 1);
3758        assert_eq!(parsed.motion_assets[0].kind_index, 0);
3759        assert_eq!(parsed.audio_assets[0].reference_index, 2);
3760        assert_eq!(parsed.audio_assets[0].extension, "wav");
3761        assert_eq!(parsed.diagnostics.len(), 4);
3762        assert_eq!(parsed.diagnostics[0].code, "PMM_PROJECT_GRAPH_PARTIAL");
3763        assert_eq!(parsed.diagnostics[1].code, "PMM_ASSET_REFERENCES_SCAN");
3764        assert!(parsed.diagnostics[1].message.contains("references=3"));
3765        assert_eq!(
3766            parsed.diagnostics[2].code,
3767            "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE"
3768        );
3769        assert_eq!(parsed.diagnostics[3].code, "PMM_ASSET_COUNT_MISMATCH");
3770    }
3771
3772    #[test]
3773    fn pmm_asset_summary_groups_manifest_derived_references() {
3774        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
3775        let summary = &parsed.asset_summary;
3776
3777        assert_eq!(summary.reference_count, 3);
3778        assert_eq!(summary.high_confidence_count, 3);
3779        assert_eq!(summary.medium_confidence_count, 0);
3780        assert_eq!(summary.low_confidence_count, 0);
3781        assert_eq!(summary.kind_counts.get("model"), Some(&1));
3782        assert_eq!(summary.kind_counts.get("motion"), Some(&1));
3783        assert_eq!(summary.kind_counts.get("audio"), Some(&1));
3784        assert_eq!(summary.extension_counts.get("pmx"), Some(&1));
3785        assert_eq!(summary.extension_counts.get("vmd"), Some(&1));
3786        assert_eq!(summary.extension_counts.get("wav"), Some(&1));
3787        assert_eq!(summary.confidence_counts.get("high"), Some(&3));
3788    }
3789
3790    #[test]
3791    fn parses_pmm_v2_document_model_keyframe_summary() {
3792        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3793        let document = parsed.document_summary.as_ref().unwrap();
3794
3795        assert_eq!(document.source, "nanoem/ext/document.c PMMv2 layout");
3796        assert_eq!(document.selected_model_index, 0);
3797        assert_eq!(document.model_count, 1);
3798        assert_eq!(document.counts.models, 1);
3799        assert_eq!(document.counts.bones, 1);
3800        assert_eq!(document.counts.morphs, 1);
3801        assert_eq!(document.counts.initial_bone_keyframes, 1);
3802        assert_eq!(document.counts.bone_keyframes, 1);
3803        assert_eq!(document.counts.initial_morph_keyframes, 1);
3804        assert_eq!(document.counts.morph_keyframes, 1);
3805        assert_eq!(document.counts.initial_model_keyframes, 1);
3806        assert_eq!(document.counts.model_keyframes, 1);
3807
3808        let model = &document.models[0];
3809        assert_eq!(model.slot_index, 0);
3810        assert_eq!(model.document_model_index, 0);
3811        assert_eq!(model.name, "Miku");
3812        assert_eq!(model.english_name, "MikuEn");
3813        assert_eq!(model.path, "UserFile\\Model\\miku.pmx");
3814        let asset_reference_index = model.asset_reference_index.unwrap();
3815        assert_eq!(
3816            parsed.asset_references[asset_reference_index].normalized_path,
3817            "UserFile/Model/miku.pmx"
3818        );
3819        assert_eq!(model.bone_count, 1);
3820        assert_eq!(model.morph_count, 1);
3821        assert_eq!(model.constraint_bone_count, 1);
3822        assert_eq!(model.outside_parent_subject_bone_count, 1);
3823        assert_eq!(model.bone_keyframes, 1);
3824        assert_eq!(model.morph_keyframes, 1);
3825        assert_eq!(model.model_keyframes, 1);
3826        assert_eq!(model.initial_model_keyframe.index, None);
3827        assert_eq!(model.initial_model_keyframe.frame_index, 0);
3828        assert_eq!(model.initial_model_keyframe.previous_keyframe_index, 0);
3829        assert_eq!(model.initial_model_keyframe.next_keyframe_index, 0);
3830        assert!(model.initial_model_keyframe.visible);
3831        assert_eq!(model.initial_model_keyframe.constraint_states, vec![true]);
3832        assert_eq!(
3833            model.initial_model_keyframe.outside_parent_indices[0].parent_model_index,
3834            2
3835        );
3836        assert_eq!(
3837            model.initial_model_keyframe.outside_parent_indices[0].parent_model_bone_index,
3838            3
3839        );
3840        assert!(model.initial_model_keyframe.self_shadow_enabled);
3841        assert_eq!(model.model_keyframe_summaries.len(), 1);
3842        assert_eq!(model.model_keyframe_summaries[0].index, Some(30));
3843        assert_eq!(model.model_keyframe_summaries[0].frame_index, 30);
3844        assert!(model.model_keyframe_summaries[0].visible);
3845        assert_eq!(
3846            model.model_keyframe_summaries[0].constraint_states,
3847            vec![true]
3848        );
3849        assert_eq!(
3850            model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_index,
3851            2
3852        );
3853        assert_eq!(
3854            model.model_keyframe_summaries[0].outside_parent_indices[0].parent_model_bone_index,
3855            3
3856        );
3857        assert!(model.model_keyframe_summaries[0].self_shadow_enabled);
3858        assert_eq!(model.draw_order_index, 0);
3859        assert_eq!(model.transform_order_index, 0);
3860        assert_eq!(model.selected_bone_index, 0);
3861        assert_eq!(model.selected_morph_indices, [-1, -1, -1, -1]);
3862        assert_eq!(model.vertical_scroll, 0);
3863        assert_eq!(model.last_frame_index, 30);
3864        assert!(model.visible);
3865        assert!(model.blend_enabled);
3866        assert_eq!(model.edge_width, 1.0);
3867        assert!(!model.self_shadow_enabled);
3868        assert!(model.offset_end > model.offset);
3869        assert!(model.path_offset > model.offset);
3870        assert!(model.sections.initial_bone_keyframes_offset > model.path_offset);
3871        assert!(model.sections.bone_keyframes_offset > model.sections.bone_keyframe_count_offset);
3872        assert!(model.sections.bone_keyframes_end_offset > model.sections.bone_keyframes_offset);
3873        assert!(model.sections.morph_keyframes_offset > model.sections.morph_keyframe_count_offset);
3874        assert!(model.sections.model_keyframes_offset > model.sections.model_keyframe_count_offset);
3875        assert!(
3876            model.sections.outside_parent_states_offset >= model.sections.constraint_states_offset
3877        );
3878        let document_diagnostic = parsed
3879            .diagnostics
3880            .iter()
3881            .find(|diagnostic| diagnostic.code == "PMM_DOCUMENT_SUMMARY_PARTIAL")
3882            .unwrap();
3883        assert!(
3884            document_diagnostic
3885                .message
3886                .contains("2 total bone keyframe(s) (1 additional)")
3887        );
3888        assert!(
3889            document_diagnostic
3890                .message
3891                .contains("2 total morph keyframe(s) (1 additional)")
3892        );
3893    }
3894
3895    #[test]
3896    fn pmm_document_bone_keyframe_summary_json_schema_is_stable() {
3897        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3898        let document = parsed.document_summary.unwrap();
3899        let model = &document.models[0];
3900        let keys =
3901            json_keys(&serde_json::to_value(&model.initial_bone_keyframe_summaries[0]).unwrap());
3902
3903        assert_eq!(
3904            keys,
3905            vec![
3906                "frameIndex",
3907                "index",
3908                "interpolation",
3909                "nextKeyframeIndex",
3910                "orientation",
3911                "physicsDisabled",
3912                "previousKeyframeIndex",
3913                "selected",
3914                "translation"
3915            ]
3916        );
3917    }
3918
3919    #[test]
3920    fn pmm_document_morph_keyframe_summary_json_schema_is_stable() {
3921        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3922        let document = parsed.document_summary.unwrap();
3923        let model = &document.models[0];
3924        let keys =
3925            json_keys(&serde_json::to_value(&model.initial_morph_keyframe_summaries[0]).unwrap());
3926
3927        assert_eq!(
3928            keys,
3929            vec![
3930                "frameIndex",
3931                "index",
3932                "nextKeyframeIndex",
3933                "previousKeyframeIndex",
3934                "selected",
3935                "weight"
3936            ]
3937        );
3938    }
3939
3940    #[test]
3941    fn parses_pmm_v2_document_bone_morph_keyframe_summaries() {
3942        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
3943        let document = parsed.document_summary.as_ref().unwrap();
3944        let model = &document.models[0];
3945
3946        assert_eq!(model.initial_bone_keyframe_summaries.len(), 1);
3947        let ibk = &model.initial_bone_keyframe_summaries[0];
3948        assert_eq!(ibk.index, None);
3949        assert_eq!(ibk.frame_index, 0);
3950        assert_eq!(ibk.previous_keyframe_index, 0);
3951        assert_eq!(ibk.next_keyframe_index, 0);
3952        assert_eq!(ibk.interpolation, [20u8; 16]);
3953        assert_eq!(ibk.translation, [0.0, 0.0, 0.0]);
3954        assert_eq!(ibk.orientation, [0.0, 0.0, 0.0, 1.0]);
3955        assert!(!ibk.physics_disabled);
3956        assert!(!ibk.selected);
3957
3958        assert_eq!(model.bone_keyframe_summaries.len(), 1);
3959        let bk = &model.bone_keyframe_summaries[0];
3960        assert_eq!(bk.index, Some(0));
3961        assert_eq!(bk.frame_index, 30);
3962        assert_eq!(bk.previous_keyframe_index, 0);
3963        assert_eq!(bk.next_keyframe_index, 0);
3964        assert_eq!(bk.interpolation, [20u8; 16]);
3965        assert_eq!(bk.translation, [0.0, 0.0, 0.0]);
3966        assert_eq!(bk.orientation, [0.0, 0.0, 0.0, 1.0]);
3967        assert!(!bk.physics_disabled);
3968        assert!(!bk.selected);
3969
3970        assert_eq!(model.initial_morph_keyframe_summaries.len(), 1);
3971        let imk = &model.initial_morph_keyframe_summaries[0];
3972        assert_eq!(imk.index, None);
3973        assert_eq!(imk.frame_index, 0);
3974        assert_eq!(imk.previous_keyframe_index, 0);
3975        assert_eq!(imk.next_keyframe_index, 0);
3976        assert!((imk.weight - 0.5).abs() < 1e-6);
3977        assert!(!imk.selected);
3978
3979        assert_eq!(model.morph_keyframe_summaries.len(), 1);
3980        let mk = &model.morph_keyframe_summaries[0];
3981        assert_eq!(mk.index, Some(0));
3982        assert_eq!(mk.frame_index, 15);
3983        assert_eq!(mk.previous_keyframe_index, 0);
3984        assert_eq!(mk.next_keyframe_index, 0);
3985        assert!((mk.weight - 0.5).abs() < 1e-6);
3986        assert!(!mk.selected);
3987    }
3988
3989    #[test]
3990    fn pmm_v2_document_summary_rejects_selected_model_index_out_of_range() {
3991        let mut data = pmm_with_document_summary();
3992        data[53] = 1;
3993
3994        let parsed = parse_pmm_manifest(&data).unwrap();
3995
3996        assert!(parsed.document_summary.is_none());
3997    }
3998
3999    #[test]
4000    fn parses_initial_pmm_model_slot_slice() {
4001        let parsed = parse_pmm_manifest(&pmm_with_initial_model_slot()).unwrap();
4002
4003        assert_eq!(parsed.model_slots.len(), 1);
4004        let slot = &parsed.model_slots[0];
4005        assert_eq!(slot.slot_index, 0);
4006        assert_eq!(slot.display_slot_index, Some(1));
4007        assert_eq!(slot.offset, 56);
4008        assert_eq!(slot.model_path_offset, 71);
4009        assert_eq!(slot.trailing_zero_padding_bytes, 0);
4010        assert_eq!(slot.next_non_zero_offset, Some(slot.offset_end + 1));
4011        assert_eq!(slot.name, "TestModel");
4012        assert_eq!(slot.name_bytes, b"TestModel");
4013        assert_eq!(slot.english_name, "Base");
4014        assert_eq!(slot.english_name_bytes, b"Base");
4015        assert_eq!(
4016            slot.model_path,
4017            "F:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx"
4018        );
4019        assert_eq!(
4020            slot.normalized_path,
4021            "F:/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
4022        );
4023        assert_eq!(slot.asset_reference_index, Some(0));
4024        assert_eq!(slot.confidence, "high");
4025        assert_eq!(parsed.asset_references[0].offset, 56);
4026        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk.vmd"]);
4027        assert!(
4028            parsed
4029                .diagnostics
4030                .iter()
4031                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_INITIAL_SLICE")
4032        );
4033        assert!(
4034            parsed
4035                .diagnostics
4036                .iter()
4037                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
4038        );
4039        assert!(
4040            parsed
4041                .diagnostics
4042                .iter()
4043                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
4044        );
4045    }
4046
4047    #[test]
4048    fn parses_sequential_pmm_model_slot_header_slice() {
4049        let parsed = parse_pmm_manifest(&pmm_with_two_initial_model_slots()).unwrap();
4050
4051        assert_eq!(parsed.model_slots.len(), 2);
4052        assert_eq!(parsed.model_slots[0].slot_index, 0);
4053        assert_eq!(parsed.model_slots[1].slot_index, 1);
4054        assert_eq!(parsed.model_slots[0].display_slot_index, Some(0));
4055        assert_eq!(parsed.model_slots[1].display_slot_index, Some(1));
4056        assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 0);
4057        assert_eq!(
4058            parsed.model_slots[0].next_non_zero_offset,
4059            Some(parsed.model_slots[1].offset)
4060        );
4061        assert_eq!(
4062            parsed.model_slots[1].offset,
4063            parsed.model_slots[0].offset_end + 1
4064        );
4065        assert_eq!(parsed.model_slots[1].name, "Sour");
4066        assert_eq!(
4067            parsed.model_slots[1].model_path,
4068            "G:\\MikuMikuDance\\MMD Models\\Sour\\sour.pmx"
4069        );
4070        assert_eq!(
4071            parsed.model_slots[1].normalized_path,
4072            "G:/MikuMikuDance/MMD Models/Sour/sour.pmx"
4073        );
4074        assert_eq!(parsed.model_slots[1].asset_reference_index, Some(1));
4075        assert_eq!(
4076            parsed.asset_references[1].offset,
4077            parsed.model_slots[1].offset
4078        );
4079        assert!(
4080            !parsed
4081                .diagnostics
4082                .iter()
4083                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
4084        );
4085    }
4086
4087    #[test]
4088    fn pmm_model_slot_scan_stops_when_next_asset_reference_is_not_matched() {
4089        let data = pmm_with_two_initial_model_slots();
4090        let display_state = parse_display_state(&data, Some(2));
4091        let references = extract_asset_references(&data);
4092        let scan = parse_model_slots_from_header(&data, &references[..1], &display_state);
4093
4094        assert_eq!(scan.slots.len(), 1);
4095        let stop = scan.stop.unwrap();
4096        assert_eq!(stop.offset, scan.slots[0].offset_end + 1);
4097        assert_eq!(stop.reason, "asset_reference_not_matched");
4098    }
4099
4100    #[test]
4101    fn pmm_model_slot_scan_stops_at_implausible_next_name_length() {
4102        let mut data = pmm_with_initial_model_slot();
4103        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
4104        data.truncate(slot_end + 1);
4105        data.push(255);
4106        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
4107        let parsed = parse_pmm_manifest(&data).unwrap();
4108
4109        assert_eq!(parsed.model_slots.len(), 1);
4110        let diagnostic = parsed
4111            .diagnostics
4112            .iter()
4113            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
4114            .unwrap();
4115        assert!(diagnostic.message.contains("missing_name"));
4116    }
4117
4118    #[test]
4119    fn pmm_model_slot_scan_stops_at_null_byte_in_next_name() {
4120        let mut data = pmm_with_initial_model_slot();
4121        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
4122        data.truncate(slot_end + 1);
4123        data.push(4);
4124        data.extend_from_slice(b"A\0BC");
4125        data.extend_from_slice(b"UserFile\\Motion\\walk.vmd\0");
4126        let parsed = parse_pmm_manifest(&data).unwrap();
4127
4128        assert_eq!(parsed.model_slots.len(), 1);
4129        let diagnostic = parsed
4130            .diagnostics
4131            .iter()
4132            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
4133            .unwrap();
4134        assert!(diagnostic.message.contains("missing_name"));
4135    }
4136
4137    #[test]
4138    fn pmm_model_slot_scan_records_zero_padding_before_binary_graph() {
4139        let mut data = pmm_with_initial_model_slot();
4140        let slot_end = parse_pmm_manifest(&data).unwrap().model_slots[0].offset_end;
4141        data.truncate(slot_end + 1);
4142        data.extend_from_slice(&[0; 12]);
4143        data.extend_from_slice(&[15, 34, 3, 0, 0, 8, 145, 128]);
4144        let parsed = parse_pmm_manifest(&data).unwrap();
4145
4146        assert_eq!(parsed.model_slots.len(), 1);
4147        assert_eq!(parsed.model_slots[0].trailing_zero_padding_bytes, 12);
4148        assert_eq!(
4149            parsed.model_slots[0].next_non_zero_offset,
4150            Some(slot_end + 13)
4151        );
4152        let diagnostic = parsed
4153            .diagnostics
4154            .iter()
4155            .find(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_SCAN_STOPPED")
4156            .unwrap();
4157        assert!(diagnostic.message.contains("missing_name"));
4158    }
4159
4160    #[test]
4161    fn parses_pmm_numeric_version() {
4162        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
4163
4164        assert_eq!(parsed.version, "0002");
4165        assert_eq!(parsed.parsed_version, Some(2));
4166    }
4167
4168    #[test]
4169    fn parses_pmm_v1_display_count_layout() {
4170        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
4171
4172        assert_eq!(parsed.version, "0001");
4173        assert_eq!(parsed.parsed_version, Some(1));
4174        assert_eq!(parsed.project_settings.screen_width, Some(512));
4175        assert_eq!(parsed.project_settings.screen_height, Some(288));
4176        assert_eq!(parsed.project_settings.timeline_frame_count, Some(435));
4177        assert_eq!(parsed.project_settings.frame_rate, Some(35.0));
4178        assert_eq!(parsed.display_state.layout, "pmm-v1-counts");
4179        assert!(parsed.display_state.model_slot_flags.is_empty());
4180        assert_eq!(parsed.display_state.declared_model_slot_count, Some(8));
4181        assert_eq!(parsed.display_state.model_slot_count, 8);
4182        assert_eq!(parsed.display_state.non_zero_model_slot_count, 8);
4183        assert_eq!(parsed.display_state.accessory_slot_count, Some(9));
4184        assert_eq!(parsed.header_text_entries.len(), 2);
4185        assert_eq!(parsed.header_text_entries[0].index, 0);
4186        assert_eq!(parsed.header_text_entries[0].offset, 54);
4187        assert_eq!(parsed.header_text_entries[0].text, "Dummy");
4188        assert_eq!(parsed.header_text_entries[0].text_bytes, b"Dummy");
4189        assert_eq!(parsed.header_text_entries[1].text, "maker Dummy");
4190        assert!(
4191            !parsed
4192                .diagnostics
4193                .iter()
4194                .any(|diagnostic| diagnostic.code == "PMM_MODEL_SLOT_COUNT_PARTIAL_DECODE")
4195        );
4196        assert!(parsed.model_slots.is_empty());
4197        assert!(
4198            parsed
4199                .diagnostics
4200                .iter()
4201                .any(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
4202        );
4203        assert!(
4204            !parsed
4205                .diagnostics
4206                .iter()
4207                .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
4208        );
4209    }
4210
4211    #[test]
4212    fn pmm_v1_invalid_display_counts_do_not_fall_through_to_v2_flags() {
4213        let mut data = pmm_v1_with_display_counts();
4214        data[52] = 255;
4215        data[53] = 255;
4216        data[54] = 1;
4217        data[55] = 1;
4218        let parsed = parse_pmm_manifest(&data).unwrap();
4219
4220        assert_eq!(parsed.display_state.layout, "unknown");
4221        assert!(parsed.display_state.model_slot_flags.is_empty());
4222        assert_eq!(parsed.display_state.declared_model_slot_count, None);
4223        assert_eq!(parsed.display_state.model_slot_count, 0);
4224        assert_eq!(parsed.display_state.accessory_slot_count, None);
4225        assert!(
4226            parsed
4227                .diagnostics
4228                .iter()
4229                .any(|diagnostic| diagnostic.code == "PMM_DISPLAY_STATE_UNPLAUSIBLE")
4230        );
4231    }
4232
4233    #[test]
4234    fn pmm_v1_header_text_entries_require_asset_boundary() {
4235        let mut data = b"Polygon Movie maker 0001".to_vec();
4236        data.resize(54, 0);
4237        data.extend_from_slice(b"Header\0Without asset boundary\0");
4238        let parsed = parse_pmm_manifest(&data).unwrap();
4239
4240        assert!(parsed.asset_references.is_empty());
4241        assert!(parsed.header_text_entries.is_empty());
4242    }
4243
4244    #[test]
4245    fn pmm_v1_header_text_entries_allow_empty_region_before_asset() {
4246        let mut data = b"Polygon Movie maker 0001".to_vec();
4247        data.resize(54, 0);
4248        data.extend_from_slice(b"\0\0\0UserFile\\Model\\miku.pmd\0");
4249        let parsed = parse_pmm_manifest(&data).unwrap();
4250
4251        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
4252        assert!(parsed.header_text_entries.is_empty());
4253    }
4254
4255    #[test]
4256    fn pmm_v1_header_text_entries_ignore_unterminated_chunk_before_asset() {
4257        let mut data = b"Polygon Movie maker 0001".to_vec();
4258        data.resize(54, 0);
4259        data.extend_from_slice(b"Unterminated header UserFile\\Model\\miku.pmd\0");
4260        let parsed = parse_pmm_manifest(&data).unwrap();
4261
4262        assert_eq!(parsed.model_paths, vec!["UserFile/Model/miku.pmd"]);
4263        assert!(parsed.header_text_entries.is_empty());
4264    }
4265
4266    #[test]
4267    fn parses_pmm_bare_asset_file_names() {
4268        let data = b"Polygon Movie maker 0002\0miku.pmx\0walk.vmd\0song.wav\0stage.avi\0.X\0";
4269        let parsed = parse_pmm_manifest(data).unwrap();
4270
4271        assert_eq!(parsed.model_paths, vec!["miku.pmx"]);
4272        assert_eq!(parsed.motion_paths, vec!["walk.vmd"]);
4273        assert_eq!(parsed.audio_paths, vec!["song.wav"]);
4274        assert_eq!(parsed.video_paths, vec!["stage.avi"]);
4275        assert!(parsed.accessory_paths.is_empty());
4276        assert_eq!(parsed.model_assets[0].confidence, "medium");
4277        assert_eq!(parsed.model_assets[0].file_name, "miku.pmx");
4278        assert_eq!(parsed.motion_assets[0].kind_index, 0);
4279        assert_eq!(parsed.audio_assets[0].reference_index, 2);
4280        assert_eq!(parsed.video_assets[0].reference_index, 3);
4281        assert_eq!(parsed.video_assets[0].kind_index, 0);
4282    }
4283
4284    #[test]
4285    fn parses_pmm_background_video_asset_paths() {
4286        let data =
4287            b"Polygon Movie maker 0002\0UserFile\\Background\\stage.avs\0C:\\Video\\movie.avi\0";
4288        let parsed = parse_pmm_manifest(data).unwrap();
4289
4290        assert_eq!(
4291            parsed.video_paths,
4292            vec!["UserFile/Background/stage.avs", "C:/Video/movie.avi"]
4293        );
4294        assert_eq!(parsed.video_assets.len(), 2);
4295        assert_eq!(parsed.video_assets[0].extension, "avs");
4296        assert_eq!(parsed.video_assets[0].confidence, "high");
4297        assert_eq!(parsed.video_assets[1].extension, "avi");
4298        assert_eq!(parsed.video_assets[1].file_name, "movie.avi");
4299    }
4300
4301    #[test]
4302    fn parses_pmm_common_audio_image_and_video_asset_extensions() {
4303        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";
4304        let parsed = parse_pmm_manifest(data).unwrap();
4305
4306        assert_eq!(
4307            parsed.audio_paths,
4308            vec!["UserFile/Audio/song.mp3", "UserFile/Audio/loop.ogg"]
4309        );
4310        assert_eq!(
4311            parsed.image_paths,
4312            vec![
4313                "UserFile/Texture/toon.png",
4314                "UserFile/Texture/photo.jpg",
4315                "UserFile/Texture/poster.jpeg",
4316                "UserFile/Texture/normal.dds"
4317            ]
4318        );
4319        assert_eq!(
4320            parsed.video_paths,
4321            vec!["UserFile/Movie/clip.mp4", "UserFile/Movie/stage.wmv"]
4322        );
4323        assert_eq!(parsed.audio_assets[0].extension, "mp3");
4324        assert_eq!(parsed.audio_assets[1].extension, "ogg");
4325        assert_eq!(parsed.image_assets[0].extension, "png");
4326        assert_eq!(parsed.image_assets[1].extension, "jpg");
4327        assert_eq!(parsed.image_assets[2].extension, "jpeg");
4328        assert_eq!(parsed.image_assets[3].extension, "dds");
4329        assert_eq!(parsed.video_assets[0].extension, "mp4");
4330        assert_eq!(parsed.video_assets[1].extension, "wmv");
4331    }
4332
4333    #[test]
4334    fn pmm_asset_scanner_rejects_partial_common_media_extensions() {
4335        let data = b"Polygon Movie maker 0002\0song.mp3extra\0toon.png2\0poster.jpeg_backup\0movie.mp4.tmp\0UserFile\\Audio\\song.mp3\0";
4336        let parsed = parse_pmm_manifest(data).unwrap();
4337
4338        assert_eq!(parsed.audio_paths, vec!["UserFile/Audio/song.mp3"]);
4339        assert!(parsed.image_paths.is_empty());
4340        assert!(parsed.video_paths.is_empty());
4341    }
4342
4343    #[test]
4344    fn parses_pmm_asset_paths_with_spaces() {
4345        let data = b"Polygon Movie maker 0002\0Label C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx\0UserFile\\Motion\\walk cycle.vmd\0";
4346        let parsed = parse_pmm_manifest(data).unwrap();
4347
4348        assert_eq!(
4349            parsed.model_paths,
4350            vec!["C:/MikuMikuDance/MMD Models/Append Miku.pmx"]
4351        );
4352        assert_eq!(parsed.motion_paths, vec!["UserFile/Motion/walk cycle.vmd"]);
4353        assert_eq!(
4354            parsed.model_assets[0].path,
4355            "C:\\MikuMikuDance\\MMD Models\\Append Miku.pmx"
4356        );
4357        assert_eq!(parsed.model_assets[0].file_name, "Append Miku.pmx");
4358        assert_eq!(parsed.motion_assets[0].file_name, "walk cycle.vmd");
4359    }
4360
4361    #[test]
4362    fn pmm_asset_reference_marks_replacement_char_as_low_confidence() {
4363        let data = "Polygon Movie maker 0002\0C:\\Models\\bad�name.pmx\0"
4364            .as_bytes()
4365            .to_vec();
4366        let parsed = parse_pmm_manifest(&data).unwrap();
4367
4368        assert_eq!(parsed.model_assets[0].confidence, "low");
4369    }
4370
4371    #[test]
4372    fn pmm_asset_reference_marks_root_relative_fragment_as_low_confidence() {
4373        let data = b"Polygon Movie maker 0002\0\\Model\\miku.pmx\0";
4374        let parsed = parse_pmm_manifest(data).unwrap();
4375
4376        assert_eq!(parsed.model_assets[0].normalized_path, "/Model/miku.pmx");
4377        assert_eq!(parsed.model_assets[0].confidence, "low");
4378    }
4379
4380    #[test]
4381    fn pmm_asset_reference_marks_missing_drive_letter_fragment_as_low_confidence() {
4382        let data =
4383            b"Polygon Movie maker 0002\0:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
4384        let parsed = parse_pmm_manifest(data).unwrap();
4385
4386        assert_eq!(
4387            parsed.model_assets[0].normalized_path,
4388            ":/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
4389        );
4390        assert_eq!(parsed.model_assets[0].confidence, "low");
4391    }
4392
4393    #[test]
4394    fn pmm_asset_scan_keeps_complete_and_fragmented_duplicate_paths_separate() {
4395        let data = b"Polygon Movie maker 0002\0F:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0:\\Develop\\MMDDev\\data\\unittest\\test_1bone_cube.pmx\0";
4396        let parsed = parse_pmm_manifest(data).unwrap();
4397
4398        assert_eq!(parsed.model_assets.len(), 2);
4399        assert_eq!(
4400            parsed.model_assets[0].normalized_path,
4401            "F:/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
4402        );
4403        assert_eq!(parsed.model_assets[0].confidence, "high");
4404        assert_eq!(
4405            parsed.model_assets[1].normalized_path,
4406            ":/Develop/MMDDev/data/unittest/test_1bone_cube.pmx"
4407        );
4408        assert_eq!(parsed.model_assets[1].confidence, "low");
4409        assert!(
4410            parsed
4411                .diagnostics
4412                .iter()
4413                .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
4414        );
4415    }
4416
4417    #[test]
4418    fn pmm_asset_reference_keeps_offset_range() {
4419        let data = b"Polygon Movie maker 0002\0Label UserFile\\Model\\miku.pmx\0";
4420        let parsed = parse_pmm_manifest(data).unwrap();
4421
4422        let reference = &parsed.asset_references[0];
4423        assert_eq!(reference.offset, 25);
4424        assert_eq!(reference.offset_end, data.len() - 1);
4425        assert_eq!(&data[reference.offset_end..reference.offset_end + 1], b"\0");
4426    }
4427
4428    #[test]
4429    fn pmm_asset_reference_offset_end_allows_truncated_final_chunk() {
4430        let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx";
4431        let parsed = parse_pmm_manifest(data).unwrap();
4432
4433        assert_eq!(parsed.asset_references[0].offset_end, data.len());
4434    }
4435
4436    #[test]
4437    fn pmm_diagnostics_flags_duplicate_asset_by_filename() {
4438        let data = b"Polygon Movie maker 0002\0C:\\Models\\miku.pmx\0UserFile\\Model\\miku.pmx\0UserFile\\Motion\\walk.vmd\0";
4439        let parsed = parse_pmm_manifest(data).unwrap();
4440
4441        assert!(
4442            parsed
4443                .diagnostics
4444                .iter()
4445                .any(|diagnostic| diagnostic.code == "PMM_ASSET_REFERENCE_DUPLICATE")
4446        );
4447    }
4448
4449    #[test]
4450    fn pmm_diagnostics_flags_missing_motion_when_models_present() {
4451        let data = b"Polygon Movie maker 0002\0UserFile\\Model\\miku.pmx\0";
4452        let parsed = parse_pmm_manifest(data).unwrap();
4453
4454        assert!(
4455            parsed
4456                .diagnostics
4457                .iter()
4458                .any(|diagnostic| diagnostic.code == "PMM_MOTION_REFERENCES_NOT_FOUND_IN_SCAN")
4459        );
4460    }
4461
4462    #[test]
4463    fn pmm_diagnostics_flags_declared_asset_count_mismatch() {
4464        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
4465        let mismatches = parsed
4466            .diagnostics
4467            .iter()
4468            .filter(|diagnostic| diagnostic.code == "PMM_ASSET_COUNT_MISMATCH")
4469            .collect::<Vec<_>>();
4470
4471        assert_eq!(mismatches.len(), 2);
4472        assert_eq!(mismatches[0].level, "info");
4473        assert_eq!(mismatches[1].level, "info");
4474        assert!(mismatches[0].message.contains("8 model slot"));
4475        assert!(mismatches[0].message.contains("1 model reference"));
4476        assert!(mismatches[1].message.contains("9 accessory slot"));
4477        assert!(mismatches[1].message.contains("1 accessory reference"));
4478    }
4479
4480    #[test]
4481    fn pmm_display_state_derives_active_and_empty_slot_indices_from_flags() {
4482        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
4483        let ds = &parsed.display_state;
4484
4485        assert_eq!(ds.layout, "pmm-v2-flags");
4486        assert_eq!(ds.model_slot_flags, vec![1, 1, 2, 0, 0, 0, 0, 0]);
4487        assert_eq!(ds.selected_model_index, Some(0));
4488        assert_eq!(ds.document_model_count, Some(3));
4489        let expand_flags = ds.document_expand_flags.as_ref().unwrap();
4490        assert!(expand_flags.editing_cla);
4491        assert!(expand_flags.camera_panel);
4492        assert!(expand_flags.light_panel);
4493        assert!(!expand_flags.accessory_panel);
4494        assert!(!expand_flags.self_shadow_panel);
4495        assert_eq!(ds.model_slot_flag_entries.len(), 8);
4496        assert_eq!(ds.model_slot_flag_entries[0].slot_index, 0);
4497        assert_eq!(ds.model_slot_flag_entries[0].flag, 1);
4498        assert!(ds.model_slot_flag_entries[0].active);
4499        assert_eq!(ds.model_slot_flag_entries[3].slot_index, 3);
4500        assert_eq!(ds.model_slot_flag_entries[3].flag, 0);
4501        assert!(!ds.model_slot_flag_entries[3].active);
4502        assert_eq!(ds.active_model_slot_indices, vec![0, 1, 2]);
4503        assert_eq!(ds.empty_model_slot_indices, vec![3, 4, 5, 6, 7]);
4504    }
4505
4506    #[test]
4507    fn pmm_display_state_flag_counts_are_grouped_and_stable() {
4508        let parsed = parse_pmm_manifest(&pmm_with_project_settings()).unwrap();
4509        let counts = &parsed.display_state.model_slot_flag_counts;
4510
4511        assert_eq!(counts.get(&0), Some(&5));
4512        assert_eq!(counts.get(&1), Some(&2));
4513        assert_eq!(counts.get(&2), Some(&1));
4514        assert_eq!(counts.len(), 3);
4515
4516        let json = serde_json::to_value(counts).unwrap();
4517        let keys: Vec<&str> = json
4518            .as_object()
4519            .unwrap()
4520            .keys()
4521            .map(|k| k.as_str())
4522            .collect();
4523        assert_eq!(keys, vec!["0", "1", "2"]);
4524    }
4525
4526    #[test]
4527    fn pmm_v1_display_state_derived_arrays_are_empty_when_no_raw_flags() {
4528        let parsed = parse_pmm_manifest(&pmm_v1_with_display_counts()).unwrap();
4529        let ds = &parsed.display_state;
4530
4531        assert_eq!(ds.layout, "pmm-v1-counts");
4532        assert!(ds.model_slot_flags.is_empty());
4533        assert!(ds.model_slot_flag_entries.is_empty());
4534        assert!(ds.document_expand_flags.is_none());
4535        assert_eq!(ds.selected_model_index, None);
4536        assert_eq!(ds.document_model_count, None);
4537        assert!(ds.active_model_slot_indices.is_empty());
4538        assert!(ds.empty_model_slot_indices.is_empty());
4539        assert!(ds.model_slot_flag_counts.is_empty());
4540    }
4541
4542    #[test]
4543    fn pmm_unknown_display_state_derived_arrays_are_empty() {
4544        let mut data = pmm_with_project_settings();
4545        data[46] = 7;
4546        let parsed = parse_pmm_manifest(&data).unwrap();
4547        let ds = &parsed.display_state;
4548
4549        assert_eq!(ds.layout, "unknown");
4550        assert!(ds.active_model_slot_indices.is_empty());
4551        assert!(ds.empty_model_slot_indices.is_empty());
4552        assert!(ds.model_slot_flag_counts.is_empty());
4553        assert!(ds.model_slot_flag_entries.is_empty());
4554    }
4555
4556    #[test]
4557    fn pmm_display_state_all_active_slots() {
4558        let flags: &[u8] = &[1, 1, 1, 1, 1, 1, 1, 1];
4559        let active = active_slot_indices(flags);
4560        let empty = empty_slot_indices(flags);
4561        let counts = slot_flag_counts(flags);
4562
4563        assert_eq!(active, vec![0, 1, 2, 3, 4, 5, 6, 7]);
4564        assert!(empty.is_empty());
4565        assert_eq!(counts.get(&1), Some(&8));
4566        assert_eq!(counts.len(), 1);
4567    }
4568
4569    #[test]
4570    fn pmm_display_state_all_empty_slots() {
4571        let flags: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0];
4572        let active = active_slot_indices(flags);
4573        let empty = empty_slot_indices(flags);
4574        let counts = slot_flag_counts(flags);
4575
4576        assert!(active.is_empty());
4577        assert_eq!(empty, vec![0, 1, 2, 3, 4, 5, 6, 7]);
4578        assert_eq!(counts.get(&0), Some(&8));
4579        assert_eq!(counts.len(), 1);
4580    }
4581
4582    #[test]
4583    fn pmm_document_bone_state_summary_json_schema_is_stable() {
4584        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
4585        let document = parsed.document_summary.unwrap();
4586        let model = &document.models[0];
4587        let keys = json_keys(&serde_json::to_value(&model.bone_state_summaries[0]).unwrap());
4588
4589        assert_eq!(
4590            keys,
4591            vec![
4592                "dirty",
4593                "orientation",
4594                "physicsDisabled",
4595                "selected",
4596                "translation"
4597            ]
4598        );
4599    }
4600
4601    #[test]
4602    fn pmm_document_morph_state_summary_json_schema_is_stable() {
4603        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
4604        let document = parsed.document_summary.unwrap();
4605        let model = &document.models[0];
4606        let keys = json_keys(&serde_json::to_value(&model.morph_state_summaries[0]).unwrap());
4607
4608        assert_eq!(keys, vec!["weight"]);
4609    }
4610
4611    #[test]
4612    fn pmm_document_constraint_state_summary_json_schema_is_stable() {
4613        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
4614        let document = parsed.document_summary.unwrap();
4615        let model = &document.models[0];
4616        let keys = json_keys(&serde_json::to_value(&model.constraint_state_summaries[0]).unwrap());
4617
4618        assert_eq!(keys, vec!["enabled"]);
4619    }
4620
4621    #[test]
4622    fn pmm_document_outside_parent_state_summary_json_schema_is_stable() {
4623        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
4624        let document = parsed.document_summary.unwrap();
4625        let model = &document.models[0];
4626        let keys =
4627            json_keys(&serde_json::to_value(&model.outside_parent_state_summaries[0]).unwrap());
4628
4629        assert_eq!(
4630            keys,
4631            vec![
4632                "parentModelBoneIndex",
4633                "parentModelIndex",
4634                "subjectBoneIndex",
4635                "targetModelIndex"
4636            ]
4637        );
4638    }
4639
4640    #[test]
4641    fn parses_pmm_v2_document_model_state_block_summaries() {
4642        let parsed = parse_pmm_manifest(&pmm_with_document_summary()).unwrap();
4643        let document = parsed.document_summary.as_ref().unwrap();
4644        let model = &document.models[0];
4645
4646        assert_eq!(model.bone_state_summaries.len(), 1);
4647        let bs = &model.bone_state_summaries[0];
4648        assert_eq!(bs.translation, [1.0, 2.0, 3.0]);
4649        assert_eq!(bs.orientation, [0.0, 0.0, 0.0, 1.0]);
4650        assert!(bs.dirty);
4651        assert!(!bs.physics_disabled);
4652        assert!(bs.selected);
4653
4654        assert_eq!(model.morph_state_summaries.len(), 1);
4655        let ms = &model.morph_state_summaries[0];
4656        assert!((ms.weight - 0.75).abs() < 1e-6);
4657
4658        assert_eq!(model.constraint_state_summaries.len(), 1);
4659        let cs = &model.constraint_state_summaries[0];
4660        assert!(cs.enabled);
4661
4662        assert_eq!(model.outside_parent_state_summaries.len(), 1);
4663        let ops = &model.outside_parent_state_summaries[0];
4664        assert_eq!(ops.parent_model_index, 0);
4665        assert_eq!(ops.parent_model_bone_index, 5);
4666        assert_eq!(ops.subject_bone_index, 7);
4667        assert_eq!(ops.target_model_index, 1);
4668    }
4669
4670    #[test]
4671    fn pmm_asset_scanner_requires_extension_and_userfile_boundaries() {
4672        let data = b"Polygon Movie maker 0002\0C:\\notuserfile\\Model\\fake.pmx\0dummy.x_v2\0model.x_v2.pmd\0";
4673        let parsed = parse_pmm_manifest(data).unwrap();
4674
4675        assert_eq!(
4676            parsed.model_paths,
4677            vec!["C:/notuserfile/Model/fake.pmx", "model.x_v2.pmd"]
4678        );
4679        assert!(parsed.accessory_paths.is_empty());
4680    }
4681
4682    #[test]
4683    fn ignores_implausible_pmm_model_slot_flags() {
4684        let mut data = pmm_with_project_settings();
4685        data[46] = 7;
4686        let parsed = parse_pmm_manifest(&data).unwrap();
4687
4688        assert!(parsed.display_state.model_slot_flags.is_empty());
4689        assert_eq!(parsed.display_state.layout, "unknown");
4690        assert_eq!(parsed.display_state.declared_model_slot_count, None);
4691        assert_eq!(parsed.display_state.model_slot_count, 0);
4692        assert_eq!(parsed.display_state.non_zero_model_slot_count, 0);
4693        assert_eq!(parsed.display_state.accessory_slot_count, None);
4694        assert_eq!(parsed.diagnostics.len(), 3);
4695        assert_eq!(parsed.diagnostics[2].code, "PMM_DISPLAY_STATE_UNPLAUSIBLE");
4696    }
4697
4698    #[test]
4699    fn parses_pmm_v2_accessory_keyframe_scalar_summaries() {
4700        let parsed = parse_pmm_manifest(&pmm_with_document_global_summary()).unwrap();
4701        let global = parsed.document_global_summary.as_ref().unwrap();
4702        let accessory = &global.accessories.accessories[0];
4703
4704        assert_eq!(accessory.keyframe_summaries.len(), 1);
4705
4706        let initial = &accessory.initial_keyframe;
4707        assert_eq!(initial.index, None);
4708        assert_eq!(initial.frame_index, 0);
4709        assert!(initial.visible);
4710        assert_eq!(initial.opacity, 1.0);
4711        assert_eq!(initial.parent_model_index, -1);
4712        assert_eq!(initial.parent_model_bone_index, -1);
4713        assert_eq!(initial.translation, [1.0, 2.0, 3.0]);
4714        assert_eq!(initial.orientation, [0.1, 0.2, 0.3]);
4715        assert_eq!(initial.scale_factor, 10.0);
4716        assert!(initial.shadow_enabled);
4717        assert!(!initial.selected);
4718
4719        let kf = &accessory.keyframe_summaries[0];
4720        assert_eq!(kf.index, Some(46));
4721        assert_eq!(kf.frame_index, 46);
4722        assert!(kf.visible);
4723        assert_eq!(kf.opacity, 1.0);
4724        assert_eq!(kf.translation, [1.0, 2.0, 3.0]);
4725        assert_eq!(kf.orientation, [0.1, 0.2, 0.3]);
4726        assert_eq!(kf.scale_factor, 10.0);
4727        assert!(kf.shadow_enabled);
4728        assert!(!kf.selected);
4729    }
4730}