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