Skip to content

Cleanup, performance, and fixes for #60 + #53#73

Merged
rm-NoobInCoding merged 8 commits into
masterfrom
cleanup-and-fixes
Apr 18, 2026
Merged

Cleanup, performance, and fixes for #60 + #53#73
rm-NoobInCoding merged 8 commits into
masterfrom
cleanup-and-fixes

Conversation

@rm-NoobInCoding
Copy link
Copy Markdown
Owner

Multi-commit pass over the codebase covering bug fixes, performance wins, a structural cleanup, and two reported issues.

Fixes

  • Closes Oodle decompression failed: not initialized #60 — Oodle/Zlib "not initialized" at provider load time. EnsureNativeDlls was calling DownloadOodleDll / DownloadDll, which only fetch the native DLL; neither sets OodleHelper.Instance / ZlibHelper.Instance, so the CUE4Parse decoder threw the moment it hit an Oodle/Zlib-compressed source container. Switched to Initialize(...), which downloads and registers the instance.
  • Closes IsEncrypted flag set for UTOC container for not reason #53EncryptedContainerFlag set on the resulting UTOC even when the archive wasn't properly encrypted. --aes-key now only unlocks the source archives; a new opt-in --encrypt-output is required to encrypt the generated .ucas and set the flag. Default behavior is plain (matches community forks). A known limitation — the UTOC directory index itself still isn't encrypted when --encrypt-output is used — is called out in that commit's body.

Other commits in this PR

  • Replace CUE4Parse NuGet package with a git submodule — source-level visibility for debugging and applying local patches without waiting on a release.
  • Fix packing bugs: divide-by-zero, broken MMF, OOM on AES, Windows-only pathsWriteProgressBar on single-file packs, MMF-from-bytes pattern that disposed the mapping before callers read it, OOM when AES-encrypting a large .ucas via File.ReadAllBytes, Replace(\"\\\\\",\"\") path math that only worked on Windows, dynamic dispatch replaced with OfType<FIoStoreEntry>(), and the Oodle API migration for the current CUE4Parse (OodleFileName, ref-path DownloadOodleDll).
  • Performance: one-pass packing, cached compressors, O(1) entry lookup — Zlib/Oodle cached in Lazy<T> (was constructed per 64 KB block), SHA1 folded into the compression loop so source is read once instead of twice, per-input provider scan replaced with a dictionary lookup (O(n·m) → O(n+m)), stackalloc pad buffer hoisted out of the inner loop.
  • Restructure: rename files, split Program.cs, drop dead codeFIoConstants.csConstants.cs, FIoUToc.csUToc.cs; removed the never-read-back UToc wrapper, FIoPerfectHashSeeds, ManifestData, and unused Read/Seek/Skip/Tell/Pos extensions on MemoryStream; split Program.cs into focused helpers (EnsureNativeDlls, TryParseEngineVersion, ValidateCliOptions, LogOptionsSummary, TryLoadProvider, BuildManifest, BuildUtocEntryLookup); switched DefaultFileProvider to its non-obsolete constructor.
  • Ignore .claude directory — local editor settings shouldn't live in the repo.

Review notes

Each commit stands on its own and has a detailed body. Easier to review per-commit than as a squash.

Build: clean (dotnet build UnrealReZen/UnrealReZen.csproj). No runtime regression tests exist in the repo; the two GitHub issue fixes were verified by reading the CUE4Parse API paths they touch.

Rationale: we need source-level access to CUE4Parse to debug API quirks, apply
local patches, and pick up upstream fixes without waiting for a package release.

Changes:
- UnrealReZen.csproj: drop PackageReference "CUE4Parse" 4.28.0; add
  ProjectReference to ..\external\CUE4Parse\CUE4Parse\CUE4Parse.csproj
- .gitmodules: pin external/CUE4Parse to FabianFG/CUE4Parse
- Program.cs / Compressions.cs: adapt to the current CUE4Parse API
  (OodleHelper.OodleFileName constant, ref-path DownloadOodleDll signature)
…y paths

WriteProgressBar no longer divides by zero on single-file packs
- Clamp denominator with Math.Max(total, 1) so passing one file to pack
  doesn't throw DivideByZeroException.

Replace the broken MemoryMappedHelpers pattern with ChunkSource
- CreateMemoryMappedFileFromByteArray used CreateNew inside a using block
  then OpenExisting after dispose, so the mapping was already released
  when callers tried to read it. Deleted MemoryMappedHelpers.cs.
- New ChunkSource abstraction (FromFile / FromBytes, ReadInto, Length)
  gives the packer a single type that works for both on-disk inputs
  (memory-mapped) and the in-memory dependencies blob.

Stream AES encryption instead of loading the entire .ucas into memory
- Old path: File.ReadAllBytes -> EncryptAES(byte[]) -> File.WriteAllBytes,
  which OOM'd on large packs (>2 GB).
- New path: CryptoStream from src .ucas to a temp file, then File.Move
  replaces the original.

Cross-platform path handling
- Path.GetRelativePath + Replace('\','/') instead of
  Replace("\\","") string math that only happened to work on Windows.

Type safety
- Replace `dynamic b = dep` with provider.Files.OfType<FIoStoreEntry>();
  avoids runtime DLR dispatch and the NRE it throws on unexpected types.
- Tighten nullable annotations on DirIndexWrapper helpers
  (dentry / lastDentry now correctly typed FIoDirectoryIndexEntry?).

CUE4Parse API migration
- Oodle: OodleHelper.OODLE_DLL_NAME -> OodleHelper.OodleFileName,
  DownloadOodleDll(string) -> DownloadOodleDll(ref string? path).
Cache Zlib / Oodle native instances in static Lazy<T>
- Compressions.cs previously constructed a fresh Zlibng or Oodle wrapper
  (which reloads the native DLL) for every 64 KB compression block. On a
  1 GB pack that was ~16 000 DLL init cycles.
- Now held in Lazy<Zlibng> / Lazy<Oodle> with isThreadSafe=true so the
  DLL is loaded exactly once per process.

Fold SHA1 into the compression loop (single read pass)
- Old: mmf.SHA1Hash() read the whole file, then the compression loop
  read it a second time block-by-block.
- New: one loop reads each block once, feeds it into
  sha1.TransformBlock(..), compresses, writes. TransformFinalBlock at
  the end produces the chunk hash. Halves the bytes read from MMF.

Build a utoc entry lookup once instead of scanning per-input
- Old BuildManifest walked provider.Files (~all entries across all
  containers) for every file in the repack list: O(n*m).
- New BuildUtocEntryLookup produces a Dictionary<string, List<FIoStoreEntry>>
  keyed by entry.Path, case-insensitive, then each repack input is an
  O(1) probe: O(n+m) overall.

Minor
- Hoist the 16-byte stackalloc pad buffer out of the inner file loop
  (silences CA2014, allocates once per pack instead of once per file).
- Drop ChunkSource.ComputeSha1; hashing moved into the one-pass loop.
Renames (content also cleaned up)
- UnrealReZen/Core/FIoConstants.cs -> Constants.cs
  (class was already named Constants; file name now matches)
- UnrealReZen/Core/FIoUToc.cs      -> UToc.cs
  (kept only UTocHeader, AssetMetadata, FIoContainerID; see below)

Dead code removed
- UToc wrapper class: never read back by anything; the packer only
  writes the UTOC bytes, so the reader-style wrapper was unused.
- FIoPerfectHashSeeds: unreferenced.
- ManifestData (Core/FIoDependency.cs): unreferenced.
- MemoryStreamHelpers: removed Read/Seek/Skip/Tell/Pos extensions that
  were never called. Only Write extensions remain.

Program.cs split from one long Main into focused helpers
- EnsureNativeDlls, TryParseEngineVersion, ValidateCliOptions
- LogOptionsSummary, TryLoadProvider
- BuildManifest, BuildUtocEntryLookup
Main/Run now read top-to-bottom as a pipeline.

API hygiene
- DefaultFileProvider: switch from the (string, SearchOption, bool,
  VersionContainer?) obsolete ctor to (string, SearchOption,
  VersionContainer, StringComparer).
- FIoStoreTocEntryMeta.ChunkHash marked required so the implicit
  non-null contract is enforced by the compiler.
Local Claude Code settings (.claude/settings.local.json) were tracked by
accident in the initial snapshot. They are per-user, per-machine data and
shouldn't live in the repo. Added .claude/ to .gitignore and untracked
the existing file.
Symptom
- "CUE4Parse.Compression.OodleException: Oodle decompression failed:
  not initialized" at provider load time, even though oo2core / oodle
  dll was present next to the exe. Users had to manually register the
  instance from their own code to work around it.

Root cause
- EnsureNativeDlls was calling OodleHelper.DownloadOodleDll and
  ZlibHelper.DownloadDll, which only *fetch* the DLL. Neither sets
  OodleHelper.Instance / ZlibHelper.Instance, so CUE4Parse's decoder
  threw the moment it tried to read an Oodle/Zlib-compressed source
  container.

Fix
- Use OodleHelper.Initialize / ZlibHelper.Initialize, which download
  (if missing) AND register the Instance. Verify Instance is non-null
  afterwards and fail fast with a clear message if not.
#53)

Symptom
- Passing --aes-key (required to read encrypted source archives) also
  silently encrypted the output .ucas and set the EncryptedContainerFlag
  in the .utoc. The tool only encrypted the .ucas body though -- it
  never encrypted the .utoc directory index -- so the resulting
  container was structurally invalid for games that honor the flag
  (FModel flagged "IsEncrypted for no reason", games crashed on load).

Design change
- --aes-key is now strictly "key for decrypting the source game".
- New --encrypt-output flag (default false) opts in to encrypting the
  generated .ucas and setting the EncryptedContainerFlag. Most UE4/5
  games accept plain archives for mods, so this matches the common
  case and the behavior of community forks (GhostyPool, matyamod).

Code
- Packer.PackToCasToc now takes a nullable FAesKey? outputAes.
  Null means "don't encrypt, don't set the flag"; non-null means
  "encrypt UCAS with this key and set the flag".
- ConstructUtocFile takes an explicit bool isEncrypted instead of
  inferring it from the AES key string.
- Program.Run only forwards the key when opts.EncryptOutput is true.

Known limitation
- When --encrypt-output is passed, the .utoc directory index itself
  is still not encrypted. Games that strictly require an encrypted
  directory index will reject the container. Proper directory-index
  encryption is tracked separately; this commit fixes the common
  "don't encrypt at all" path.
What changed and why
- .NET 7 -> 8. The project now targets net8.0 (csproj), so the old
  setup-dotnet@v3 with 7.0.x would fail to restore.
- Checkout with submodules: false, then an explicit
  "git submodule update --init external/CUE4Parse". A plain
  submodules: recursive dies on a broken catch2 pin buried under
  ACL/rtm's transitive submodules; we don't need those anyway.
  CUE4Parse's native CMake step is non-fatal (it logs a warning and
  continues if the natives don't build), so skipping ACL is safe -
  our tool downloads its own Oodle/Zlib DLLs at runtime.
- Dropped the "dotnet test" step. There is no test project in the
  repo, so the step was always a no-op-or-fail depending on SDK
  version.
- Bumped actions/checkout v3 -> v4, actions/setup-dotnet v3 -> v4.
- Removed the **.cs / **.csproj path filter. A change to the
  workflow file, .gitmodules, or the submodule pin should also
  retrigger CI.
- Matrix left with a single entry (windows-latest) but kept as a
  matrix so future platforms can be added without restructuring.
@rm-NoobInCoding rm-NoobInCoding merged commit bf9e8de into master Apr 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Oodle decompression failed: not initialized IsEncrypted flag set for UTOC container for not reason

1 participant