// Configuration parser // Copyright (C) 2021 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 allocator = std.heap.c_allocator; 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 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 { width: c_int = 800, height: c_int = 600, vsync: bool = true, blur: bool = false, blood: bool = true, music: bool = true, mouse_sensitivity: f32 = 1.0, levels: extern struct { ptr: [*]Level, len: usize, } = .{ .ptr = undefined, .len = 0 }, debug: bool = false, }; /// 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, "blur")) config.blur = 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 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; }