I’ve been working a lot recently in migrating from a project to another, troubleshooting can be hard when you get stuck in a infinite uncommunicative progressbar “migrating…”. I created a custom python script that run in Unreal Engine Headless
TL;DR
-
run_migrate.ps1
- run it from the source project folder;
- it auto‑detects:
- the current
.uproject
; - the target project at
..\game\*.uproject
; (change this at your needs) - the engine path (reads EngineAssociation engine version from the uproject and checks the corresponding engine location from the windows registry)
- the current
- sets
UE_MIGRATE_DRYRUN
and launches UnrealEditor‑Cmd.exe with
-run=pythonscript -script="migrate.py"
; - the
-DryRun
switch = safe preview (no files copied).
-
migrate.py (runs inside the editor‑cmd)
- loads the Asset Registry and gathers every package under
/Game/FolderToMigrate
; - walks only “hard” dependencies (textures, materials, etc.) and adds them to the list;
- filters anything not in
ALLOWED_PREFIXES
(or that matchesEXCLUDED_PREFIXES
); - if
UE_MIGRATE_DRYRUN==1
(or--dry-run
) prints the list and exits — otherwise asks for TTY confirmation; - finally calls
AssetToolsHelpers.get_asset_tools().migrate_packages()
withignore_dependencies=True
because we already resolved them ourselves.
- loads the Asset Registry and gathers every package under
run_migrate.ps1
<#
Usage: .\run_migrate.ps1 → migrate with confirmation
.\run_migrate.ps1 -DryRun → preview, no files copied
#>
param(
[Alias('dry-run','dry_run')][switch]$DryRun
)
function Write-Info { param($m) ; Write-Host $m -ForegroundColor Cyan }
function Write-Ok { param($m) ; Write-Host $m -ForegroundColor Green }
function Write-Warn { param($m) ; Write-Host $m -ForegroundColor Yellow }
function Write-ErrorC { param($m) ; Write-Host $m -ForegroundColor Red }
# Locate source & target .uproject
$uproject = Get-ChildItem $PSScriptRoot -Filter *.uproject | Select-Object -First 1
$destRoot = Join-Path $PSScriptRoot "..\game"
$destUproject= Get-ChildItem $destRoot -Filter *.uproject -Recurse | Select-Object -First 1
# Resolve engine folder from EngineAssociation
$engineAssoc = (Get-Content $uproject.FullName -Raw | ConvertFrom-Json).EngineAssociation
function Get-EnginePath($assoc){
$reg="Registry::HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine\$assoc"
if(Test-Path $reg){ return (Get-ItemProperty $reg -Name 'InstalledDirectory').InstalledDirectory }
}
$engineRoot = Get-EnginePath $engineAssoc
$editorCmd = Join-Path $engineRoot "Engine\Binaries\Win64\UnrealEditor-Cmd.exe"
# Wrapper for dry‑run var/args
$pyScript = Join-Path $PSScriptRoot "migrate.py"
$env:UE_MIGRATE_DRYRUN = $DryRun ? "1" : "0"
Write-Info "┌──── ASSET MIGRATION ──────────────────────────────"
Write-Info " Source Project : $($uproject.Name)"
Write-Info " Target Project : $($destUproject.Name)"
Write-Info " Engine : $engineAssoc"
Write-Info " Mode : $(if($DryRun){'DRY‑RUN'}else{'LIVE'})"
Write-Info "└───────────────────────────────────────────────────"
& "$editorCmd" $uproject.FullName `
-stdout -FullStdOutLogOutput `
-run=pythonscript -script="$pyScript"
if($LASTEXITCODE -ne 0){ Write-ErrorC "❌ ExitCode $LASTEXITCODE"; exit $LASTEXITCODE }
Write-Ok "✅ Done!"
migrate.py
# --dry-run → prints the package list and exits
# no flag → asks for confirmation then migrates
import unreal, sys, os
from pathlib import Path
DRY_RUN = os.getenv("UE_MIGRATE_DRYRUN") == "1" or "--dry-run" in sys.argv
SOURCE_FOLDER = "/Game/FolderToMigrate"
ALLOWED_PREFIXES = [SOURCE_FOLDER] # add more folders here
EXCLUDED_PREFIXES = [] # or exclude them here
# Resolve folders
script_dir = Path(__file__).resolve().parent
dest_content = (script_dir.parent / "game" / "Content").resolve()
log = unreal.log
reg = unreal.AssetRegistryHelpers.get_asset_registry()
queue = list({a.package_name for a in reg.get_assets_by_path(SOURCE_FOLDER, recursive=True)})
pkg_set = set(queue)
dep_opts = unreal.AssetRegistryDependencyOptions(
include_soft_package_references=False,
include_hard_package_references=True,
include_searchable_names=False,
include_soft_management_references=False,
include_hard_management_references=False)
while queue:
pkg = queue.pop()
for dep in reg.get_dependencies(pkg, dep_opts):
if dep not in pkg_set:
pkg_set.add(dep); queue.append(dep)
def is_allowed(p): # filter sub‑folders + excludes
s = str(p)
return any(s.startswith(ap) for ap in ALLOWED_PREFIXES) and \
not any(s.startswith(ep) for ep in EXCLUDED_PREFIXES)
filtered = [p for p in pkg_set if is_allowed(p)]
log(f"[MIG] {len(filtered)} package(s) selected")
if DRY_RUN:
unreal.log_warning("[MIG] ** DRY‑RUN **")
for p in sorted(filtered): log(f" {p}")
sys.exit(0)
if os.isatty(0) and input("Proceed? [y/N] ").lower()!="y":
sys.exit(0)
tools = unreal.AssetToolsHelpers.get_asset_tools()
opts = unreal.MigrationOptions(); opts.ignore_dependencies = True
tools.migrate_packages(filtered, str(dest_content), options=opts)
log("[MIG] ✅ Migration completed"); print("✅ Done.")
How to use / customise
- Copy both scripts into the source project root.
- Create (or clone) the target project in
..\game\
relative to the scripts. - In Python edit:
SOURCE_FOLDER
if you want to migrate a different folder;- add/remove paths in
ALLOWED_PREFIXES
orEXCLUDED_PREFIXES
.
- Open PowerShell and run:
.\run_migrate.ps1 -DryRun # preview only
.\run_migrate.ps1 # actual copy (asks for confirmation)
Handy notes
- No external plugins required: it relies only on
AssetRegistry
andAssetTools
exposed by UE‑Python. - Soft dependencies (BP references, data‑only BPs, etc.) are ignored by design. If you need them, just tweak the flags in
dep_opts
. - Tested and works great on UE 5.6; some apis might change
- If you prefer running the Python script from the editor UI, simply open any level and use Window
Developer Tools
Output Log →
py "migrate.py --dry-run"
.
Hope this helps someone out there!