Skip to content

Commit bae4c03

Browse files
committed
dynamic cosmetics editor manifest
1 parent 9a16880 commit bae4c03

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

fast64_internal/hm64/f3d/soh_xml_exporter.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import os
77
import bpy
88
from html import escape
9+
from pathlib import Path
910
from struct import pack
11+
import xml.etree.ElementTree as ET
1012

1113
from ...f3d.f3d_gbi import (
1214
DPFullSync,
@@ -137,6 +139,127 @@ def getDynamicCosmeticXmlAttrs(cosmeticEntry: str, cosmeticCategory: str):
137139
return attrs
138140

139141

142+
def _get_cosmetic_manifest_path(modelDirPath: str, objectPath: str) -> str:
143+
model_path = Path(modelDirPath)
144+
object_parts = [part for part in (objectPath or "").replace("\\", "/").split("/") if part]
145+
if not object_parts:
146+
manifest_root = model_path.parent if model_path.name.lower() == "alt" else model_path
147+
return str(manifest_root / "CosmeticEntries")
148+
149+
model_parts = list(model_path.parts)
150+
if len(model_parts) >= len(object_parts):
151+
tail = model_parts[-len(object_parts) :]
152+
if [part.lower() for part in tail] == [part.lower() for part in object_parts]:
153+
manifest_root = Path(*model_parts[: len(model_parts) - len(object_parts)])
154+
if manifest_root.name.lower() == "alt":
155+
manifest_root = manifest_root.parent
156+
return str(manifest_root / "CosmeticEntries")
157+
158+
manifest_root = model_path.parent if model_path.name.lower() == "alt" else model_path
159+
return str(manifest_root / "CosmeticEntries")
160+
def _read_existing_cosmetic_manifest_entries(manifestPath: str) -> list[dict[str, str]]:
161+
if not os.path.exists(manifestPath):
162+
return []
163+
164+
try:
165+
root = ET.parse(manifestPath).getroot()
166+
except ET.ParseError as exc:
167+
raise PluginError(f"Unable to parse existing cosmetic manifest at {manifestPath}: {exc}") from exc
168+
169+
if root.tag != "CustomCosmetics":
170+
raise PluginError(f'Unexpected cosmetic manifest root "{root.tag}" in {manifestPath}.')
171+
172+
entries: list[dict[str, str]] = []
173+
for entry in root.findall("Entry"):
174+
entries.append(
175+
{
176+
"CosmeticCategory": entry.get("CosmeticCategory", ""),
177+
"CosmeticEntry": entry.get("CosmeticEntry", ""),
178+
"MaterialPath": entry.get("MaterialPath", ""),
179+
"CosmeticType": entry.get("CosmeticType", ""),
180+
}
181+
)
182+
return entries
183+
184+
185+
def _serialize_cosmetic_manifest(entries: list[dict[str, str]]) -> str:
186+
lines = ["<CustomCosmetics>"]
187+
for entry in entries:
188+
attrs = " ".join(
189+
[
190+
f'CosmeticCategory="{escape(entry["CosmeticCategory"], quote=True)}"',
191+
f'CosmeticEntry="{escape(entry["CosmeticEntry"], quote=True)}"',
192+
f'MaterialPath="{escape(entry["MaterialPath"], quote=True)}"',
193+
f'CosmeticType="{escape(entry["CosmeticType"], quote=True)}"',
194+
]
195+
)
196+
lines.append(f" <Entry {attrs} />")
197+
lines.append("</CustomCosmetics>")
198+
return "\n".join(lines)
199+
200+
201+
def _collect_material_cosmetic_manifest_entries(fMaterial, objectPath: str) -> list[dict[str, str]]:
202+
materialPath = format_asset_path(objectPath, fMaterial.material.name)
203+
entries: list[dict[str, str]] = []
204+
205+
for command in fMaterial.material.commands:
206+
cosmeticType = None
207+
if isinstance(command, DPSetPrimColor):
208+
cosmeticType = "Prim"
209+
elif isinstance(command, DPSetEnvColor):
210+
cosmeticType = "Env"
211+
212+
if cosmeticType is None:
213+
continue
214+
215+
cosmeticEntry = (getattr(command, "cosmeticEntry", "") or "").strip()
216+
if not cosmeticEntry:
217+
continue
218+
219+
entries.append(
220+
{
221+
"CosmeticCategory": (getattr(command, "cosmeticCategory", "") or "").strip(),
222+
"CosmeticEntry": cosmeticEntry,
223+
"MaterialPath": materialPath,
224+
"CosmeticType": cosmeticType,
225+
}
226+
)
227+
228+
return entries
229+
230+
231+
def _write_custom_cosmetics_manifest(modelDirPath: str, objectPath: str, manifestEntries: list[dict[str, str]]):
232+
if not manifestEntries:
233+
return
234+
235+
manifestPath = _get_cosmetic_manifest_path(modelDirPath, objectPath)
236+
existingEntries = _read_existing_cosmetic_manifest_entries(manifestPath)
237+
mergedEntries = list(existingEntries)
238+
existingKeys = {
239+
(
240+
entry["CosmeticCategory"],
241+
entry["CosmeticEntry"],
242+
entry["MaterialPath"],
243+
entry["CosmeticType"],
244+
)
245+
for entry in existingEntries
246+
}
247+
248+
for entry in manifestEntries:
249+
key = (
250+
entry["CosmeticCategory"],
251+
entry["CosmeticEntry"],
252+
entry["MaterialPath"],
253+
entry["CosmeticType"],
254+
)
255+
if key in existingKeys:
256+
continue
257+
existingKeys.add(key)
258+
mergedEntries.append(entry)
259+
260+
writeXMLData(_serialize_cosmetic_manifest(mergedEntries), manifestPath)
261+
262+
140263
# --- Extracted methods ---
141264

142265

@@ -509,6 +632,11 @@ def _FMaterial_to_soh_xml(self, modelDirPath, objectPath):
509632
scrollData = self.scrollData.to_soh_xml()
510633
matData = matData.replace("</DisplayList>", scrollData + "</DisplayList>")
511634
writeXMLData(matData, os.path.join(modelDirPath, self.material.name))
635+
_write_custom_cosmetics_manifest(
636+
modelDirPath,
637+
objectPath,
638+
_collect_material_cosmetic_manifest_entries(self, objectPath),
639+
)
512640

513641
if self.revert is not None and self.revert.tag.Export:
514642
revData = self.revert.to_soh_xml(modelDirPath, objectPath)

0 commit comments

Comments
 (0)