Skip to content

Commit 2833ae0

Browse files
authored
dynamic cosmetics editor manifest (#63)
* dynamic cosmetics editor manifest * black
1 parent 0a5f487 commit 2833ae0

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

fast64_internal/hm64/f3d/soh_xml_exporter.py

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

142267

@@ -509,6 +634,11 @@ def _FMaterial_to_soh_xml(self, modelDirPath, objectPath):
509634
scrollData = self.scrollData.to_soh_xml()
510635
matData = matData.replace("</DisplayList>", scrollData + "</DisplayList>")
511636
writeXMLData(matData, os.path.join(modelDirPath, self.material.name))
637+
_write_custom_cosmetics_manifest(
638+
modelDirPath,
639+
objectPath,
640+
_collect_material_cosmetic_manifest_entries(self, objectPath),
641+
)
512642

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

0 commit comments

Comments
 (0)