Skip to main content

mmd_anim_format/
xfile.rs

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    // Format: "x;y;z;[,|;]"
715    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    // Format: "<nVerts>;<i0>,<i1>,...;<,|;>"
727    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    // Format: "vertexIndex;r;g;b;a;[,|;;]"
814    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, &quoted);
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}