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#[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 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); data.push(0); data.push(1); push_f32(&mut data, 0.75);
2748 data.push(1);
2750 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}