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