summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--data/config.ini2
-rw-r--r--data/customlevels.txt41
-rw-r--r--data/levels.ini126
-rw-r--r--src/Game.h2
-rw-r--r--src/GameInitDispose.cpp243
-rw-r--r--src/config.h15
-rw-r--r--src/config.zig138
-rw-r--r--src/main.zig12
8 files changed, 287 insertions, 292 deletions
diff --git a/data/config.ini b/data/config.ini
index fdd3793..7a9e150 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -12,5 +12,5 @@ music = true
 mouse sensitivity = 1.0
 
 [misc]
-custom levels = false
+custom levels = 0
 debug = false
diff --git a/data/customlevels.txt b/data/customlevels.txt
deleted file mode 100644
index 4aa9b93..0000000
--- a/data/customlevels.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-Number of missions:
-2
-Level type: (assassin=0, zombie=1)
-0
-Environment: (sunny=0, foggy=1, snowy=2, rainy=3, red=4, night=5)
-3
-How many different guns can assassins have:
-2
-List them: (0=unarmed, 1=sniper rifle, 2=assaultrifle, 3=magnum, 4=handgun, 5=grenade, 6=knife, 7=shotgun
-1
-3
-One out of how many civilians is an assassin:
-4
-Starting gun: (see above)
-6
-How many magazines/grenades:
-5
-Time requirement:
-30
-Difficulty:(0 to 2, 1=normal)
-.6
-
-Level type: (assassin=0, zombie=1)
-1
-Environment: (sunny=0, foggy=1, snowy=2, rainy=3, red=4, night=5)
-4
-How many different guns can assassins have:
-2
-List them: (0=unarmed, 1=sniper rifle, 2=assaultrifle, 3=magnum, 4=handgun, 5=grenade, 6=knife, 7=shotgun
-1
-3
-One out of how many civilians is an assassin:
-4
-Starting gun: (see above)
-3
-How many magazines/grenades:
-10
-Time requirement:
-25
-Difficulty:(0 to 2, 1=normal)
-.8
diff --git a/data/levels.ini b/data/levels.ini
new file mode 100644
index 0000000..7eb6be1
--- /dev/null
+++ b/data/levels.ini
@@ -0,0 +1,126 @@
+; Weapons enums:
+; 0: none/zombie
+; 1: sniper rifle
+; 2: assault rifle
+; 3: magnum
+; 4: handgun
+; 5: grenade
+; 6: knife
+; 7: shotgun
+
+[level 0]
+environment = sunny
+evil weapons = 3 4 7
+evil rarity = 6
+guard weapon = 2
+guard reloads = 6
+time = 50
+difficulty = 0.6
+
+[level 1]
+environment = snowy
+evil weapons = 2 6
+evil rarity = 5
+guard weapon = 4
+guard reloads = 3
+time = 40
+difficulty = 0.6
+
+[level 2]
+environment = foggy
+evil weapons = 1
+evil rarity = 5
+guard weapon = 1
+guard reloads = 4
+time = 50
+difficulty = 0.9
+
+[level 3]
+environment = firey
+evil weapons = 0
+evil rarity = 5
+guard weapon = 7
+guard reloads = 5
+time = 35
+difficulty = 0.7
+
+[level 4]
+environment = snowy
+evil weapons = 1 2
+evil rarity = 5
+guard weapon = 5
+guard reloads = 20
+time = 30
+difficulty = 0.5
+
+[level 5]
+environment = rainy
+evil weapons = 2 3 7
+evil rarity = 6
+guard weapon = 6
+guard reloads = 3
+time = 40
+difficulty = 0.8
+
+[level 6]
+environment = night
+evil weapons = 3 4 7
+evil rarity = 5
+guard weapon = 3
+guard reloads = 4
+time = 30
+difficulty = 1.0
+
+[level 7]
+environment = firey
+evil weapons = 0
+evil rarity = 5
+guard weapon = 2
+guard reloads = 5
+time = 30
+difficulty = 1.0
+
+[level 8]
+environment = rainy
+evil weapons = 1 2 3 4 7
+evil rarity = 5
+guard weapon = 0
+guard reloads = 3
+time = 40
+difficulty = 0.8
+
+[level 9]
+environment = snowy
+evil weapons = 1 2 3 4 6 7
+evil rarity = 4
+guard weapon = 3
+guard reloads = 3
+time = 90
+difficulty = 1.0
+
+[level 10]
+environment = night
+evil weapons = 1
+evil rarity = 5
+guard weapon = 1
+guard reloads = 4
+time = 30
+difficulty = 1.3
+
+[level 11]
+environment = sunny
+evil weapons = 1 6
+evil rarity = 4
+guard weapon = 6
+guard reloads = 4
+time = 30
+difficulty = 1.5
+
+[level 12]
+environment = firey
+evil weapons = 0
+evil rarity = 4
+guard weapon = 4
+guard reloads = 10
+time = 60
+difficulty = 1.5
diff --git a/src/Game.h b/src/Game.h
index 39fdcbc..f6400d9 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -127,7 +127,6 @@ public:
 	int numpeople;
 	float spawndelay;
 
-	bool customlevels;
 	bool musictoggle;
 	float psychicpower;
 	int type;
@@ -151,6 +150,7 @@ public:
 	int score;
 	int mission;
 	int nummissions;
+	struct Level* levels;
 	int numpossibleguns;
 	int possiblegun[6];
 	int evilprobability;
diff --git a/src/GameInitDispose.cpp b/src/GameInitDispose.cpp
index bd8a05f..9700e25 100644
--- a/src/GameInitDispose.cpp
+++ b/src/GameInitDispose.cpp
@@ -27,7 +27,6 @@
 #include <AL/alc.h>
 #include <GL/glu.h>
 
-#include "config.h"
 #include "misc.h"
 #include "Game.h"
 #include "Support.h"
@@ -90,7 +89,8 @@ Game* makeGame(Config config)
 	psychicaimkey = GLFW_KEY_Q;
 	psychickey = GLFW_KEY_Z;
 
-	game->customlevels = config.custom_levels;
+	game->levels = config.levels.ptr;
+	game->nummissions = config.levels.len;
 	game->debug = config.debug;
 
 	// TODO: Read high score
@@ -240,228 +240,22 @@ void initGame(Game* game)
 
 	// Level setup
 	game->killedinnocent = 0; // haven't shot any civilians yet...
-
-	if (game->customlevels) {
-		game->nummissions = 1; // default level in case of load failure
-		game->type = randomshoot_type;
-		game->possiblegun[0] = handgun1;
-		game->possiblegun[1] = handgun2;
-		game->possiblegun[2] = shotgun;
-		game->numpossibleguns = 3;
-		game->evilprobability = 6;
-		bodyguard.whichgun = knife;
-		bodyguard.reloads[bodyguard.whichgun] = 6;
-		if (!game->gameinprogress)
-			game->score = 0;
-		game->timeremaining = 50;
-		game->difficulty= 0.8f;
-
-		std::ifstream ipstream {"data/customlevels.txt"};
-		if (ipstream) {
-			ipstream.ignore(256,'\n');//ignore descriptive text
-			ipstream >> game->nummissions;
-			for (int j = 0; j <= game->mission; ++j) {
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> game->type;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> environment;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> game->numpossibleguns;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-
-				for (int i = 0; i < game->numpossibleguns; ++i)
-					ipstream >> game->possiblegun[i];
-
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> game->evilprobability;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> bodyguard.whichgun;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> bodyguard.reloads[bodyguard.whichgun];
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> game->timeremaining;
-				ipstream.ignore(256,'\n');
-				ipstream.ignore(256,'\n');
-				ipstream >> game->difficulty;
-				ipstream.ignore(256,'\n');
-			}
-			ipstream.close();
-		} else {
-			game->customlevels = 0;
-		}
-	}
-
-	if (!game->customlevels) {
-		game->nummissions = 13;
-		if (!game->gameinprogress)
-			game->score = 0;
-
-		switch (game->mission) {
-		case 0:
-			environment = sunny_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = handgun1;
-			game->possiblegun[1] = handgun2;
-			game->possiblegun[2] = shotgun;
-			game->numpossibleguns = 3;
-			game->evilprobability = 6;
-			bodyguard.whichgun = assaultrifle;
-			bodyguard.reloads[bodyguard.whichgun] = 6;
-			game->timeremaining = 50;
-			game->difficulty = 0.6f;
-			break;
-		case 1:
-			environment = snowy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = knife;
-			game->possiblegun[1] = assaultrifle;
-			game->numpossibleguns = 2;
-			game->evilprobability = 5;
-			bodyguard.whichgun = handgun2;
-			bodyguard.reloads[bodyguard.whichgun] = 3;
-			game->timeremaining = 40;
-			game->difficulty = 0.6f;
-			break;
-		case 2:
-			environment = foggy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = sniperrifle;
-			game->numpossibleguns = 1;
-			game->evilprobability = 5;
-			bodyguard.whichgun = sniperrifle;
-			bodyguard.reloads[bodyguard.whichgun] = 4;
-			game->timeremaining = 50;
-			game->difficulty = 0.9f;
-			break;
-		case 3:
-			environment = firey_environment;
-			game->type = zombie_type;
-			game->numpossibleguns = 0;
-			game->evilprobability = 5;
-			bodyguard.whichgun = shotgun;
-			bodyguard.reloads[bodyguard.whichgun] = 5;
-			game->timeremaining = 35;
-			game->difficulty = 0.7f;
-			break;
-		case 4:
-			environment = snowy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = sniperrifle;
-			game->possiblegun[1] = assaultrifle;
-			game->numpossibleguns = 2;
-			game->evilprobability = 5;
-			bodyguard.whichgun = grenade;
-			bodyguard.reloads[bodyguard.whichgun] = 20;
-			game->timeremaining = 30;
-			game->difficulty = 0.5f;
-			break;
-		case 5:
-			environment = rainy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = handgun1;
-			game->possiblegun[1] = shotgun;
-			game->possiblegun[2] = assaultrifle;
-			game->numpossibleguns = 3;
-			game->evilprobability = 6;
-			bodyguard.whichgun = knife;
-			bodyguard.reloads[bodyguard.whichgun] = 3;
-			game->timeremaining = 40;
-			game->difficulty = 0.8f;
-			break;
-		case 6:
-			environment = night_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[1] = handgun1;
-			game->possiblegun[2] = handgun2;
-			game->possiblegun[3] = shotgun;
-			game->numpossibleguns = 3;
-			game->evilprobability = 5;
-			bodyguard.whichgun = handgun1;
-			bodyguard.reloads[bodyguard.whichgun] = 4;
-			game->timeremaining = 30;
-			game->difficulty = 1.0f;
-			break;
-		case 7:
-			environment = firey_environment;
-			game->type = zombie_type;
-			bodyguard.whichgun = assaultrifle;
-			bodyguard.reloads[bodyguard.whichgun] = 5;
-			game->timeremaining = 30;
-			game->difficulty = 1.0f;
-			break;
-		case 8:
-			environment = rainy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = handgun1;
-			game->possiblegun[1] = handgun2;
-			game->possiblegun[2] = shotgun;
-			game->possiblegun[3] = sniperrifle;
-			game->possiblegun[4] = assaultrifle;
-			game->numpossibleguns = 5;
-			game->evilprobability = 5;
-			bodyguard.whichgun = nogun;
-			bodyguard.reloads[bodyguard.whichgun] = 3;
-			game->timeremaining = 40;
-			game->difficulty = 0.8f;
-			break;
-		case 9:
-			environment = snowy_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = knife;
-			game->possiblegun[1] = handgun1;
-			game->possiblegun[2] = handgun2;
-			game->possiblegun[3] = shotgun;
-			game->possiblegun[4] = sniperrifle;
-			game->possiblegun[5] = assaultrifle;
-			game->numpossibleguns = 6;
-			game->evilprobability = 4;
-			bodyguard.whichgun = handgun1;
-			bodyguard.reloads[bodyguard.whichgun] = 3;
-			game->timeremaining = 90;
-			game->difficulty = 1.0f;
-			break;
-		case 10:
-			environment = night_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = sniperrifle;
-			game->numpossibleguns = 1;
-			game->evilprobability = 5;
-			bodyguard.whichgun = sniperrifle;
-			bodyguard.reloads[bodyguard.whichgun] = 4;
-			game->timeremaining = 30;
-			game->difficulty = 1.3f;
-			break;
-		case 11:
-			environment = sunny_environment;
-			game->type = randomshoot_type;
-			game->possiblegun[0] = knife;
-			game->possiblegun[1] = sniperrifle;
-			game->numpossibleguns = 2;
-			game->evilprobability = 4;
-			bodyguard.whichgun = knife;
-			game->timeremaining = 30;
-			game->difficulty = 1.5f;
-			break;
-		case 12:
-			environment = firey_environment;
-			game->type = zombie_type;
-			game->possiblegun[0] = knife;
-			game->possiblegun[1] = sniperrifle;
-			bodyguard.whichgun = handgun2;
-			bodyguard.reloads[bodyguard.whichgun] = 10;
-			game->timeremaining = 60;
-			game->difficulty = 1.5f;
-			break;
-		}
-	}
+	if (!game->gameinprogress)
+		game->score = 0;
+	auto& level = game->levels[game->mission];
+	environment = level.environment;
+	game->type = (level.evil_weapons & 1 << nogun)
+		? zombie_type : randomshoot_type;
+	game->numpossibleguns = 0;
+	for (auto weapon : { sniperrifle, assaultrifle, handgun1,
+	                     handgun2, shotgun, grenade, knife })
+		if (level.evil_weapons & 1 << weapon)
+			game->possiblegun[game->numpossibleguns++] = weapon;
+	game->evilprobability = level.evil_rarity;
+	bodyguard.whichgun = level.guard_weapon;
+	bodyguard.reloads[bodyguard.whichgun] = level.guard_reloads;
+	game->timeremaining = level.time;
+	game->difficulty = level.difficulty;
 
 	//Setup fast radian to degree conversion
 	rad2deg= 56.54866776;
@@ -509,7 +303,6 @@ void initGame(Game* game)
 		alSourcePlay(sound_source);
 		alSourcef(sound_source, AL_MIN_GAIN, 1);
 		alSourcef(sound_source, AL_MAX_GAIN, 1);
-
 	}
 
 	// Setup random seed
diff --git a/src/config.h b/src/config.h
index 693ca69..f6d7283 100644
--- a/src/config.h
+++ b/src/config.h
@@ -4,6 +4,16 @@
 #include <stdbool.h>
 #include <stdint.h>
 
+struct Level {
+	int environment;
+	unsigned char evil_weapons;
+	unsigned char evil_rarity;
+	unsigned char guard_weapon;
+	unsigned char guard_reloads;
+	int time;
+	float difficulty;
+};
+
 struct Config {
 	int width;
 	int height;
@@ -15,7 +25,10 @@ struct Config {
 
 	float mouse_sensitivity;
 
-	bool custom_levels;
+	struct {
+		struct Level *ptr;
+		size_t len;
+	} levels;
 	bool debug;
 };
 
diff --git a/src/config.zig b/src/config.zig
index dbe0f7d..4a1840d 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -16,19 +16,116 @@
 // You should have received a copy of the GNU General Public License
 // along with Black Shades.  If not, see <https://www.gnu.org/licenses/>.
 
+const Dir = std.fs.Dir;
+const File = std.fs.File;
 const allocator = std.heap.c_allocator;
-const createFile = std.fs.createFileAbsolute;
+const c = @import("main.zig").c;
 const cwd = std.fs.cwd;
 const data_dir = @import("build_options").data_dir;
 const eql = std.mem.eql;
 const ini = @import("ini");
 const join = std.fs.path.join;
+const maxInt = std.math.maxInt;
 const mkdir = std.os.mkdir;
-const openFile = std.fs.openFileAbsolute;
 const parseBool = @import("misc.zig").parseBool;
 const parseFloat = std.fmt.parseFloat;
 const parseInt = std.fmt.parseInt;
 const std = @import("std");
+const tokenize = std.mem.tokenize;
+
+const default_levels_len = 13;
+
+/// Open the given file for reading,
+/// on error copy it from the game's data and try again.
+fn openData(dir: Dir, filename: []const u8) !File {
+    return dir.openFile(filename, .{}) catch {
+        var data = try cwd().makeOpenPath(data_dir, .{});
+        defer data.close();
+        try data.copyFile(filename, dir, filename, .{});
+        return try dir.openFile(filename, .{});
+    };
+}
+
+const Level = extern struct {
+    environment: c_int,
+    evil_weapons: u8,
+    evil_rarity: u8,
+    guard_weapon: u8,
+    guard_reloads: u8,
+    time: c_int,
+    difficulty: f32,
+};
+
+const Weapons = packed struct {
+    // TODO: remove nogun and sort
+    jaw: bool = false,
+    sniper_rifle: bool = false,
+    assault_rifle: bool = false,
+    magnum: bool = false,
+    handgun: bool = false,
+    grenade: bool = false,
+    knife: bool = false,
+    shotgun: bool = false,
+};
+
+fn parseLevels(dir_path: []const u8, length: usize) ![*]Level {
+    var dir = try cwd().makeOpenPath(dir_path, .{});
+    defer dir.close();
+    const input = try openData(dir, "levels.ini");
+    defer input.close();
+    const levels = try allocator.alloc(Level, length);
+    errdefer allocator.free(levels);
+    var parser = ini.parse(allocator, input.reader());
+    defer parser.deinit();
+
+    var i: usize = maxInt(usize);
+    while (try parser.next()) |record|
+        switch (record) {
+            .section => i +%= 1,
+            .property => |kv| if (eql(u8, kv.key, "environment")) {
+                levels[i].environment = if (eql(u8, kv.value, "sunny"))
+                    c.sunny_environment
+                else if (eql(u8, kv.value, "foggy"))
+                    c.foggy_environment
+                else if (eql(u8, kv.value, "snowy"))
+                    c.snowy_environment
+                else if (eql(u8, kv.value, "rainy"))
+                    c.rainy_environment
+                else if (eql(u8, kv.value, "firey"))
+                    c.firey_environment
+                else if (eql(u8, kv.value, "night"))
+                    c.night_environment
+                else return error.InvalidData;
+            } else if (eql(u8, kv.key, "evil weapons")) {
+                var weapons = Weapons{};
+                var enums = tokenize(kv.value, " ");
+                while (enums.next()) |weapon|
+                    switch (try parseInt(u3, weapon, 10)) {
+                        c.nogun => weapons.jaw = true,
+                        c.sniperrifle => weapons.sniper_rifle = true,
+                        c.assaultrifle => weapons.assault_rifle = true,
+                        c.handgun1 => weapons.magnum = true,
+                        c.handgun2 => weapons.handgun = true,
+                        c.grenade => weapons.grenade = true,
+                        c.knife => weapons.knife = true,
+                        c.shotgun => weapons.shotgun = true,
+                    };
+                levels[i].evil_weapons = @bitCast(u8, weapons);
+            } else if (eql(u8, kv.key, "evil rarity")) {
+                levels[i].evil_rarity = try parseInt(u8, kv.value, 10);
+            } else if (eql(u8, kv.key, "guard weapon")) {
+                levels[i].guard_weapon = try parseInt(u3, kv.value, 10);
+            } else if (eql(u8, kv.key, "guard reloads")) {
+                levels[i].guard_reloads = try parseInt(u8, kv.value, 10);
+            } else if (eql(u8, kv.key, "time")) {
+                levels[i].time = try parseInt(c_int, kv.value, 10);
+            } else if (eql(u8, kv.key, "difficulty")) {
+                levels[i].difficulty = try parseFloat(f32, kv.value);
+            } else return error.InvalidData,
+            else => return error.InvalidData,
+        };
+    return levels.ptr;
+}
 
 /// Game configuration.
 pub const Config = extern struct {
@@ -42,7 +139,10 @@ pub const Config = extern struct {
 
     mouse_sensitivity: f32 = 1.0,
 
-    custom_levels: bool = false,
+    levels: extern struct {
+        ptr: [*]Level,
+        len: usize,
+    } = .{ .ptr = undefined, .len = 0 },
     debug: bool = false,
 };
 
@@ -52,19 +152,12 @@ pub fn parse(base_dir: []const u8) !Config {
     defer allocator.free(config_dir);
     var dir = try cwd().makeOpenPath(config_dir, .{});
     defer dir.close();
-
-    var config = Config{};
-    const input = dir.openFile("config.ini", .{}) catch blk: {
-        var source = try cwd().makeOpenPath(data_dir, .{});
-        defer source.close();
-        try source.copyFile("config.ini", dir, "config.ini", .{});
-        break :blk try dir.openFile("config.ini", .{});
-    };
+    const input = try openData(dir, "config.ini");
     defer input.close();
-
     var parser = ini.parse(allocator, input.reader());
     defer parser.deinit();
 
+    var config = Config{};
     var section: []u8 = "";
     defer allocator.free(section);
     while (try parser.next()) |record|
@@ -84,23 +177,30 @@ pub fn parse(base_dir: []const u8) !Config {
                     config.blur = try parseBool(kv.value)
                 else if (eql(u8, kv.key, "blood"))
                     config.blood = try parseBool(kv.value)
-                else unreachable;
+                else return error.InvalidData;
             } else if (eql(u8, section, "audio")) {
                 if (eql(u8, kv.key, "music"))
                     config.music = try parseBool(kv.value)
-                else unreachable;
+                else return error.InvalidData;
             } else if (eql(u8, section, "input")) {
                 if (eql(u8, kv.key, "mouse sensitivity"))
                     config.mouse_sensitivity = try parseFloat(f32, kv.value)
-                else unreachable;
+                else return error.InvalidData;
             } else if (eql(u8, section, "misc")) {
                 if (eql(u8, kv.key, "custom levels"))
-                    config.custom_levels = try parseBool(kv.value)
+                    config.levels.len = try parseInt(usize, kv.value, 10)
                 else if (eql(u8, kv.key, "debug"))
                     config.debug = try parseBool(kv.value)
-                else unreachable;
-            } else unreachable,
-            else => unreachable,
+                else return error.InvalidData;
+            } else return error.InvalidData,
+            else => return error.InvalidData,
         };
+
+    if (config.levels.len > 0) {
+        config.levels.ptr = try parseLevels(config_dir, config.levels.len);
+    } else {
+        config.levels.len = default_levels_len;
+        config.levels.ptr = try parseLevels(data_dir, config.levels.len);
+    }
     return config;
 }
diff --git a/src/main.zig b/src/main.zig
index d40f012..11775ec 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -16,17 +16,20 @@
 // You should have received a copy of the GNU General Public License
 // along with Black Shades.  If not, see <https://www.gnu.org/licenses/>.
 
-const Loca = @import("loca").Loca;
-const al = @import("zeal");
-const allocator = @import("std").heap.c_allocator;
-const c = @cImport({
+pub const c = @cImport({
     @cInclude("Game.h");
     @cInclude("Constants.h");
 });
+
+const Loca = @import("loca").Loca;
+const al = @import("zeal");
+const allocator = std.heap.c_allocator;
 const configuration = @import("config.zig");
+const free = std.c.free;
 const gf = @import("gfz");
 const gl = @import("zgl");
 const misc = @import("misc.zig");
+const std = @import("std");
 
 var game: *c.Game = undefined;
 
@@ -53,6 +56,7 @@ pub fn main() !void {
     const loca = try Loca.init(allocator, .{});
     defer loca.deinit();
     const config = try configuration.parse(loca.user_config);
+    defer free(config.levels.ptr);
 
     try gf.init();
     defer gf.deinit() catch unreachable;