1use serde::{Deserialize, Serialize};
2
3use crate::error::ImportError;
4use crate::sjis::{decode_sjis, encode_sjis};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct AccessoryParsedManifest {
9 pub format: String,
10 pub byte_length: usize,
11 pub text: bool,
12 pub header: String,
13 pub mesh_count: usize,
14 pub material_count: usize,
15 pub mesh_summaries: Vec<AccessoryMeshSummary>,
16 pub materials: Vec<AccessoryMaterial>,
17 pub vac_settings: Option<AccessoryVacSettings>,
18 pub texture_references: Vec<String>,
19 pub diagnostics: Vec<AccessoryDiagnostic>,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct AccessoryMeshSummary {
25 pub vertex_count: usize,
26 pub face_count: usize,
27 pub positions: Vec<[f32; 3]>,
28 pub face_indices: Vec<Vec<u32>>,
29 pub normals: Vec<[f32; 3]>,
30 pub normal_face_indices: Vec<Vec<u32>>,
31 pub texture_coordinates: Vec<[f32; 2]>,
32 pub vertex_colors: Vec<AccessoryVertexColor>,
33 pub material_indices: Vec<u32>,
34 pub material_start_index: usize,
35 pub material_count: usize,
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct AccessoryVertexColor {
41 pub vertex_index: u32,
42 pub color: [f32; 4],
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct AccessoryMaterial {
48 pub name: Option<String>,
49 pub face_color: Option<[f32; 4]>,
50 pub power: Option<f32>,
51 pub specular_color: Option<[f32; 3]>,
52 pub emissive_color: Option<[f32; 3]>,
53 pub texture_references: Vec<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct AccessoryVacSettings {
59 pub raw_lines: Vec<String>,
60 pub x_file: Option<String>,
61 pub scale: Option<f32>,
62 pub position: Option<[f32; 3]>,
63 pub rotation: Option<[f32; 3]>,
64 pub numeric_values: Vec<f32>,
65 pub attachment_target: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AccessoryDiagnostic {
70 pub level: String,
71 pub code: String,
72 pub message: String,
73}
74
75pub fn export_accessory_manifest(manifest: &AccessoryParsedManifest) -> Vec<u8> {
76 match manifest.format.as_str() {
77 "vac" => export_vac_manifest(manifest),
78 _ => export_x_manifest(manifest),
79 }
80}
81
82pub fn parse_accessory_manifest(
83 data: &[u8],
84 file_name: Option<&str>,
85) -> Result<AccessoryParsedManifest, ImportError> {
86 let extension = file_name
87 .and_then(|name| {
88 name.rsplit_once('.')
89 .map(|(_, ext)| ext.to_ascii_lowercase())
90 })
91 .unwrap_or_default();
92 if extension == "vac" {
93 return Ok(parse_vac_manifest(data));
94 }
95 if !data.starts_with(b"xof ") {
96 return Err(ImportError::InvalidMagic { format: "X" });
97 }
98 let header = std::str::from_utf8(&data[..data.len().min(16)])
99 .unwrap_or("")
100 .trim_end_matches('\0')
101 .to_owned();
102 let text = header.contains("txt");
103 let body = if text {
104 String::from_utf8_lossy(data).into_owned()
105 } else {
106 decode_sjis(data)
107 };
108 let mesh_summaries = if text {
109 parse_x_mesh_summaries(&body)
110 } else {
111 Vec::new()
112 };
113 let materials = if text {
114 parse_x_materials(&body)
115 } else {
116 Vec::new()
117 };
118 let diagnostics = if text {
119 text_x_diagnostics(&mesh_summaries, &materials)
120 } else {
121 vec![AccessoryDiagnostic {
122 level: "warning".to_owned(),
123 code: "X_BINARY_LAYOUT_NOT_EXPANDED".to_owned(),
124 message: "Binary DirectX .x payload is identified but not fully decoded yet."
125 .to_owned(),
126 }]
127 };
128 Ok(AccessoryParsedManifest {
129 format: "x".to_owned(),
130 byte_length: data.len(),
131 text,
132 header,
133 mesh_count: if text {
134 count_x_blocks(&body, "Mesh")
135 } else {
136 0
137 },
138 material_count: if text {
139 count_x_blocks(&body, "Material")
140 } else {
141 0
142 },
143 mesh_summaries,
144 materials,
145 vac_settings: None,
146 texture_references: extract_texture_references(&body),
147 diagnostics,
148 })
149}
150
151fn text_x_diagnostics(
152 mesh_summaries: &[AccessoryMeshSummary],
153 materials: &[AccessoryMaterial],
154) -> Vec<AccessoryDiagnostic> {
155 let mut diagnostics = Vec::new();
156 if mesh_summaries.len() > 1
157 && !materials.is_empty()
158 && mesh_summaries.iter().any(|mesh| mesh.material_count == 0)
159 {
160 diagnostics.push(AccessoryDiagnostic {
161 level: "warning".to_owned(),
162 code: "X_MULTI_MESH_MATERIAL_EXPORT_PARTIAL".to_owned(),
163 message: "Text .x contains multiple Mesh blocks and Material blocks; parser keeps global material DTOs, but the current exporter slice does not preserve per-mesh material ownership.".to_owned(),
164 });
165 }
166 diagnostics
167}
168
169fn parse_vac_manifest(data: &[u8]) -> AccessoryParsedManifest {
170 let decoded = decode_sjis(data);
171 let lines = decoded
172 .lines()
173 .map(|line| line.trim().to_owned())
174 .filter(|line| !line.is_empty())
175 .collect::<Vec<_>>();
176 let texture_references = lines
177 .iter()
178 .filter(|line| line.to_ascii_lowercase().ends_with(".x"))
179 .cloned()
180 .collect::<Vec<_>>();
181 AccessoryParsedManifest {
182 format: "vac".to_owned(),
183 byte_length: data.len(),
184 text: true,
185 header: lines.first().cloned().unwrap_or_default(),
186 mesh_count: 0,
187 material_count: 0,
188 mesh_summaries: Vec::new(),
189 materials: Vec::new(),
190 vac_settings: Some(parse_vac_settings(&lines, &texture_references)),
191 texture_references,
192 diagnostics: vec![AccessoryDiagnostic {
193 level: "warning".to_owned(),
194 code: "VAC_ACCESSORY_WRAPPER".to_owned(),
195 message: "VAC is parsed as an accessory wrapper manifest; the referenced .x file should be parsed separately.".to_owned(),
196 }],
197 }
198}
199
200fn export_x_manifest(manifest: &AccessoryParsedManifest) -> Vec<u8> {
201 let mut text = if manifest.header.starts_with("xof ") {
202 format!("{}\n", manifest.header)
203 } else {
204 String::from("xof 0303txt 0032\n")
205 };
206 if !manifest.mesh_summaries.is_empty() {
207 export_x_meshes(&mut text, manifest);
208 return text.into_bytes();
209 }
210 for texture in &manifest.texture_references {
211 text.push_str("TextureFilename {\n \"");
212 text.push_str(&escape_x_string(texture));
213 text.push_str("\";\n}\n");
214 }
215 text.into_bytes()
216}
217
218fn export_x_meshes(text: &mut String, manifest: &AccessoryParsedManifest) {
219 for mesh in &manifest.mesh_summaries {
220 text.push_str("Mesh {\n");
221 text.push_str(&format!(" {};\n", mesh.positions.len()));
222 for (index, position) in mesh.positions.iter().enumerate() {
223 let suffix = if index + 1 == mesh.positions.len() {
224 ";;"
225 } else {
226 ","
227 };
228 text.push_str(&format!(
229 " {};{};{};{}\n",
230 format_x_float(position[0]),
231 format_x_float(position[1]),
232 format_x_float(position[2]),
233 suffix
234 ));
235 }
236 text.push_str(&format!(" {};\n", mesh.face_indices.len()));
237 for (index, face) in mesh.face_indices.iter().enumerate() {
238 let suffix = if index + 1 == mesh.face_indices.len() {
239 ";;"
240 } else {
241 ","
242 };
243 let indices = face
244 .iter()
245 .map(u32::to_string)
246 .collect::<Vec<_>>()
247 .join(",");
248 text.push_str(&format!(" {};{};{}\n", face.len(), indices, suffix));
249 }
250 if !mesh.normals.is_empty() {
251 export_x_mesh_normals(text, mesh);
252 }
253 if !mesh.texture_coordinates.is_empty() {
254 export_x_mesh_texture_coords(text, mesh);
255 }
256 if !mesh.vertex_colors.is_empty() {
257 export_x_mesh_vertex_colors(text, mesh);
258 }
259 if !mesh.material_indices.is_empty() || !manifest.materials.is_empty() {
260 export_x_mesh_material_list(text, mesh, &manifest.materials);
261 }
262 text.push_str("}\n");
263 }
264 let material_textures = manifest
265 .materials
266 .iter()
267 .flat_map(|material| material.texture_references.iter())
268 .collect::<Vec<_>>();
269 for texture in &manifest.texture_references {
270 if material_textures
271 .iter()
272 .any(|material_texture| material_texture.eq_ignore_ascii_case(texture))
273 {
274 continue;
275 }
276 text.push_str("TextureFilename {\n \"");
277 text.push_str(&escape_x_string(texture));
278 text.push_str("\";\n}\n");
279 }
280}
281
282fn export_x_mesh_normals(text: &mut String, mesh: &AccessoryMeshSummary) {
283 text.push_str(" MeshNormals {\n");
284 text.push_str(&format!(" {};\n", mesh.normals.len()));
285 for (index, normal) in mesh.normals.iter().enumerate() {
286 let suffix = if index + 1 == mesh.normals.len() {
287 ";;"
288 } else {
289 ","
290 };
291 text.push_str(&format!(
292 " {};{};{};{}\n",
293 format_x_float(normal[0]),
294 format_x_float(normal[1]),
295 format_x_float(normal[2]),
296 suffix
297 ));
298 }
299 text.push_str(&format!(" {};\n", mesh.normal_face_indices.len()));
300 for (index, face) in mesh.normal_face_indices.iter().enumerate() {
301 let suffix = if index + 1 == mesh.normal_face_indices.len() {
302 ";;"
303 } else {
304 ","
305 };
306 let indices = face
307 .iter()
308 .map(u32::to_string)
309 .collect::<Vec<_>>()
310 .join(",");
311 text.push_str(&format!(" {};{};{}\n", face.len(), indices, suffix));
312 }
313 text.push_str(" }\n");
314}
315
316fn export_x_mesh_texture_coords(text: &mut String, mesh: &AccessoryMeshSummary) {
317 text.push_str(" MeshTextureCoords {\n");
318 text.push_str(&format!(" {};\n", mesh.texture_coordinates.len()));
319 for (index, uv) in mesh.texture_coordinates.iter().enumerate() {
320 let suffix = if index + 1 == mesh.texture_coordinates.len() {
321 ";;"
322 } else {
323 ","
324 };
325 text.push_str(&format!(
326 " {};{};{}\n",
327 format_x_float(uv[0]),
328 format_x_float(uv[1]),
329 suffix
330 ));
331 }
332 text.push_str(" }\n");
333}
334
335fn export_x_mesh_vertex_colors(text: &mut String, mesh: &AccessoryMeshSummary) {
336 text.push_str(" MeshVertexColors {\n");
337 text.push_str(&format!(" {};\n", mesh.vertex_colors.len()));
338 for (index, color) in mesh.vertex_colors.iter().enumerate() {
339 let suffix = if index + 1 == mesh.vertex_colors.len() {
340 ";;"
341 } else {
342 ","
343 };
344 text.push_str(&format!(
345 " {};{};{};{};{};{}\n",
346 color.vertex_index,
347 format_x_float(color.color[0]),
348 format_x_float(color.color[1]),
349 format_x_float(color.color[2]),
350 format_x_float(color.color[3]),
351 suffix
352 ));
353 }
354 text.push_str(" }\n");
355}
356
357fn export_x_mesh_material_list(
358 text: &mut String,
359 mesh: &AccessoryMeshSummary,
360 materials: &[AccessoryMaterial],
361) {
362 let max_material_index = mesh.material_indices.iter().copied().max().unwrap_or(0) as usize;
363 let material_start = mesh.material_start_index.min(materials.len());
364 let owned_material_count = mesh
365 .material_count
366 .min(materials.len().saturating_sub(material_start));
367 let material_count = owned_material_count.max(max_material_index + 1).max(1);
368 text.push_str(" MeshMaterialList {\n");
369 text.push_str(&format!(" {};\n", material_count));
370 text.push_str(&format!(" {};\n", mesh.face_indices.len()));
371 for face_index in 0..mesh.face_indices.len() {
372 let material_index = mesh.material_indices.get(face_index).copied().unwrap_or(0);
373 let suffix = if face_index + 1 == mesh.face_indices.len() {
374 ";"
375 } else {
376 ","
377 };
378 text.push_str(&format!(" {}{}\n", material_index, suffix));
379 }
380 for index in 0..material_count {
381 let fallback;
382 let material = if let Some(material) = materials.get(material_start + index) {
383 material
384 } else {
385 fallback = default_accessory_material();
386 &fallback
387 };
388 export_x_material(text, material);
389 }
390 text.push_str(" }\n");
391}
392
393fn default_accessory_material() -> AccessoryMaterial {
394 AccessoryMaterial {
395 name: None,
396 face_color: Some([1.0, 1.0, 1.0, 1.0]),
397 power: Some(0.0),
398 specular_color: Some([0.0, 0.0, 0.0]),
399 emissive_color: Some([0.0, 0.0, 0.0]),
400 texture_references: Vec::new(),
401 }
402}
403
404fn export_x_material(text: &mut String, material: &AccessoryMaterial) {
405 text.push_str(" Material");
406 if let Some(name) = &material.name {
407 text.push(' ');
408 text.push_str(name);
409 }
410 text.push_str(" {\n");
411 let face_color = material.face_color.unwrap_or([1.0, 1.0, 1.0, 1.0]);
412 text.push_str(&format!(
413 " {};{};{};{};;\n",
414 format_x_float(face_color[0]),
415 format_x_float(face_color[1]),
416 format_x_float(face_color[2]),
417 format_x_float(face_color[3])
418 ));
419 text.push_str(&format!(
420 " {};\n",
421 format_x_float(material.power.unwrap_or(0.0))
422 ));
423 let specular = material.specular_color.unwrap_or([0.0, 0.0, 0.0]);
424 text.push_str(&format!(
425 " {};{};{};;\n",
426 format_x_float(specular[0]),
427 format_x_float(specular[1]),
428 format_x_float(specular[2])
429 ));
430 let emissive = material.emissive_color.unwrap_or([0.0, 0.0, 0.0]);
431 text.push_str(&format!(
432 " {};{};{};;\n",
433 format_x_float(emissive[0]),
434 format_x_float(emissive[1]),
435 format_x_float(emissive[2])
436 ));
437 for texture in &material.texture_references {
438 text.push_str(" TextureFilename { \"");
439 text.push_str(&escape_x_string(texture));
440 text.push_str("\"; }\n");
441 }
442 text.push_str(" }\n");
443}
444
445fn format_x_float(value: f32) -> String {
446 if value == 0.0 {
447 "0".to_owned()
448 } else {
449 value.to_string()
450 }
451}
452
453fn export_vac_manifest(manifest: &AccessoryParsedManifest) -> Vec<u8> {
454 let mut text = String::new();
455 if let Some(settings) = &manifest.vac_settings
456 && !settings.raw_lines.is_empty()
457 {
458 for line in &settings.raw_lines {
459 text.push_str(line);
460 text.push('\n');
461 }
462 return encode_sjis(&text);
463 }
464 if manifest.header.is_empty() {
465 text.push_str("accessory\n");
466 } else {
467 text.push_str(&manifest.header);
468 text.push('\n');
469 }
470 if let Some(settings) = &manifest.vac_settings {
471 if let Some(x_file) = settings
472 .x_file
473 .as_ref()
474 .or_else(|| manifest.texture_references.first())
475 && x_file != &manifest.header
476 {
477 text.push_str(x_file);
478 text.push('\n');
479 }
480 if let Some(scale) = settings.scale {
481 text.push_str(&format_x_float(scale));
482 text.push('\n');
483 }
484 if let Some(position) = settings.position {
485 push_vac_vec3(&mut text, position);
486 }
487 if let Some(rotation) = settings.rotation {
488 push_vac_vec3(&mut text, rotation);
489 }
490 if let Some(target) = &settings.attachment_target {
491 text.push_str(target);
492 text.push('\n');
493 }
494 return encode_sjis(&text);
495 }
496 for reference in &manifest.texture_references {
497 if reference != &manifest.header {
498 text.push_str(reference);
499 text.push('\n');
500 }
501 }
502 encode_sjis(&text)
503}
504
505fn push_vac_vec3(text: &mut String, value: [f32; 3]) {
506 text.push_str(&format!(
507 "{},{},{}\n",
508 format_x_float(value[0]),
509 format_x_float(value[1]),
510 format_x_float(value[2])
511 ));
512}
513
514fn parse_vac_settings(lines: &[String], texture_references: &[String]) -> AccessoryVacSettings {
515 let x_file = texture_references.first().cloned();
516 let first_x_index = lines
517 .iter()
518 .position(|line| line.to_ascii_lowercase().ends_with(".x"));
519 let numeric_values = lines
520 .iter()
521 .flat_map(|line| parse_vac_numeric_values(line))
522 .collect::<Vec<_>>();
523 let attachment_target = first_x_index.and_then(|index| {
524 lines
525 .iter()
526 .skip(index + 1)
527 .find(|line| {
528 !line.to_ascii_lowercase().ends_with(".x")
529 && !is_vac_numeric_line(line)
530 && !is_vac_comment_line(line)
531 })
532 .cloned()
533 });
534
535 AccessoryVacSettings {
536 raw_lines: lines.to_vec(),
537 x_file,
538 scale: first_x_index
539 .and_then(|index| lines.get(index + 1))
540 .and_then(|line| parse_vac_scalar(line)),
541 position: first_x_index
542 .and_then(|index| lines.get(index + 2))
543 .and_then(|line| parse_vac_vec3(line)),
544 rotation: first_x_index
545 .and_then(|index| lines.get(index + 3))
546 .and_then(|line| parse_vac_vec3(line)),
547 numeric_values,
548 attachment_target,
549 }
550}
551
552fn is_vac_comment_line(line: &str) -> bool {
553 line.trim_start().starts_with("//")
554}
555
556fn is_vac_numeric_line(line: &str) -> bool {
557 !parse_vac_numeric_values(line).is_empty()
558}
559
560fn parse_vac_scalar(line: &str) -> Option<f32> {
561 let values = parse_vac_numeric_values(line);
562 if values.len() == 1 {
563 Some(values[0])
564 } else {
565 None
566 }
567}
568
569fn parse_vac_vec3(line: &str) -> Option<[f32; 3]> {
570 let values = parse_vac_numeric_values(line);
571 if values.len() == 3 {
572 Some([values[0], values[1], values[2]])
573 } else {
574 None
575 }
576}
577
578fn parse_vac_numeric_values(line: &str) -> Vec<f32> {
579 if is_vac_comment_line(line) {
580 return Vec::new();
581 }
582 let values = line
583 .split(',')
584 .map(str::trim)
585 .map(str::parse::<f32>)
586 .collect::<Result<Vec<_>, _>>();
587 values.unwrap_or_default()
588}
589
590fn escape_x_string(value: &str) -> String {
591 value.replace('"', "\\\"")
592}
593
594fn count_x_blocks(text: &str, keyword: &str) -> usize {
595 text.lines()
596 .map(str::trim_start)
597 .filter(|line| !line.starts_with("template "))
598 .filter(|line| {
599 let Some(rest) = line.strip_prefix(keyword) else {
600 return false;
601 };
602 rest.chars()
603 .next()
604 .is_some_and(|ch| ch.is_whitespace() || ch == '{')
605 })
606 .count()
607}
608
609fn parse_x_mesh_summaries(text: &str) -> Vec<AccessoryMeshSummary> {
610 let lines = text.lines().collect::<Vec<_>>();
611 let mut summaries = Vec::new();
612 let mut index = 0usize;
613 let mut material_start_index = 0usize;
614 while index < lines.len() {
615 let line = lines[index].trim_start();
616 if line.starts_with("template ") || !is_x_block_start(line, "Mesh") {
617 index += 1;
618 continue;
619 }
620 let (block, next_index) = collect_x_block(&lines, index);
621 index = next_index;
622 if let Some(mut summary) = parse_x_mesh_summary_block(&block) {
623 summary.material_start_index = material_start_index;
624 material_start_index += summary.material_count;
625 summaries.push(summary);
626 }
627 }
628 summaries
629}
630
631fn parse_x_mesh_summary_block(block: &str) -> Option<AccessoryMeshSummary> {
632 let lines = block.lines().collect::<Vec<_>>();
633 let (vertex_count, mut index) = next_usize_line(&lines, 1)?;
634 let mut positions = Vec::with_capacity(vertex_count);
635 for _ in 0..vertex_count {
636 index = next_non_empty_after(&lines, index);
637 if index >= lines.len() {
638 break;
639 }
640 if let Some(pos) = parse_x_position(lines[index]) {
641 positions.push(pos);
642 }
643 index += 1;
644 }
645 let (face_count, next_index) = next_usize_line(&lines, index)?;
646 index = next_index;
647 let mut face_indices = Vec::with_capacity(face_count);
648 for _ in 0..face_count {
649 index = next_non_empty_after(&lines, index);
650 if index >= lines.len() {
651 break;
652 }
653 if let Some(fi) = parse_x_face_indices(lines[index]) {
654 face_indices.push(fi);
655 }
656 index += 1;
657 }
658 let mut normals = Vec::new();
659 let mut normal_face_indices = Vec::new();
660 let mut texture_coordinates = Vec::new();
661 let mut vertex_colors = Vec::new();
662 let mut material_indices = Vec::new();
663 let mut material_count = 0usize;
664 while index < lines.len() {
665 let line = lines[index].trim_start();
666 if is_x_block_start(line, "MeshNormals") {
667 let (parsed_normals, parsed_faces, next_index) = parse_x_mesh_normals(&lines, index);
668 normals = parsed_normals;
669 normal_face_indices = parsed_faces;
670 index = next_index;
671 continue;
672 }
673 if is_x_block_start(line, "MeshTextureCoords") {
674 let (coords, next_index) = parse_x_mesh_texture_coords(&lines, index);
675 texture_coordinates = coords;
676 index = next_index;
677 continue;
678 }
679 if is_x_block_start(line, "MeshVertexColors") {
680 let (colors, next_index) = parse_x_mesh_vertex_colors(&lines, index);
681 vertex_colors = colors;
682 index = next_index;
683 continue;
684 }
685 if is_x_block_start(line, "MeshMaterialList") {
686 let (count, indices, next_index) = parse_x_mesh_material_list(&lines, index);
687 material_count = count;
688 material_indices = indices;
689 index = next_index;
690 continue;
691 }
692 index += 1;
693 }
694 Some(AccessoryMeshSummary {
695 vertex_count,
696 face_count,
697 positions,
698 face_indices,
699 normals,
700 normal_face_indices,
701 texture_coordinates,
702 vertex_colors,
703 material_indices,
704 material_start_index: 0,
705 material_count,
706 })
707}
708
709fn parse_x_position(line: &str) -> Option<[f32; 3]> {
710 let parts: Vec<&str> = line.trim().split(';').collect();
712 if parts.len() < 3 {
713 return None;
714 }
715 let x = parts[0].trim().parse::<f32>().ok()?;
716 let y = parts[1].trim().parse::<f32>().ok()?;
717 let z = parts[2].trim().parse::<f32>().ok()?;
718 Some([x, y, z])
719}
720
721fn parse_x_face_indices(line: &str) -> Option<Vec<u32>> {
722 let (count_part, rest) = line.trim().split_once(';')?;
724 let _count: usize = count_part.trim().parse().ok()?;
725 let indices_str = rest.split(';').next().unwrap_or("").trim();
726 let indices = indices_str
727 .split(',')
728 .filter_map(|s| s.trim().parse::<u32>().ok())
729 .collect();
730 Some(indices)
731}
732
733fn parse_x_mesh_normals(lines: &[&str], start: usize) -> (Vec<[f32; 3]>, Vec<Vec<u32>>, usize) {
734 let (block, next_index) = collect_x_block(lines, start);
735 let block_lines = block.lines().collect::<Vec<_>>();
736 let Some((normal_count, mut index)) = next_usize_line(&block_lines, 1) else {
737 return (Vec::new(), Vec::new(), next_index);
738 };
739 let mut normals = Vec::with_capacity(normal_count);
740 for _ in 0..normal_count {
741 index = next_non_empty_after(&block_lines, index);
742 if index >= block_lines.len() {
743 break;
744 }
745 if let Some(normal) = parse_x_position(block_lines[index]) {
746 normals.push(normal);
747 }
748 index += 1;
749 }
750 let Some((face_count, next_line)) = next_usize_line(&block_lines, index) else {
751 return (normals, Vec::new(), next_index);
752 };
753 index = next_line;
754 let mut face_indices = Vec::with_capacity(face_count);
755 for _ in 0..face_count {
756 index = next_non_empty_after(&block_lines, index);
757 if index >= block_lines.len() {
758 break;
759 }
760 if let Some(face) = parse_x_face_indices(block_lines[index]) {
761 face_indices.push(face);
762 }
763 index += 1;
764 }
765 (normals, face_indices, next_index)
766}
767
768fn parse_x_mesh_texture_coords(lines: &[&str], start: usize) -> (Vec<[f32; 2]>, usize) {
769 let (block, next_index) = collect_x_block(lines, start);
770 let block_lines = block.lines().collect::<Vec<_>>();
771 let Some((coord_count, mut index)) = next_usize_line(&block_lines, 1) else {
772 return (Vec::new(), next_index);
773 };
774 let mut coords = Vec::with_capacity(coord_count);
775 for _ in 0..coord_count {
776 index = next_non_empty_after(&block_lines, index);
777 if index >= block_lines.len() {
778 break;
779 }
780 if let Some(coord) = parse_x_texture_coordinate(block_lines[index]) {
781 coords.push(coord);
782 }
783 index += 1;
784 }
785 (coords, next_index)
786}
787
788fn parse_x_mesh_vertex_colors(lines: &[&str], start: usize) -> (Vec<AccessoryVertexColor>, usize) {
789 let (block, next_index) = collect_x_block(lines, start);
790 let block_lines = block.lines().collect::<Vec<_>>();
791 let Some((color_count, mut index)) = next_usize_line(&block_lines, 1) else {
792 return (Vec::new(), next_index);
793 };
794 let mut colors = Vec::with_capacity(color_count);
795 for _ in 0..color_count {
796 index = next_non_empty_after(&block_lines, index);
797 if index >= block_lines.len() {
798 break;
799 }
800 if let Some(color) = parse_x_vertex_color(block_lines[index]) {
801 colors.push(color);
802 }
803 index += 1;
804 }
805 (colors, next_index)
806}
807
808fn parse_x_vertex_color(line: &str) -> Option<AccessoryVertexColor> {
809 let parts: Vec<&str> = line.trim().split(';').collect();
811 if parts.len() < 5 {
812 return None;
813 }
814 let vertex_index = parts[0].trim().parse::<u32>().ok()?;
815 let r = parts[1].trim().parse::<f32>().ok()?;
816 let g = parts[2].trim().parse::<f32>().ok()?;
817 let b = parts[3].trim().parse::<f32>().ok()?;
818 let a = parts[4].trim().parse::<f32>().ok()?;
819 Some(AccessoryVertexColor {
820 vertex_index,
821 color: [r, g, b, a],
822 })
823}
824
825fn parse_x_texture_coordinate(line: &str) -> Option<[f32; 2]> {
826 let parts: Vec<&str> = line.trim().split(';').collect();
827 if parts.len() < 2 {
828 return None;
829 }
830 let u = parts[0].trim().parse::<f32>().ok()?;
831 let v = parts[1].trim().parse::<f32>().ok()?;
832 Some([u, v])
833}
834
835fn parse_x_mesh_material_list(lines: &[&str], start: usize) -> (usize, Vec<u32>, usize) {
836 let (block, next_index) = collect_x_block(lines, start);
837 let block_lines = block.lines().collect::<Vec<_>>();
838 let Some((material_count, index)) = next_usize_line(&block_lines, 1) else {
839 return (0, Vec::new(), next_index);
840 };
841 let Some((face_count, mut index)) = next_usize_line(&block_lines, index) else {
842 return (material_count, Vec::new(), next_index);
843 };
844 let mut indices = Vec::with_capacity(face_count);
845 for _ in 0..face_count {
846 let Some((material_index, next_line)) = next_usize_line(&block_lines, index) else {
847 break;
848 };
849 indices.push(material_index as u32);
850 index = next_line;
851 }
852 (material_count, indices, next_index)
853}
854
855fn parse_x_materials(text: &str) -> Vec<AccessoryMaterial> {
856 let lines = text.lines().collect::<Vec<_>>();
857 let mut materials = Vec::new();
858 let mut index = 0usize;
859 while index < lines.len() {
860 let line = lines[index].trim_start();
861 if line.starts_with("template ") || !is_x_block_start(line, "Material") {
862 index += 1;
863 continue;
864 }
865 let (block, next_index) = collect_x_block(&lines, index);
866 index = next_index;
867 if let Some(material) = parse_x_material_block(&block) {
868 materials.push(material);
869 }
870 }
871 materials
872}
873
874fn collect_x_block(lines: &[&str], start: usize) -> (String, usize) {
875 let mut block = String::new();
876 let mut depth = 0i32;
877 let mut saw_open = false;
878 let mut index = start;
879 while index < lines.len() {
880 let line = lines[index];
881 for ch in line.chars() {
882 match ch {
883 '{' => {
884 depth += 1;
885 saw_open = true;
886 }
887 '}' => {
888 depth -= 1;
889 }
890 _ => {}
891 }
892 }
893 block.push_str(line);
894 block.push('\n');
895 index += 1;
896 if saw_open && depth <= 0 {
897 break;
898 }
899 }
900 (block, index)
901}
902
903fn parse_x_material_block(block: &str) -> Option<AccessoryMaterial> {
904 let header = block
905 .split_once('{')
906 .map(|(header, _)| header.trim())
907 .unwrap_or_default();
908 let name = header
909 .strip_prefix("Material")
910 .map(str::trim)
911 .filter(|name| !name.is_empty())
912 .map(ToOwned::to_owned);
913 let content = block.split_once('{')?.1.rsplit_once('}')?.0;
914 let numeric_values = parse_x_numeric_values(content);
915
916 Some(AccessoryMaterial {
917 name,
918 face_color: numeric_values
919 .get(0..4)
920 .and_then(|values| values.try_into().ok()),
921 power: numeric_values.get(4).copied(),
922 specular_color: numeric_values
923 .get(5..8)
924 .and_then(|values| values.try_into().ok()),
925 emissive_color: numeric_values
926 .get(8..11)
927 .and_then(|values| values.try_into().ok()),
928 texture_references: extract_texture_references(content),
929 })
930}
931
932fn parse_x_numeric_values(text: &str) -> Vec<f32> {
933 text.split(|ch: char| ch.is_whitespace() || ch == ';' || ch == ',' || ch == '{' || ch == '}')
934 .filter_map(|part| part.trim().parse::<f32>().ok())
935 .collect()
936}
937
938fn next_usize_line(lines: &[&str], mut index: usize) -> Option<(usize, usize)> {
939 index = next_non_empty_after(lines, index);
940 if index >= lines.len() {
941 return None;
942 }
943 parse_leading_usize(lines[index]).map(|value| (value, index + 1))
944}
945
946fn next_non_empty_after(lines: &[&str], mut index: usize) -> usize {
947 while index < lines.len() && lines[index].trim().is_empty() {
948 index += 1;
949 }
950 index
951}
952
953fn parse_leading_usize(line: &str) -> Option<usize> {
954 line.trim()
955 .split_once([';', ','])
956 .map(|(value, _)| value.trim())
957 .and_then(|value| value.parse().ok())
958}
959
960fn is_x_block_start(line: &str, keyword: &str) -> bool {
961 let Some(rest) = line.strip_prefix(keyword) else {
962 return false;
963 };
964 rest.chars()
965 .next()
966 .is_some_and(|ch| ch.is_whitespace() || ch == '{')
967}
968
969fn extract_texture_references(text: &str) -> Vec<String> {
970 let mut values = Vec::new();
971 let mut unquoted_text = String::with_capacity(text.len());
972 let mut quoted = String::new();
973 let mut in_quote = false;
974 let mut quote = '\0';
975 let mut escaped = false;
976 for ch in text.chars() {
977 if in_quote {
978 unquoted_text.push(' ');
979 if escaped {
980 if ch == quote {
981 quoted.push(ch);
982 } else {
983 quoted.push('\\');
984 quoted.push(ch);
985 }
986 escaped = false;
987 } else if ch == '\\' {
988 escaped = true;
989 } else if ch == quote {
990 add_texture_reference(&mut values, "ed);
991 quoted.clear();
992 in_quote = false;
993 } else {
994 quoted.push(ch);
995 }
996 } else if ch == '"' || ch == '\'' {
997 unquoted_text.push(' ');
998 in_quote = true;
999 quote = ch;
1000 quoted.clear();
1001 escaped = false;
1002 } else {
1003 unquoted_text.push(ch);
1004 }
1005 }
1006
1007 for token in unquoted_text.split(|c: char| c.is_whitespace() || c == ';' || c == ',') {
1008 let cleaned = token.trim_matches(|c: char| c == '"' || c == '\'');
1009 add_texture_reference(&mut values, cleaned);
1010 }
1011 values
1012}
1013
1014fn add_texture_reference(values: &mut Vec<String>, candidate: &str) {
1015 let lower = candidate.to_ascii_lowercase();
1016 if [".bmp", ".png", ".jpg", ".jpeg", ".tga", ".dds"]
1017 .iter()
1018 .any(|ext| lower.ends_with(ext))
1019 && !values
1020 .iter()
1021 .any(|value: &String| value.eq_ignore_ascii_case(candidate))
1022 {
1023 values.push(candidate.to_owned());
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 fn json_keys(value: &serde_json::Value) -> Vec<String> {
1032 let mut keys = value
1033 .as_object()
1034 .unwrap()
1035 .keys()
1036 .map(ToOwned::to_owned)
1037 .collect::<Vec<_>>();
1038 keys.sort();
1039 keys
1040 }
1041
1042 #[test]
1043 fn exports_text_x_manifest_texture_references() {
1044 let data = br#"xof 0303txt 0032
1045TextureFilename { "tex/main.png"; }
1046TextureFilename { "tex/sub.tga"; }
1047"#;
1048 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1049 let exported = export_accessory_manifest(&parsed);
1050 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1051
1052 assert_eq!(reparsed.format, "x");
1053 assert!(reparsed.text);
1054 assert_eq!(reparsed.header, "xof 0303txt 0032");
1055 assert_eq!(parsed.mesh_count, 0);
1056 assert_eq!(parsed.material_count, 0);
1057 assert_eq!(reparsed.texture_references, parsed.texture_references);
1058 assert!(reparsed.diagnostics.is_empty());
1059 }
1060
1061 #[test]
1062 fn accessory_mesh_summary_json_schema_is_stable() {
1063 let summary = AccessoryMeshSummary {
1064 vertex_count: 1,
1065 face_count: 1,
1066 positions: vec![[0.0, 0.0, 0.0]],
1067 face_indices: vec![vec![0u32]],
1068 normals: Vec::new(),
1069 normal_face_indices: Vec::new(),
1070 texture_coordinates: Vec::new(),
1071 vertex_colors: Vec::new(),
1072 material_indices: vec![0],
1073 material_start_index: 0,
1074 material_count: 1,
1075 };
1076 let keys = json_keys(&serde_json::to_value(&summary).unwrap());
1077 assert_eq!(
1078 keys,
1079 vec![
1080 "faceCount",
1081 "faceIndices",
1082 "materialCount",
1083 "materialIndices",
1084 "materialStartIndex",
1085 "normalFaceIndices",
1086 "normals",
1087 "positions",
1088 "textureCoordinates",
1089 "vertexColors",
1090 "vertexCount",
1091 ]
1092 );
1093 }
1094
1095 #[test]
1096 fn accessory_vertex_color_json_schema_is_stable() {
1097 let vertex_color = AccessoryVertexColor {
1098 vertex_index: 2,
1099 color: [1.0, 0.5, 0.25, 1.0],
1100 };
1101 let keys = json_keys(&serde_json::to_value(&vertex_color).unwrap());
1102 assert_eq!(keys, vec!["color", "vertexIndex"]);
1103 }
1104
1105 #[test]
1106 fn accessory_material_json_schema_is_stable() {
1107 let material = AccessoryMaterial {
1108 name: Some("mat".to_owned()),
1109 face_color: Some([1.0, 0.5, 0.25, 1.0]),
1110 power: Some(32.0),
1111 specular_color: Some([0.1, 0.2, 0.3]),
1112 emissive_color: Some([0.0, 0.0, 0.0]),
1113 texture_references: vec!["tex.png".to_owned()],
1114 };
1115 let keys = json_keys(&serde_json::to_value(&material).unwrap());
1116 assert_eq!(
1117 keys,
1118 vec![
1119 "emissiveColor",
1120 "faceColor",
1121 "name",
1122 "power",
1123 "specularColor",
1124 "textureReferences",
1125 ]
1126 );
1127 }
1128
1129 #[test]
1130 fn accessory_vac_settings_json_schema_is_stable() {
1131 let settings = AccessoryVacSettings {
1132 raw_lines: vec!["sample".to_owned(), "model.x".to_owned()],
1133 x_file: Some("model.x".to_owned()),
1134 scale: Some(1.0),
1135 position: Some([0.0, 1.0, 2.0]),
1136 rotation: Some([10.0, 20.0, 30.0]),
1137 numeric_values: vec![1.0, 0.0, 1.0, 2.0, 10.0, 20.0, 30.0],
1138 attachment_target: Some("右手首".to_owned()),
1139 };
1140 let keys = json_keys(&serde_json::to_value(&settings).unwrap());
1141 assert_eq!(
1142 keys,
1143 vec![
1144 "attachmentTarget",
1145 "numericValues",
1146 "position",
1147 "rawLines",
1148 "rotation",
1149 "scale",
1150 "xFile",
1151 ]
1152 );
1153 }
1154
1155 #[test]
1156 fn parses_text_x_mesh_vertex_positions() {
1157 let data = br#"xof 0303txt 0032
1158Mesh {
1159 3;
1160 0.0;0.0;0.0;,
1161 1.0;0.0;0.0;,
1162 0.0;1.0;0.0;;
1163 1;
1164 3;0,1,2;;
1165}
1166"#;
1167 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1168 assert_eq!(parsed.mesh_summaries.len(), 1);
1169 let summary = &parsed.mesh_summaries[0];
1170 assert_eq!(summary.vertex_count, 3);
1171 assert_eq!(summary.positions.len(), 3);
1172 assert_eq!(summary.positions[0], [0.0f32, 0.0, 0.0]);
1173 assert_eq!(summary.positions[1], [1.0f32, 0.0, 0.0]);
1174 assert_eq!(summary.positions[2], [0.0f32, 1.0, 0.0]);
1175 }
1176
1177 #[test]
1178 fn parses_text_x_mesh_face_indices() {
1179 let data = br#"xof 0303txt 0032
1180Mesh {
1181 4;
1182 0;0;0;,
1183 1;0;0;,
1184 1;1;0;,
1185 0;1;0;;
1186 2;
1187 3;0,1,2;;
1188 3;0,2,3;;
1189}
1190"#;
1191 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1192 assert_eq!(parsed.mesh_summaries.len(), 1);
1193 let summary = &parsed.mesh_summaries[0];
1194 assert_eq!(summary.face_count, 2);
1195 assert_eq!(summary.face_indices.len(), 2);
1196 assert_eq!(summary.face_indices[0], vec![0u32, 1, 2]);
1197 assert_eq!(summary.face_indices[1], vec![0u32, 2, 3]);
1198 }
1199
1200 #[test]
1201 fn parses_text_x_mesh_texture_coordinates() {
1202 let data = br#"xof 0303txt 0032
1203Mesh {
1204 3;
1205 0;0;0;,
1206 1;0;0;,
1207 0;1;0;;
1208 1;
1209 3;0,1,2;;
1210 MeshNormals {
1211 1;
1212 0;0;1;;
1213 1;
1214 3;0,0,0;;
1215 }
1216 MeshTextureCoords {
1217 3;
1218 0;0;,
1219 1;0;,
1220 0;1;;
1221 }
1222}
1223"#;
1224 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1225
1226 assert_eq!(parsed.mesh_summaries.len(), 1);
1227 assert_eq!(
1228 parsed.mesh_summaries[0].texture_coordinates,
1229 vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]
1230 );
1231 }
1232
1233 #[test]
1234 fn parses_text_x_mesh_normals() {
1235 let data = br#"xof 0303txt 0032
1236Mesh {
1237 3;
1238 0;0;0;,
1239 1;0;0;,
1240 0;1;0;;
1241 1;
1242 3;0,1,2;;
1243 MeshNormals {
1244 1;
1245 0;0;1;;
1246 1;
1247 3;0,0,0;;
1248 }
1249}
1250"#;
1251 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1252
1253 assert_eq!(parsed.mesh_summaries.len(), 1);
1254 assert_eq!(parsed.mesh_summaries[0].normals, vec![[0.0, 0.0, 1.0]]);
1255 assert_eq!(
1256 parsed.mesh_summaries[0].normal_face_indices,
1257 vec![vec![0, 0, 0]]
1258 );
1259 }
1260
1261 #[test]
1262 fn parses_text_x_mesh_material_indices() {
1263 let data = br#"xof 0303txt 0032
1264Mesh {
1265 4;
1266 0;0;0;,
1267 1;0;0;,
1268 1;1;0;,
1269 0;1;0;;
1270 2;
1271 3;0,1,2;,
1272 3;0,2,3;;
1273 MeshMaterialList {
1274 2;
1275 2;
1276 0,
1277 1;
1278 Material { 1;1;1;1;; 5; 0;0;0;; 0;0;0;; }
1279 Material { 0;0;0;1;; 5; 0;0;0;; 0;0;0;; }
1280 }
1281}
1282"#;
1283 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1284 assert_eq!(parsed.mesh_summaries.len(), 1);
1285 assert_eq!(parsed.mesh_summaries[0].material_indices, vec![0, 1]);
1286 assert_eq!(parsed.materials.len(), 2);
1287 }
1288
1289 #[test]
1290 fn parses_text_x_mesh_material_indices_after_child_blocks() {
1291 let data = br#"xof 0303txt 0032
1292Mesh {
1293 3;
1294 0;0;0;,
1295 1;0;0;,
1296 0;1;0;;
1297 1;
1298 3;0,1,2;;
1299 MeshNormals {
1300 1;
1301 0;0;1;;
1302 1;
1303 3;0,0,0;;
1304 }
1305 MeshMaterialList {
1306 1;
1307 1;
1308 0;
1309 Material { 1;1;1;1;; 5; 0;0;0;; 0;0;0;; }
1310 }
1311}
1312"#;
1313 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1314
1315 assert_eq!(parsed.mesh_summaries.len(), 1);
1316 assert_eq!(parsed.mesh_summaries[0].material_indices, vec![0]);
1317 assert_eq!(parsed.materials.len(), 1);
1318 }
1319
1320 #[test]
1321 fn parses_text_x_mesh_and_material_counts() {
1322 let data = br#"xof 0303txt 0032
1323template Mesh { <template-guid> }
1324Mesh {
1325 3;
1326 0;0;0;,
1327 1;0;0;,
1328 0;1;0;;
1329 1;
1330 3;0,1,2;;
1331 MeshMaterialList {
1332 2;
1333 1;
1334 0;
1335 Material { 1.0;1.0;1.0;1.0;; 1.0; 0.0;0.0;0.0;; 0.0;0.0;0.0;; }
1336 Material namedMat { 0.5;0.5;0.5;1.0;; 1.0; 0.0;0.0;0.0;; 0.0;0.0;0.0;; }
1337 }
1338}
1339"#;
1340 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1341
1342 assert_eq!(parsed.mesh_count, 1);
1343 assert_eq!(parsed.material_count, 2);
1344 assert_eq!(parsed.mesh_summaries.len(), 1);
1345 assert_eq!(parsed.mesh_summaries[0].vertex_count, 3);
1346 assert_eq!(parsed.mesh_summaries[0].face_count, 1);
1347 assert_eq!(parsed.mesh_summaries[0].material_indices, vec![0]);
1348 assert_eq!(parsed.materials.len(), 2);
1349 assert_eq!(parsed.materials[0].name, None);
1350 assert_eq!(parsed.materials[0].face_color, Some([1.0, 1.0, 1.0, 1.0]));
1351 assert_eq!(parsed.materials[1].name, Some("namedMat".to_owned()));
1352 assert_eq!(parsed.materials[1].face_color, Some([0.5, 0.5, 0.5, 1.0]));
1353 }
1354
1355 #[test]
1356 fn exports_text_x_mesh_material_roundtrip() {
1357 let data = br#"xof 0303txt 0032
1358Mesh {
1359 3;
1360 0;0;0;,
1361 1;0;0;,
1362 0;1;0;;
1363 1;
1364 3;0,1,2;;
1365 MeshNormals {
1366 1;
1367 0;0;1;;
1368 1;
1369 3;0,0,0;;
1370 }
1371 MeshTextureCoords {
1372 3;
1373 0;0;,
1374 1;0;,
1375 0;1;;
1376 }
1377 MeshVertexColors {
1378 3;
1379 2;1.0;0.5;0.25;1.0;,
1380 0;0.0;1.0;0.0;0.75;,
1381 1;0.0;0.0;1.0;0.5;;
1382 }
1383 MeshMaterialList {
1384 1;
1385 1;
1386 0;
1387 Material namedMat {
1388 0.5;0.25;0.75;1.0;;
1389 8.0;
1390 0.1;0.2;0.3;;
1391 0.0;0.0;0.0;;
1392 TextureFilename { "mesh.png"; }
1393 }
1394 }
1395}
1396"#;
1397 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1398 let exported = export_accessory_manifest(&parsed);
1399 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1400
1401 assert_eq!(reparsed.mesh_count, 1);
1402 assert_eq!(reparsed.material_count, 1);
1403 assert_eq!(reparsed.mesh_summaries.len(), 1);
1404 assert_eq!(
1405 reparsed.mesh_summaries[0].positions,
1406 parsed.mesh_summaries[0].positions
1407 );
1408 assert_eq!(
1409 reparsed.mesh_summaries[0].face_indices,
1410 parsed.mesh_summaries[0].face_indices
1411 );
1412 assert_eq!(reparsed.mesh_summaries[0].normals, vec![[0.0, 0.0, 1.0]]);
1413 assert_eq!(
1414 reparsed.mesh_summaries[0].normal_face_indices,
1415 vec![vec![0, 0, 0]]
1416 );
1417 assert_eq!(
1418 reparsed.mesh_summaries[0].texture_coordinates,
1419 vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]
1420 );
1421 assert_eq!(
1422 reparsed.mesh_summaries[0].vertex_colors,
1423 parsed.mesh_summaries[0].vertex_colors
1424 );
1425 assert_eq!(reparsed.mesh_summaries[0].vertex_colors[0].vertex_index, 2);
1426 assert_eq!(
1427 reparsed.mesh_summaries[0].vertex_colors[0].color,
1428 [1.0, 0.5, 0.25, 1.0]
1429 );
1430 assert_eq!(reparsed.mesh_summaries[0].material_indices, vec![0]);
1431 assert_eq!(reparsed.materials.len(), 1);
1432 assert_eq!(reparsed.materials[0].name, Some("namedMat".to_owned()));
1433 assert_eq!(
1434 reparsed.materials[0].face_color,
1435 Some([0.5, 0.25, 0.75, 1.0])
1436 );
1437 assert_eq!(reparsed.materials[0].power, Some(8.0));
1438 assert_eq!(reparsed.materials[0].texture_references, vec!["mesh.png"]);
1439 assert_eq!(reparsed.texture_references, vec!["mesh.png"]);
1440 }
1441
1442 #[test]
1443 fn tracks_text_x_multi_mesh_material_ownership() {
1444 let data = br#"xof 0303txt 0032
1445Mesh {
1446 3;
1447 0;0;0;,
1448 1;0;0;,
1449 0;1;0;;
1450 1;
1451 3;0,1,2;;
1452 MeshMaterialList {
1453 1;
1454 1;
1455 0;
1456 Material { 1;1;1;1;; 5; 0;0;0;; 0;0;0;; }
1457 }
1458}
1459Mesh {
1460 3;
1461 0;0;1;,
1462 1;0;1;,
1463 0;1;1;;
1464 1;
1465 3;0,2,1;;
1466 MeshMaterialList {
1467 1;
1468 1;
1469 0;
1470 Material { 0;0;0;1;; 5; 0;0;0;; 0;0;0;; }
1471 }
1472}
1473"#;
1474 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1475
1476 assert_eq!(parsed.mesh_summaries.len(), 2);
1477 assert_eq!(parsed.materials.len(), 2);
1478 assert!(parsed.diagnostics.is_empty());
1479 assert_eq!(parsed.mesh_summaries[0].material_start_index, 0);
1480 assert_eq!(parsed.mesh_summaries[0].material_count, 1);
1481 assert_eq!(parsed.mesh_summaries[1].material_start_index, 1);
1482 assert_eq!(parsed.mesh_summaries[1].material_count, 1);
1483 }
1484
1485 #[test]
1486 fn exports_text_x_multiple_mesh_material_indices_roundtrip() {
1487 let manifest = AccessoryParsedManifest {
1488 format: "x".to_owned(),
1489 byte_length: 0,
1490 text: true,
1491 header: "xof 0303txt 0032".to_owned(),
1492 mesh_count: 2,
1493 material_count: 1,
1494 mesh_summaries: vec![
1495 AccessoryMeshSummary {
1496 vertex_count: 3,
1497 face_count: 1,
1498 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1499 face_indices: vec![vec![0, 1, 2]],
1500 normals: vec![[0.0, 0.0, 1.0]],
1501 normal_face_indices: vec![vec![0, 0, 0]],
1502 texture_coordinates: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
1503 vertex_colors: Vec::new(),
1504 material_indices: vec![0],
1505 material_start_index: 0,
1506 material_count: 1,
1507 },
1508 AccessoryMeshSummary {
1509 vertex_count: 3,
1510 face_count: 1,
1511 positions: vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]],
1512 face_indices: vec![vec![0, 2, 1]],
1513 normals: vec![[0.0, 0.0, -1.0]],
1514 normal_face_indices: vec![vec![0, 0, 0]],
1515 texture_coordinates: vec![[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]],
1516 vertex_colors: Vec::new(),
1517 material_indices: vec![0],
1518 material_start_index: 1,
1519 material_count: 1,
1520 },
1521 ],
1522 materials: vec![
1523 AccessoryMaterial {
1524 name: Some("sharedMat".to_owned()),
1525 face_color: Some([1.0, 1.0, 1.0, 1.0]),
1526 power: Some(4.0),
1527 specular_color: Some([0.0, 0.0, 0.0]),
1528 emissive_color: Some([0.0, 0.0, 0.0]),
1529 texture_references: Vec::new(),
1530 },
1531 AccessoryMaterial {
1532 name: Some("secondMat".to_owned()),
1533 face_color: Some([0.5, 0.5, 0.5, 1.0]),
1534 power: Some(2.0),
1535 specular_color: Some([0.0, 0.0, 0.0]),
1536 emissive_color: Some([0.0, 0.0, 0.0]),
1537 texture_references: Vec::new(),
1538 },
1539 ],
1540 vac_settings: None,
1541 texture_references: Vec::new(),
1542 diagnostics: Vec::new(),
1543 };
1544
1545 let exported = export_accessory_manifest(&manifest);
1546 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1547
1548 assert_eq!(reparsed.mesh_summaries.len(), 2);
1549 assert_eq!(reparsed.mesh_summaries[0].material_indices, vec![0]);
1550 assert_eq!(reparsed.mesh_summaries[1].material_indices, vec![0]);
1551 assert_eq!(reparsed.mesh_summaries[0].material_start_index, 0);
1552 assert_eq!(reparsed.mesh_summaries[0].material_count, 1);
1553 assert_eq!(reparsed.mesh_summaries[1].material_start_index, 1);
1554 assert_eq!(reparsed.mesh_summaries[1].material_count, 1);
1555 assert_eq!(reparsed.mesh_summaries[1].face_indices, vec![vec![0, 2, 1]]);
1556 assert_eq!(reparsed.mesh_summaries[0].normals, vec![[0.0, 0.0, 1.0]]);
1557 assert_eq!(reparsed.mesh_summaries[1].normals, vec![[0.0, 0.0, -1.0]]);
1558 assert_eq!(
1559 reparsed.mesh_summaries[0].normal_face_indices,
1560 vec![vec![0, 0, 0]]
1561 );
1562 assert_eq!(
1563 reparsed.mesh_summaries[1].texture_coordinates,
1564 vec![[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]]
1565 );
1566 assert_eq!(reparsed.materials.len(), 2);
1567 assert_eq!(reparsed.materials[0].name, Some("sharedMat".to_owned()));
1568 assert_eq!(reparsed.materials[1].name, Some("secondMat".to_owned()));
1569 }
1570
1571 #[test]
1572 fn parses_text_x_material_texture_reference() {
1573 let data = br#"xof 0303txt 0032
1574Material screenMat {
1575 1.0;0.5;0.25;1.0;;
1576 32.0;
1577 0.1;0.2;0.3;;
1578 0.0;0.0;0.0;;
1579 TextureFilename { "screen.png"; }
1580}
1581"#;
1582 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1583
1584 assert_eq!(parsed.material_count, 1);
1585 assert_eq!(parsed.materials.len(), 1);
1586 assert_eq!(parsed.materials[0].name, Some("screenMat".to_owned()));
1587 assert_eq!(parsed.materials[0].face_color, Some([1.0, 0.5, 0.25, 1.0]));
1588 assert_eq!(parsed.materials[0].power, Some(32.0));
1589 assert_eq!(parsed.materials[0].specular_color, Some([0.1, 0.2, 0.3]));
1590 assert_eq!(parsed.materials[0].emissive_color, Some([0.0, 0.0, 0.0]));
1591 assert_eq!(parsed.materials[0].texture_references, vec!["screen.png"]);
1592 }
1593
1594 #[test]
1595 fn parses_single_line_x_material() {
1596 let data = br#"xof 0303txt 0032
1597Material namedMat { 0.5;0.5;0.5;1.0;; 8.0; 0.0;0.0;0.0;; 0.1;0.2;0.3;; }
1598"#;
1599 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1600
1601 assert_eq!(parsed.materials.len(), 1);
1602 assert_eq!(parsed.materials[0].name, Some("namedMat".to_owned()));
1603 assert_eq!(parsed.materials[0].power, Some(8.0));
1604 assert_eq!(parsed.materials[0].emissive_color, Some([0.1, 0.2, 0.3]));
1605 }
1606
1607 #[test]
1608 fn parses_text_x_mesh_vertex_colors() {
1609 let data = br#"xof 0303txt 0032
1610Mesh {
1611 3;
1612 0;0;0;,
1613 1;0;0;,
1614 0;1;0;;
1615 1;
1616 3;0,1,2;;
1617 MeshVertexColors {
1618 2;
1619 2;1.0;1.0;1.0;1.0;,
1620 0;0.5;0.5;0.5;1.0;;
1621 }
1622}
1623"#;
1624 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1625 assert_eq!(parsed.mesh_summaries.len(), 1);
1626 let summary = &parsed.mesh_summaries[0];
1627 assert_eq!(summary.vertex_colors.len(), 2);
1628 assert_eq!(summary.vertex_colors[0].vertex_index, 2);
1629 assert_eq!(summary.vertex_colors[0].color, [1.0, 1.0, 1.0, 1.0]);
1630 assert_eq!(summary.vertex_colors[1].vertex_index, 0);
1631 assert_eq!(summary.vertex_colors[1].color, [0.5, 0.5, 0.5, 1.0]);
1632 }
1633
1634 #[test]
1635 fn roundtrip_text_x_mesh_vertex_colors() {
1636 let data = br#"xof 0303txt 0032
1637Mesh {
1638 3;
1639 0;0;0;,
1640 1;0;0;,
1641 0;1;0;;
1642 1;
1643 3;0,1,2;;
1644 MeshVertexColors {
1645 3;
1646 2;1.0;0.5;0.25;1.0;,
1647 0;0.0;1.0;0.0;0.75;,
1648 1;0.0;0.0;1.0;0.5;;
1649 }
1650}
1651"#;
1652 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1653 let exported = export_accessory_manifest(&parsed);
1654 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1655
1656 assert_eq!(reparsed.mesh_summaries.len(), 1);
1657 assert_eq!(
1658 reparsed.mesh_summaries[0].vertex_colors,
1659 parsed.mesh_summaries[0].vertex_colors
1660 );
1661 assert_eq!(reparsed.mesh_summaries[0].vertex_colors.len(), 3);
1662 assert_eq!(reparsed.mesh_summaries[0].vertex_colors[0].vertex_index, 2);
1663 assert_eq!(
1664 reparsed.mesh_summaries[0].vertex_colors[0].color,
1665 [1.0, 0.5, 0.25, 1.0]
1666 );
1667 assert_eq!(reparsed.mesh_summaries[0].vertex_colors[1].vertex_index, 0);
1668 assert_eq!(
1669 reparsed.mesh_summaries[0].vertex_colors[1].color,
1670 [0.0, 1.0, 0.0, 0.75]
1671 );
1672 assert_eq!(reparsed.mesh_summaries[0].vertex_colors[2].vertex_index, 1);
1673 assert_eq!(
1674 reparsed.mesh_summaries[0].vertex_colors[2].color,
1675 [0.0, 0.0, 1.0, 0.5]
1676 );
1677 }
1678
1679 #[test]
1680 fn binary_x_is_diagnostic_only() {
1681 let data = b"xof 0303bin 0032\x01\x02\x03\x04";
1682 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1683
1684 assert!(!parsed.text);
1685 assert_eq!(parsed.mesh_count, 0);
1686 assert_eq!(parsed.material_count, 0);
1687 assert!(parsed.mesh_summaries.is_empty());
1688 assert!(parsed.materials.is_empty());
1689 assert_eq!(parsed.diagnostics.len(), 1);
1690 assert_eq!(parsed.diagnostics[0].code, "X_BINARY_LAYOUT_NOT_EXPANDED");
1691 }
1692
1693 #[test]
1694 fn exports_text_x_manifest_preserves_windows_texture_paths() {
1695 let data = br#"xof 0302txt 0064
1696TextureFilename { "D:\\MikuMikuDance_v932x64\\UserFile\\Accessory\\screen.png"; }
1697"#;
1698 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1699 let exported = export_accessory_manifest(&parsed);
1700 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1701
1702 assert_eq!(reparsed.header, "xof 0302txt 0064");
1703 assert_eq!(reparsed.texture_references, parsed.texture_references);
1704 }
1705
1706 #[test]
1707 fn exports_text_x_manifest_preserves_quoted_texture_paths_with_separators() {
1708 let data = br#"xof 0303txt 0032
1709TextureFilename { "C:\My Files\tex,main;01.png"; }
1710"#;
1711 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1712 let exported = export_accessory_manifest(&parsed);
1713 let reparsed = parse_accessory_manifest(&exported, Some("stage.x")).unwrap();
1714
1715 assert_eq!(
1716 parsed.texture_references,
1717 vec!["C:\\My Files\\tex,main;01.png"]
1718 );
1719 assert_eq!(reparsed.texture_references, parsed.texture_references);
1720 }
1721
1722 #[test]
1723 fn exports_vac_manifest_references() {
1724 let data = "sample accessory\r\nmodel.x\r\n".as_bytes();
1725 let parsed = parse_accessory_manifest(data, Some("model.vac")).unwrap();
1726 let exported = export_accessory_manifest(&parsed);
1727 let reparsed = parse_accessory_manifest(&exported, Some("model.vac")).unwrap();
1728
1729 assert_eq!(reparsed.format, "vac");
1730 assert_eq!(reparsed.header, "sample accessory");
1731 assert_eq!(reparsed.texture_references, vec!["model.x"]);
1732 assert_eq!(reparsed.diagnostics[0].code, "VAC_ACCESSORY_WRAPPER");
1733 }
1734
1735 #[test]
1736 fn exports_vac_manifest_preserves_raw_display_and_attachment_lines() {
1737 let data =
1738 encode_sjis("sample accessory\r\nmodel.x\r\n1.5\r\n0,1,2\r\n10,20,30\r\n右手首\r\n");
1739 let parsed = parse_accessory_manifest(&data, Some("model.vac")).unwrap();
1740 let settings = parsed.vac_settings.as_ref().unwrap();
1741
1742 assert_eq!(settings.x_file, Some("model.x".to_owned()));
1743 assert_eq!(settings.scale, Some(1.5));
1744 assert_eq!(settings.position, Some([0.0, 1.0, 2.0]));
1745 assert_eq!(settings.rotation, Some([10.0, 20.0, 30.0]));
1746 assert_eq!(
1747 settings.numeric_values,
1748 vec![1.5, 0.0, 1.0, 2.0, 10.0, 20.0, 30.0]
1749 );
1750 assert_eq!(settings.attachment_target, Some("右手首".to_owned()));
1751
1752 let exported = export_accessory_manifest(&parsed);
1753 let reparsed = parse_accessory_manifest(&exported, Some("model.vac")).unwrap();
1754
1755 assert_eq!(reparsed.texture_references, vec!["model.x"]);
1756 assert_eq!(
1757 reparsed.vac_settings.unwrap().raw_lines,
1758 vec![
1759 "sample accessory",
1760 "model.x",
1761 "1.5",
1762 "0,1,2",
1763 "10,20,30",
1764 "右手首",
1765 ]
1766 );
1767 }
1768
1769 #[test]
1770 fn exports_vac_manifest_from_semantic_settings_without_raw_lines() {
1771 let manifest = AccessoryParsedManifest {
1772 format: "vac".to_owned(),
1773 byte_length: 0,
1774 text: true,
1775 header: "sample accessory".to_owned(),
1776 mesh_count: 0,
1777 material_count: 0,
1778 mesh_summaries: Vec::new(),
1779 materials: Vec::new(),
1780 vac_settings: Some(AccessoryVacSettings {
1781 raw_lines: Vec::new(),
1782 x_file: Some("model.x".to_owned()),
1783 scale: Some(1.5),
1784 position: Some([0.0, 1.0, 2.0]),
1785 rotation: Some([10.0, 20.0, 30.0]),
1786 numeric_values: Vec::new(),
1787 attachment_target: Some("右手首".to_owned()),
1788 }),
1789 texture_references: Vec::new(),
1790 diagnostics: Vec::new(),
1791 };
1792
1793 let exported = export_accessory_manifest(&manifest);
1794 let reparsed = parse_accessory_manifest(&exported, Some("model.vac")).unwrap();
1795 let settings = reparsed.vac_settings.as_ref().unwrap();
1796
1797 assert_eq!(reparsed.header, "sample accessory");
1798 assert_eq!(reparsed.texture_references, vec!["model.x"]);
1799 assert_eq!(settings.scale, Some(1.5));
1800 assert_eq!(settings.position, Some([0.0, 1.0, 2.0]));
1801 assert_eq!(settings.rotation, Some([10.0, 20.0, 30.0]));
1802 assert_eq!(settings.attachment_target, Some("右手首".to_owned()));
1803 }
1804
1805 #[test]
1806 fn parses_real_mmd_vac_scale_position_rotation_layout() {
1807 let data = encode_sjis(
1808 "ネギ(右手)\r\nnegi.x\r\n1.0\r\n-0.5,-1.0,0.00\r\n0.0,0.0,0.0\r\n右手首\r\n",
1809 );
1810 let parsed = parse_accessory_manifest(&data, Some("negi.vac")).unwrap();
1811 let settings = parsed.vac_settings.as_ref().unwrap();
1812
1813 assert_eq!(parsed.header, "ネギ(右手)");
1814 assert_eq!(settings.x_file, Some("negi.x".to_owned()));
1815 assert_eq!(settings.scale, Some(1.0));
1816 assert_eq!(settings.position, Some([-0.5, -1.0, 0.0]));
1817 assert_eq!(settings.rotation, Some([0.0, 0.0, 0.0]));
1818 assert_eq!(settings.attachment_target, Some("右手首".to_owned()));
1819 assert_eq!(
1820 settings.numeric_values,
1821 vec![1.0, -0.5, -1.0, 0.0, 0.0, 0.0, 0.0]
1822 );
1823 }
1824
1825 #[test]
1826 fn vac_attachment_target_ignores_comment_lines_after_numeric_fields() {
1827 let data = encode_sjis(
1828 "sample accessory\r\nmodel.x\r\n1.0\r\n0,1,2\r\n10,20,30\r\n// comment\r\n右手首\r\n",
1829 );
1830 let parsed = parse_accessory_manifest(&data, Some("model.vac")).unwrap();
1831 let settings = parsed.vac_settings.as_ref().unwrap();
1832
1833 assert_eq!(settings.scale, Some(1.0));
1834 assert_eq!(settings.position, Some([0.0, 1.0, 2.0]));
1835 assert_eq!(settings.rotation, Some([10.0, 20.0, 30.0]));
1836 assert_eq!(settings.attachment_target, Some("右手首".to_owned()));
1837 }
1838
1839 #[test]
1840 fn accessory_manifest_json_top_level_schema_is_stable() {
1841 let data = br#"xof 0303txt 0032
1842TextureFilename { "tex/main.png"; }
1843"#;
1844 let parsed = parse_accessory_manifest(data, Some("stage.x")).unwrap();
1845 let keys = json_keys(&serde_json::to_value(&parsed).unwrap());
1846
1847 assert_eq!(
1848 keys,
1849 vec![
1850 "byteLength",
1851 "diagnostics",
1852 "format",
1853 "header",
1854 "materialCount",
1855 "materials",
1856 "meshCount",
1857 "meshSummaries",
1858 "text",
1859 "textureReferences",
1860 "vacSettings",
1861 ]
1862 );
1863 }
1864}