Skip to content

Commit cdb7dfe

Browse files
Zayn995claude
andcommitted
No more repak.exe: pak files are read and written in pure Python (1.23.0)
- s2tweaker/pakfile.py: pak reader (versions 1-11; raw, Zlib, Gzip and Oodle entries, Oodle through ctypes) and a V8B writer, standard library only. pakio.py keeps its API on top of it; the Oodle DLL is looked up locally and loaded directly, never downloaded. - Verified against the real game and against repak 0.2.3 (tests/test_pakfile.py, also in the CI subset): the 36 needed game files come out byte for byte as repak extracted them, 27 third-party mod paks (V3/V8B/V11; raw, Zlib, Oodle) read identically, and a pak we write has the same index, entry headers and SHA-1s as repak's - repak reads it. - Why: Microsoft's ML engine flagged the CI-built repak.exe as a trojan (Wacatac.B!ml, then Bearfoos.A!ml) although two builds differed in 27 bytes of linker timestamp and PDB GUID only. With the pak code in Python there is no unsigned executable left in the package. - Build and CI: no repak build steps; the cross-check fails on any unsigned binary or a repak.exe in the folder; the self-test does a pak roundtrip; the launcher removes a stale _internal/repak.exe on start. - tools/repak.exe and tools/build_repak.py are gone from the repository (the locally built binary carried the build machine's user name in its Cargo paths); source ZIPs exclude binaries from now on. - Docs, FAQ and Nexus texts updated; version 1.23.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 422fbe2 commit cdb7dfe

24 files changed

Lines changed: 1010 additions & 476 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ jobs:
4040
- name: Abhaengigkeiten
4141
run: pip install -r requirements.txt
4242

43-
- name: repak aus dem Quellcode bauen
44-
# Ersetzt das im Repo liegende Binaerteil durch einen frischen Build
45-
# OHNE Download-Funktion (tools/build_repak.py erklaert warum).
46-
# Damit stammt am Ende jede ausgelieferte Datei aus offenem Quellcode.
47-
run: python tools/build_repak.py
48-
4943
- name: Tests ohne Spieldaten
5044
# Die volle Batterie braucht den vanilla/-Ordner mit extrahierten
5145
# GameData. Die duerfen nie ins Repo (GSC-Copyright), also laeuft
@@ -59,7 +53,8 @@ jobs:
5953
# Unabhaengig vom Bau-Skript, mit Windows-Bordmitteln: der Starter
6054
# muss Byte fuer Byte die pythonw.exe des Runners sein und von der
6155
# Python Software Foundation signiert; jede DLL/PYD muss gueltig
62-
# signiert sein (einzige Ausnahme: das selbst gebaute repak.exe);
56+
# signiert sein - ohne Ausnahme (seit 05.09.2026 gibt es kein repak.exe
57+
# mehr; Paks liest und schreibt s2tweaker/pakfile.py in reinem Python);
6358
# und nichts, was Netz oder TLS kann, darf im Paket liegen.
6459
shell: pwsh
6560
run: |
@@ -80,9 +75,8 @@ jobs:
8075
$bins = Get-ChildItem $dist -Recurse -Include *.exe,*.dll,*.pyd
8176
$unsigned = @($bins | Where-Object { (Get-AuthenticodeSignature $_.FullName).Status -ne 'Valid' })
8277
"Binaerdateien : $($bins.Count), davon unsigniert: $($unsigned.Name -join ', ')"
83-
if (@($unsigned | Where-Object { $_.Name -ne 'repak.exe' }).Count -gt 0) {
84-
throw "Unsignierte Binaerdatei ausser repak.exe im Paket"
85-
}
78+
if ($unsigned.Count -gt 0) { throw "Unsignierte Binaerdatei im Paket" }
79+
if (Get-ChildItem $dist -Recurse -Filter repak.exe) { throw "repak.exe im Paket" }
8680
foreach ($bad in '_ssl', '_socket', '_hashlib', 'libssl', 'libcrypto', 'sqlite3', '_multiprocessing') {
8781
if (Get-ChildItem $dist -Recurse -Filter "$bad*") { throw "$bad* im Paket" }
8882
}

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ tools/oo2core*
2222
.s2tweaker_write_test
2323
**/.s2tweaker_write_test
2424

25-
# Build artifacts
25+
# Build artifacts. tools/repak.exe ist seit 05.09.2026 KEIN Repo-Inhalt
26+
# mehr: ein lokal gebautes Binaerteil trug den Windows-Benutzernamen des
27+
# Bau-Rechners in sich (Cargo-Pfade). Es entsteht per tools/build_repak.py
28+
# - reproduzierbar, in der CI vor jedem Bau.
29+
tools/repak.exe
2630
build/
31+
build_check/
2732
dist/
2833
__pycache__/
2934
*.spec

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Everyone is free to use it. This README tells you everything you need.
1717
- Generates **`{bpatch}` config patches** (the official patch system since
1818
game version 1.6): only the values you change are written; everything at
1919
"(vanilla)" is untouched and cannot conflict with other mods.
20-
- Packs them with **repak** into `zzz_<Name>_P.pak` (pak V8B, mount point
20+
- Packs them into `zzz_<Name>_P.pak` with its own pure-Python pak writer (pak V8B, mount point
2121
`../../../`) — into an `output` folder, or directly into `~mods`.
2222
- Fully **portable**: settings, cache and output live next to the exe.
2323

@@ -57,13 +57,13 @@ built lazily when you expand a category, so startup stays fast.
5757

5858
| Layer | Details |
5959
|---|---|
60-
| Network | **None.** Since 1.19.2 the program contains no networking code at all — no `urllib`, no sockets, no HTTP client — and the bundled repak is compiled without its HTTP/TLS stack. Both are enforced on every build ([tests/test_no_network.py](tests/test_no_network.py), [tests/test_no_download.py](tests/test_no_download.py)) and can be verified with one `grep` of this repository. The update check was removed with 1.19.2: Nexus' file submission guidelines prohibit internet-connecting executables "unless where it is crucial" and say "'auto update' functionality does not qualify as crucial". Updating is a manual file swap. |
61-
| Vanilla data | `repak unpack` of `pakchunk0-Windows.pak` (only the 29 needed GameData files), then `.cfg.bin` → text via the vendored decoder ([s2tweaker/vendor_bin2cfg.py](s2tweaker/vendor_bin2cfg.py)). Cached in `cache/vanilla-<pakSize>-s<schema>/`; a game update changes the fingerprint → automatic re-extraction. |
62-
| Oodle | The game's config archives are Oodle-compressed, so reading them needs the proprietary `oo2core_9_win64.dll`. The game does **not** ship it (Oodle is linked into the game executable), and **S2Tweaker never downloads it**: a program that pulls a library off the internet and then runs it looks exactly like a dropper, which is one reason scanners flag tools like this. The user places the file once; the tool looks in the usual local spots, always verifies the SHA-256, and says so at startup with a link and a target folder if it is missing. The bundled repak cannot fetch it either - it is built from source with the download function and its HTTP/TLS stack removed ([tools/build_repak.py](tools/build_repak.py)). Packing never needs Oodle. |
60+
| Network | **None.** Since 1.19.2 the program contains no networking code at all — no `urllib`, no sockets, no HTTP client — and since 1.23.0 there is no bundled helper program at all: pak files are read and written by [s2tweaker/pakfile.py](s2tweaker/pakfile.py) in plain Python. Both are enforced on every build ([tests/test_no_network.py](tests/test_no_network.py), [tests/test_no_download.py](tests/test_no_download.py)) and can be verified with one `grep` of this repository. The update check was removed with 1.19.2: Nexus' file submission guidelines prohibit internet-connecting executables "unless where it is crucial" and say "'auto update' functionality does not qualify as crucial". Updating is a manual file swap. |
61+
| Vanilla data | The needed GameData files are read straight out of `pakchunk0-Windows.pak` by [pakfile.py](s2tweaker/pakfile.py) (index parsed, only those entries decompressed), then `.cfg.bin` → text via the vendored decoder ([s2tweaker/vendor_bin2cfg.py](s2tweaker/vendor_bin2cfg.py)). Cached in `cache/vanilla-<pakSize>-s<schema>/`; a game update changes the fingerprint → automatic re-extraction. |
62+
| Oodle | The game's config archives are Oodle-compressed, so reading them needs the proprietary `oo2core_9_win64.dll`. The game does **not** ship it (Oodle is linked into the game executable), and **S2Tweaker never downloads it**: a program that pulls a library off the internet and then runs it looks exactly like a dropper, which is one reason scanners flag tools like this. The user places the file once; the tool looks in the usual local spots, always verifies the SHA-256, and says so at startup with a link and a target folder if it is missing. The DLL is loaded through `ctypes` only after its SHA-256 matched. Packing never needs Oodle. |
6363
| Ammunition swap | Per weapon, in the overrides tree: `AmmoCaliber` plus the `ProjectilePrototypeSID` of every existing `AmmoTypeProjectiles` slot in `WeaponGeneralSetupPrototypes` — the same struct `MaxAmmo` already lives in, so no new game file and no cache bump. Only existing slots are rewritten, never added or removed (whether `{bpatch}` can grow an array is untested in-game). The sort is read per index, never inferred from position: six sniper rifles carry `Supersonic` on `[0]`. The offered calibers are harvested from the weapon data, so 7.62×39 — real ammunition that no weapon uses and no generator drops — never appears. Nothing is blocked: damage lives on the weapon (`BaseDamage`), the round only multiplies, and that multiplier is 1.0 for every rifle and pistol caliber but 0.084 for 12 gauge, which shotguns offset with a much higher base value. Broken combinations are allowed and labelled with the computed factor. |
6464
| Parsing | [cfgparse.py](s2tweaker/cfgparse.py) parses GSC's cfg text format (`Name : struct.begin {refkey=...}``struct.end`) into a tree; `refkey` inheritance chains are resolved to get effective vanilla values. |
6565
| Patch output | [emit.py](s2tweaker/emit.py) writes `{bpatch}` structs. Patch files follow the proven convention `<BaseCfg>/<BaseCfg>_patch_<Mod>.cfg` under `Stalker2/Content/GameLite/GameData/`. |
66-
| Packing | [pakio.py](s2tweaker/pakio.py) stages the files and calls the bundled `tools/repak.exe` (defaults are exactly what the game wants: V8B, mount `../../../`, uncompressed). |
66+
| Packing | [pakio.py](s2tweaker/pakio.py) stages the files and [pakfile.py](s2tweaker/pakfile.py) writes the pak (exactly what the game wants: V8B, mount `../../../`, uncompressed, SHA-1 per entry). Verified against repak 0.2.3: same index, same entries, same hashes, and repak reads the result. |
6767
| Key game files | `ObjPrototypes` (player + mutants), `ItemPrototypes`, `TradePrototypes`, `DifficultyPrototypes` (per-difficulty multiplier groups), `EffectPrototypes` (overweight effects), `FloatProviderPrototypes` (scope-sway constant — patched instead of the sway effects so offset-aiming keeps working), `WeaponData/*` (damage, wear, spread, recoil), `CoreVariables` (repair costs, stamina drain), `ObjWeightParamsPrototypes` (carry weight), `ObjHoldBreathParamsPrototypes`, `StashPrototypes` (smart loot in stashes and on bodies), `ItemGeneratorPrototypes` (9.3 MB — the world's loot generators; only `MinCount`/`MaxCount` under `PossibleItems` is scaled, and only on generators that pass a two-stage safety filter, see below), `RelationPrototypes` (faction relations: 582 pair baselines + the RelationVersion counter the patch raises so saves notice changes — research: [docs/FACTION_RELATIONS_RESEARCH.md](docs/FACTION_RELATIONS_RESEARCH.md)). |
6868

6969
Deep research notes with sources, vanilla values and risk analysis:
@@ -163,10 +163,10 @@ s2tweaker/
163163
gamedata.py extraction pipeline, parsing, inheritance resolution
164164
cfgparse.py GSC cfg text parser
165165
emit.py {bpatch} cfg writer
166-
pakio.py repak wrapper (pack/unpack)
166+
pakfile.py pak reader/writer in pure Python (V1-V11 read, V8B write, Oodle via ctypes)
167+
pakio.py pack/unpack/list on top of it, Oodle DLL lookup (never a download)
167168
game.py game folder auto-detection (Steam/GOG/Xbox)
168169
vendor_bin2cfg.py cfg.bin → cfg decoder (vendored, public domain)
169-
tools/repak.exe pak tool (MIT/Apache-2.0, by trumank)
170170
tools/build_exe.py assembles the program folder dist/S2Tweaker/ (no PyInstaller)
171171
tools/launcher.py shipped as _internal/sitecustomize.py: starts the GUI
172172
docs/SPEC.md research: every tweak's mechanism + sources
@@ -184,7 +184,7 @@ python main.py # run the GUI directly
184184
build.bat # or assemble dist/S2Tweaker/ (needs a python.org install)
185185
```
186186

187-
`tools/repak.exe` is not the upstream release binary: `python tools/build_repak.py` rebuilds it from source (pinned tag) with the runtime Oodle download removed, so nothing in the shipped folder can fetch anything. The CI does this on every build.
187+
**There is no repak.exe since 1.23.0.** Until 1.22.0 the folder carried a repak binary compiled from source by the CI, the only unsigned executable in the package. On 2026-09-05 Microsoft's machine-learning engine flagged two such builds as trojans although they differed in 27 bytes of linker timestamp and PDB GUID only. Rather than fight a classifier that dislikes the shape of an unsigned Rust binary, the pak handling moved into Python: [s2tweaker/pakfile.py](s2tweaker/pakfile.py) reads pak versions 1 to 11 (Zlib, Gzip, Oodle) and writes V8B, using nothing but the standard library. [tests/test_pakfile.py](tests/test_pakfile.py) checks it against the real game: the 36 needed files come out byte for byte as repak extracted them, 27 third-party mod paks read identically, and a pak we write has the same index, entries and hashes as repak's.
188188

189189
**There is no PyInstaller since 1.21.0.** `S2Tweaker.exe` is `pythonw.exe`
190190
from python.org, byte for byte, signed by the Python Software Foundation;
@@ -195,7 +195,7 @@ library as a `.pyc` zip compiled from the same python.org installation —
195195
without `socket`, `ssl`, `asyncio` and `sqlite3`, and without any OpenSSL
196196
library, so the package has no networking capability at all. Every DLL,
197197
PYD and EXE in the folder is signed by the PSF or Microsoft except
198-
`repak.exe`, which the CI compiles from source.
198+
nothing - there is no unsigned executable left.
199199

200200
Why: the PyInstaller builds kept tripping antivirus heuristics. 1.20.0 was
201201
flagged by two engines on VirusTotal although its launcher was byte for

THIRD_PARTY_LICENSES.txt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,17 @@ as readable source.
4242

4343

4444
-------------------------------------------------------------------------
45-
repak (tools/repak.exe)
45+
repak (format reference only - no repak code or binary ships since 1.23.0)
4646
https://github.com/trumank/repak
4747
Dual licensed MIT OR Apache-2.0; the MIT terms are used here.
4848

49-
MODIFIED BUILD: S2Tweaker does not ship the upstream release binary. It
50-
builds repak from source (pinned tag, see tools/build_repak.py) with one
51-
change: the function that downloads the Oodle library at runtime is
52-
removed, together with its HTTP/TLS stack. The build can therefore use an
53-
Oodle library that is already on the machine, but can never fetch one.
54-
Both licences permit modification; this note records that it happened.
55-
-------------------------------------------------------------------------
49+
NOT BUNDLED ANY MORE: until 1.22.0 S2Tweaker shipped a repak binary
50+
compiled from source (with its Oodle download removed). Since 1.23.0 the
51+
pak files are read and written by the tool's own Python module
52+
(s2tweaker/pakfile.py), which implements the Unreal pak format as repak
53+
reads and writes it and was verified byte for byte against repak's
54+
output. The MIT notice below is kept in acknowledgement of that
55+
reference; no repak code is included.
5656

5757
MIT License
5858

@@ -101,10 +101,9 @@ never redistributed by this project.
101101
That library is required to decompress the game's own .pak archives. Note
102102
that S.T.A.L.K.E.R. 2 does not ship it as a separate file (Oodle is linked
103103
into the game executable), so it cannot be taken from the installation.
104-
S2Tweaker never downloads it, and neither does the bundled repak (see the
105-
note on the modified build above). The user places the file themselves;
104+
S2Tweaker never downloads it (and bundles no helper program that could). The user places the file themselves;
106105
the tool looks for it in the usual local places, verifies it against the
107-
SHA-256 checksum repak expects, and keeps it in the user's own S2Tweaker
106+
SHA-256 checksum it expects, and keeps it in the user's own S2Tweaker
108107
folder ("tools" next to the executable; if that folder is not writable,
109108
%LOCALAPPDATA%\S2Tweaker\tools is used instead). If it is missing, the
110109
tool says so at startup and names both the source and the target folder.

docs/CODE_SIGNING_POLICY.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ machine:
4040
- `python3XX.dll` and the extension modules in `_internal` are the ones from
4141
that installation, signed by the Python Software Foundation; the Visual C++
4242
runtime DLLs are signed by Microsoft. The workflow fails if any DLL, PYD
43-
or EXE in the folder is unsigned — with exactly one exception, `repak.exe`
44-
(see below).
43+
or EXE in the folder is unsigned — without exception since 1.23.0.
4544
- The tool's own code ships as readable `.py` files in
4645
`_internal/s2tweaker`, next to the pure-Python libraries it uses
4746
(customtkinter, darkdetect, packaging). A small start-up module,
@@ -54,7 +53,8 @@ machine:
5453

5554
Each build runs a self-test before it is accepted: a copy of the folder is
5655
started, imports every bundled module, proves that `socket` and `ssl` are
57-
absent, runs the bundled `repak.exe` once, builds the main window and tears
56+
absent, writes and reads back a small pak in pure Python, builds the main
57+
window and tears
5858
it down again.
5959

6060
Each release is approved manually.
@@ -64,18 +64,18 @@ Each release is approved manually.
6464
Two things in the shipped folder are not built from this repository's
6565
source, and both are stated openly:
6666

67-
- **`repak.exe`** (inside `_internal`) — the pak packer/unpacker by
68-
[trumank](https://github.com/trumank/repak), MIT OR Apache-2.0. The
69-
release does not use the upstream binary: the workflow compiles repak from
70-
source at a pinned tag ([`tools/build_repak.py`](../tools/build_repak.py))
71-
with its runtime download function and its whole HTTP/TLS stack removed.
72-
It is the only unsigned executable in the folder. Its licence ships with
73-
the tool ([THIRD_PARTY_LICENSES.txt](../THIRD_PARTY_LICENSES.txt)).
67+
- **No repak.exe since 1.23.0.** Until 1.22.0 the folder carried a repak
68+
binary compiled from source by the workflow — the only unsigned
69+
executable. On 2026-09-05 Microsoft's ML engine flagged two such builds
70+
(differing in 27 bytes of timestamp and PDB GUID) as trojans. The pak
71+
handling now lives in [`s2tweaker/pakfile.py`](../s2tweaker/pakfile.py),
72+
plain Python verified against the game files and against repak's output
73+
([tests/test_pakfile.py](../tests/test_pakfile.py)).
7474
- **`oo2core_9_win64.dll`** (Oodle, proprietary, by RAD Game Tools /
7575
Epic Games) — **not** part of the download and never fetched by the tool.
7676
Reading the game's packed configuration files requires it, so the user
7777
places that file once, guided by a setup window that names the source and
78-
the target folder. The tool verifies it against the SHA-256 checksum repak
78+
the target folder. The tool verifies it against the SHA-256 checksum it
7979
expects. It is never redistributed by this project.
8080

8181
## Privacy
@@ -85,8 +85,7 @@ and no usage reporting.
8585

8686
Since 1.19.2 it makes **no outbound requests at all**. There is no networking
8787
code left in the program: no `urllib`, no sockets, no HTTP client. The
88-
bundled `repak.exe` is compiled from source with its download function and
89-
its entire HTTP/TLS stack removed, so it cannot make a request either. Since
88+
tool bundles no helper program that could make a request either. Since
9089
1.21.0 the package does not even contain Python's `socket` and `ssl` modules.
9190
These claims are enforced by
9291
[tests/test_no_network.py](../tests/test_no_network.py),
@@ -115,6 +114,5 @@ tool's own folder on the user's machine.
115114
the unmodified files from python.org; the Visual C++ runtime DLLs carry
116115
Microsoft's.
117116
- S2Tweaker's own code ships as readable Python source and is not signed.
118-
`repak.exe` is compiled from source by the workflow and is not signed.
119117
- No code-signing service is used by this project at present. Should that
120118
change, the signer and the certificate will be named here.

0 commit comments

Comments
 (0)