pub const ConfigRegistry = struct { pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This(), "core.ConfigRegistry"); allocator: std.mem.Allocator = undefined, configMap: ?ConfigMap = null, pub fn create(self: *@This(), allocator: std.mem.Allocator, first: bool) !void { if (!first) return; self.* = .{ .allocator = allocator, }; } // by convention this should be in the root // content path // // with the name .toml pub fn loadConfigFile(self: *@This(), configName: []const u8) !void { const configFile = core.fs().loadFile(configName) catch { core.engine_log("unable to load configuration file {s}", .{configName}); return; }; defer core.fs().unmap(configFile); const parser = try Parser.create(self.allocator); parser.fileName = configName; defer parser.destroy(); self.configMap = try parser.parseBytes(self.allocator, configFile.bytesNoEnd()); core.engine_log("config file loaded {d} keys set", .{self.configMap.?.mapValues.count()}); // parse and tokenize the config file // self.tokenizeConfigFile(); } pub fn destroy(self: *@This()) void { if (self.configMap) |*map| { map.deinit(); } } }; pub fn configVar(comptime T: type, configName: []const u8, default: T) T { return getConfigVar(T, configName) orelse default; } pub const getConfigRegistry = core.EngineObject(ConfigRegistry).get; pub fn getConfigVar(comptime T: type, configName: []const u8) ?T { const ctx = getConfigRegistry(); if (ctx.configMap) |map| { if (map.mapValues.get(configName)) |v| { switch (v.value) { .number => |number| { switch (@typeInfo(T)) { .int => { return @intFromFloat(number); }, .float => { return @floatCast(number); }, else => { core.engine_err("tried to get config {s} but it isn't a number", .{configName}); return null; }, } }, .string => |string| { switch (@typeInfo(T)) { .pointer => |pointerInfo| { if (pointerInfo.child == u8) { return string; } }, else => { core.engine_err("tried to get config {s} but it isn't a string", .{configName}); return null; }, } }, } } } return null; } const lastSetBy_referenceInit: []const u8 = "reference"; const lastSetBy_configFile: []const u8 = "config file"; pub const ConfigValueEntry = struct { lastSetBy: []const u8 = lastSetBy_referenceInit, value: union(enum(u8)) { number: f64, string: []const u8, }, }; pub const ConfigMap = struct { backingAllocator: std.mem.Allocator, mapValues: std.StringHashMapUnmanaged(ConfigValueEntry) = .{}, arena: std.heap.ArenaAllocator, //stringList: std.ArrayListUnmanaged([]const u8) = .{}, pub fn init(allocator: std.mem.Allocator) !@This() { return .{ .backingAllocator = allocator, .arena = std.heap.ArenaAllocator.init(allocator), }; } pub fn pushMapValue(self: *@This(), key: []const u8, value: []const u8) !void { // take ownership of both strings const allocator = self.arena.allocator(); const k = try allocator.dupe(u8, key); var mapValue: ConfigValueEntry = .{ .value = undefined }; mapValue.lastSetBy = lastSetBy_configFile; if (value[0] == '"') { if (value[value.len - 1] != '"') { return error.MissingClosingQuote; } mapValue.value = .{ .string = try allocator.dupe(u8, value[1 .. value.len - 1]), }; } else if (std.fmt.parseFloat(f64, value) catch null) |flt| { mapValue.value = .{ .number = flt }; } else { return error.UnableToParse; } try self.mapValues.put(allocator, k, mapValue); } pub fn deinit(self: *@This()) void { self.arena.deinit(); } }; // A Very nonstandard and very dumb ini parser pub const Parser = struct { backingAllocator: std.mem.Allocator, arena: std.heap.ArenaAllocator, allocator: std.mem.Allocator = undefined, fileName: []const u8 = "unknown file", lineNumber: usize = 0, lineEnd: usize = 0, lineStart: usize = 0, bytes: []const u8 = undefined, outputMap: ?ConfigMap = null, configSection: ?[]const u8 = null, pub fn create(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .backingAllocator = allocator, .arena = std.heap.ArenaAllocator.init(allocator), }; self.allocator = self.arena.allocator(); return self; } pub fn nextLine(self: *@This()) ?[]const u8 { while (std.ascii.isWhitespace(self.bytes[self.lineEnd])) : (self.lineEnd += 1) { if (self.lineEnd + 1 >= self.bytes.len) { return null; } } self.lineStart = self.lineEnd; while (self.bytes[self.lineEnd] != '\r' and self.bytes[self.lineEnd] != '\n') : (self.lineEnd += 1) { if (self.lineEnd + 1 >= self.bytes.len) { break; } } self.lineNumber += 1; if (self.bytes[self.lineStart] == 0) { return null; } return self.bytes[self.lineStart..self.lineEnd]; } fn stripWhiteSpace(l: []const u8) []const u8 { if (l.len == 0) { return l; } var s: usize = 0; var e: usize = l.len; while (std.ascii.isWhitespace(l[s]) and s < l.len) : (s += 1) {} while (std.ascii.isWhitespace(l[e - 1]) and e - 1 > 0) : (e -= 1) {} return l[s..e]; } fn printError(self: *@This(), comptime fmt: []const u8, args: anytype) !void { if (core.getLogger()) |logger| { try logger.print(core.colors.Color.Red, fmt, args); try logger.print(core.colors.Color.Red, "[Config ]: Config File Parse Error> {s}:{d} \n", .{ self.fileName, self.lineNumber }); } } pub fn parseLabel(self: *@This(), l: []const u8) !?[]const u8 { if (l[l.len - 1] != ']') { core.engine_err("\n expected closing brace ] in label..\n fatal error\n{d}: {s} \n{s}:{d}", .{ self.lineNumber, l, self.fileName, self.lineNumber }); try self.printError("expected closing brace ] in label statement. fatal.", .{}); return error.UnableToParse; } var line = l[1 .. l.len - 1]; line = stripWhiteSpace(line); // core.engine_log("label : '{s}'", .{line}); return line; } pub fn parseStatement(self: *@This(), line: []const u8) !?struct { left: []const u8, right: []const u8 } { var s: usize = 0; while (s < line.len and line[s] != '=') : (s += 1) {} if (s == line.len) { try self.printError("missing right hand side of assignment after '=' {s} skipping..", .{line}); return null; } const left = stripWhiteSpace(line[0..s]); const right = stripWhiteSpace(line[s + 1 .. line.len]); // core.engine_log("statement : left '{s}' '{s}'", .{ left, right }); return .{ .left = left, .right = right }; } // line should already be whitespace stripped pub fn parseLine(self: *@This(), l: []const u8) !void { var i: usize = 0; var line = l; // strip comments while (i < line.len) : (i += 1) { if (line[i] == '#') { line = line[0..i]; break; } } i = 0; while (i < line.len) : (i += 1) { if (line[i] == '[') { const section = try self.parseLabel(line[i..line.len]); if (section) |sect| { self.configSection = sect; } break; } if (line[i] == '=') { if (try self.parseStatement(line)) |statement| { var fullVarName = statement.left; defer self.allocator.free(fullVarName); if (self.configSection) |section| { // use temporary allocator fullVarName = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ section, fullVarName }); } // takes ownership of both strings; try self.outputMap.?.pushMapValue(fullVarName, statement.right); } break; } } } pub fn parseBytes(self: *@This(), allocator: std.mem.Allocator, bytes: []const u8) !ConfigMap { self.outputMap = try ConfigMap.init(allocator); self.bytes = bytes; if (self.bytes.len == 0) { return self.outputMap.?; } self.lineStart = 0; self.lineEnd = 0; while (self.nextLine()) |line| { // core.engine_log("ConfigParser > {s}", .{line}); self.parseLine(line) catch break; } return self.outputMap.?; } pub fn destroy(self: *@This()) void { self.arena.deinit(); self.backingAllocator.destroy(self); } }; pub fn setupConfigs(configFilePath: []const u8) !void { _ = try core.createObject(ConfigRegistry, .{}); core.engine_log("loading configs {s}", .{configFilePath}); try getConfigRegistry().loadConfigFile(configFilePath); } pub fn ConfigEntry(comptime T: type) type { return struct { value: ?T = null, default: T, configName: []const u8, pub fn make(name: []const u8, default: T) void { return .{ .default = default, .name = name }; } }; } const core = @import("core.zig"); const std = @import("std");