""" Unreal editor script: the asset-import RUNNER. It imports and runs every Tools/ue/assets_*.py module, in name order. Run it through the editor commandlet (Tools/import_assets.ps1 does this): UnrealEditor-Cmd.exe TheDescent.uproject -run=pythonscript -script=" [--force] [--force-map] [--only ]" -unattended -nullrhi How a track adds assets WITHOUT touching anyone else's file: drop a module `Tools/ue/assets__.py` next to this runner. The runner finds it by its name (`assets_*.py`, sorted: the two-digit number is the order), imports it and calls its one entry point def run(ctx): ... Rules for a module: * It must be safe to re-run: skip work that is already done (compare a hash, check `ctx.eal.does_asset_exist`) unless `ctx.force` is set, and never duplicate anything. * It reports problems with `ctx.fail("reason")` (the run goes on with the next module and ends FAIL) or by raising an exception (same effect, with a traceback in the log). * It keeps its own helpers in its own file (or in a `_helpers_.py` file nobody else edits). The runner only provides what every module needs. * Modules may leave results for later modules in `ctx.shared` (a dict), but must also work when run alone (`--only`), so they look assets up by path when `ctx.shared` has no entry. What the context offers (all attributes of `ctx`): project_dir absolute project folder, with a trailing slash force, force_map the --force / --force-map switches (environment DESCENT_FORCE_IMPORT / DESCENT_FORCE_MAP) tools, mel, eal unreal.AssetToolsHelpers asset tools, unreal.MaterialEditingLibrary, unreal.EditorAssetLibrary shared dict shared between modules of one run errors list of failure messages so far log(msg) writes "[import] +12.3s msg" (the lines the PowerShell script echoes) fail(msg) records a failure try_set(obj, prop, value, what="") set_editor_property that warns about a bad name instead of raising sha256(path) hex digest of a file load_state(name) / save_state(name, dict) JSON kept in Saved/ImportState/.json (for "did the source change?") get_or_create(asset_dir, name, asset_class, factory) load the asset if it exists, create it otherwise Modules in this folder (core task): assets_10_kazah_materials, assets_11_kazah_meshes, assets_90_map. Numbers: 10-19 vehicles, 30-49 world / terrain / look, 50-79 other tracks, 90 the map (last, so it can place what the others made). Everything goes through unreal.log so it lands in the editor log; the lines this runner writes start with "[import]", and the last one is "[import] +..s PASS" or "[import] FAIL". """ import hashlib import importlib.util import json import os import sys import time import traceback import unreal sys.dont_write_bytecode = True # modules are imported from Tools/ue: leave no __pycache__ folder behind PROJECT_DIR = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) MODULE_DIR = os.path.join(PROJECT_DIR, "Tools", "ue") STATE_DIR = os.path.join(PROJECT_DIR, "Saved", "ImportState") FORCE = ("--force" in sys.argv) or os.environ.get("DESCENT_FORCE_IMPORT") == "1" # Rebuild only the map (cheap, ~10 s) after changing it, without re-importing meshes. FORCE_MAP = FORCE or ("--force-map" in sys.argv) or os.environ.get("DESCENT_FORCE_MAP") == "1" def _only_filter(): """--only or DESCENT_ONLY=: run only the modules whose name contains the text.""" if "--only" in sys.argv: i = sys.argv.index("--only") if i + 1 < len(sys.argv): return sys.argv[i + 1] return os.environ.get("DESCENT_ONLY", "") ONLY = _only_filter() _T0 = time.time() def log(msg): unreal.log("[import] +%5.1fs %s" % (time.time() - _T0, msg)) class ImportContext(object): """What every assets_*.py module gets as `ctx`.""" def __init__(self): self.project_dir = PROJECT_DIR self.force = FORCE self.force_map = FORCE_MAP self.tools = unreal.AssetToolsHelpers.get_asset_tools() self.mel = unreal.MaterialEditingLibrary self.eal = unreal.EditorAssetLibrary self.shared = {} self.errors = [] self.log = log def fail(self, msg): self.errors.append(msg) unreal.log_error("[import] FAIL: " + msg) def try_set(self, obj, prop, value, what=""): """set_editor_property that reports a bad name instead of aborting the whole import.""" try: obj.set_editor_property(prop, value) return True except Exception as e: # noqa: BLE001 unreal.log_warning("[import] could not set %s.%s%s: %s" % (type(obj).__name__, prop, (" (" + what + ")") if what else "", e)) return False def sha256(self, path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def load_state(self, name): try: with open(os.path.join(STATE_DIR, name + ".json"), "r", encoding="utf-8") as f: return json.load(f) except Exception: # noqa: BLE001 return {} def save_state(self, name, state): os.makedirs(STATE_DIR, exist_ok=True) with open(os.path.join(STATE_DIR, name + ".json"), "w", encoding="utf-8") as f: json.dump(state, f, indent=2) def get_or_create(self, asset_dir, name, asset_class, factory): path = asset_dir + "/" + name if self.eal.does_asset_exist(path): return self.eal.load_asset(path) asset = self.tools.create_asset(name, asset_dir, asset_class, factory) if asset is None: raise RuntimeError("could not create asset " + path) return asset def find_modules(): """assets_*.py in this folder, in name order.""" names = sorted(f for f in os.listdir(MODULE_DIR) if f.startswith("assets_") and f.endswith(".py")) if ONLY: names = [n for n in names if ONLY in n] return names def run_module(ctx, filename): stem = filename[:-3] spec = importlib.util.spec_from_file_location("descent_" + stem, os.path.join(MODULE_DIR, filename)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if not hasattr(module, "run"): raise RuntimeError("%s has no run(ctx) function" % filename) t = time.time() log("---- %s" % stem) module.run(ctx) log("---- %s done in %.1fs" % (stem, time.time() - t)) def main(): log("project: " + PROJECT_DIR) log("force: %s force-map: %s only: %r" % (FORCE, FORCE_MAP, ONLY)) names = find_modules() if not names: raise RuntimeError("no assets_*.py module found in %s%s" % (MODULE_DIR, (" matching %r" % ONLY) if ONLY else "")) log("modules, in order: " + ", ".join(n[:-3] for n in names)) ctx = ImportContext() for filename in names: try: run_module(ctx, filename) except Exception as exc: # noqa: BLE001 - a broken module must not hide the others' results ctx.fail("%s raised %s: %s" % (filename, type(exc).__name__, exc)) unreal.log_error(traceback.format_exc()) if ctx.errors: raise RuntimeError("%d problem(s): %s" % (len(ctx.errors), "; ".join(ctx.errors))) try: main() log("PASS") except Exception as exc: # noqa: BLE001 unreal.log_error("[import] FAIL: %s" % (exc,)) unreal.log_error(traceback.format_exc()) unreal.log_error("[import] FAIL") sys.exit(1)