about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.build.yml15
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules11
-rw-r--r--.reuse/dep5102
-rw-r--r--REUSE.toml101
-rw-r--r--build.zig74
-rw-r--r--build.zig.zon36
m---------lib/gfz0
m---------lib/ini0
m---------lib/loca0
m---------lib/qoi0
m---------lib/zeal0
-rw-r--r--src/Decals.zig4
-rw-r--r--src/Skeleton.cpp2
-rw-r--r--src/config.zig15
-rw-r--r--src/geom.zig14
-rw-r--r--src/main.zig22
-rw-r--r--src/misc.zig6
-rw-r--r--src/model.zig14
19 files changed, 213 insertions, 205 deletions
diff --git a/.build.yml b/.build.yml
deleted file mode 100644
index 7ef127b..0000000
--- a/.build.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-image: alpine/edge
-packages:
-  - glfw-dev
-  - glu-dev
-  - libsndfile-dev
-  - openal-soft-dev
-  - reuse
-  - zig
-sources:
-  - https://git.sr.ht/~cnx/blackshades
-tasks:
-  - build: |
-      cd blackshades
-      zig build -Doptimize=ReleaseSafe
-  - reuse: reuse --root blackshades lint
diff --git a/.gitignore b/.gitignore
index e73c965..3389c86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-zig-cache/
+.zig-cache/
 zig-out/
diff --git a/.gitmodules b/.gitmodules
index c564c4d..43b758c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,15 +1,6 @@
-[submodule "lib/zeal"]
-	path = lib/zeal
-	url = https://git.sr.ht/~cnx/zeal
-[submodule "lib/loca"]
-	path = lib/loca
-	url = https://git.sr.ht/~cnx/zig-loca
 [submodule "lib/ini"]
 	path = lib/ini
 	url = https://github.com/ziglibs/ini
-[submodule "lib/gfz"]
-	path = lib/gfz
-	url = https://git.sr.ht/~cnx/gfz
 [submodule "lib/qoi"]
 	path = lib/qoi
-	url = https://github.com/MasterQ32/zig-qoi
+	url = https://github.com/ikskuh/zig-qoi
diff --git a/.reuse/dep5 b/.reuse/dep5
deleted file mode 100644
index d51c471..0000000
--- a/.reuse/dep5
+++ /dev/null
@@ -1,102 +0,0 @@
-Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Source: https://sr.ht/~cnx/blackshades
-Upstream-Name: Black Shades
-Upstream-Contact: Nguyễn Gia Phong <cnx@loang.net>
-
-Files: *.cpp *.h *.zig
-Copyright: See individual files
-License: GPL-3.0-or-later
-
-Files:
- *.tsv
- data/models/blocks/*
- data/models/collide/*
- data/models/knife.off
- data/models/skeleton/*
- data/models/streets/*
- data/sounds/Lose.ogg
- data/sounds/bounce.ogg
- data/sounds/bounce2.ogg
- data/sounds/disguise-kill.ogg
- data/sounds/explosion.ogg
- data/sounds/footstep/*
- data/sounds/handlerelease.ogg
- data/sounds/headland.ogg
- data/sounds/headshot.ogg
- data/sounds/riflewhack.ogg
- data/sounds/soul-in.ogg
- data/sounds/soul-out.ogg
- data/sounds/underwater.ogg
- data/textures/blood/*
- data/textures/flare.qoi
- data/textures/font.qoi
- data/textures/scope.qoi
- data/textures/sprites/blood.qoi
- data/textures/sprites/flash-hit.qoi
- data/textures/sprites/flash-muzzle.qoi
- data/textures/sprites/person-dead.qoi
- data/textures/sprites/person.qoi
- data/textures/sprites/smoke.qoi
-Copyright: David Rosen
-License: GPL-3.0-or-later
-
-Files: data/levels.ini
-Copyright:
- David Rosen
- Nguyễn Gia Phong
-License: GPL-3.0-or-later
-
-Files:
- data/models/grenade/*
- data/models/guns/*
-Copyright: David Drew
-License: GPL-3.0-or-later
-
-Files:
- data/sounds/gun/*
- data/sounds/impact/body-fall.wav
- data/sounds/impact/body-hit.wav
- data/sounds/impact/knife-stab.wav
- data/sounds/impact/munch.wav
- data/sounds/music/menu.opus
- data/sounds/rain.ogg
- data/textures/black.qoi
- data/textures/bullet-hole.qoi
- data/textures/sprites/snowflake.qoi
- data/textures/sprites/white.qoi
-Copyright: N/A
-License: CC0-1.0
-
-Files: data/sounds/grenade/*
-Copyright: CGEffex
-License: CC-BY-3.0
-
-Files: data/sounds/gun/near-bullet.wav
-Copyright: _def
-License: CC-BY-3.0
-
-Files: data/sounds/music/assassin.opus
-Copyright: remaxim
-License: CC-BY-4.0
-
-Files: data/sounds/music/zombie.opus
-Copyright: remaxim
-License: CC-BY-SA-3.0
-
-Files: data/sounds/impact/wall-hit.wav
-Copyright: toxicwafflezz
-License: CC-BY-3.0
-
-Files: screenshot.png
-Copyright: Wolfire Games
-License: GPL-3.0-or-later
-
-Files:
- .build.yml
- .gitignore
- .gitmodules
- CHANGES
- README.md
- data/config.ini
-Copyright: N/A
-License: CC0-1.0
diff --git a/REUSE.toml b/REUSE.toml
new file mode 100644
index 0000000..fd19dcd
--- /dev/null
+++ b/REUSE.toml
@@ -0,0 +1,101 @@
+version = 1
+
+[[annotations]]
+path = ["**.cpp", "**.h", "**.zig"]
+SPDX-FileCopyrightText = "See individual files"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = [ "data/models/grenade/*.off", "data/models/guns/*.off" ]
+SPDX-FileCopyrightText = "David Drew"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = [ "**.tsv", "**.off",
+         "data/sounds/Lose.ogg",
+         "data/sounds/bounce.ogg",
+         "data/sounds/bounce2.ogg",
+         "data/sounds/disguise-kill.ogg",
+         "data/sounds/explosion.ogg",
+         "data/sounds/footstep/*.ogg",
+         "data/sounds/handlerelease.ogg",
+         "data/sounds/headland.ogg",
+         "data/sounds/headshot.ogg",
+         "data/sounds/riflewhack.ogg",
+         "data/sounds/soul-in.ogg",
+         "data/sounds/soul-out.ogg",
+         "data/sounds/underwater.ogg",
+         "data/textures/blood/*.qoi",
+         "data/textures/flare.qoi",
+         "data/textures/font.qoi",
+         "data/textures/scope.qoi",
+         "data/textures/sprites/blood.qoi",
+         "data/textures/sprites/flash-hit.qoi",
+         "data/textures/sprites/flash-muzzle.qoi",
+         "data/textures/sprites/person-dead.qoi",
+         "data/textures/sprites/person.qoi",
+         "data/textures/sprites/smoke.qoi" ]
+SPDX-FileCopyrightText = "David Rosen"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = "data/levels.ini"
+precedence = "aggregate"
+SPDX-FileCopyrightText = [ "David Rosen", "Nguyễn Gia Phong" ]
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = [ "data/sounds/gun/**.wav",
+         "data/sounds/impact/body-fall.wav",
+         "data/sounds/impact/body-hit.wav",
+         "data/sounds/impact/knife-stab.wav",
+         "data/sounds/impact/munch.wav",
+         "data/sounds/music/menu.opus",
+         "data/sounds/rain.ogg",
+         "data/textures/black.qoi",
+         "data/textures/bullet-hole.qoi",
+         "data/textures/sprites/snowflake.qoi",
+         "data/textures/sprites/white.qoi"]
+SPDX-FileCopyrightText = "None"
+SPDX-License-Identifier = "CC0-1.0"
+
+[[annotations]]
+path = "data/sounds/grenade/**"
+SPDX-FileCopyrightText = "CGEffex"
+SPDX-License-Identifier = "CC-BY-3.0"
+
+[[annotations]]
+path = "data/sounds/gun/near-bullet.wav"
+SPDX-FileCopyrightText = "_def"
+SPDX-License-Identifier = "CC-BY-3.0"
+
+[[annotations]]
+path = "data/sounds/music/assassin.opus"
+SPDX-FileCopyrightText = "remaxim"
+SPDX-License-Identifier = "CC-BY-4.0"
+
+[[annotations]]
+path = "data/sounds/music/zombie.opus"
+SPDX-FileCopyrightText = "remaxim"
+SPDX-License-Identifier = "CC-BY-SA-3.0"
+
+[[annotations]]
+path = "data/sounds/impact/wall-hit.wav"
+SPDX-FileCopyrightText = "toxicwafflezz"
+SPDX-License-Identifier = "CC-BY-3.0"
+
+[[annotations]]
+path = "screenshot.png"
+SPDX-FileCopyrightText = "Wolfire Games"
+SPDX-License-Identifier = "GPL-3.0-or-later"
+
+[[annotations]]
+path = [ ".gitignore",
+         ".gitmodules",
+         "CHANGES",
+         "README.md",
+         "REUSE.toml",
+         "build.zig.zon",
+         "data/config.ini" ]
+SPDX-FileCopyrightText = "None"
+SPDX-License-Identifier = "CC0-1.0"
diff --git a/build.zig b/build.zig
index 8c5c4c8..97957bd 100644
--- a/build.zig
+++ b/build.zig
@@ -1,5 +1,5 @@
 // Build recipe
-// Copyright (C) 2021-2023  Nguyễn Gia Phong
+// Copyright (C) 2021-2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -18,61 +18,51 @@
 
 const Build = @import("std").Build;
 const Compile = Build.Step.Compile;
-const InstallDirectoryOptions = Build.InstallDirectoryOptions;
-
-const data = InstallDirectoryOptions{
-    .source_dir = .{ .path = "data" },
-    .install_dir = .{ .custom = "share" },
-    .install_subdir = "blackshades",
-};
 
 pub fn build(b: *Build) void {
-    const bin = b.addExecutable(.{
-        .name = "blackshades",
-        .root_source_file = .{ .path = "src/main.zig" },
+    const mod = b.createModule(.{
+        .root_source_file = b.path("src/main.zig"),
         .target = b.standardTargetOptions(.{}),
         .optimize = b.standardOptimizeOption(.{}),
     });
-    bin.addIncludePath(.{ .path = "src" });
-    bin.addCSourceFiles(&.{
-        "src/GameDraw.cpp",
-        "src/GameInitDispose.cpp",
-        "src/GameLoop.cpp",
-        "src/GameTick.cpp",
-        "src/Globals.cpp",
-        "src/Person.cpp",
-        "src/Skeleton.cpp",
-        "src/Sprites.cpp",
-    }, &.{ "--std=c++17", "-Wall", "-Werror", "-fno-sanitize=undefined" });
-
-    for ([_]struct { []const u8, []const u8 }{
-        .{ "gfz", "lib/gfz/src/gfz.zig" },
-        .{ "ini", "lib/ini/src/ini.zig" },
-        .{ "loca", "lib/loca/src/main.zig" },
-        .{ "qoi", "lib/qoi/src/qoi.zig" },
-        .{ "zeal", "lib/zeal/src/zeal.zig" },
-    }) |lib|
-        bin.addModule(lib[0], b.createModule(.{
-            .source_file = .{ .path = lib[1] },
-        }));
-    @import("lib/gfz/build.zig").link(bin);
-    @import("lib/zeal/build.zig").link(bin);
-    bin.linkSystemLibrary("GL");
-    bin.linkSystemLibrary("GLU");
-    bin.linkSystemLibrary("c++");
+    mod.addIncludePath(b.path("src"));
+    mod.addCSourceFiles(.{
+        .files = &.{
+            "src/GameDraw.cpp",
+            "src/GameInitDispose.cpp",
+            "src/GameLoop.cpp",
+            "src/GameTick.cpp",
+            "src/Globals.cpp",
+            "src/Person.cpp",
+            "src/Skeleton.cpp",
+            "src/Sprites.cpp",
+        },
+        .flags = &.{ "--std=c++17", "-Wall", "-Werror" },
+    });
+    mod.linkSystemLibrary("GL", .{});
+    mod.linkSystemLibrary("GLU", .{});
+    mod.linkSystemLibrary("c++", .{});
 
+    inline for (.{ "gfz", "ini", "known-folders", "qoi", "zeal" }) |lib|
+        mod.addImport(lib, b.dependency(lib, .{}).module(lib));
+    const data = Build.Step.InstallDir.Options{
+        .source_dir = b.path("data"),
+        .install_dir = .{ .custom = "share" },
+        .install_subdir = "blackshades",
+    };
     b.installDirectory(data);
     const options = b.addOptions();
     const data_dir = b.getInstallPath(data.install_dir, data.install_subdir);
     options.addOption([]const u8, "data_dir", data_dir);
-    bin.addOptions("build_options", options);
-    b.installArtifact(bin);
+    mod.addOptions("build_options", options);
 
+    const bin = b.addExecutable(.{ .name = "blackshades", .root_module = mod });
+    b.installArtifact(bin);
     const run_cmd = b.addRunArtifact(bin);
-    run_cmd.step.dependOn(b.getInstallStep());
+    //run_cmd.step.dependOn(b.getInstallStep());
     if (b.args) |args|
         run_cmd.addArgs(args);
 
-    const run_step = b.step("run", "Run the app");
+    const run_step = b.step("run", "Run the game");
     run_step.dependOn(&run_cmd.step);
 }
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..e5e0969
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,36 @@
+.{
+    .name = .blackshades,
+    .version = "2.5.3",
+    .fingerprint = 0xd1f5f56cdf18153,
+    .minimum_zig_version = "0.14.0",
+    .dependencies = .{
+        .gfz = .{
+            .url = "git+https://trong.loang.net/~cnx/gfz?ref=0.1.0#601c886f4b40d6a513563b5a883010633e52bf43",
+            .hash = "gfz-0.1.0-3UBgOH-DAAD1E3l0QB8DihsttSChM4VWHeU1QKeG6btY",
+        },
+        .ini = .{
+            .url = "lib/ini",
+            .hash = "ini-0.1.0-AAAAAJ4lAAAeuOSEbouG5uNtHCmngXktnF3PTsfjxvDq",
+        },
+        .@"known-folders" = .{
+            .url = "git+https://github.com/ziglibs/known-folders#aa24df42183ad415d10bc0a33e6238c437fc0f59",
+            .hash = "known_folders-0.0.0-Fy-PJtLDAADGDOwYwMkVydMSTp_aN-nfjCZw6qPQ2ECL",
+        },
+        .qoi = .{
+            .url = "lib/qoi",
+            .hash = "qoi-0.1.0-5Hmo8rpqAAD1pnE4OYX2tdvxLupff9nkjBGxuxVNtky8",
+        },
+        .zeal = .{
+            .url = "git+https://trong.loang.net/~cnx/zeal?ref=0.1.0#7580c25a081e39732afaaa7a630d7f58005d5ab1",
+            .hash = "zeal-0.1.0-90hghYyhAAAKkKkGWzVjxtxlZiwyRD2YNd3xDZRqrC9a",
+        },
+    },
+    .paths = .{
+        "LICENSES",
+        "README.md",
+        "build.zig",
+        "build.zig.zon",
+        "data",
+        "src",
+    },
+}
diff --git a/lib/gfz b/lib/gfz
deleted file mode 160000
-Subproject d30ae1d4a46ad4b1f7b3c45c59dee268ab95c6b
diff --git a/lib/ini b/lib/ini
-Subproject 2b11e8fef86d0eefb225156e695be1c1d5c35cb
+Subproject 684113312f9d8a70fa6cf928277a77be3316fc7
diff --git a/lib/loca b/lib/loca
deleted file mode 160000
-Subproject aac1315920bb9f2c1e3492825c69f764d4a8525
diff --git a/lib/qoi b/lib/qoi
-Subproject 524efec9e9522f8e2b72e9d67b36d5d5e69a767
+Subproject 63caa52c9d590ef76405922f419540e47bf393d
diff --git a/lib/zeal b/lib/zeal
deleted file mode 160000
-Subproject eb65c5633e1bfae26ae95c6183cc74c2b593794
diff --git a/src/Decals.zig b/src/Decals.zig
index ecbc71c..b81b030 100644
--- a/src/Decals.zig
+++ b/src/Decals.zig
@@ -1,7 +1,7 @@
 // Decal construction and drawing
 // Copyright (C) 2002  David Rosen
 // Copyright (C) 2003  Steven Fuller
-// Copyright (C) 2023  Nguyễn Gia Phong
+// Copyright (C) 2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -83,7 +83,7 @@ export fn addDecal(d: *Decals, kind: Kind, location: XYZ,
                    size: f32, normal: XYZ, poly: c_int,
                    m: *const Model, move: XYZ, rot: f32) void {
     const n: @Vector(3, f32) = @bitCast(normal);
-    const abs_n = @fabs(n);
+    const abs_n = @abs(n);
     var major: u2 = 0;
     if (abs_n[1] > abs_n[major])
         major = 1;
diff --git a/src/Skeleton.cpp b/src/Skeleton.cpp
index 8692213..b7f1684 100644
--- a/src/Skeleton.cpp
+++ b/src/Skeleton.cpp
@@ -450,7 +450,7 @@ void Skeleton::reload()
 	lowforwardjoints[2] = lefthip;
 
 	num_muscles = 29;
-	MuscleData muscles_data[num_muscles];
+	MuscleData muscles_data[29];
 	loadMuscles(muscles_data);
 	for (int i = 0; i < num_muscles; ++i) {
 		muscles[i].length = muscles_data[i].length;
diff --git a/src/config.zig b/src/config.zig
index 0d18223..723e95c 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -1,5 +1,5 @@
 // Configuration parser
-// Copyright (C) 2021-2022  Nguyễn Gia Phong
+// Copyright (C) 2021-2022, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -23,6 +23,7 @@ const allocator = std.heap.c_allocator;
 const cwd = std.fs.cwd;
 const endian = @import("builtin").target.cpu.arch.endian();
 const eql = std.mem.eql;
+const free = std.c.free;
 const join = std.fs.path.join;
 const maxInt = std.math.maxInt;
 const mkdir = std.os.mkdir;
@@ -30,7 +31,7 @@ const parseFloat = std.fmt.parseFloat;
 const parseInt = std.fmt.parseInt;
 const std = @import("std");
 const stringToEnum = std.meta.stringToEnum;
-const tokenize = std.mem.tokenize;
+const tokenizeScalar = std.mem.tokenizeScalar;
 
 const Key = @import("gfz").Key;
 const ini = @import("ini");
@@ -69,7 +70,7 @@ fn parseLevels(dir_path: []const u8, length: usize) ![*]Level {
     defer input.close();
     const levels = try allocator.alloc(Level, length);
     errdefer allocator.free(levels);
-    var parser = ini.parse(allocator, input.reader());
+    var parser = ini.parse(allocator, input.reader(), "#;");
     defer parser.deinit();
 
     var i: usize = maxInt(usize);
@@ -92,7 +93,7 @@ fn parseLevels(dir_path: []const u8, length: usize) ![*]Level {
                 else return error.InvalidData;
             } else if (eql(u8, kv.key, "evil weapons")) {
                 var weapons = IntegerBitSet(8).initEmpty();
-                var enums = tokenize(u8, kv.value, " ");
+                var enums = tokenizeScalar(u8, kv.value, ' ');
                 while (enums.next()) |weapon|
                     weapons.set(try parseInt(u3, weapon, 10));
                 levels[i].evil_weapons = weapons.mask;
@@ -145,6 +146,10 @@ pub const Config = extern struct {
 
     levels: extern struct { ptr: [*]Level = undefined, len: usize = 0 } = .{},
     debug: bool = false,
+
+    pub fn deinit(self: Config) void {
+        defer free(self.levels.ptr);
+    }
 };
 
 /// Parse config.ini in the given base directory.
@@ -155,7 +160,7 @@ pub fn parse(base_dir: []const u8) !Config {
     defer dir.close();
     const input = try openData(dir, "config.ini");
     defer input.close();
-    var parser = ini.parse(allocator, input.reader());
+    var parser = ini.parse(allocator, input.reader(), "#;");
     defer parser.deinit();
 
     var config = Config{};
diff --git a/src/geom.zig b/src/geom.zig
index d86c682..4bb92e1 100644
--- a/src/geom.zig
+++ b/src/geom.zig
@@ -1,6 +1,6 @@
 // Geometry functions
 // Copyright (C) 2002  David Rosen
-// Copyright (C) 2023  Nguyễn Gia Phong
+// Copyright (C) 2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -81,9 +81,9 @@ pub fn rotate2d(i: *f32, j: *f32, a: f32) void {
 export fn rotate(v: XYZ, deg_x: f32, deg_y: f32, deg_z: f32) XYZ {
     var u = v;
     // TODO: optimize
-    rotate2d(&u.x, &u.y, degreesToRadians(f32, deg_z));
-    rotate2d(&u.z, &u.x, degreesToRadians(f32, deg_y));
-    rotate2d(&u.y, &u.z, degreesToRadians(f32, deg_x));
+    rotate2d(&u.x, &u.y, degreesToRadians(deg_z));
+    rotate2d(&u.z, &u.x, degreesToRadians(deg_y));
+    rotate2d(&u.y, &u.z, degreesToRadians(deg_x));
     return u;
 }
 
@@ -106,7 +106,7 @@ pub export fn segCrossTrigon(start: XYZ, end: XYZ,
     const q: @Vector(3, f32) = @bitCast(end);
     const n: @Vector(3, f32) = @bitCast(normal.*);
     const denom = dot(q - p, n);
-    if (@fabs(denom) < floatEps(f32))
+    if (@abs(denom) < floatEps(f32))
         return false; // parallel segment and triangle
 
     const a: @Vector(3, f32) = @bitCast(p_a.*);
@@ -116,7 +116,7 @@ pub export fn segCrossTrigon(start: XYZ, end: XYZ,
         return false; // intersection not within segment
 
     // Check if intersection is in the triangle
-    const n_abs = @fabs(n);
+    const n_abs = @abs(n);
     const n_max = @reduce(.Max, n_abs);
     const k: struct { usize, usize } = if (n_max == n_abs[0])
         .{ 1, 2 }
@@ -132,7 +132,7 @@ pub export fn segCrossTrigon(start: XYZ, end: XYZ,
     const v = @Vector(3, f32){ i[k[1]], b[k[1]], c[k[1]] } - splat(3, a[k[1]]);
     intersection.* = @bitCast(i);
 
-    if (@fabs(u[1]) < floatEps(f32)) {
+    if (@abs(u[1]) < floatEps(f32)) {
         const s = u[0] / u[2];
         if (s >= 0 and s <= 1) {
             const t = (v[0] - s * v[2]) / v[1];
diff --git a/src/main.zig b/src/main.zig
index 9accfc8..08c84ab 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,5 +1,5 @@
 // Main function
-// Copyright (C) 2021-2023  Nguyễn Gia Phong
+// Copyright (C) 2021-2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -16,13 +16,12 @@
 // 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 DefaultPrng = std.rand.DefaultPrng;
+const DefaultPrng = std.Random.DefaultPrng;
 const allocator = std.heap.c_allocator;
-const free = std.c.free;
 const std = @import("std");
 
-const Loca = @import("loca").Loca;
 const al = @import("zeal");
+const folders = @import("known-folders");
 const gf = @import("gfz");
 const gl = @import("zgl");
 
@@ -84,10 +83,10 @@ fn click(window: gf.Window, button: gf.MouseButton,
 }
 
 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);
+    const user_config = (try folders.getPath(allocator, .local_configuration)).?;
+    defer allocator.free(user_config);
+    const config = try configuration.parse(user_config);
+    defer config.deinit();
 
     try gf.init();
     defer gf.deinit() catch unreachable;
@@ -100,10 +99,11 @@ pub fn main() !void {
     defer textures.deinit();
     var decals = Decals.init(allocator, &textures);
     defer decals.deinit();
+    const user_data = (try folders.getPath(allocator, .data)).?;
+    defer allocator.free(user_data);
     game = c.makeGame(&decals, @bitCast(config),
-                      @bitCast(try loadScores(loca.user_data))).?;
-    defer saveScores(loca.user_data, @bitCast(c.getScores(game)))
-        catch unreachable;
+                      @bitCast(try loadScores(user_data))).?;
+    defer saveScores(user_data, @bitCast(c.getScores(game))) catch unreachable;
 
     try window.setSizeCallback(resizeWindow);
     try gf.swapInterval(@intFromBool(config.vsync));
diff --git a/src/misc.zig b/src/misc.zig
index 46237cb..3bed694 100644
--- a/src/misc.zig
+++ b/src/misc.zig
@@ -1,6 +1,6 @@
 // Miscellaneous functions
 // Copyright (C) 2002  David Rosen
-// Copyright (C) 2021-2023  Nguyễn Gia Phong
+// Copyright (C) 2021-2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -193,7 +193,7 @@ pub fn loadScores(base_dir: []const u8) !Scores {
     const path = "blackshades" ++ [_]u8{ sep } ++ "scores.ini";
     const input = dir.openFile(path, .{}) catch return scores;
     defer input.close();
-    var parser = ini.parse(allocator, input.reader());
+    var parser = ini.parse(allocator, input.reader(), "#;");
     defer parser.deinit();
 
     while (try parser.next()) |record|
@@ -241,7 +241,7 @@ pub fn saveScores(base_dir: []const u8, current: Scores) !void {
         previous.completed or current.completed,
     });
     defer allocator.free(data);
-    try dir.writeFile("scores.ini", data);
+    try dir.writeFile(.{ .sub_path = "scores.ini", .data = data });
 }
 
 const Text = struct {
diff --git a/src/model.zig b/src/model.zig
index 9773027..3acfe9d 100644
--- a/src/model.zig
+++ b/src/model.zig
@@ -1,6 +1,6 @@
 // 3D model
 // Copyright (C) 2002  David Rosen
-// Copyright (C) 2023  Nguyễn Gia Phong
+// Copyright (C) 2023, 2025  Nguyễn Gia Phong
 //
 // This file is part of Black Shades.
 //
@@ -25,7 +25,7 @@ const c = @import("cimport.zig");
 const crossProduct = geom.crossProduct;
 const cwd = std.fs.cwd;
 const data_dir = misc.data_dir;
-const degToRad = std.math.degreesToRadians;
+const degreesToRadians = std.math.degreesToRadians;
 const endsWith = std.mem.endsWith;
 const eql = std.mem.eql;
 const floatMax = std.math.floatMax;
@@ -63,7 +63,9 @@ const OffIterator = struct {
     token_iterator: TokenIterator,
 
     pub fn init(buffer: []const u8) OffIterator {
-        var self = .{ .token_iterator = tokenizeScalar(u8, buffer, '\n') };
+        var self = OffIterator{
+            .token_iterator = tokenizeScalar(u8, buffer, '\n'),
+        };
         if (!endsWith(u8, self.token_iterator.next().?, "OFF"))
             self.token_iterator.reset();
         return self;
@@ -198,12 +200,12 @@ pub export fn segCrossModelTrans(start: XYZ, end: XYZ, m: *const Model,
                                  intersection: *XYZ) c_int {
     const t: @Vector(3, f32) = @bitCast(move);
     var p = @as(@Vector(3, f32), @bitCast(start)) - t;
-    rotate2d(&p[2], &p[0], degToRad(f32, -rot));
+    rotate2d(&p[2], &p[0], degreesToRadians(-rot));
     var q = @as(@Vector(3, f32), @bitCast(end)) - t;
-    rotate2d(&q[2], &q[0], degToRad(f32, -rot));
+    rotate2d(&q[2], &q[0], degreesToRadians(-rot));
 
     defer {
-        rotate2d(&intersection.z, &intersection.x, degToRad(f32, rot));
+        rotate2d(&intersection.z, &intersection.x, degreesToRadians(rot));
         intersection.* = @bitCast(@as(@Vector(3, f32),
                                       @bitCast(intersection.*)) + t);
     }