Godot + .NET Internals: PCK Files, Dual Runtimes, and Why Decompiled C# Looks Terrible
A technical deep-dive for Slay the Spire 2 modders and Godot C# developers.
The Puzzle: Why Is C# in Two Places?
If you extract a Godot 4 C# game - like Slay the Spire 2 - you'll notice something odd: C# appears to live in two places simultaneously.
sts2.pck- Godot's packed asset archivests2.dll- a .NET assembly sitting next to the executable
It looks like duplication. It isn't. They serve two completely different consumers.
What Each File Is For
The .pck File
A .pck is Godot's proprietary virtual filesystem archive - essentially a zip of everything the engine needs: scenes, textures, audio, shaders, and yes, C# source files. When the game launches, Godot mounts the .pck and exposes its contents through its own virtual file path system (res://).
The C# files bundled here are stored for Godot's own purposes - tooling, the editor, and resource consistency. In a Godot 4 C# project, the original .cs source files are bundled in as-is.
The .dll File
The .dll is a standard .NET assembly - compiled IL bytecode that the CoreCLR runtime loads and executes. It lives on disk next to the game executable because the .NET runtime finds assemblies via normal OS filesystem paths, not through Godot's virtual filesystem. It has no concept of .pck files.
The Flow
Source .cs files
|
v (Roslyn compiler)
sts2.dll <--- CoreCLR loads this for execution
|
v (bundled into)
sts2.pck <--- Godot mounts this as its virtual filesystem
Same assembly. Two consumers. Two locations.
Two Runtimes, Side by Side
This is the part that trips people up. When you run a Godot 4 C# game, two separate runtimes are operating simultaneously.
Godot Engine Runtime
Godot is responsible for:
- The scene tree, nodes, physics, and rendering
- Its own virtual filesystem, where
.pckgets mounted - Resource loading via
GD.Load<T>()andres://paths - The main game loop and signal system
.NET Runtime (CoreCLR)
Microsoft's CoreCLR is responsible for:
- Actually executing compiled C# IL bytecode
- Garbage collection and memory management
- Assembly resolution and loading
- The type system and reflection
How They Connect
Godot doesn't execute C# itself. Instead, it embeds CoreCLR as a hosted runtime - similar to how Unity embeds Mono or IL2CPP. Godot bootstraps CoreCLR on startup, and the two sides communicate through a native interop bridge called GodotSharp.
Godot Engine
|
|-- starts up, mounts sts2.pck
|
|-- initializes CoreCLR as embedded host
| |
| `-- CoreCLR loads sts2.dll from disk
| `-- your C# code runs here
|
|-- calls into C# via GodotSharp bindings
`-- C# calls back into Godot via the same bridge
Your C# mod code is executed by CoreCLR, but the objects you're manipulating - Node, Resource, PackedScene - are Godot-side objects accessed through the GodotSharp bridge. A crash in either runtime breaks everything.
Why PCK-Extracted C# Looks So Much Cleaner
Here's something you'll notice immediately when modding: if you extract C# from the .pck using gdre_tools, the code looks clean and readable. But if you decompile the .dll with ILSpy or dnSpy, you get something much uglier. Same game. Same code. Why?
What the .pck Contains
When gdre_tools extracts C# from the .pck, it's recovering files that Godot itself stored there. In a Godot 4 C# project, the original .cs source files are bundled into the .pck directly - they haven't been through any transformation. What you get is close to, or literally, the original source.
What the .dll Contains
The .dll contains compiled IL bytecode. The source has already been processed by the Roslyn compiler - and in a Release build, also by the optimizer:
.cs source
|
v Roslyn compiler
IL bytecode + metadata
|
v Release optimizations
|-- method inlining
|-- dead code elimination
`-- local variable merging
|
v
stored in .dll
When you run that through a decompiler, it's doing reverse engineering - reconstructing C# from IL. That's an inherently lossy process:
| What's Lost | Result in Decompiled Output |
|---|---|
| Variable names | local_0, V_3, b__4 |
| Comments | Gone entirely |
| Lambdas / closures | Expanded into generated classes (<>c__DisplayClass) |
| LINQ expressions | Unrolled into state machines |
| Async/await | Explicit state machine structs |
| Compiler hints | All made explicit and ugly |
At a Glance
.pck extracted |
.dll decompiled |
|
|---|---|---|
| Source | Original .cs bundled by Godot |
IL -> reconstructed C# |
| Variable names | Real, as written | Lost or mangled |
| Comments | Present | Gone |
| Lambdas | Clean one-liners | Ugly generated classes |
| Async methods | Clean | Explicit state machines |
| Accuracy | Original source | Approximation |
Practical Takeaway for Modders
The .pck is the primary target when reverse engineering a Godot C# game. gdre_tools will give you clean, readable, close-to-original source code directly from it.
The decompiled .dll is a fallback - useful when something isn't bundled in the .pck, or when you need to verify what's actually being executed at the IL level. But for general modding and understanding game logic, start with the .pck extraction every time.
Tools Referenced
- gdre_tools / gdsdecomp - the correct tool for
.pckextraction; supports Godot 3 & 4, recovers GDScript from compiled.gdcbytecode, and extracts C# source from.pck - ILSpy / dnSpy - .NET decompilers for inspecting the
.dllat the IL level - GodotSharp - the native interop bridge between the Godot and .NET runtimes