Resolve vehicle livery mappings from matching illustration assets

This commit is contained in:
Golumpa 2026-08-19 13:27:47 +01:00
parent 7f7ad80b46
commit 593b23e1e2
5 changed files with 50 additions and 7 deletions

View file

@ -80,6 +80,9 @@ from the committed mappings. It still writes the staged artifacts.
- Other pull rewards are selected by `GachaIllustrate` and resolved to the
inventory tables for quality and localization keys. Appearance-table IDs
provide canonical casing for glider rewards.
- Vehicle liveries are resolved from their illustration name and icon. Rows
whose icon identifies a different reward are treated as unreleased upstream
placeholders and skipped until the assets are corrected.
- Mystery Box rewards are selected from every row in
`DT_GashaponLotteryGlobal`. No event IDs are hard-coded, so future rotations
are included automatically. Rewards resolve through the inventory, capital,
@ -89,8 +92,8 @@ from the committed mappings. It still writes the staged artifacts.
currencies, are categorized as items.
- Names are resolved strictly through `Localization/en/game.json`. A missing or
ambiguous key fails the update instead of falling back to a DT value.
- Character-awakening and character-vehicle illustration entries are not
independent pull rewards and are excluded.
- Character-awakening illustration entries are not independent pull rewards
and are excluded.
- Orange, purple, and blue item qualities map to `S`, `A`, and `B`.
## UID compatibility boundary

View file

@ -324,9 +324,16 @@ def _build_item_mapping(
candidate_id.isdigit()
or folded.startswith("fork_")
or folded.startswith("characterawaken_")
or folded.startswith("fashion_vehicle_")
):
continue
if folded.startswith("fashion_vehicle_"):
illustration = tables["illustrations"].get(candidate_id)
if illustration is not None:
vehicle_meta = _normalise_vehicle_livery(candidate_id, illustration, translations)
if vehicle_meta is None:
continue
result[candidate_id] = vehicle_meta
continue
sources = (inventory, capital, appearances, vehicle_inventory)
match = next((source.get(folded) for source in sources if folded in source), None)
if match is None:
@ -343,6 +350,31 @@ def _build_item_mapping(
return dict(sorted(result.items(), key=lambda pair: (pair[0].casefold(), pair[0])))
def _normalise_vehicle_livery(
item_id: str,
row: Any,
translations: dict[str, list[tuple[str, str]]],
) -> dict[str, str] | None:
"""Resolve a livery from its illustration, ignoring mismatched placeholder rows."""
if not isinstance(row, dict):
raise MappingUpdateError(f"NTE_Assets row {item_id} must be an object")
head_icon = row.get("HeadIcon")
asset_path = head_icon.get("AssetPathName") if isinstance(head_icon, dict) else None
if not isinstance(asset_path, str) or not asset_path:
raise MappingUpdateError(f"NTE_Assets vehicle livery row {item_id} has no HeadIcon")
icon_id = asset_path.rsplit("/", 1)[-1].split(".", 1)[0]
if icon_id.casefold() != item_id.casefold():
return None
item_name = row.get("ItemName_Override")
if not isinstance(item_name, dict):
raise MappingUpdateError(f"NTE_Assets vehicle livery row {item_id} has no ItemName_Override")
return {
"type": "cosmetic",
"name": _translate_name(item_id, item_name, translations).strip(),
"rank": "S",
}
def _build_achievement_mapping(
rows: dict[str, Any],
translations: dict[str, list[tuple[str, str]]],

View file

@ -1 +1 @@
[{"Rows":{"1003":{},"1099":{},"fork_alpha":{},"fork_new":{},"DIceNormal":{},"Dice_ticket_03":{},"Fashion_glide_2000":{},"Characterawaken_1003":{}}}]
[{"Rows":{"1003":{},"1099":{},"fork_alpha":{},"fork_new":{},"DIceNormal":{},"Dice_ticket_03":{},"Fashion_glide_2000":{},"Characterawaken_1003":{},"Fashion_vehicle_1010_V008":{"HeadIcon":{"AssetPathName":"/Game/UI/UI_Icon/Item/Fashion_vehicle_1010_V008.Fashion_vehicle_1010_V008"},"ItemName_Override":{"TableId":"/Game/Text/ST_VehicleData.ST_VehicleData","Key":"vehicle_1010_livery"}},"Fashion_vehicle_1072_V006":{"HeadIcon":{"AssetPathName":"/Game/UI/UI_Icon/Item/Fashion_vehicle_1036_V043.Fashion_vehicle_1036_V043"},"ItemName_Override":{"TableId":"/Game/Text/ST_VehicleData.ST_VehicleData","Key":"vehicle_placeholder_livery"}}}}]

View file

@ -18,5 +18,9 @@
},
"ST_Appearance": {
"glider_2000": "Inventory Glider"
},
"ST_VehicleData": {
"vehicle_1010_livery": "Tiger Incoming! - Livery",
"vehicle_placeholder_livery": "Sandy Mirage - Livery"
}
}

View file

@ -55,7 +55,7 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("fork_retired", result.mappings["arcs.json"])
self.assertNotIn("OldTicket", result.mappings["items.json"])
self.assertNotEqual(result.mappings["characters.json"]["1003"], current["characters.json"]["1003"])
self.assertEqual(result.report["changes"], {"additions": 5, "updates": 2, "deletions": 3})
self.assertEqual(result.report["changes"], {"additions": 6, "updates": 2, "deletions": 3})
self.assertIs(result.report["safety"]["authoritative_snapshot"], True)
self.assertIs(result.report["safety"]["deletions_allowed"], True)
self.assertIs(result.report["safety"]["uid_inputs_touched"], False)
@ -83,7 +83,11 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("DIceNormal", result.mappings["items.json"])
self.assertNotIn("UnusedItem", result.mappings["items.json"])
self.assertFalse(any(key.startswith("Characterawaken_") for key in result.mappings["items.json"]))
self.assertFalse(any(key.startswith("Fashion_vehicle_") for key in result.mappings["items.json"]))
self.assertEqual(
result.mappings["items.json"]["Fashion_vehicle_1010_V008"],
{"type": "cosmetic", "name": "Tiger Incoming! - Livery", "rank": "S"},
)
self.assertNotIn("Fashion_vehicle_1072_V006", result.mappings["items.json"])
def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self):
assets = load_assets(assets_root=SAMPLE_ASSETS)
@ -113,7 +117,7 @@ class MappingUpdateTests(unittest.TestCase):
tables["localization"]["ST_Item"].update(
{"wow_ticket": "Wowzers Ticket", "wow_frame": "Wowzers Frame"}
)
tables["localization"]["ST_VehicleData"] = {"vehicle_wow": "Wowmobile"}
tables["localization"]["ST_VehicleData"]["vehicle_wow"] = "Wowmobile"
result = build_mapping_update(sample_current(), replace(assets, tables=tables))