// Configuration parser // Copyright (C) 2021-2022, 2025 Nguyễn Gia Phong // // This file is part of Black Shades. // // Black Shades is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Black Shades is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Black Shades. If not, see . const Dir = std.fs.Dir; const File = std.fs.File; const IntegerBitSet = std.bit_set.IntegerBitSet; 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; const parseFloat = std.fmt.parseFloat; const parseInt = std.fmt.parseInt; const std = @import("std"); const stringToEnum = std.meta.stringToEnum; const tokenizeScalar = std.mem.tokenizeScalar; const Key = @import("gfz").Key; const ini = @import("ini"); const data_dir = @import("build_options").data_dir; const c = @import("cimport.zig"); const parseBool = @import("misc.zig").parseBool; 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, }; 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 = IntegerBitSet(8).initEmpty(); var enums = tokenizeScalar(u8, kv.value, ' '); while (enums.next()) |weapon| weapons.set(try parseInt(u3, weapon, 10)); levels[i].evil_weapons = weapons.mask; } 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 { width: c_int = 800, height: c_int = 600, vsync: bool = true, blood: bool = true, music: bool = true, mouse_sensitivity: f32 = 1.0, key: extern struct { forwards: Key = .W, backwards: Key = .S, left: Key = .A, right: Key = .D, crouch: Key = .LEFT_CONTROL, accelerate: Key = .LEFT_SHIFT, dive: Key = .SPACE, reload: Key = .R, aim: Key = .E, psychic_aim: Key = .Q, psychic: Key = .Z, switch_view: Key = .TAB, switch_weapon: Key = .X, skip: Key = .K, pause: Key = .P, slomo: Key = .B, force: Key = .F, } = .{}, 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. pub fn parse(base_dir: []const u8) !Config { const config_dir = try join(allocator, &.{ base_dir, "blackshades" }); defer allocator.free(config_dir); var dir = try cwd().makeOpenPath(config_dir, .{}); defer dir.close(); 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| switch (record) { .section => |heading| { allocator.free(section); section = try allocator.dupe(u8, heading); }, .property => |kv| if (eql(u8, section, "graphics")) { if (eql(u8, kv.key, "width")) config.width = try parseInt(c_int, kv.value, 10) else if (eql(u8, kv.key, "height")) config.height = try parseInt(c_int, kv.value, 10) else if (eql(u8, kv.key, "vsync")) config.vsync = try parseBool(kv.value) else if (eql(u8, kv.key, "blood")) config.blood = try parseBool(kv.value) else return error.InvalidData; } else if (eql(u8, section, "audio")) { if (eql(u8, kv.key, "music")) config.music = try parseBool(kv.value) 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 if (eql(u8, kv.key, "forwards key")) config.key.forwards = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "backwards key")) config.key.backwards = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "left key")) config.key.left = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "right key")) config.key.right = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "crouch key")) config.key.crouch = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "accelerate key")) config.key.accelerate = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "dive key")) config.key.dive = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "reload key")) config.key.reload = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "aim key")) config.key.aim = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "psychic aim key")) config.key.psychic_aim = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "psychic key")) config.key.psychic = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "switch view key")) config.key.switch_view = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "switch weapon key")) config.key.switch_weapon = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "skip key")) config.key.skip = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "pause key")) config.key.pause = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "slomo key")) config.key.slomo = stringToEnum(Key, kv.value).? else if (eql(u8, kv.key, "force key")) config.key.force = stringToEnum(Key, kv.value).? else return error.InvalidData; } else if (eql(u8, section, "misc")) { if (eql(u8, kv.key, "custom levels")) config.levels.len = try parseInt(usize, kv.value, 10) else if (eql(u8, kv.key, "debug")) config.debug = try parseBool(kv.value) 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; }