-
Notifications
You must be signed in to change notification settings - Fork 31
Description
Problem
Currently, STS2 mod developers need the Godot editor (or MegaDot fork) installed just to export a .pck file containing localization JSON and card art. For simple content mods that only add cards, relics, or potions, this is a heavy requirement — the actual mod code is a single .dll, but the assets need a full Godot project and export pipeline.
This creates friction for new mod developers and makes CI/CD difficult (Godot headless export requires specific binaries).
Would this fit BaseLib's vision?
I ran into this while building a simple content mod (sts2-secret-script) and ended up reverse-engineering the PCK and ctex formats to avoid needing the Godot editor. I have a working proof of concept and would be happy to contribute a proper C# implementation if this fits the direction you see for BaseLib/the mod template.
If you'd prefer this lives as a separate package or in the mod template rather than BaseLib itself, I'm open to that too — just wanted to check before building it out.
Proposed Solution
A standalone PCK packer that runs as part of the dotnet build pipeline, potentially distributed as a NuGet package (e.g. Alchyr.Sts2.PckPacker alongside the existing Alchyr.Sts2.BaseLib and Alchyr.Sts2.ModAnalyzers).
It would:
- Pack localization JSON files — passed through as-is into the PCK
- Convert PNG images to Godot
.ctexformat — lossless WebP with the GST2 header - Generate
.importremap files — so Godot's resource loader finds the textures - Produce a valid Godot 4.5 format-v3 PCK — ready to deploy to the mods directory
Usage
Integrated as an MSBuild target, something like:
<PackageReference Include="Alchyr.Sts2.PckPacker" Version="*" />With a build target that runs after compile:
<Target Name="PackPck" AfterTargets="Build">
<!-- automatically packs ModName/ directory into ModName.pck -->
</Target>Technical Findings
PCK Format (Godot 4.5, format version 3)
The PCK is a simple binary archive. Header (104 bytes):
| Offset | Size | Field | Value |
|---|---|---|---|
| 0x00 | 4 | Magic | GDPC |
| 0x04 | 4 | Format version | 3 |
| 0x08 | 4 | Engine major | 4 |
| 0x0C | 4 | Engine minor | 5 |
| 0x10 | 4 | Engine patch | 1 |
| 0x14 | 4 | Pack flags | 0x02 (PACK_REL_FILEBASE) |
| 0x18 | 8 | Files base offset | (header size) |
| 0x20 | 8 | Directory offset | (after all file data) |
| 0x28 | 64 | Reserved | zeros |
File data follows immediately after the header, aligned to 32-byte boundaries. The directory index (at the end) contains per-file entries:
| Size | Field |
|---|---|
| 4 | Path length (padded to 4-byte boundary) |
| var | UTF-8 path (e.g. res://ModName/localization/eng/cards.json) |
| 8 | File offset (relative to files base) |
| 8 | File size |
| 16 | MD5 hash |
| 4 | Flags (0 = none) |
ctex Format (CompressedTexture2D)
Godot cannot load raw PNGs from a PCK — they must be imported to .ctex format. The ctex file is a 56-byte header followed by a WebP payload:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0x00 | 4 | Magic | GST2 |
| 0x04 | 4 | Version | 1 |
| 0x08 | 4 | Width | image width |
| 0x0C | 4 | Height | image height |
| 0x10 | 4 | Flags | 0x0D000000 (lossless, no mipmaps) |
| 0x14 | 4 | Limiter | -1 (0xFFFFFFFF) |
| 0x18 | 12 | Padding | zeros |
| 0x24 | 4 | Data format | 2 |
| 0x28 | 4 | Packed dimensions | (height << 16) | width |
| 0x2C | 4 | Padding | 0 |
| 0x30 | 4 | Image format | 5 (WebP) |
| 0x34 | 4 | WebP data size | size of following RIFF/WebP blob |
| 0x38 | var | WebP data | lossless WebP image (RGBA) |
This was reverse-engineered from BaseLib.pck and the game's own imported textures. The compress/mode=0 + lossy_quality=1.0 settings used by the game correspond to lossless WebP encoding.
Import Remap Files
For Godot to find textures via their original res://ModName/images/... paths, the PCK must also contain .import remap files:
[remap]
importer="texture"
type="CompressedTexture2D"
path="res://.godot/imported/filename.png-imported.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://ModName/images/card_portraits/filename.png"
dest_files=["res://.godot/imported/filename.png-imported.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=1.0
mipmaps/generate=falseWhat Goes in a Typical Content Mod PCK
| Type | Format in PCK | Conversion needed |
|---|---|---|
| Localization JSON | .json |
None (passthrough) |
| Card/relic/power art | .ctex |
PNG → lossless WebP → GST2 header |
| Import remaps | .import |
Auto-generated per PNG |
Scenes (.tscn) |
.tscn or .scn |
None (passthrough) |
Resources (.tres) |
.tres |
None (passthrough) |
Proof of Concept
We have a working Python implementation (~130 lines) that successfully:
- Converts PNGs to ctex (lossless WebP with GST2 header)
- Generates import remap files
- Packs everything into a valid format-v3 PCK
- Produces PCKs that load correctly in STS2
See: pack_pck.py in BSchneppe/sts2-secret-script — a working STS2 mod that uses this packer to build its PCK in CI without the Godot editor.
Implementation Options
-
C# MSBuild task in a NuGet package — Best integration. Runs automatically on build. Could use
ImageSharp+ a WebP encoder for PNG→ctex conversion. Ships alongside BaseLib and ModAnalyzers. -
Standalone
dotnet tool— Simpler to build, less integrated. Developers wire it into their build manually. -
Extend the existing ModTemplate csproj targets — Add the packer as a build step in the template, removing the need for Godot export entirely for simple content mods.
Benefits
- No Godot editor needed for simple content mods (cards, relics, potions with art + localization)
- CI/CD friendly —
dotnet buildproduces everything needed - Lower barrier to entry for new mod developers
- Consistent with existing BaseLib ecosystem (NuGet-based toolchain)
Offer
Happy to implement this as a PR if it fits the project's direction — either as part of BaseLib, a companion NuGet package, or an addition to the mod template. Let me know what you think and where you'd want it to live.