cue4parse 0.0.2__py3-none-any.whl
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/__init__.py +13 -0
- cue4parse/__main__.py +3 -0
- cue4parse/cli.py +521 -0
- cue4parse/core.py +90 -0
- cue4parse/example.py +38 -0
- cue4parse/includes.py +34 -0
- cue4parse/setup.bat +98 -0
- cue4parse/setup.sh +71 -0
- cue4parse/setup_deps.py +42 -0
- cue4parse-0.0.2.dist-info/METADATA +37 -0
- cue4parse-0.0.2.dist-info/RECORD +14 -0
- cue4parse-0.0.2.dist-info/WHEEL +4 -0
- cue4parse-0.0.2.dist-info/entry_points.txt +2 -0
- cue4parse-0.0.2.dist-info/licenses/LICENSE +24 -0
cue4parse/__init__.py
ADDED
|
@@ -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 *
|
cue4parse/__main__.py
ADDED
cue4parse/cli.py
ADDED
|
@@ -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())
|
cue4parse/core.py
ADDED
|
@@ -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()
|
cue4parse/example.py
ADDED
|
@@ -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())
|
cue4parse/includes.py
ADDED
|
@@ -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
|
+
|
cue4parse/setup.bat
ADDED
|
@@ -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%
|
cue4parse/setup.sh
ADDED
|
@@ -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"
|
cue4parse/setup_deps.py
ADDED
|
@@ -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,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,14 @@
|
|
|
1
|
+
cue4parse/__init__.py,sha256=vj70COk4kOVScWghfHT4GFVxjgkunqKO37NjgoKoFxQ,364
|
|
2
|
+
cue4parse/__main__.py,sha256=FQLdJhpIynUFvonuqQv8BEe3tXXEmxJyZdT_yvKm4mk,39
|
|
3
|
+
cue4parse/cli.py,sha256=QprV3Y1yKVhzcyGub_zKp8lfNjKdGHQjfwPnbARHtkI,21662
|
|
4
|
+
cue4parse/core.py,sha256=1ivz6TJ2IXs6YJeQtGvkR5g30HOUrV_NuGskvIHDgds,2903
|
|
5
|
+
cue4parse/example.py,sha256=maysnp8tgpdSwn78uzVwS9b4Lv28SUPPlzytWX5yC3s,1161
|
|
6
|
+
cue4parse/includes.py,sha256=NTSSAo8wV8lZNgvWdD96dACtGi3PpXQdfNindgdz8RU,1905
|
|
7
|
+
cue4parse/setup.bat,sha256=3JSkuY33v_by3Wb3jXoT8jBYyyU3cs7ljC95cAQPvgw,2836
|
|
8
|
+
cue4parse/setup.sh,sha256=y_VGCzBMN-XpcLuJKVYKnO8y_4HqDl9O41XJEkBvgA0,2415
|
|
9
|
+
cue4parse/setup_deps.py,sha256=cqpaLqNJCc4mL0TDGMRjIdxISSC0iThj1WiYNoS1iKM,1321
|
|
10
|
+
cue4parse-0.0.2.dist-info/METADATA,sha256=UoCBG0ysJalLzsoAfZeG51qZzLxntCau78dIYnHnBKo,936
|
|
11
|
+
cue4parse-0.0.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
12
|
+
cue4parse-0.0.2.dist-info/entry_points.txt,sha256=yXb0-BfXgv_MvJpIg9cy4xPgoTYjJGhxZP79s7p1YJw,49
|
|
13
|
+
cue4parse-0.0.2.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
14
|
+
cue4parse-0.0.2.dist-info/RECORD,,
|
|
@@ -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>
|