cue4parse 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ /dist
2
+ __pycache__
3
+ *.pyc
4
+ build.cmd
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: cue4parse
3
+ Version: 0.0.2
4
+ Summary: CUE4Parse Python wrapper and CLI tool to extract packages from Unreal Engine games
5
+ Author-email: joric <joric@users.noreply.github.com>
6
+ License: Unlicense
7
+ License-File: LICENSE
8
+ Keywords: cli,cue4parse,unreal
9
+ Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: pythonnet>=3.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # CUE4Parse-Python
17
+
18
+ cue4parse package for python
19
+
20
+ ## Installation
21
+
22
+ ### Development
23
+
24
+ `pip install -e . --force`
25
+
26
+ ### User
27
+
28
+ `pip install cue4parse`
29
+
30
+ ## Usage
31
+
32
+ Make sure executable is added to paths, e.g. `C:\Users\user\AppData\Local\Python\pythoncore-3.14-64\Scripts`.
33
+
34
+ Needs CUE4Parse libraries in `%LOCALAPPDATA%/cue4parse/libs` (automatically downloads and unpacks FModel on Windows).
35
+
36
+ To run CLI: `cue4parse`
37
+
@@ -0,0 +1,22 @@
1
+ # CUE4Parse-Python
2
+
3
+ cue4parse package for python
4
+
5
+ ## Installation
6
+
7
+ ### Development
8
+
9
+ `pip install -e . --force`
10
+
11
+ ### User
12
+
13
+ `pip install cue4parse`
14
+
15
+ ## Usage
16
+
17
+ Make sure executable is added to paths, e.g. `C:\Users\user\AppData\Local\Python\pythoncore-3.14-64\Scripts`.
18
+
19
+ Needs CUE4Parse libraries in `%LOCALAPPDATA%/cue4parse/libs` (automatically downloads and unpacks FModel on Windows).
20
+
21
+ To run CLI: `cue4parse`
22
+
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cue4parse"
7
+ version = "0.0.2"
8
+ description = "CUE4Parse Python wrapper and CLI tool to extract packages from Unreal Engine games"
9
+ requires-python = ">=3.9"
10
+ dependencies = ["pythonnet>=3.0"]
11
+ readme = "README.md"
12
+ license = { text = "Unlicense" }
13
+ authors = [{ name = "joric", email = "joric@users.noreply.github.com" }]
14
+ keywords = ["unreal", "cue4parse", "cli"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: The Unlicense (Unlicense)",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+
21
+ [project.scripts]
22
+ cue4parse = "cue4parse.cli:main"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/cue4parse"]
@@ -0,0 +1,13 @@
1
+ import sys
2
+ import os
3
+ from pathlib import Path
4
+
5
+ _DLL_DIR = Path(os.environ.get('LOCALAPPDATA', os.path.expanduser('~/.local/share'))) / "cue4parse" / "libs"
6
+
7
+ if not _DLL_DIR.exists():
8
+ print("DLLs not found. Downloading native libraries.")
9
+ from cue4parse import setup_deps
10
+ setup_deps.main()
11
+
12
+ from cue4parse.core import *
13
+ from cue4parse.includes import *
@@ -0,0 +1,3 @@
1
+ from cue4parse.cli import main
2
+
3
+ main()
@@ -0,0 +1,521 @@
1
+ from cue4parse import *
2
+
3
+ import tempfile
4
+ import fnmatch
5
+ import time
6
+ from importlib.metadata import version
7
+
8
+ export_directory = ""
9
+ overwrite_files = False
10
+
11
+ DEBUG = False
12
+
13
+ class ExportType(Flag):
14
+ NONE = 0
15
+ TEXTURE = auto()
16
+ SOUND = auto()
17
+ MESH = auto()
18
+ ANIMATION = auto()
19
+ OTHER = auto()
20
+
21
+ @dataclass
22
+ class CliOptions:
23
+ sources: list[str]
24
+ pak: str | None
25
+ output: str | None
26
+ package: list[str]
27
+ config: list[str]
28
+ game: str
29
+ key: list[str]
30
+ mappings: str | None
31
+ format: str
32
+ list_only: bool
33
+ yes: bool
34
+ verbose: bool
35
+ mesh_format: str
36
+ anim_format: str
37
+ texture_format: str
38
+ material_format: str
39
+ lod_format: str
40
+ socket_format: str
41
+ nanite_format: str # kept for parity (not assigned if enum unavailable)
42
+ export_morph_targets: bool
43
+ export_materials: bool
44
+ export_hdr_as_hdr: bool
45
+ compression: str
46
+
47
+ def norm_path(path: str) -> str:
48
+ if not path:
49
+ return "."
50
+ return path.replace("/", os.sep).replace("\\\\", "\\")
51
+
52
+
53
+ def parse_enum(enum_type: Any, value: str, default_name: str | None = None) -> Any:
54
+ # pythonnet enum access: getattr(EnumType, "ValueName")
55
+ if hasattr(enum_type, value):
56
+ return getattr(enum_type, value)
57
+ for name in dir(enum_type):
58
+ if name.lower() == value.lower():
59
+ return getattr(enum_type, name)
60
+ if default_name and hasattr(enum_type, default_name):
61
+ print(f"Warning: Unknown value '{value}' for {enum_type.__name__}, using default '{default_name}'", file=sys.stderr)
62
+ return getattr(enum_type, default_name)
63
+ print(f"Warning: Unknown value '{value}' for {enum_type.__name__}, using default", file=sys.stderr)
64
+ # best-effort default
65
+ names = [n for n in dir(enum_type) if not n.startswith("_")]
66
+ return getattr(enum_type, names[0]) if names else None
67
+
68
+
69
+ def check_file(out_path: Path, silent: bool = False) -> bool:
70
+ skip = (not overwrite_files) and out_path.exists()
71
+ if not silent:
72
+ if skip:
73
+ Log.Warning("Already exists, skipping {Path}", norm_path(str(out_path)))
74
+ else:
75
+ Log.Information("Writing {Path}", norm_path(str(out_path)))
76
+ return not skip
77
+
78
+
79
+ def write_to_log(_folder: str, _log_message: str, counter: list[int]) -> None:
80
+ counter[0] += 1
81
+
82
+
83
+ def write_to_file(folder: str, file_name: str, data: bytes, log_message: str, counter: list[int]) -> None:
84
+ out_path = Path(export_directory) / folder / file_name
85
+ if not check_file(out_path):
86
+ return
87
+ out_path.parent.mkdir(parents=True, exist_ok=True)
88
+ out_path.write_bytes(data)
89
+ write_to_log(folder, log_message, counter)
90
+
91
+ def save_json(folder: str, name: str, exports: Any, counter: list[int]) -> None:
92
+ #text = JsonConvert.SerializeObject(exports, Formatting.Indented) # alternative way
93
+
94
+ serializer = JsonSerializer();
95
+ serializer.Formatting = Formatting.Indented;
96
+
97
+ output_to_stdout = export_directory==""
98
+ if output_to_stdout:
99
+ writer = JsonTextWriter(Console.Out)
100
+ serializer.Serialize(writer, exports)
101
+ writer.Flush()
102
+ return
103
+
104
+ file_name = f"{Path(name).stem}.json"
105
+ out_path = Path(export_directory) / folder / file_name
106
+
107
+ if not check_file(out_path):
108
+ return
109
+
110
+ out_path.parent.mkdir(parents=True, exist_ok=True)
111
+
112
+ stream = StreamWriter(str(out_path), False, Encoding.UTF8);
113
+ try:
114
+ writer = JsonTextWriter(stream);
115
+ try:
116
+ serializer.Serialize(writer, exports);
117
+ writer.Flush()
118
+ finally:
119
+ writer.Close()
120
+ finally:
121
+ stream.Close()
122
+
123
+ write_to_log(folder, name, counter)
124
+
125
+
126
+ def save_raw(folder: str, package, provider, counter: list[int]) -> None:
127
+ name = str(package.Name)
128
+ out_path = Path(export_directory) / folder / name
129
+ if not check_file(out_path):
130
+ return
131
+ out_path.parent.mkdir(parents=True, exist_ok=True)
132
+ data = bytes(provider.Files[package.Path].Read())
133
+ write_to_file(folder, name, data, name, counter)
134
+
135
+
136
+ def try_load_locmeta(package) -> tuple[bool, Any]:
137
+ ok, reader = package.TryCreateReader()
138
+ if not ok or reader is None:
139
+ return False, None
140
+ try:
141
+ return True, FTextLocalizationMetaDataResource(reader)
142
+ except Exception:
143
+ return False, None
144
+
145
+
146
+ def try_load_locres(package) -> tuple[bool, Any]:
147
+ ok, reader = package.TryCreateReader()
148
+ if not ok or reader is None:
149
+ return False, None
150
+ try:
151
+ return True, FTextLocalizationResource(reader)
152
+ except Exception:
153
+ return False, None
154
+
155
+
156
+ def save_texture(folder: str, texture: UTexture, platform: ETexturePlatform, options: ExporterOptions, counter: list[int], name_prefix='') -> None:
157
+ out_base = Path(export_directory) / folder / str(texture.Name)
158
+ for ext in (".png", ".hdr"):
159
+ p = out_base.with_suffix(ext)
160
+ if not check_file(p, silent=True):
161
+ check_file(p, silent=False)
162
+ return
163
+
164
+ if isinstance(texture, UTexture2DArray):
165
+ bitmaps = list(TextureDecoder.DecodeTextureArray(texture, platform))
166
+ else:
167
+ bitmap = TextureDecoder.Decode(texture, platform)
168
+ if isinstance(texture, UTextureCube):
169
+ bitmap = CubemapConverter.ToPanorama(bitmap) if bitmap is not None else None
170
+ bitmaps = [bitmap]
171
+
172
+ for bitmap in bitmaps or []:
173
+ if bitmap is None:
174
+ continue
175
+ result = TextureEncoder.Encode(bitmap, options.TextureFormat, options.ExportHdrTexturesAsHdr)
176
+ if isinstance(result, tuple):
177
+ encoded, extension = result
178
+ else:
179
+ encoded, extension = result, "png"
180
+ file_name = f"{name_prefix}{texture.Name}.{extension}"
181
+ write_to_file(folder, file_name, bytes(encoded), f"{file_name} ({bitmap.Width}x{bitmap.Height})", counter)
182
+
183
+
184
+ def filter_paths(paths: Iterable[str], pattern: str) -> list[str]:
185
+ return [p for p in paths if fnmatch.fnmatch(p, pattern)]
186
+
187
+
188
+ def execute(options: CliOptions) -> int:
189
+ global export_directory, overwrite_files
190
+
191
+ overwrite_files = options.yes
192
+
193
+ if options.verbose:
194
+ Log.Logger = with_console(LoggerConfiguration().MinimumLevel.Verbose()).CreateLogger()
195
+ else:
196
+ Log.Logger = with_console(LoggerConfiguration().MinimumLevel.Fatal()).CreateLogger()
197
+
198
+ try:
199
+ game_version = parse_enum(EGame, options.game, "GAME_UE5_LATEST")
200
+ except Exception:
201
+ raise ValueError(f"Invalid game version: {options.game}")
202
+
203
+ if options.mappings and (not Path(options.mappings).exists()):
204
+ print(f"Mappings not found: {options.mappings}", file=sys.stderr)
205
+ return 1
206
+
207
+ version = VersionContainer(game_version, ETexturePlatform.DesktopMobile)
208
+
209
+ single_pak_mode = bool(options.pak)
210
+ if single_pak_mode:
211
+ if not Path(options.pak).exists():
212
+ print(f"Pak file not found: {options.pak}", file=sys.stderr)
213
+ return 1
214
+ print(f"Loading single pak: {norm_path(options.pak)}...", file=sys.stderr)
215
+ pak_dir = str(Path(options.pak).resolve().parent)
216
+ provider = DefaultFileProvider(pak_dir, SearchOption.TopDirectoryOnly, VersionContainer(game_version))
217
+ else:
218
+ directory = options.sources[0] if options.sources else None
219
+ if not directory:
220
+ return 0
221
+ print(f"Loading {norm_path(directory)}...", file=sys.stderr)
222
+ if directory.endswith(".apk"):
223
+ provider = ApkFileProvider(directory, VersionContainer(game_version))
224
+ else:
225
+ provider = DefaultFileProvider(directory, SearchOption.AllDirectories, VersionContainer(game_version), StringComparer.OrdinalIgnoreCase)
226
+
227
+ if options.mappings and Path(options.mappings).exists():
228
+ provider.MappingsContainer = FileUsmapTypeMappingsProvider(options.mappings)
229
+
230
+ oodle_path = os.path.join(tempfile.gettempdir(), str(OodleHelper.OodleFileName))
231
+ OodleHelper.DownloadOodleDll(oodle_path)
232
+ OodleHelper.Initialize(oodle_path)
233
+
234
+ zlib_path = os.path.join(tempfile.gettempdir(), str(ZlibHelper.DLL_NAME))
235
+ ZlibHelper.DownloadDll(zlib_path)
236
+ ZlibHelper.Initialize(zlib_path)
237
+
238
+ if single_pak_mode:
239
+ provider.RegisterVfs(options.pak)
240
+ else:
241
+ provider.Initialize()
242
+
243
+ for key_entry in options.key:
244
+ provider.SubmitKey(FGuid(), FAesKey(key_entry))
245
+
246
+ provider.SubmitKey(FGuid(0,0,0,0), FAesKey(bytearray(32)))
247
+ provider.PostMount()
248
+
249
+ try:
250
+ provider.ChangeCulture(provider.GetLanguageCode(ELanguage.English))
251
+ except Exception as exc:
252
+ msg = "expected for single-pak mode" if single_pak_mode else "culture loading failed"
253
+ Log.Warning(f"Warning: Culture loading failed ({msg}): {exc}")
254
+
255
+ print(f"Total assets: {provider.Files.Count}", file=sys.stderr)
256
+ print(f"Output format: {options.format}", file=sys.stderr)
257
+
258
+ detex_path = os.path.join(tempfile.gettempdir(), str(DetexHelper.DLL_NAME))
259
+ if not Path(detex_path).exists():
260
+ DetexHelper.LoadDllAsync(detex_path).GetAwaiter().GetResult()
261
+ DetexHelper.Initialize(detex_path)
262
+
263
+ package_paths: list[str] = []
264
+ for list_file in options.config:
265
+ if list_file and Path(list_file).exists():
266
+ print(f"Loading file list: {norm_path(list_file)}", file=sys.stderr)
267
+ for line in Path(list_file).read_text(encoding="utf-8", errors="ignore").splitlines():
268
+ t = line.strip()
269
+ if not t or t.startswith("#") or t.startswith("["):
270
+ continue
271
+ package_paths.append(t)
272
+
273
+ for item in options.package:
274
+ if item:
275
+ package_paths.append(item)
276
+
277
+ if not package_paths:
278
+ package_paths = ["*"]
279
+
280
+ packages = []
281
+ keys = list(provider.Files.Keys)
282
+ for path in package_paths:
283
+ if any(ch in path for ch in ("*", "?")):
284
+ matched_keys = filter_paths(keys, path)
285
+ matched = [provider.Files[k] for k in matched_keys]
286
+ print(f"Added wildcard: {path} ({len(matched)} matches)", file=sys.stderr)
287
+ else:
288
+ ok, obj = provider.Files.TryGetValue(path)
289
+ matched = [obj] if ok else []
290
+ packages.extend(matched)
291
+
292
+ if not packages:
293
+ print("No matches, exiting.", file=sys.stderr)
294
+ return 0
295
+
296
+ export_type = ExportType.TEXTURE | ExportType.SOUND | ExportType.MESH | ExportType.ANIMATION | ExportType.OTHER
297
+
298
+ exp_options = ExporterOptions()
299
+ exp_options.LodFormat = parse_enum(ELodFormat, options.lod_format, "AllLods")
300
+ exp_options.MeshFormat = parse_enum(EMeshFormat, options.mesh_format, "ActorX")
301
+ exp_options.AnimFormat = parse_enum(EAnimFormat, options.anim_format, "ActorX")
302
+ exp_options.MaterialFormat = parse_enum(EMaterialFormat, options.material_format, "AllLayersNoRef")
303
+ exp_options.TextureFormat = parse_enum(ETextureFormat, options.texture_format, "Png")
304
+ exp_options.CompressionFormat = parse_enum(EFileCompressionFormat, options.compression, "None")
305
+ exp_options.Platform = version.Platform
306
+ exp_options.SocketFormat = parse_enum(ESocketFormat, options.socket_format, "Bone")
307
+ exp_options.ExportMorphTargets = options.export_morph_targets
308
+ exp_options.ExportMaterials = options.export_materials
309
+ exp_options.ExportHdrTexturesAsHdr = options.export_hdr_as_hdr
310
+
311
+ needs_output_dir = (not options.list_only) and options.format not in ("csv", "json")
312
+ if not options.output:
313
+ if needs_output_dir:
314
+ print("Output directory is not specified. Use -o <dir> or use -f json/csv/-l for stdout output.", file=sys.stderr)
315
+ return 1
316
+ export_directory = ""
317
+ else:
318
+ export_directory = norm_path(options.output)
319
+
320
+ import threading
321
+ from concurrent.futures import ThreadPoolExecutor, as_completed
322
+
323
+ counter_lock = threading.Lock()
324
+ export_count = [0]
325
+ counter = 0
326
+ start = time.monotonic()
327
+
328
+ def process_package(package):
329
+ folder = str(package.Path).rsplit("/", 1)[0] if "/" in str(package.Path) else ""
330
+ ext = Path(str(package.Name)).suffix.lower()
331
+ target_format = options.format
332
+
333
+ if target_format != "raw":
334
+ if ext == ".umap" and target_format != "png":
335
+ target_format = "json"
336
+ elif ext == ".locmeta":
337
+ ok, locmeta = try_load_locmeta(package)
338
+ if ok:
339
+ save_json(folder, str(package.Name), locmeta, export_count)
340
+ return True
341
+ elif ext == ".locres":
342
+ ok, locres = try_load_locres(package)
343
+ if ok:
344
+ save_json(folder, str(package.Name), locres, export_count)
345
+ return True
346
+
347
+ ok, pkg = provider.TryLoadPackage(package)
348
+ if not ok or pkg is None:
349
+ if ext in (".uasset", ".umap"):
350
+ raise RuntimeError("Could not load standard asset, check game version, mappings or keys.")
351
+ if target_format in ("auto", "raw"):
352
+ target_format = "raw"
353
+ else:
354
+ return True
355
+
356
+ if target_format == "raw":
357
+ save_raw(folder, package, provider, export_count)
358
+ return True
359
+
360
+ if target_format == "json":
361
+ save_json(folder, str(package.Name), pkg.GetExports(), export_count)
362
+ return True
363
+
364
+ if target_format not in ("auto", "png"):
365
+ return True
366
+
367
+ if target_format == "png":
368
+ local_export_type = ExportType.TEXTURE
369
+ else:
370
+ local_export_type = ExportType.TEXTURE | ExportType.SOUND | ExportType.MESH | ExportType.ANIMATION | ExportType.OTHER
371
+
372
+ parsed = False
373
+
374
+ for i in range(pkg.ExportMapLength):
375
+ pointer = FPackageIndex(pkg, i + 1).ResolvedObject
376
+ if pointer is None or pointer.Object is None:
377
+ continue
378
+ dummy = AbstractUePackage.ConstructObject(pkg, pointer.Class, pkg)
379
+ if dummy is None:
380
+ continue
381
+
382
+ value = pointer.Object.Value
383
+ if isinstance(dummy, UTexture) and (ExportType.TEXTURE in local_export_type) and isinstance(value, UTexture):
384
+
385
+ name_prefix = folder if ext != ".umap" else str(package.Path).rsplit(".", 1)[0]+'.'+str(i)+'.'
386
+ try:
387
+ save_texture(folder, value, exp_options.Platform, exp_options, export_count, name_prefix)
388
+ except Exception as exc:
389
+ Log.Warning("failed to decode {ValueName}: {Error}", value.Name, exc)
390
+ parsed = True
391
+
392
+ elif isinstance(dummy, (USoundWave, UAkMediaAssetData)) and (ExportType.SOUND in local_export_type):
393
+ fmt_ref = clr.Reference[str]("")
394
+ data_ref = clr.Reference[object](None)
395
+ value.Decode(True, fmt_ref, data_ref)
396
+ if data_ref.Value is not None:
397
+ file_name = f"{value.Name}.{str(fmt_ref.Value).lower()}"
398
+ write_to_file(folder, file_name, bytes(data_ref.Value), file_name, export_count)
399
+ parsed = True
400
+
401
+ elif isinstance(dummy, (UAnimSequenceBase, USkeletalMesh, UStaticMesh, USkeleton)) and (
402
+ ExportType.ANIMATION in local_export_type or ExportType.MESH in local_export_type
403
+ ):
404
+ exporter = CUE4Exporter(value, exp_options)
405
+ ok_write, _, file_path = exporter.TryWriteToDir(DirectoryInfo(export_directory), None, None)
406
+ if ok_write:
407
+ write_to_log(folder, os.path.basename(str(file_path)), export_count)
408
+ parsed = True
409
+
410
+ if not parsed and (target_format == 'json' or target_format == 'auto'):
411
+ save_json(folder, str(package.Name), pkg.GetExports(), export_count)
412
+
413
+ return True
414
+
415
+ if options.list_only:
416
+ for package in packages:
417
+ if options.format == "csv":
418
+ print(f"{package.Path},{provider.Files[package.Path].Size}")
419
+ else:
420
+ print(package.Path)
421
+ else:
422
+
423
+ max_workers = os.cpu_count() or 4 if not DEBUG else 1
424
+ fatal_error = [None] # shared error slot
425
+
426
+ print(f"Using parallel processing, {max_workers} threads", file=sys.stderr)
427
+
428
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
429
+ futures = {executor.submit(process_package, pkg): pkg for pkg in packages}
430
+ for future in as_completed(futures):
431
+ if fatal_error[0]:
432
+ executor.shutdown(wait=False, cancel_futures=True)
433
+ break
434
+ with counter_lock:
435
+ counter += 1
436
+ if not options.verbose:
437
+ print(f"Exporting package {counter} of {len(packages)}... \r", end="", file=sys.stderr)
438
+ try:
439
+ future.result()
440
+ except RuntimeError as e:
441
+ fatal_error[0] = e
442
+ print(str(e), file=sys.stderr)
443
+
444
+ if fatal_error[0]:
445
+ return 1
446
+
447
+ elapsed = time.monotonic() - start
448
+
449
+ if not options.verbose:
450
+ print(f"Processed {len(packages)} packages in {elapsed:.2f}s", file=sys.stderr)
451
+
452
+ Log.Information(f"Processed {len(packages)} packages in {elapsed:.2f}s")
453
+ return 0
454
+
455
+ import argparse
456
+
457
+ class CustomHelpFormatter(argparse.RawTextHelpFormatter):
458
+ def __init__(self, prog):
459
+ super().__init__(prog, max_help_position=60, width=120)
460
+ self.show_extended = False
461
+ def _get_help_string(self, action):
462
+ help_string = action.help or ''
463
+ if action.default is not None and action.default != argparse.SUPPRESS:
464
+ if action.default == [] or action.default is False:
465
+ return help_string
466
+ if '%(default)' not in help_string:
467
+ help_string += ' (default: %(default)s)'
468
+ return help_string
469
+
470
+ examples = [
471
+ ["Export all package names to a text file:", "cue4parse -i MyGame -l > packages.txt"],
472
+ ["Export a single package to stdout in json format:", "cue4parse -i MyGame -p Assets/MyAsset.uasset -f json"],
473
+ ["Export multiple packages matching wildcard patterns to a directory:", "cue4parse -i MyGame -p */Textures* -p */Icons* -o Exports"],
474
+ ["Export packages from list, overwrite existing files:", "cue4parse -i MyGame -c packages.txt -o Exports -y"],
475
+ ["Export with PSK meshes and PSA animations:", "cue4parse -i MyGame -p */SkeletalMeshes/* -o Exports --mesh-format ActorX --anim-format ActorX"],
476
+ ["Process a single .pak file:", "cue4parse --pak MyMod.pak -o Exports -m Mappings.usmap -g GAME_UE5_1"],
477
+ ]
478
+
479
+ parser = argparse.ArgumentParser(
480
+ prog="cue4parse",
481
+ description=f"CUE4Parse-Python {version('cue4parse')}: a command line tool to extract resources from Unreal Engine games",
482
+ epilog='\n\n'.join(f'{a}\n {b}'for a,b in examples),
483
+ formatter_class=CustomHelpFormatter,
484
+ add_help = True,
485
+ )
486
+
487
+ parser.add_argument("-i", "--input", metavar="INPUT", action="append", default=[], dest="sources", help="Input game directory or pak/asset file (repeatable)")
488
+ parser.add_argument("-o", "--output", help="Output directory (optional for list/json/csv modes)")
489
+ parser.add_argument("-p", "--package", action="append", default=[], help="Package path or wildcard pattern (repeatable)")
490
+ parser.add_argument("-c", "--config", action="append", default=[], help="Package list file (repeatable)")
491
+ parser.add_argument("-g", "--game", default="GAME_UE5_LATEST", help="Game version")
492
+ parser.add_argument("-k", "--key", action="append", default=[], help="AES key in hex format (repeatable)")
493
+ parser.add_argument("-m", "--mappings", help="Mappings file")
494
+ parser.add_argument("-f", "--format", default="auto", help="Output format: auto, raw, json, csv, png")
495
+ parser.add_argument("-l", "--list", action="store_true", dest="list_only", help="List matching packages (supports auto and csv)")
496
+ parser.add_argument("-y", "--yes", action="store_true", help="Overwrite existing files")
497
+ parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
498
+
499
+ # more options
500
+ parser.add_argument("--pak", help="Single .pak file to process (no directory scanning)")
501
+ parser.add_argument("--mesh-format", default="ActorX", help="Mesh format: ActorX (psk), Gltf2 (glb), UEFormat (uemodel), OBJ")
502
+ parser.add_argument("--anim-format", default="ActorX", help="Animation format: ActorX (psa), UEFormat (ueanim)")
503
+ parser.add_argument("--texture-format", default="Png", help="Texture format: Png, Jpeg, Tga, Dds")
504
+ parser.add_argument("--material-format", default="AllLayersNoRef", help="Material format: FirstLayer, AllLayersNoRef, AllLayers")
505
+ parser.add_argument("--lod-format", default="AllLods", help="LOD format: FirstLod, AllLods")
506
+ parser.add_argument("--socket-format", default="Bone", help="Socket format: Bone, Socket, None")
507
+ parser.add_argument("--nanite-format", default="AllLayersNaniteFirst", help="Nanite format: OnlyNaniteLOD, OnlyNormalLODs, AllLayersNaniteFirst, AllLayersNaniteLast")
508
+ parser.add_argument("--export-morph-targets", action=argparse.BooleanOptionalAction, default=True, help="Export morph targets")
509
+ parser.add_argument("--export-materials", action=argparse.BooleanOptionalAction, default=True, help="Export materials with meshes")
510
+ parser.add_argument("--export-hdr-as-hdr", action=argparse.BooleanOptionalAction, default=True, help="Export HDR textures as .hdr")
511
+ parser.add_argument("--compression", default="None", help="Compression: None, GZIP, ZSTD")
512
+
513
+ def main(argv: list[str] | None = None) -> int:
514
+ argv = argv if argv is not None else sys.argv[1:]
515
+ if not argv:
516
+ parser.print_help()
517
+ return 0
518
+ return execute(parser.parse_args(argv))
519
+
520
+ if __name__ == "__main__":
521
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ import time
9
+ from dataclasses import dataclass
10
+ from enum import Flag, auto
11
+ from pathlib import Path
12
+ from typing import Any, Iterable
13
+
14
+ from cue4parse import _DLL_DIR
15
+
16
+ def _find_dotnet_root() -> Path | None:
17
+ candidates = [
18
+ Path.home() / ".dotnet",
19
+ Path("/usr/share/dotnet"),
20
+ Path("/usr/lib/dotnet"),
21
+ ]
22
+ for candidate in candidates:
23
+ if (candidate / "host" / "fxr").exists():
24
+ return candidate
25
+ return None
26
+
27
+ def _find_runtime_config() -> str | None:
28
+ libs_dir = Path(__file__).parent / "libs"
29
+ configs = list(libs_dir.glob("*.runtimeconfig.json"))
30
+ return str(configs[0]) if configs else None
31
+
32
+ dotnet_root = _find_dotnet_root()
33
+ if dotnet_root:
34
+ os.environ["DOTNET_ROOT"] = str(dotnet_root)
35
+
36
+ import pythonnet
37
+
38
+ runtime_config = _find_runtime_config()
39
+ if runtime_config:
40
+ pythonnet.load("coreclr", runtime_config=runtime_config)
41
+ else:
42
+ pythonnet.load("coreclr")
43
+
44
+ import clr
45
+
46
+ libs_dir = _DLL_DIR
47
+ cue4parse_dll_dir = str(libs_dir)
48
+ sys.path.append(cue4parse_dll_dir)
49
+
50
+ clr.AddReference("CUE4Parse")
51
+ clr.AddReference("CUE4Parse-Conversion")
52
+
53
+ clr.AddReference("System")
54
+ clr.AddReference("System.Core")
55
+ clr.AddReference("Newtonsoft.Json")
56
+
57
+ clr.AddReference("Serilog")
58
+ clr.AddReference("Serilog.Sinks.Console")
59
+
60
+ import System
61
+ from Serilog import Log, LoggerConfiguration
62
+ from Serilog.Events import LogEventLevel
63
+ from Serilog.Configuration import LoggerSinkConfiguration
64
+
65
+ def with_console(cfg: LoggerConfiguration) -> LoggerConfiguration:
66
+ asm = System.Reflection.Assembly.Load("Serilog.Sinks.Console")
67
+ bf = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static
68
+ lsc_t = clr.GetClrType(LoggerSinkConfiguration)
69
+ for t in asm.GetTypes():
70
+ if not (t.IsSealed and t.IsAbstract):
71
+ continue
72
+ for m in t.GetMethods(bf):
73
+ if m.Name != "Console":
74
+ continue
75
+ ps = m.GetParameters()
76
+ if len(ps) >= 1 and ps[0].ParameterType == lsc_t:
77
+ args = [System.Type.Missing] * len(ps)
78
+ args[0] = cfg.WriteTo
79
+ # Find and set the standardErrorFromLevel parameter so it's stderr, not stdout
80
+ for i, p in enumerate(ps):
81
+ if p.Name == "standardErrorFromLevel":
82
+ lel_t = System.Type.GetType("Serilog.Events.LogEventLevel, Serilog")
83
+ args[i] = System.Enum.Parse(lel_t, "Verbose")
84
+ break
85
+ m.Invoke(None, System.Array[System.Object](args))
86
+ return cfg
87
+ raise RuntimeError("Serilog console sink extension method not found")
88
+
89
+ # you can re-define it in user code later with Verbose() or any other level
90
+ Log.Logger = with_console(LoggerConfiguration().MinimumLevel.Fatal()).CreateLogger()
@@ -0,0 +1,38 @@
1
+ import time
2
+ from cue4parse import *
3
+
4
+ GAME_DIR = r"E:\Games\Subnautica2\Subnautica2\Content\Paks"
5
+ MAPPINGS_FILE = r"E:\Games\Subnautica2\Subnautica2.usmap"
6
+
7
+ def main():
8
+ start = time.perf_counter()
9
+
10
+ Log.Logger = with_console(LoggerConfiguration().MinimumLevel.Verbose()).CreateLogger()
11
+
12
+ version = VersionContainer(EGame.GAME_UE5_6, ETexturePlatform.DesktopMobile)
13
+ provider = DefaultFileProvider(GAME_DIR, SearchOption.TopDirectoryOnly, version, StringComparer.OrdinalIgnoreCase)
14
+
15
+ provider.MappingsContainer = FileUsmapTypeMappingsProvider(MAPPINGS_FILE)
16
+ provider.Initialize()
17
+
18
+ provider.SubmitKey(FGuid(0,0,0,0), FAesKey(bytearray(32)))
19
+ provider.PostMount()
20
+
21
+ package_path = 'Subnautica2/Content/Maps/Main/L_Main.umap'
22
+
23
+ ok, package = provider.TryLoadPackage(package_path)
24
+
25
+ exports = package.GetExports()
26
+
27
+ name_filter = lambda name: 'Lifepod' in str(name)
28
+
29
+ for export in exports:
30
+ name = str(export)
31
+ if not name_filter(name):
32
+ continue
33
+ print(export.ExportType)
34
+
35
+ print(f'Finished in {time.perf_counter() - start:.4f}s')
36
+
37
+ if __name__ == '__main__':
38
+ raise SystemExit(main())
@@ -0,0 +1,34 @@
1
+ from cue4parse import *
2
+
3
+ from System import Console
4
+ from System.IO import StreamWriter, StringWriter, TextWriter
5
+ from System.Text import Encoding
6
+ from Newtonsoft.Json import JsonSerializer, Formatting, JsonTextWriter, JsonConvert
7
+
8
+ from System import Guid, StringComparer
9
+ from System.IO import SearchOption, DirectoryInfo
10
+ from CUE4Parse.Compression import OodleHelper, ZlibHelper
11
+ from CUE4Parse.Encryption.Aes import FAesKey
12
+ from CUE4Parse.FileProvider import DefaultFileProvider, ApkFileProvider
13
+ from CUE4Parse.MappingsProvider import FileUsmapTypeMappingsProvider
14
+ from CUE4Parse.UE4.Assets import AbstractUePackage
15
+ from CUE4Parse.UE4.Assets.Exports.Animation import UAnimSequenceBase, USkeleton
16
+ from CUE4Parse.UE4.Assets.Exports.SkeletalMesh import USkeletalMesh
17
+ from CUE4Parse.UE4.Assets.Exports.Sound import USoundWave
18
+ from CUE4Parse.UE4.Assets.Exports.StaticMesh import UStaticMesh
19
+ from CUE4Parse.UE4.Assets.Exports.Texture import UTexture, UTexture2DArray, UTextureCube
20
+ from CUE4Parse.UE4.Assets.Exports.Wwise import UAkMediaAssetData
21
+ from CUE4Parse.UE4.Localization import FTextLocalizationResource, FTextLocalizationMetaDataResource
22
+ from CUE4Parse.UE4.Objects.Core.Misc import FGuid
23
+ from CUE4Parse.UE4.Objects.UObject import FPackageIndex
24
+ from CUE4Parse.UE4.Versions import VersionContainer, EGame, ELanguage
25
+ from CUE4Parse.UE4.Assets.Exports.Texture import ETexturePlatform
26
+
27
+ from CUE4Parse_Conversion import Exporter as CUE4Exporter, ExporterOptions
28
+ from CUE4Parse_Conversion.Animations import EAnimFormat
29
+ from CUE4Parse_Conversion.Meshes import EMeshFormat, ELodFormat, ESocketFormat
30
+ from CUE4Parse_Conversion.Textures import ETextureFormat, TextureDecoder, TextureEncoder, CubemapConverter, CTexture
31
+ from CUE4Parse_Conversion.Textures.BC import DetexHelper
32
+ from CUE4Parse_Conversion.UEFormat.Enums import EFileCompressionFormat
33
+ from CUE4Parse.UE4.Assets.Exports.Material import EMaterialFormat
34
+
@@ -0,0 +1,98 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ set "SCRIPT_DIR=%~dp0"
5
+ set "LIBS_DIR=%LOCALAPPDATA%\cue4parse\libs"
6
+ set "TMP_DIR=%TEMP%\cue4parse-setup"
7
+
8
+ echo === cue4parse dependency setup (Windows) ===
9
+
10
+ if exist "%TMP_DIR%" rmdir /s /q "%TMP_DIR%"
11
+ mkdir "%TMP_DIR%"
12
+
13
+ rem add "goto git" here if you want to build dlls locally
14
+
15
+ echo Downloading fmodel
16
+ for /f "usebackq tokens=*" %%i in (`powershell -command "(Invoke-RestMethod -Uri 'https://api.fmodel.app/v1/infos/Qa').downloadUrl"`) do set DOWNLOAD_URL=%%i
17
+ if errorlevel 1 goto git
18
+
19
+ curl.exe -L -o "%TMP_DIR%\FModel.zip" "%DOWNLOAD_URL%" || goto git
20
+ powershell -command "Expand-Archive -Force '%TMP_DIR%\FModel.zip' '%TMP_DIR%'" || goto git
21
+
22
+ where dotnet >nul 2>&1 || (
23
+ echo Installig dotnet
24
+ winget install Microsoft.DotNet.SDK.10 --accept-package-agreements --accept-source-agreements || goto git
25
+ )
26
+
27
+ where sfextract >nul 2>&1 || (
28
+ echo Installig sfextract
29
+ dotnet tool install -g sfextract || goto git
30
+ )
31
+
32
+ echo Running sfextract
33
+ sfextract "%TMP_DIR%\FModel.exe" --output "%LIBS_DIR%" || goto git
34
+
35
+ goto success
36
+
37
+ :git
38
+
39
+ :: Check git
40
+ where git >nul 2>&1
41
+ if %errorlevel% neq 0 (
42
+ echo ERROR: git is required. Install from https://git-scm.com
43
+ exit /b 1
44
+ )
45
+
46
+ :: Check/install dotnet
47
+ where dotnet >nul 2>&1
48
+ if %errorlevel% neq 0 (
49
+ echo dotnet not found. Installing via winget...
50
+ winget install Microsoft.DotNet.SDK.10 --accept-package-agreements --accept-source-agreements
51
+ if %errorlevel% neq 0 (
52
+ echo ERROR: Failed to install dotnet. Install manually: https://dotnet.microsoft.com/download
53
+ exit /b 1
54
+ )
55
+ :: Refresh PATH so dotnet is available in this session
56
+ for /f "tokens=*" %%i in ('where dotnet 2^>nul') do set "DOTNET_PATH=%%i"
57
+ if "!DOTNET_PATH!"=="" (
58
+ set "PATH=%PATH%;%ProgramFiles%\dotnet"
59
+ )
60
+ )
61
+
62
+
63
+ echo Cloning CUE4Parse...
64
+ git clone --depth=1 https://github.com/FabianFG/CUE4Parse.git "%TMP_DIR%\CUE4Parse"
65
+ if %errorlevel% neq 0 (echo ERROR: git clone failed. && exit /b 1)
66
+
67
+ :: Build
68
+ echo Building...
69
+
70
+ set "SLN=%TMP_DIR%\CUE4Parse\CUE4Parse.slnx"
71
+ if not exist "%SLN%" (
72
+ echo ERROR: CUE4Parse.slnx not found.
73
+ exit /b 1
74
+ )
75
+
76
+ echo Building solution...
77
+ dotnet publish "%SLN%" --configuration Release --output "%TMP_DIR%\build" --self-contained false --runtime win-x64
78
+
79
+ if %errorlevel% neq 0 (echo ERROR: Build failed. && exit /b 1)
80
+
81
+ :: Copy DLLs
82
+ echo Copying libraries to %LIBS_DIR% ...
83
+ if not exist "%LIBS_DIR%" mkdir "%LIBS_DIR%"
84
+ set COUNT=0
85
+ for %%f in ("%TMP_DIR%\build\*.dll") do (
86
+ copy /y "%%f" "%LIBS_DIR%\" >nul
87
+ set /a COUNT+=1
88
+ echo copied: %%~nxf
89
+ )
90
+ echo Copied !COUNT! libraries to %LIBS_DIR%
91
+
92
+ :success
93
+
94
+ :: Cleanup
95
+ rmdir /s /q "%TMP_DIR%"
96
+
97
+ echo.
98
+ echo Done. Libraries installed to %LIBS_DIR%
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ LIBS_DIR="$HOME/.local/share/cue4parse/libs"
6
+
7
+ echo "=== cue4parse Linux dependency setup ==="
8
+
9
+ # Install dotnet if missing
10
+ if ! command -v dotnet &>/dev/null; then
11
+ echo "dotnet not found, installing..."
12
+ if command -v apt-get &>/dev/null; then
13
+ sudo apt-get install -y dotnet-sdk-10.0
14
+ elif command -v dnf &>/dev/null; then
15
+ sudo dnf install -y dotnet-sdk-10.0
16
+ elif command -v pacman &>/dev/null; then
17
+ sudo pacman -Sy --noconfirm dotnet-sdk
18
+ elif command -v zypper &>/dev/null; then
19
+ sudo zypper install -y dotnet-sdk-10.0
20
+ else
21
+ echo "No supported package manager found. Installing via Microsoft script..."
22
+ curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0
23
+ export DOTNET_ROOT="$HOME/.dotnet"
24
+ export PATH="$DOTNET_ROOT:$DOTNET_ROOT/tools:$PATH"
25
+ fi
26
+ fi
27
+
28
+ export PATH="$HOME/.dotnet/tools:$PATH"
29
+
30
+ # Clone and build CUE4Parse
31
+ TMP=$(mktemp -d)
32
+ trap 'rm -rf "$TMP"' EXIT
33
+
34
+ for cmd in git make gcc; do
35
+ command -v "$cmd" &>/dev/null || { echo "ERROR: $cmd is required. Install it with your package manager."; exit 1; }
36
+ done
37
+
38
+ echo "Cloning CUE4Parse..."
39
+ git clone --depth=1 https://github.com/FabianFG/CUE4Parse.git "$TMP/CUE4Parse"
40
+
41
+ echo "Building..."
42
+
43
+ SLN="$TMP/CUE4Parse/CUE4Parse.slnx"
44
+ if [ ! -f "$SLN" ]; then
45
+ echo "ERROR: CUE4Parse.slnx not found."
46
+ exit 1
47
+ fi
48
+
49
+ echo "Building solution..."
50
+ dotnet publish "$SLN" --configuration Release --output "$TMP/build" --self-contained false --runtime linux-x64
51
+
52
+ # Build Detex from source
53
+ echo "Building Detex..."
54
+ git clone --depth=1 https://github.com/hglm/detex "$TMP/detex"
55
+
56
+ # Set SHARED library configuration
57
+ sed -i 's/^LIBRARY_CONFIGURATION.*/LIBRARY_CONFIGURATION = SHARED/' "$TMP/detex/Makefile.conf"
58
+
59
+ # Patch detexDecompressTextureLinear to use DETEX_HELPER_SHARED_EXPORT
60
+ sed -i 's/DETEX_API bool detexDecompressTextureLinear/DETEX_HELPER_SHARED_EXPORT bool detexDecompressTextureLinear/' "$TMP/detex/detex.h"
61
+
62
+ make -C "$TMP/detex"
63
+ ls "$TMP/detex/"
64
+ cp $TMP/detex/libdetex.so* "$LIBS_DIR/Detex.so"
65
+ echo " copied: Detex.so"
66
+
67
+ echo "Copying libraries to $LIBS_DIR..."
68
+ mkdir -p "$LIBS_DIR"
69
+ find "$TMP/build" \( -name "*.so" -o -name "*.so.*" -o -name "*.dll" -o -name "*.runtimeconfig.json" \) -exec cp {} "$LIBS_DIR/" \;
70
+
71
+ echo "Done. Libraries installed to $LIBS_DIR"
@@ -0,0 +1,42 @@
1
+ import sys
2
+ import os
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from cue4parse import _DLL_DIR
7
+
8
+ PLATFORM = sys.platform
9
+
10
+ def natives_present() -> bool:
11
+ glob = "*.dll" if PLATFORM == "win32" else "*.so"
12
+ return _DLL_DIR.exists() and any(_DLL_DIR.glob(glob))
13
+
14
+ def _run_script(script: Path, *extra_args: str) -> int:
15
+ """Run a script and return the returncode (0 for success, non-zero for failure)"""
16
+ if not script.exists():
17
+ raise RuntimeError(f"Setup script not found: {script}")
18
+ args = [*extra_args, "--force"] if "--force" in sys.argv else list(extra_args)
19
+ if PLATFORM == "win32":
20
+ result = subprocess.run([str(script), *args], shell=True)
21
+ return result.returncode
22
+ else:
23
+ result = subprocess.run(["bash", str(script), *args])
24
+ return result.returncode
25
+
26
+ def main() -> None:
27
+ if PLATFORM not in ("win32", "linux"):
28
+ print(f"Unsupported platform: {PLATFORM}. Only Windows and Linux are supported.")
29
+ sys.exit(1)
30
+
31
+ if natives_present() and "--force" not in sys.argv:
32
+ print(f"Already set up. Use --force to reinstall.")
33
+ return
34
+
35
+ scripts = Path(__file__).parent
36
+ if PLATFORM == "win32":
37
+ _run_script(scripts / "setup.bat")
38
+ else:
39
+ _run_script(scripts / "setup.sh")
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -0,0 +1 @@
1
+ python -m twine upload dist/*