Skip to main content

mmd_anim_format/
xfile.rs

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