Files
Unreal_EmberWild/Scripts/reorganize_content.py
T
2026-08-16 19:35:27 +02:00

367 lines
15 KiB
Python

# -*- coding: utf-8 -*-
"""
Rangement unique de Content/Game.
A lancer depuis l'editeur : Tools > Execute Python Script.
Mettre DRY_RUN a True pour une simulation qui ne touche a rien, puis a False.
Pourquoi un script et pas du drag & drop : ~70 operations, dont des renommages
qui doivent se faire dans un ordre precis (un materiau prend le nom qu'une
instance vient de liberer). Une erreur de destination se voit ici, pas trois
jours plus tard sur un asset au mauvais endroit.
Unreal se charge lui-meme de reecrire les references : rename_asset laisse un
redirector derriere lui, et fixup_referencers() les resout a la fin.
"""
import unreal
DRY_RUN = True
EAL = unreal.EditorAssetLibrary
# ---------------------------------------------------------------------------
# Assets a supprimer -- verifie sans aucun referenceur dans tout le projet.
# ---------------------------------------------------------------------------
DELETIONS = [
# Doublon reel : le M_Pickaxe utilise par le mesh est celui de 3d/, pas celui-ci.
"/Game/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe",
"/Game/Game/Audio/Chest/Chest_Closes_2",
]
# ---------------------------------------------------------------------------
# Renommages d'assets. L'ORDRE COMPTE dans le bloc Table : l'instance libere
# le nom M_Table avant que le materiau parent ne le prenne.
# ---------------------------------------------------------------------------
ASSET_MOVES = [
# --- Art / Building / Table -------------------------------------------
("/Game/Game/3d/Building/Table/M_Table",
"/Game/Game/Art/Building/Table/MI_Table"),
("/Game/Game/3d/Building/Table/textures/M_table_albedo_Mat",
"/Game/Game/Art/Building/Table/M_Table"),
("/Game/Game/3d/Building/Table/Table_FBX",
"/Game/Game/Art/Building/Table/SM_Table"),
("/Game/Game/3d/Building/Table/textures/M_table_albedo",
"/Game/Game/Art/Building/Table/T_Table_BaseColor"),
("/Game/Game/3d/Building/Table/textures/M_table_Metal",
"/Game/Game/Art/Building/Table/T_Table_Metallic"),
("/Game/Game/3d/Building/Table/textures/M_table_Normal",
"/Game/Game/Art/Building/Table/T_Table_Normal"),
("/Game/Game/3d/Building/Table/textures/M_table_Roughness",
"/Game/Game/Art/Building/Table/T_Table_Roughness"),
# --- Art / Cooking -----------------------------------------------------
# Les suffixes numeriques sont conserves : rien ne dit lequel est lequel,
# et inventer un nom parlant ici serait une supposition.
("/Game/Game/3d/Cooking/Campfire_Logs",
"/Game/Game/Art/Cooking/SM_Campfire_Logs"),
("/Game/Game/3d/Cooking/Campfire_Logs_001",
"/Game/Game/Art/Cooking/SM_Campfire_Logs_001"),
("/Game/Game/3d/Cooking/Campfire_Logs_002",
"/Game/Game/Art/Cooking/SM_Campfire_Logs_002"),
("/Game/Game/3d/Cooking/Campfire_Logs_003",
"/Game/Game/Art/Cooking/SM_Campfire_Logs_003"),
("/Game/Game/3d/Cooking/LogS",
"/Game/Game/Art/Cooking/SM_LogS"),
("/Game/Game/3d/Cooking/Logs1",
"/Game/Game/Art/Cooking/SM_Logs1"),
("/Game/Game/3d/Cooking/Rocks_003",
"/Game/Game/Art/Cooking/MI_Rocks_003"),
("/Game/Game/3d/Cooking/Wood_004",
"/Game/Game/Art/Cooking/MI_Wood_004"),
("/Game/Game/3d/Cooking/Wood_light_001",
"/Game/Game/Art/Cooking/MI_Wood_light_001"),
# --- Art / Tools / Axe -------------------------------------------------
# Le materiau s'appelait "Wood_Axe", exactement comme le mesh : deux assets
# homonymes a deux endroits, c'est ce qui rendait le dossier illisible.
("/Game/Game/3d/Tools/Wood_Axe",
"/Game/Game/Art/Tools/Axe/SM_Wood_Axe"),
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe",
"/Game/Game/Art/Tools/Axe/M_Wood_Axe"),
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor",
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_BaseColor"),
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal",
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_Normal"),
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic",
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_ORM"),
# Orphelin, conserve : la hache en bois est un chantier ouvert.
("/Game/Game/Material/Tools/Woods/Axe/M_Axe",
"/Game/Game/Art/Tools/Axe/M_Axe"),
# --- Art / Tools / Pickaxe ---------------------------------------------
# Les textures portaient le nom de la hache alors qu'elles alimentent la
# pioche : on remet le nom en accord avec l'usage reel.
("/Game/Game/3d/Tools/Wood_Pickaxe",
"/Game/Game/Art/Tools/Pickaxe/SM_Wood_Pickaxe"),
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe",
"/Game/Game/Art/Tools/Pickaxe/M_Wood_Pickaxe"),
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB",
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_BaseColor"),
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw",
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_Normal"),
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw",
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_ORM"),
# --- Art / Tools / Sword -----------------------------------------------
("/Game/Game/3d/Tools/Wood_Sword",
"/Game/Game/Art/Tools/Sword/SM_Wood_Sword"),
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor",
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_BaseColor"),
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal",
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_Normal"),
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic",
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_ORM"),
# --- Art / Character / Arms --------------------------------------------
("/Game/Game/Material/Fps_Arm/M_Fps_Arm",
"/Game/Game/Art/Character/Arms/M_Fps_Arm"),
("/Game/Game/Material/Fps_Arm/MaleBody_M_Base_color_sRGB",
"/Game/Game/Art/Character/Arms/T_Fps_Arm_BaseColor"),
# --- Blueprints --------------------------------------------------------
# Framework = ce qui n'est pas un acteur du monde : GameMode, GameInstance,
# PlayerControllers. Le critere est celui de CLAUDE.md, pas le type d'asset.
("/Game/Game/Player/BP_FpsPlayer",
"/Game/Game/Blueprints/Player/BP_FpsPlayer"),
("/Game/Game/Player/BP_FpsPlayerController",
"/Game/Game/Blueprints/Framework/BP_FpsPlayerController"),
("/Game/Game/Menu/BP_MainMenuPlayerController",
"/Game/Game/Blueprints/Framework/BP_MainMenuPlayerController"),
("/Game/Game/GameMode/GM_FpsPlayer",
"/Game/Game/Blueprints/Framework/GM_FpsPlayer"),
("/Game/Game/GameMode/GM_MainMenu",
"/Game/Game/Blueprints/Framework/GM_MainMenu"),
("/Game/Game/Game-Instance/BP_SurvivalGameInstance",
"/Game/Game/Blueprints/Framework/BP_SurvivalGameInstance"),
("/Game/Game/PickupItem/BP_Pickup_Test",
"/Game/Game/Blueprints/World/BP_Pickup_Test"),
("/Game/Game/Building/Cooking/BP_Campfire",
"/Game/Game/Blueprints/World/BP_Campfire"),
("/Game/Game/Building/Crafting/BP_Workbench",
"/Game/Game/Blueprints/World/BP_Workbench"),
("/Game/Game/Building/Storage/BP_Chest_Wooden",
"/Game/Game/Blueprints/World/BP_Chest_Wooden"),
("/Game/Game/BP/Menu/CharaterCustomisation/BP_CharacterPreview",
"/Game/Game/Blueprints/Menu/BP_CharacterPreview"),
]
# Renommages qui doivent avoir lieu APRES un deplacement de dossier, parce que
# leur chemin source n'existe qu'une fois le dossier deplace.
ASSET_MOVES_LATE = [
# Le squelette n'est pas une animation : il sort de Sequences/.
("/Game/Game/Animation/FirstPerson/Sequences/BasePos_Skeleton",
"/Game/Game/Animation/FirstPerson/BasePos_Skeleton"),
# Nom colle deux fois par un import rate.
("/Game/Game/UI/Widgets/Player/WBP_PlayerNameplateWBP_PlayerNameplate",
"/Game/Game/UI/Widgets/Player/WBP_PlayerNameplate"),
]
# ---------------------------------------------------------------------------
# Deplacements de dossiers entiers.
# ---------------------------------------------------------------------------
DIR_MOVES = [
# --- Animation ---------------------------------------------------------
("/Game/Game/Animation/Fps",
"/Game/Game/Animation/FirstPerson/Sequences"),
("/Game/Game/Animation/ThirdPerson/RetargetedAnimations",
"/Game/Game/Animation/ThirdPerson/Sequences"),
# --- Audio : fusion de Sounds/ dans Audio/ -----------------------------
("/Game/Game/Sounds/Mix", "/Game/Game/Audio/Mix"),
("/Game/Game/Sounds/Music", "/Game/Game/Audio/Music"),
("/Game/Game/Sounds/Playlists", "/Game/Game/Audio/Playlists"),
("/Game/Game/Sounds/UI", "/Game/Game/Audio/UI"),
# FootSteps -> Footsteps : Windows ne distingue pas la casse, donc on passe
# par un nom intermediaire, sinon le renommage est un no-op silencieux.
("/Game/Game/Audio/FootSteps", "/Game/Game/Audio/_TmpFootsteps"),
("/Game/Game/Audio/_TmpFootsteps", "/Game/Game/Audio/Footsteps"),
# --- Data --------------------------------------------------------------
("/Game/Game/Data/Harvetable", "/Game/Game/Data/Harvestable"),
("/Game/Game/Data/Item", "/Game/Game/Data/Items"),
# --- UI : les widgets d'un cote, les ressources de l'autre -------------
("/Game/Game/UI/Images/512x512", "/Game/Game/UI/Icons/Items"),
("/Game/Game/UI/Images/Tools", "/Game/Game/UI/Icons/Tools"),
("/Game/Game/UI/Font", "/Game/Game/UI/Fonts"),
("/Game/Game/UI/Confirm", "/Game/Game/UI/Widgets/Confirm"),
("/Game/Game/UI/Craft", "/Game/Game/UI/Widgets/Craft"),
("/Game/Game/UI/CrossHair", "/Game/Game/UI/Widgets/Crosshair"),
("/Game/Game/UI/Customization", "/Game/Game/UI/Widgets/Customization"),
("/Game/Game/UI/Fade", "/Game/Game/UI/Widgets/Fade"),
("/Game/Game/UI/Interaction", "/Game/Game/UI/Widgets/Interaction"),
("/Game/Game/UI/Inventory", "/Game/Game/UI/Widgets/Inventory"),
("/Game/Game/UI/Menu", "/Game/Game/UI/Widgets/MainMenu"),
("/Game/Game/UI/Pause", "/Game/Game/UI/Widgets/Pause"),
("/Game/Game/UI/Player", "/Game/Game/UI/Widgets/Player"),
("/Game/Game/UI/Setting", "/Game/Game/UI/Widgets/Settings"),
("/Game/Game/UI/Stats", "/Game/Game/UI/Widgets/Stats"),
]
# Les deux icones egarees hors de Content/Game.
STRAY_MOVES = [
("/Game/UI/Icons/T_Icon_Food_06", "/Game/Game/UI/Icons/Items/T_Icon_Food_06"),
("/Game/UI/Icons/T_Icon_Food_07", "/Game/Game/UI/Icons/Items/T_Icon_Food_07"),
]
# Dossiers vides une fois tout deplace ET les redirectors resolus.
EMPTY_DIRS = [
"/Game/Game/3d",
"/Game/Game/Material",
"/Game/Game/Player",
"/Game/Game/Menu",
"/Game/Game/GameMode",
"/Game/Game/Game-Instance",
"/Game/Game/PickupItem",
"/Game/Game/Building",
"/Game/Game/BP",
"/Game/Game/Sounds",
"/Game/Game/Animation/Anim_BP",
"/Game/Game/UI/Images",
"/Game/UI",
]
# ---------------------------------------------------------------------------
stats = {"ok": 0, "skip": 0, "fail": 0}
def log(kind, message):
stats[kind] = stats.get(kind, 0) + 1
prefix = {"ok": " OK ", "skip": " SKIP ", "fail": " FAIL "}[kind]
unreal.log("{}{}".format(prefix, message))
def move_asset(src, dst):
if not EAL.does_asset_exist(src):
log("skip", "source absente : {}".format(src))
return
if EAL.does_asset_exist(dst):
log("fail", "destination DEJA OCCUPEE : {}".format(dst))
return
if DRY_RUN:
log("ok", "{} -> {}".format(src, dst))
return
if EAL.rename_asset(src, dst):
log("ok", "{} -> {}".format(src, dst))
else:
log("fail", "rename_asset a echoue : {}".format(src))
def move_dir(src, dst):
if not EAL.does_directory_exist(src):
log("skip", "dossier absent : {}".format(src))
return
if DRY_RUN:
log("ok", "[dir] {} -> {}".format(src, dst))
return
if EAL.rename_directory(src, dst):
log("ok", "[dir] {} -> {}".format(src, dst))
else:
log("fail", "rename_directory a echoue : {}".format(src))
def delete_asset(path):
if not EAL.does_asset_exist(path):
log("skip", "deja absent : {}".format(path))
return
if DRY_RUN:
log("ok", "[del] {}".format(path))
return
if EAL.delete_asset(path):
log("ok", "[del] {}".format(path))
else:
log("fail", "delete_asset a echoue : {}".format(path))
def fixup_redirectors():
"""Resout tous les ObjectRedirector du projet et les supprime.
C'est l'etape que personne n'avait lancee : deux redirectors de 2024
trainaient encore dans Data/Item.
"""
registry = unreal.AssetRegistryHelpers.get_asset_registry()
registry.search_all_assets(True)
asset_filter = unreal.ARFilter(
class_paths=[unreal.TopLevelAssetPath("/Script/CoreUObject", "ObjectRedirector")],
package_paths=["/Game"],
recursive_paths=True,
)
found = registry.get_assets(asset_filter)
unreal.log("Redirectors trouves : {}".format(len(found)))
if not found:
return
for data in found:
unreal.log(" {}".format(data.package_name))
if DRY_RUN:
return
redirectors = [data.get_asset() for data in found]
unreal.AssetToolsHelpers.get_asset_tools().fixup_referencers(redirectors)
def run():
mode = "SIMULATION (rien n'est modifie)" if DRY_RUN else "EXECUTION REELLE"
unreal.log("=" * 70)
unreal.log("Rangement de Content/Game -- {}".format(mode))
unreal.log("=" * 70)
unreal.log("\n--- 1. Redirectors existants ---")
fixup_redirectors()
unreal.log("\n--- 2. Suppressions ---")
for path in DELETIONS:
delete_asset(path)
unreal.log("\n--- 3. Assets ---")
for src, dst in ASSET_MOVES:
move_asset(src, dst)
unreal.log("\n--- 4. Dossiers ---")
for src, dst in DIR_MOVES:
move_dir(src, dst)
unreal.log("\n--- 5. Assets dependant d'un dossier deplace ---")
for src, dst in ASSET_MOVES_LATE:
move_asset(src, dst)
unreal.log("\n--- 6. Icones egarees hors de Content/Game ---")
for src, dst in STRAY_MOVES:
move_asset(src, dst)
# Obligatoire avant de supprimer les dossiers d'origine : chaque
# deplacement y a laisse un redirector, donc ils ne sont pas vides.
unreal.log("\n--- 7. Resolution des redirectors crees ---")
fixup_redirectors()
unreal.log("\n--- 8. Dossiers vides ---")
for path in EMPTY_DIRS:
if not EAL.does_directory_exist(path):
log("skip", "deja absent : {}".format(path))
continue
if DRY_RUN:
log("ok", "[rmdir] {}".format(path))
continue
if EAL.delete_directory(path):
log("ok", "[rmdir] {}".format(path))
else:
log("fail", "dossier non vide : {}".format(path))
if not DRY_RUN:
unreal.log("\n--- 9. Sauvegarde ---")
EAL.save_directory("/Game/Game", only_if_is_dirty=False, recursive=True)
unreal.log("\n" + "=" * 70)
unreal.log("Termine -- OK: {} SKIP: {} FAIL: {}".format(
stats["ok"], stats["skip"], stats["fail"]))
if stats["fail"]:
unreal.log_error("{} operation(s) en echec : lire le log ci-dessus.".format(stats["fail"]))
unreal.log("=" * 70)
run()