Factorio Blueprint Decoder
Factorio Blueprint Decoder
Project Overview
Decoding Factorio blueprint strings to understand:
- Base64 encoding/decoding
- Zlib decompression
- Stream-based I/O
- Working with dynamic buffers
- Navigating API changes in Zig 0.15.2
Code location: /home/wes/projects/game_of_zig/src/blueprint.zig
Blueprint String Format
0eNqrVkrKKU0tKMrMK1GyqlbKLEnNVbJSKi5JTM7My0xOVbIqKSpN1VEqSCwCcZQqihJzU4Gy...
^
|
Version byte (0 = version 0)
Rest is: Base64(Zlib(JSON))
The format:
- Version byte (
0) - Base64-encoded data
- Which contains zlib-compressed JSON
Implementation Steps
Step 1: Strip Version Byte
const blueprint_string = "0eJyl19tuoz...";
const encoded_data = blueprint_string[1..];
Slice syntax:
[1..]- from index 1 to end[0..3]- from 0 to 3 (exclusive)- Strings are just
[]const u8slices!
Step 2: Base64 Decoding
const base64 = std.base64.standard;
// Calculate required buffer size
const decoded_size = try base64.Decoder.calcSizeForSlice(encoded_data);
// Allocate buffer
const decoded = try allocator.alloc(u8, decoded_size);
defer allocator.free(decoded);
// Decode
try base64.Decoder.decode(decoded, encoded_data);
Key points:
calcSizeForSlicereturns!usize(error union), needstry- Must allocate exact size before decoding
deferensures cleanup
Verification:
std.debug.print("First few bytes: ", .{});
for (decoded[0..@min(20, decoded.len)]) |byte| {
std.debug.print("{x:0>2} ", .{byte});
}
Should see 78 9c ... (zlib header).
Step 3: Zlib Decompression (Detailed)
This is where it gets interesting!
Creating the Input Reader
var in_reader: std.Io.Reader = .fixed(decoded);
What’s happening:
std.Io.Readeris an interface type (vtable pattern).fixed(decoded)creates a reader that reads from a fixed buffer- Shorthand for
std.Io.Reader.fixed(decoded)
Memory layout of a Reader:
struct {
vtable: *const VTable, // Function pointers
buffer: []const u8, // Points to 'decoded'
pos: usize, // Current position
}
The Decompression Window Buffer
var window_buffer: [flate.max_window_len]u8 = undefined;
Why this is needed:
- Deflate uses a “sliding window” algorithm
- Looks back up to 32KB to find repeated patterns
- “The word ‘solar’ appeared 50 bytes ago” → copies it
max_window_len= 65536 bytes (32KB * 2)
Stack vs Heap:
var window: [65536]u8 = undefined; // Stack - fast, auto-cleanup
// vs
var window = try alloc(u8, 65536); // Heap - needs defer
defer free(window);
Stack allocation is faster and doesn’t need manual cleanup!
The undefined keyword:
- Means “don’t initialize, I’ll fill it”
- Optimization - skip zeroing 64KB
- Safe because decompressor will write before reading
Initializing the Decompressor
var decompressor = flate.Decompress.init(&in_reader, .zlib, &window_buffer);
Parameters breakdown:
-
&in_reader- Pointer to input reader- Decompressor calls
in_reader.read()for compressed bytes - Needs pointer because it modifies internal position
- Decompressor calls
-
.zlib- Container format (enum)- Could be
.raw,.gzip, or.zlib - Determines header/footer handling:
.raw = just deflate data .zlib = [2-byte header] + deflate + [4-byte checksum] .gzip = [10-byte header] + deflate + [8-byte footer]
- Could be
-
&window_buffer- Pointer to working memory- Stores recently decompressed data
- Needed for “back-references” in compressed stream
What gets returned:
struct Decompress {
input: *Reader, // Input compressed data
reader: Reader, // Output reader (what we use!)
container_metadata: ..., // Tracks zlib state
next_bits: usize, // Bit buffer
state: State, // State machine
// ... window buffer, etc.
}
The decompressor IS a reader that wraps another reader!
Creating the Output Buffer
var decompressed: std.ArrayList(u8) = .{};
defer decompressed.deinit(allocator);
Zig 0.15.2 ArrayList is “unmanaged”:
struct {
items: []u8 = &[_]u8{}, // Empty initially
capacity: usize = 0, // No allocation yet
}
Why ArrayList?
- Don’t know decompressed size (531 bytes → 3987 bytes!)
- Grows automatically as needed
- More efficient than repeated reallocation
The .{} syntax:
- Type is known from annotation:
std.ArrayList(u8) .{}means “use default values”- Expands to
.{ .items = &[_]u8{}, .capacity = 0 }
The Actual Decompression
try decompressor.reader.appendRemainingUnlimited(allocator, &decompressed);
What happens:
-
decompressor.reader- The output reader- Reading from it triggers decompression
- It’s wrapped around the input reader
-
.appendRemainingUnlimited(allocator, &decompressed)- Reads until EOF
- Appends all bytes to ArrayList
- “Unlimited” = no max size limit
Internal process (pseudocode):
loop {
// Ensure ArrayList has space
try list.ensureUnusedCapacity(allocator, CHUNK_SIZE);
// Read into ArrayList's buffer
const bytes_read = try decompressor.reader.read(
list.unusedCapacitySlice()
);
if (bytes_read == 0) break; // EOF
list.items.len += bytes_read;
}
The decompression state machine:
appendRemainingUnlimitedcallsdecompressor.reader.read()read()executes the deflate algorithm:- Reads compressed bits from
in_reader - Decodes Huffman codes
- Handles back-references using
window_buffer - Writes decompressed bytes
- Reads compressed bits from
- Bytes appended to
decompressed.items - Repeat until EOF
Accessing Results
std.debug.print("JSON: {s}\n", .{decompressed.items});
Why .items?
- ArrayList separates “actual data” from “allocated space”
items: []u8- the data slice (length = 3987)capacity: usize- allocated space (might be 4096)
API Changes in Zig 0.15.2
1. Compression Module Reorganization
// Old (pre-0.15):
const zlib = std.compress.zlib;
var decompressor = zlib.decompressor(reader);
// New (0.15.2):
const flate = std.compress.flate;
var decompressor = flate.Decompress.init(&reader, .zlib, &buffer);
Zlib is now part of the flate module with explicit container types.
2. ArrayList Now Unmanaged
// Old:
var list = std.ArrayList(u8).init(allocator);
defer list.deinit(); // Allocator stored in struct
// New:
var list: std.ArrayList(u8) = .{};
defer list.deinit(allocator); // Pass allocator to methods!
try list.append(allocator, item);
3. Reader Creation
// New simple way:
var reader: std.Io.Reader = .fixed(buffer);
// Instead of:
var fbs = std.io.fixedBufferStream(buffer);
var reader = fbs.reader();
4. Reading into ArrayList
// New method:
try reader.appendRemainingUnlimited(allocator, &list);
// Old method (deprecated):
try reader.readAllArrayList(&list, max_size);
Complete Decompression Flow
Blueprint String
↓
[Strip '0']
↓
Base64 String
↓ (base64.Decoder.decode)
Compressed Bytes (zlib format)
↓
std.Io.Reader.fixed(buffer)
↓
flate.Decompress.init(&reader, .zlib, &window)
↓
decompressor.reader (output reader)
↓ (appendRemainingUnlimited)
ArrayList growing dynamically
↓
decompressed.items = JSON!
Output
{
"blueprint": {
"icons": [{"signal": {"type": "item", "name": "solar-panel"}, "index": 1}],
"entities": [
{"entity_number": 1, "name": "solar-panel", "position": {"x": 237.5, "y": -450.5}},
{"entity_number": 2, "name": "solar-panel", "position": {"x": 240.5, "y": -450.5}},
...
],
"item": "blueprint",
"label": "solar tile",
"version": 281479276920832
}
}
Stats:
- Original: 709 characters
- Base64 decoded: 531 bytes
- Decompressed: 3987 bytes
- Compression ratio: ~7.5:1
Key Lessons
1. Reader/Writer Pattern
Composable I/O: buffer → reader → decompressor → reader → ArrayList
2. Stack Allocation for Large Buffers
If size is known at compile-time, stack is faster than heap.
3. Defer for Resource Management
RAII pattern without destructors - cleanup is guaranteed.
4. Unmanaged Collections
Pass allocator to methods rather than storing it - more explicit.
5. State Machines
The decompressor maintains complex internal state across read calls.
6. Type System Precision
*Reader vs *const Reader vs GenericReader - types tell you exactly what’s happening.
Running the Code
cd /home/wes/projects/game_of_zig
zig build run
Further Exploration
The next step would be parsing the JSON using std.json.parseFromSlice(), but the main learning objectives were accomplished:
- Understanding streams and readers
- Memory management patterns
- Navigating Zig’s evolving APIs
- Real-world data processing
This project demonstrates that Zig is suitable for everyday tasks (like Python), but with predictable performance and no runtime surprises.