cue4parse 0.0.4__tar.gz → 0.0.6__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.
- {cue4parse-0.0.4 → cue4parse-0.0.6}/PKG-INFO +3 -10
- cue4parse-0.0.6/README.md +5 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/pyproject.toml +1 -1
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/cli.py +66 -103
- cue4parse-0.0.6/src/cue4parse/core.py +110 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/example.py +7 -7
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/includes.py +8 -9
- cue4parse-0.0.4/README.md +0 -12
- cue4parse-0.0.4/src/cue4parse/core.py +0 -90
- {cue4parse-0.0.4 → cue4parse-0.0.6}/.github/workflows/workflow.yml +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/.gitignore +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/LICENSE +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/__init__.py +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/__main__.py +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/setup.bat +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/setup.sh +0 -0
- {cue4parse-0.0.4 → cue4parse-0.0.6}/src/cue4parse/setup_deps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cue4parse
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: CUE4Parse Python wrapper and CLI tool to extract packages from Unreal Engine games
|
|
5
5
|
Project-URL: Homepage, https://github.com/joric/CUE4Parse-Python/wiki
|
|
6
6
|
Project-URL: Repository, https://github.com/joric/CUE4Parse-Python
|
|
@@ -20,11 +20,4 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
CUE4Parse Python wrapper, CLI tool, and scripting engine
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`pip install cue4parse`
|
|
26
|
-
|
|
27
|
-
* Make sure executable is added to paths, e.g. `%LOCALAPPDATA%\Python\pythoncore-3.14-64\Scripts`.
|
|
28
|
-
* Needs CUE4Parse libraries in `%LOCALAPPDATA%/cue4parse/libs` (downloads and unpacks FModel on Windows).
|
|
29
|
-
* To run CLI: `cue4parse`
|
|
30
|
-
|
|
23
|
+
* Documentation: https://github.com/joric/CUE4Parse-Python/wiki
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cue4parse"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.6"
|
|
8
8
|
description = "CUE4Parse Python wrapper and CLI tool to extract packages from Unreal Engine games"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = ["pythonnet>=3.0"]
|
|
@@ -3,7 +3,10 @@ from cue4parse import *
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import fnmatch
|
|
5
5
|
import time
|
|
6
|
+
import argparse
|
|
7
|
+
import threading
|
|
6
8
|
from importlib.metadata import version
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
10
|
|
|
8
11
|
export_directory = ""
|
|
9
12
|
overwrite_files = False
|
|
@@ -33,12 +36,14 @@ class CliOptions:
|
|
|
33
36
|
yes: bool
|
|
34
37
|
verbose: bool
|
|
35
38
|
mesh_format: str
|
|
36
|
-
anim_format: str
|
|
37
39
|
texture_format: str
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
material_depth: str
|
|
41
|
+
mesh_quality: str
|
|
40
42
|
socket_format: str
|
|
41
|
-
nanite_format: str
|
|
43
|
+
nanite_format: str
|
|
44
|
+
texture_platform: str
|
|
45
|
+
texture_quality: int
|
|
46
|
+
export_all_mips: bool
|
|
42
47
|
export_morph_targets: bool
|
|
43
48
|
export_materials: bool
|
|
44
49
|
export_hdr_as_hdr: bool
|
|
@@ -49,9 +54,7 @@ def norm_path(path: str) -> str:
|
|
|
49
54
|
return "."
|
|
50
55
|
return path.replace("/", os.sep).replace("\\\\", "\\")
|
|
51
56
|
|
|
52
|
-
|
|
53
57
|
def parse_enum(enum_type: Any, value: str, default_name: str | None = None) -> Any:
|
|
54
|
-
# pythonnet enum access: getattr(EnumType, "ValueName")
|
|
55
58
|
if hasattr(enum_type, value):
|
|
56
59
|
return getattr(enum_type, value)
|
|
57
60
|
for name in dir(enum_type):
|
|
@@ -61,11 +64,9 @@ def parse_enum(enum_type: Any, value: str, default_name: str | None = None) -> A
|
|
|
61
64
|
print(f"Warning: Unknown value '{value}' for {enum_type.__name__}, using default '{default_name}'", file=sys.stderr)
|
|
62
65
|
return getattr(enum_type, default_name)
|
|
63
66
|
print(f"Warning: Unknown value '{value}' for {enum_type.__name__}, using default", file=sys.stderr)
|
|
64
|
-
# best-effort default
|
|
65
67
|
names = [n for n in dir(enum_type) if not n.startswith("_")]
|
|
66
68
|
return getattr(enum_type, names[0]) if names else None
|
|
67
69
|
|
|
68
|
-
|
|
69
70
|
def check_file(out_path: Path, silent: bool = False) -> bool:
|
|
70
71
|
skip = (not overwrite_files) and out_path.exists()
|
|
71
72
|
if not silent:
|
|
@@ -75,11 +76,9 @@ def check_file(out_path: Path, silent: bool = False) -> bool:
|
|
|
75
76
|
Log.Information("Writing {Path}", norm_path(str(out_path)))
|
|
76
77
|
return not skip
|
|
77
78
|
|
|
78
|
-
|
|
79
79
|
def write_to_log(_folder: str, _log_message: str, counter: list[int]) -> None:
|
|
80
80
|
counter[0] += 1
|
|
81
81
|
|
|
82
|
-
|
|
83
82
|
def write_to_file(folder: str, file_name: str, data: bytes, log_message: str, counter: list[int]) -> None:
|
|
84
83
|
out_path = Path(export_directory) / folder / file_name
|
|
85
84
|
if not check_file(out_path):
|
|
@@ -89,12 +88,10 @@ def write_to_file(folder: str, file_name: str, data: bytes, log_message: str, co
|
|
|
89
88
|
write_to_log(folder, log_message, counter)
|
|
90
89
|
|
|
91
90
|
def save_json(folder: str, name: str, exports: Any, counter: list[int]) -> None:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
serializer = JsonSerializer();
|
|
95
|
-
serializer.Formatting = Formatting.Indented;
|
|
91
|
+
serializer = JsonSerializer()
|
|
92
|
+
serializer.Formatting = Formatting.Indented
|
|
96
93
|
|
|
97
|
-
output_to_stdout = export_directory==""
|
|
94
|
+
output_to_stdout = export_directory == ""
|
|
98
95
|
if output_to_stdout:
|
|
99
96
|
writer = JsonTextWriter(Console.Out)
|
|
100
97
|
serializer.Serialize(writer, exports)
|
|
@@ -108,12 +105,11 @@ def save_json(folder: str, name: str, exports: Any, counter: list[int]) -> None:
|
|
|
108
105
|
return
|
|
109
106
|
|
|
110
107
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
-
|
|
112
|
-
stream = StreamWriter(str(out_path), False, Encoding.UTF8);
|
|
108
|
+
stream = StreamWriter(str(out_path), False, Encoding.UTF8)
|
|
113
109
|
try:
|
|
114
|
-
writer = JsonTextWriter(stream)
|
|
110
|
+
writer = JsonTextWriter(stream)
|
|
115
111
|
try:
|
|
116
|
-
serializer.Serialize(writer, exports)
|
|
112
|
+
serializer.Serialize(writer, exports)
|
|
117
113
|
writer.Flush()
|
|
118
114
|
finally:
|
|
119
115
|
writer.Close()
|
|
@@ -122,7 +118,6 @@ def save_json(folder: str, name: str, exports: Any, counter: list[int]) -> None:
|
|
|
122
118
|
|
|
123
119
|
write_to_log(folder, name, counter)
|
|
124
120
|
|
|
125
|
-
|
|
126
121
|
def save_raw(folder: str, package, provider, counter: list[int]) -> None:
|
|
127
122
|
name = str(package.Name)
|
|
128
123
|
out_path = Path(export_directory) / folder / name
|
|
@@ -132,7 +127,6 @@ def save_raw(folder: str, package, provider, counter: list[int]) -> None:
|
|
|
132
127
|
data = bytes(provider.Files[package.Path].Read())
|
|
133
128
|
write_to_file(folder, name, data, name, counter)
|
|
134
129
|
|
|
135
|
-
|
|
136
130
|
def try_load_locmeta(package) -> tuple[bool, Any]:
|
|
137
131
|
ok, reader = package.TryCreateReader()
|
|
138
132
|
if not ok or reader is None:
|
|
@@ -142,7 +136,6 @@ def try_load_locmeta(package) -> tuple[bool, Any]:
|
|
|
142
136
|
except Exception:
|
|
143
137
|
return False, None
|
|
144
138
|
|
|
145
|
-
|
|
146
139
|
def try_load_locres(package) -> tuple[bool, Any]:
|
|
147
140
|
ok, reader = package.TryCreateReader()
|
|
148
141
|
if not ok or reader is None:
|
|
@@ -152,48 +145,18 @@ def try_load_locres(package) -> tuple[bool, Any]:
|
|
|
152
145
|
except Exception:
|
|
153
146
|
return False, None
|
|
154
147
|
|
|
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
148
|
def filter_paths(paths: Iterable[str], pattern: str) -> list[str]:
|
|
185
149
|
return [p for p in paths if fnmatch.fnmatch(p, pattern)]
|
|
186
150
|
|
|
187
|
-
|
|
188
151
|
def execute(options: CliOptions) -> int:
|
|
189
152
|
global export_directory, overwrite_files
|
|
190
153
|
|
|
191
154
|
overwrite_files = options.yes
|
|
192
155
|
|
|
193
156
|
if options.verbose:
|
|
194
|
-
Log.Logger =
|
|
157
|
+
Log.Logger = LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Console().CreateLogger()
|
|
195
158
|
else:
|
|
196
|
-
Log.Logger =
|
|
159
|
+
Log.Logger = LoggerConfiguration().MinimumLevel.Fatal().WriteTo.Console().CreateLogger()
|
|
197
160
|
|
|
198
161
|
try:
|
|
199
162
|
game_version = parse_enum(EGame, options.game, "GAME_UE5_LATEST")
|
|
@@ -204,7 +167,8 @@ def execute(options: CliOptions) -> int:
|
|
|
204
167
|
print(f"Mappings not found: {options.mappings}", file=sys.stderr)
|
|
205
168
|
return 1
|
|
206
169
|
|
|
207
|
-
|
|
170
|
+
texture_platform_value = parse_enum(ETexturePlatform, options.texture_platform, "DesktopMobile")
|
|
171
|
+
version = VersionContainer(game_version, texture_platform_value)
|
|
208
172
|
|
|
209
173
|
single_pak_mode = bool(options.pak)
|
|
210
174
|
if single_pak_mode:
|
|
@@ -255,11 +219,6 @@ def execute(options: CliOptions) -> int:
|
|
|
255
219
|
print(f"Total assets: {provider.Files.Count}", file=sys.stderr)
|
|
256
220
|
print(f"Output format: {options.format}", file=sys.stderr)
|
|
257
221
|
|
|
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
222
|
package_paths: list[str] = []
|
|
264
223
|
for list_file in options.config:
|
|
265
224
|
if list_file and Path(list_file).exists():
|
|
@@ -293,20 +252,21 @@ def execute(options: CliOptions) -> int:
|
|
|
293
252
|
print("No matches, exiting.", file=sys.stderr)
|
|
294
253
|
return 0
|
|
295
254
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
255
|
+
exp_options = ExportOptions(
|
|
256
|
+
parse_enum(EMeshFormat, options.mesh_format, "ActorX"),
|
|
257
|
+
parse_enum(ENaniteMeshFormat, options.nanite_format, "NaniteFirst") if 'ENaniteMeshFormat' in globals() else None,
|
|
258
|
+
parse_enum(EMeshQuality, options.mesh_quality, "Highest"),
|
|
259
|
+
texture_platform_value,
|
|
260
|
+
parse_enum(ETextureFormat, options.texture_format, "Png"),
|
|
261
|
+
options.texture_quality,
|
|
262
|
+
options.export_hdr_as_hdr,
|
|
263
|
+
options.export_all_mips,
|
|
264
|
+
parse_enum(EMaterialDepth, options.material_depth, "TopLayerOnly"),
|
|
265
|
+
options.export_materials,
|
|
266
|
+
options.export_morph_targets,
|
|
267
|
+
parse_enum(ESocketFormat, options.socket_format, "Bone"),
|
|
268
|
+
parse_enum(EFileCompressionFormat, options.compression, "None")
|
|
269
|
+
)
|
|
310
270
|
|
|
311
271
|
needs_output_dir = (not options.list_only) and options.format not in ("csv", "json")
|
|
312
272
|
if not options.output:
|
|
@@ -317,13 +277,13 @@ def execute(options: CliOptions) -> int:
|
|
|
317
277
|
else:
|
|
318
278
|
export_directory = norm_path(options.output)
|
|
319
279
|
|
|
320
|
-
import threading
|
|
321
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
322
280
|
|
|
323
281
|
counter_lock = threading.Lock()
|
|
324
282
|
export_count = [0]
|
|
325
283
|
counter = 0
|
|
326
284
|
start = time.monotonic()
|
|
285
|
+
|
|
286
|
+
session_objects = []
|
|
327
287
|
|
|
328
288
|
def process_package(package):
|
|
329
289
|
folder = str(package.Path).rsplit("/", 1)[0] if "/" in str(package.Path) else ""
|
|
@@ -347,7 +307,7 @@ def execute(options: CliOptions) -> int:
|
|
|
347
307
|
ok, pkg = provider.TryLoadPackage(package)
|
|
348
308
|
if not ok or pkg is None:
|
|
349
309
|
if ext in (".uasset", ".umap"):
|
|
350
|
-
raise RuntimeError("Could not load standard asset, check game version, mappings or keys.")
|
|
310
|
+
raise RuntimeError(f"Could not load standard asset, check game version, mappings or keys: {package.Name}")
|
|
351
311
|
if target_format in ("auto", "raw"):
|
|
352
312
|
target_format = "raw"
|
|
353
313
|
else:
|
|
@@ -364,10 +324,9 @@ def execute(options: CliOptions) -> int:
|
|
|
364
324
|
if target_format not in ("auto", "png"):
|
|
365
325
|
return True
|
|
366
326
|
|
|
367
|
-
if target_format == "png"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
local_export_type = ExportType.TEXTURE | ExportType.SOUND | ExportType.MESH | ExportType.ANIMATION | ExportType.OTHER
|
|
327
|
+
local_export_type = ExportType.TEXTURE if target_format == "png" else (
|
|
328
|
+
ExportType.TEXTURE | ExportType.SOUND | ExportType.MESH | ExportType.ANIMATION | ExportType.OTHER
|
|
329
|
+
)
|
|
371
330
|
|
|
372
331
|
parsed = False
|
|
373
332
|
|
|
@@ -381,12 +340,8 @@ def execute(options: CliOptions) -> int:
|
|
|
381
340
|
|
|
382
341
|
value = pointer.Object.Value
|
|
383
342
|
if isinstance(dummy, UTexture) and (ExportType.TEXTURE in local_export_type) and isinstance(value, UTexture):
|
|
384
|
-
|
|
385
|
-
|
|
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)
|
|
343
|
+
with counter_lock:
|
|
344
|
+
session_objects.append(value)
|
|
390
345
|
parsed = True
|
|
391
346
|
|
|
392
347
|
elif isinstance(dummy, (USoundWave, UAkMediaAssetData)) and (ExportType.SOUND in local_export_type):
|
|
@@ -401,10 +356,8 @@ def execute(options: CliOptions) -> int:
|
|
|
401
356
|
elif isinstance(dummy, (UAnimSequenceBase, USkeletalMesh, UStaticMesh, USkeleton)) and (
|
|
402
357
|
ExportType.ANIMATION in local_export_type or ExportType.MESH in local_export_type
|
|
403
358
|
):
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if ok_write:
|
|
407
|
-
write_to_log(folder, os.path.basename(str(file_path)), export_count)
|
|
359
|
+
with counter_lock:
|
|
360
|
+
session_objects.append(value)
|
|
408
361
|
parsed = True
|
|
409
362
|
|
|
410
363
|
if not parsed and (target_format == 'json' or target_format == 'auto'):
|
|
@@ -419,9 +372,8 @@ def execute(options: CliOptions) -> int:
|
|
|
419
372
|
else:
|
|
420
373
|
print(package.Path)
|
|
421
374
|
else:
|
|
422
|
-
|
|
423
375
|
max_workers = os.cpu_count() or 4 if not DEBUG else 1
|
|
424
|
-
fatal_error = [None]
|
|
376
|
+
fatal_error = [None]
|
|
425
377
|
|
|
426
378
|
print(f"Using parallel processing, {max_workers} threads", file=sys.stderr)
|
|
427
379
|
|
|
@@ -443,6 +395,17 @@ def execute(options: CliOptions) -> int:
|
|
|
443
395
|
|
|
444
396
|
if fatal_error[0]:
|
|
445
397
|
return 1
|
|
398
|
+
|
|
399
|
+
if session_objects:
|
|
400
|
+
print(f"Exporting {len(session_objects)} assets via ExportSession...", file=sys.stderr)
|
|
401
|
+
session = ExportSession()
|
|
402
|
+
session.MaxDegreeOfParallelism = 1 if options.verbose else max_workers
|
|
403
|
+
for obj in session_objects:
|
|
404
|
+
session.Add(obj)
|
|
405
|
+
|
|
406
|
+
task = session.RunAsync(export_directory, exp_options)
|
|
407
|
+
task.GetAwaiter().GetResult()
|
|
408
|
+
export_count[0] += len(session_objects)
|
|
446
409
|
|
|
447
410
|
elapsed = time.monotonic() - start
|
|
448
411
|
|
|
@@ -452,8 +415,6 @@ def execute(options: CliOptions) -> int:
|
|
|
452
415
|
Log.Information(f"Processed {len(packages)} packages in {elapsed:.2f}s")
|
|
453
416
|
return 0
|
|
454
417
|
|
|
455
|
-
import argparse
|
|
456
|
-
|
|
457
418
|
class CustomHelpFormatter(argparse.RawTextHelpFormatter):
|
|
458
419
|
def __init__(self, prog):
|
|
459
420
|
super().__init__(prog, max_help_position=60, width=120)
|
|
@@ -472,7 +433,7 @@ examples = [
|
|
|
472
433
|
["Export a single package to stdout in json format:", "cue4parse -i MyGame -p Assets/MyAsset.uasset -f json"],
|
|
473
434
|
["Export multiple packages matching wildcard patterns to a directory:", "cue4parse -i MyGame -p */Textures* -p */Icons* -o Exports"],
|
|
474
435
|
["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
|
|
436
|
+
["Export with PSK meshes and PSA animations:", "cue4parse -i MyGame -p */SkeletalMeshes/* -o Exports --mesh-format ActorX"],
|
|
476
437
|
["Process a single .pak file:", "cue4parse --pak MyMod.pak -o Exports -m Mappings.usmap -g GAME_UE5_1"],
|
|
477
438
|
]
|
|
478
439
|
|
|
@@ -496,15 +457,17 @@ parser.add_argument("-l", "--list", action="store_true", dest="list_only", help=
|
|
|
496
457
|
parser.add_argument("-y", "--yes", action="store_true", help="Overwrite existing files")
|
|
497
458
|
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
|
|
498
459
|
|
|
499
|
-
#
|
|
460
|
+
# Options synced with Program.cs CUE4Parse_Conversion updates
|
|
500
461
|
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),
|
|
502
|
-
parser.add_argument("--
|
|
503
|
-
parser.add_argument("--
|
|
504
|
-
parser.add_argument("--
|
|
505
|
-
parser.add_argument("--lod-format", default="AllLods", help="LOD format: FirstLod, AllLods")
|
|
462
|
+
parser.add_argument("--mesh-format", default="ActorX", help="Mesh format: ActorX (psk), Gltf2 (glb), UEFormat (uemodel), USD (usda)")
|
|
463
|
+
parser.add_argument("--texture-format", default="Png", help="Texture format: Png, Jpeg, Tga, Webp")
|
|
464
|
+
parser.add_argument("--material-depth", default="TopLayerOnly", help="Material depth: TopLayerOnly, AllLayersNoRef, AllLayers")
|
|
465
|
+
parser.add_argument("--mesh-quality", default="Highest", help="Mesh quality: Highest, Lowest, All")
|
|
506
466
|
parser.add_argument("--socket-format", default="Bone", help="Socket format: Bone, Socket, None")
|
|
507
|
-
parser.add_argument("--nanite-format", default="
|
|
467
|
+
parser.add_argument("--nanite-format", default="NaniteFirst", help="Nanite format: NaniteOnly, NoNanite, NaniteFirst, NaniteLast")
|
|
468
|
+
parser.add_argument("--texture-platform", default="DesktopMobile", help="Texture platform: DesktopMobile, XboxAndPlaystation4, NintendoSwitch, Playstation5")
|
|
469
|
+
parser.add_argument("--texture-quality", type=int, default=100, help="Texture quality 1-100")
|
|
470
|
+
parser.add_argument("--export-all-mips", action=argparse.BooleanOptionalAction, default=False, help="Export all texture mip levels")
|
|
508
471
|
parser.add_argument("--export-morph-targets", action=argparse.BooleanOptionalAction, default=True, help="Export morph targets")
|
|
509
472
|
parser.add_argument("--export-materials", action=argparse.BooleanOptionalAction, default=True, help="Export materials with meshes")
|
|
510
473
|
parser.add_argument("--export-hdr-as-hdr", action=argparse.BooleanOptionalAction, default=True, help="Export HDR textures as .hdr")
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
# enable logger
|
|
58
|
+
|
|
59
|
+
clr.AddReference("Serilog")
|
|
60
|
+
clr.AddReference("Serilog.Sinks.Console")
|
|
61
|
+
|
|
62
|
+
import System
|
|
63
|
+
from Serilog import Log, LoggerConfiguration
|
|
64
|
+
from Serilog.Configuration import LoggerSinkConfiguration
|
|
65
|
+
from Serilog.Events import LogEventLevel
|
|
66
|
+
|
|
67
|
+
def patch_serilog_console() -> None:
|
|
68
|
+
flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static
|
|
69
|
+
sink_type = clr.GetClrType(LoggerSinkConfiguration)
|
|
70
|
+
logger_type = clr.GetClrType(LoggerConfiguration)
|
|
71
|
+
assembly = System.Reflection.Assembly.Load("Serilog.Sinks.Console")
|
|
72
|
+
|
|
73
|
+
console_method = next(
|
|
74
|
+
method
|
|
75
|
+
for typ in assembly.GetTypes()
|
|
76
|
+
for method in typ.GetMethods(flags)
|
|
77
|
+
if method.Name == "Console"
|
|
78
|
+
and (params := method.GetParameters())
|
|
79
|
+
and params[0].ParameterType == sink_type
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parent_field = next(
|
|
83
|
+
field
|
|
84
|
+
for field in sink_type.GetFields(
|
|
85
|
+
System.Reflection.BindingFlags.Public
|
|
86
|
+
| System.Reflection.BindingFlags.NonPublic
|
|
87
|
+
| System.Reflection.BindingFlags.Instance
|
|
88
|
+
)
|
|
89
|
+
if field.FieldType == logger_type
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def console(self, *args, **kwargs):
|
|
93
|
+
overrides = {"standardErrorFromLevel": LogEventLevel.Verbose, **kwargs}
|
|
94
|
+
positional = (self, *args)
|
|
95
|
+
values = [
|
|
96
|
+
overrides[p.Name]
|
|
97
|
+
if p.Name in overrides
|
|
98
|
+
else positional[i]
|
|
99
|
+
if i < len(positional)
|
|
100
|
+
else System.Type.Missing
|
|
101
|
+
for i, p in enumerate(console_method.GetParameters())
|
|
102
|
+
]
|
|
103
|
+
console_method.Invoke(None, System.Array[System.Object](values))
|
|
104
|
+
return parent_field.GetValue(self)
|
|
105
|
+
|
|
106
|
+
setattr(LoggerSinkConfiguration, "Console", console)
|
|
107
|
+
|
|
108
|
+
patch_serilog_console()
|
|
109
|
+
|
|
110
|
+
Log.Logger = LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Console().CreateLogger()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import time
|
|
1
|
+
import time, sys
|
|
2
|
+
|
|
2
3
|
from cue4parse import *
|
|
3
4
|
|
|
4
5
|
GAME_DIR = r"E:\Games\Subnautica2\Subnautica2\Content\Paks"
|
|
@@ -7,7 +8,7 @@ MAPPINGS_FILE = r"E:\Games\Subnautica2\Subnautica2.usmap"
|
|
|
7
8
|
def main():
|
|
8
9
|
start = time.perf_counter()
|
|
9
10
|
|
|
10
|
-
Log.Logger =
|
|
11
|
+
Log.Logger = LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Console().CreateLogger()
|
|
11
12
|
|
|
12
13
|
version = VersionContainer(EGame.GAME_UE5_6, ETexturePlatform.DesktopMobile)
|
|
13
14
|
provider = DefaultFileProvider(GAME_DIR, SearchOption.TopDirectoryOnly, version, StringComparer.OrdinalIgnoreCase)
|
|
@@ -24,15 +25,14 @@ def main():
|
|
|
24
25
|
|
|
25
26
|
exports = package.GetExports()
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
filter = lambda name: 'Lifepod' in str(name)
|
|
28
29
|
|
|
29
30
|
for export in exports:
|
|
30
|
-
|
|
31
|
-
if not name_filter(name):
|
|
31
|
+
if not filter(export.ExportType):
|
|
32
32
|
continue
|
|
33
|
-
print(export.ExportType)
|
|
33
|
+
print(export.ExportType, export.Name)
|
|
34
34
|
|
|
35
|
-
print(f'Finished in {time.perf_counter() - start:.4f}s')
|
|
35
|
+
print(f'Finished in {time.perf_counter() - start:.4f}s', file=sys.stderr)
|
|
36
36
|
|
|
37
37
|
if __name__ == '__main__':
|
|
38
38
|
raise SystemExit(main())
|
|
@@ -6,13 +6,14 @@ from System.Text import Encoding
|
|
|
6
6
|
from Newtonsoft.Json import JsonSerializer, Formatting, JsonTextWriter, JsonConvert
|
|
7
7
|
|
|
8
8
|
from System import Guid, StringComparer
|
|
9
|
-
from System.IO import SearchOption
|
|
9
|
+
from System.IO import SearchOption
|
|
10
10
|
from CUE4Parse.Compression import OodleHelper, ZlibHelper
|
|
11
11
|
from CUE4Parse.Encryption.Aes import FAesKey
|
|
12
12
|
from CUE4Parse.FileProvider import DefaultFileProvider, ApkFileProvider
|
|
13
|
-
from CUE4Parse.MappingsProvider import FileUsmapTypeMappingsProvider
|
|
13
|
+
from CUE4Parse.MappingsProvider.Usmap import FileUsmapTypeMappingsProvider
|
|
14
14
|
from CUE4Parse.UE4.Assets import AbstractUePackage
|
|
15
15
|
from CUE4Parse.UE4.Assets.Exports.Animation import UAnimSequenceBase, USkeleton
|
|
16
|
+
from CUE4Parse.UE4.Assets.Exports.Material import EMaterialDepth
|
|
16
17
|
from CUE4Parse.UE4.Assets.Exports.SkeletalMesh import USkeletalMesh
|
|
17
18
|
from CUE4Parse.UE4.Assets.Exports.Sound import USoundWave
|
|
18
19
|
from CUE4Parse.UE4.Assets.Exports.StaticMesh import UStaticMesh
|
|
@@ -24,11 +25,9 @@ from CUE4Parse.UE4.Objects.UObject import FPackageIndex
|
|
|
24
25
|
from CUE4Parse.UE4.Versions import VersionContainer, EGame, ELanguage
|
|
25
26
|
from CUE4Parse.UE4.Assets.Exports.Texture import ETexturePlatform
|
|
26
27
|
|
|
27
|
-
from CUE4Parse_Conversion import
|
|
28
|
-
from CUE4Parse_Conversion.
|
|
29
|
-
from CUE4Parse_Conversion.
|
|
30
|
-
from CUE4Parse_Conversion.
|
|
31
|
-
from CUE4Parse_Conversion.
|
|
32
|
-
from CUE4Parse_Conversion.UEFormat.Enums import EFileCompressionFormat
|
|
33
|
-
from CUE4Parse.UE4.Assets.Exports.Material import EMaterialFormat
|
|
28
|
+
from CUE4Parse_Conversion import ExportSession
|
|
29
|
+
from CUE4Parse_Conversion.Options import ExportOptions
|
|
30
|
+
from CUE4Parse_Conversion.Options import EMeshFormat, EMeshQuality, ESocketFormat, ENaniteMeshFormat
|
|
31
|
+
from CUE4Parse_Conversion.Options import ETextureFormat
|
|
32
|
+
from CUE4Parse_Conversion.Writers.UEFormat.Enums import EFileCompressionFormat
|
|
34
33
|
|
cue4parse-0.0.4/README.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# CUE4Parse-Python
|
|
2
|
-
|
|
3
|
-
CUE4Parse Python wrapper, CLI tool, and scripting engine
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
`pip install cue4parse`
|
|
8
|
-
|
|
9
|
-
* Make sure executable is added to paths, e.g. `%LOCALAPPDATA%\Python\pythoncore-3.14-64\Scripts`.
|
|
10
|
-
* Needs CUE4Parse libraries in `%LOCALAPPDATA%/cue4parse/libs` (downloads and unpacks FModel on Windows).
|
|
11
|
-
* To run CLI: `cue4parse`
|
|
12
|
-
|
|
@@ -1,90 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|