192 lines
7.8 KiB
Python
192 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""One-shot reorganization: move audio assets from theme-specific dir to
|
|
shared `public/resources/audio/`, and normalize the SFX layout into a
|
|
consistent flat-categorical structure.
|
|
|
|
Idempotent enough to re-run if interrupted, but designed for a single pass.
|
|
|
|
Old layout:
|
|
public/games/age-of-dwarves/assets/audio/sfx/turn_started.ogg (flat)
|
|
public/games/age-of-dwarves/assets/audio/sfx/buildings/... (nested)
|
|
public/games/age-of-dwarves/assets/audio/sfx/units/melee_spawn.ogg (flat-but-named)
|
|
public/games/age-of-dwarves/assets/audio/sfx/units/melee/... (nested)
|
|
public/games/age-of-dwarves/assets/audio/music/...
|
|
|
|
New layout:
|
|
public/resources/audio/sfx/<category>/<event>.ogg
|
|
public/resources/audio/music/<track_id>.ogg
|
|
public/resources/audio/sources.csv
|
|
public/resources/audio/LICENSES.md
|
|
|
|
Categories: ui, city, combat, era, buildings, units/<class>, fauna,
|
|
weather, generic.
|
|
"""
|
|
|
|
import json, csv, io, shutil, sys, re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
OLD_ROOT = REPO_ROOT / "public/games/age-of-dwarves/assets/audio"
|
|
NEW_ROOT = REPO_ROOT / "public/resources/audio"
|
|
MANIFEST = REPO_ROOT / "public/games/age-of-dwarves/data/audio.json"
|
|
|
|
# Map of old asset-relative path -> new asset-relative path. The asset-relative
|
|
# part is what shows up as `audio/...` in the manifest streams + sources.csv.
|
|
RENAMES: dict[str, str] = {
|
|
# ── flat sfx top-level → categorical subfolders ─────────────────────
|
|
"audio/sfx/turn_started.ogg": "audio/sfx/ui/turn_started.ogg",
|
|
"audio/sfx/turn_ended.ogg": "audio/sfx/ui/turn_ended.ogg",
|
|
"audio/sfx/unit_moved.ogg": "audio/sfx/ui/unit_moved.ogg",
|
|
"audio/sfx/unit_promoted.ogg": "audio/sfx/ui/unit_promoted.ogg",
|
|
"audio/sfx/border_expanded.ogg": "audio/sfx/ui/border_expanded.ogg",
|
|
"audio/sfx/research_start.ogg": "audio/sfx/ui/research_start.ogg",
|
|
"audio/sfx/tech_researched.ogg": "audio/sfx/ui/tech_researched.ogg",
|
|
"audio/sfx/culture_researched.ogg": "audio/sfx/ui/culture_researched.ogg",
|
|
|
|
"audio/sfx/city_founded.ogg": "audio/sfx/city/city_founded.ogg",
|
|
|
|
"audio/sfx/combat_hit.ogg": "audio/sfx/combat/combat_hit.ogg",
|
|
"audio/sfx/unit_killed.ogg": "audio/sfx/combat/unit_killed.ogg",
|
|
"audio/sfx/unit_defeated.ogg": "audio/sfx/combat/unit_defeated.ogg",
|
|
"audio/sfx/unit_victorious.ogg": "audio/sfx/combat/unit_victorious.ogg",
|
|
|
|
"audio/sfx/era_advanced.ogg": "audio/sfx/era/era_advanced.ogg",
|
|
"audio/sfx/golden_age_swell.ogg": "audio/sfx/era/golden_age_swell.ogg",
|
|
"audio/sfx/victory_fanfare.ogg": "audio/sfx/era/victory_fanfare.ogg",
|
|
"audio/sfx/defeat_stinger.ogg": "audio/sfx/era/defeat_stinger.ogg",
|
|
|
|
"audio/sfx/wonder_built.ogg": "audio/sfx/buildings/wonder_built.ogg",
|
|
"audio/sfx/wonder_built_own.ogg": "audio/sfx/buildings/wonder_built_own.ogg",
|
|
"audio/sfx/wonder_built_rival.ogg": "audio/sfx/buildings/wonder_built_rival.ogg",
|
|
|
|
# ── normalize unit subcategory paths (flat-but-named → nested) ──────
|
|
"audio/sfx/units/melee_spawn.ogg": "audio/sfx/units/melee/spawn.ogg",
|
|
"audio/sfx/units/ranged_spawn.ogg": "audio/sfx/units/ranged/spawn.ogg",
|
|
"audio/sfx/units/siege_hit.ogg": "audio/sfx/units/siege/hit.ogg",
|
|
"audio/sfx/units/siege_death.ogg": "audio/sfx/units/siege/death.ogg",
|
|
"audio/sfx/units/support_attack.ogg":"audio/sfx/units/support/attack.ogg",
|
|
"audio/sfx/units/support_hit.ogg": "audio/sfx/units/support/hit.ogg",
|
|
"audio/sfx/units/support_death.ogg": "audio/sfx/units/support/death.ogg",
|
|
}
|
|
|
|
def step_move_files() -> tuple[int, int]:
|
|
"""Move every audio file from OLD_ROOT to NEW_ROOT, applying RENAMES."""
|
|
moved = 0
|
|
skipped = 0
|
|
for src in OLD_ROOT.rglob("*"):
|
|
if not src.is_file():
|
|
continue
|
|
rel = src.relative_to(OLD_ROOT)
|
|
# The manifest path is "audio/..." — equivalent to the relative path
|
|
# under OLD_ROOT prefixed with "audio/". So a file at
|
|
# OLD_ROOT/sfx/turn_started.ogg matches "audio/sfx/turn_started.ogg".
|
|
manifest_key = f"audio/{rel.as_posix()}"
|
|
new_manifest_key = RENAMES.get(manifest_key, manifest_key)
|
|
# Special-case: LICENSES.md and sources.csv at OLD_ROOT root keep
|
|
# their basename, no "audio/" prefix.
|
|
if rel.parent == Path("."):
|
|
new_path = NEW_ROOT / rel.name
|
|
else:
|
|
assert new_manifest_key.startswith("audio/")
|
|
new_path = NEW_ROOT / new_manifest_key[len("audio/"):]
|
|
new_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if new_path.exists():
|
|
skipped += 1
|
|
continue
|
|
shutil.move(str(src), str(new_path))
|
|
moved += 1
|
|
return moved, skipped
|
|
|
|
|
|
def step_update_manifest() -> int:
|
|
"""Rewrite audio.json stream paths."""
|
|
with open(MANIFEST) as f:
|
|
data = json.load(f)
|
|
rewrites = 0
|
|
for key, entry in data.get("sfx", {}).items():
|
|
if "stream" in entry and entry["stream"] in RENAMES:
|
|
entry["stream"] = RENAMES[entry["stream"]]
|
|
rewrites += 1
|
|
if "streams" in entry:
|
|
new_list = []
|
|
for s in entry["streams"]:
|
|
if s in RENAMES:
|
|
new_list.append(RENAMES[s])
|
|
rewrites += 1
|
|
else:
|
|
new_list.append(s)
|
|
entry["streams"] = new_list
|
|
# Music tracks didn't move (they were never in the flat-confusion zone),
|
|
# but defensive.
|
|
for track in data.get("music", {}).get("tracks", []):
|
|
if "stream" in track and track["stream"] in RENAMES:
|
|
track["stream"] = RENAMES[track["stream"]]
|
|
rewrites += 1
|
|
with open(MANIFEST, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
f.write("\n")
|
|
return rewrites
|
|
|
|
|
|
def step_update_sources_csv() -> int:
|
|
"""Rewrite output_path column in sources.csv."""
|
|
csv_path = NEW_ROOT / "sources.csv"
|
|
if not csv_path.exists():
|
|
return 0
|
|
head_lines: list[str] = []
|
|
body_lines: list[str] = []
|
|
with open(csv_path) as f:
|
|
for ln in f:
|
|
if ln.startswith("#"):
|
|
head_lines.append(ln)
|
|
else:
|
|
body_lines.append(ln)
|
|
reader = csv.reader(io.StringIO("".join(body_lines)))
|
|
rows = list(reader)
|
|
hdr, body = rows[0], rows[1:]
|
|
op_idx = hdr.index("output_path")
|
|
rewrites = 0
|
|
for r in body:
|
|
if r[op_idx] in RENAMES:
|
|
r[op_idx] = RENAMES[r[op_idx]]
|
|
rewrites += 1
|
|
with open(csv_path, "w") as f:
|
|
f.writelines(head_lines)
|
|
w = csv.writer(f)
|
|
w.writerow(hdr)
|
|
w.writerows(body)
|
|
return rewrites
|
|
|
|
|
|
def step_remove_old_root() -> None:
|
|
"""Delete OLD_ROOT if empty after the move."""
|
|
if not OLD_ROOT.exists():
|
|
return
|
|
leftover = list(OLD_ROOT.rglob("*"))
|
|
leftover_files = [p for p in leftover if p.is_file()]
|
|
if leftover_files:
|
|
print(f" WARN: {len(leftover_files)} files remain at {OLD_ROOT}")
|
|
for p in leftover_files[:5]:
|
|
print(f" {p}")
|
|
return
|
|
shutil.rmtree(OLD_ROOT)
|
|
# Walk up and prune empty parent dirs (assets/) but never above public/games/age-of-dwarves
|
|
parent = OLD_ROOT.parent # .../assets
|
|
if parent.is_dir() and not any(parent.iterdir()):
|
|
parent.rmdir()
|
|
|
|
|
|
def main() -> None:
|
|
NEW_ROOT.mkdir(parents=True, exist_ok=True)
|
|
moved, skipped = step_move_files()
|
|
print(f"moved {moved} files, skipped {skipped} (already at destination)")
|
|
rewrites = step_update_manifest()
|
|
print(f"manifest rewrites: {rewrites}")
|
|
csv_rewrites = step_update_sources_csv()
|
|
print(f"sources.csv rewrites: {csv_rewrites}")
|
|
step_remove_old_root()
|
|
print("done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|