feat(LuaParser): add custom bind and custom label parsing

Adds support for custom_commands and custom_labels tables in the lua
config file.

Fixes memory leak.
Fixes wrong use of lateConfigHandler.
This commit is contained in:
Titanium Brain 2026-06-18 19:20:52 +01:00
commit 9783da93d1
2 changed files with 130 additions and 6 deletions

View file

@ -8,6 +8,7 @@ pub const interop = @import("interop.zig");
pub const UidRange = @import("UidRange.zig");
pub const LogFile = @import("LogFile.zig");
pub const SharedError = @import("SharedError.zig");
pub const custom = @import("custom.zig");
pub fn Parser(comptime T: type) type {
return union(enum) {
@ -120,6 +121,7 @@ pub fn LuaParser(comptime Struct: type) type {
structure: Struct,
errors: std.ArrayList(Error),
maybe_load_error: ?anyerror,
allocator: std.mem.Allocator,
pub fn init(
allocator: std.mem.Allocator,
@ -142,6 +144,10 @@ pub fn LuaParser(comptime Struct: type) type {
var data: Struct = .{};
switch (@typeInfo(Struct)) {
.@"struct" => |struc| {
const ly_type = lua.getGlobal("ly");
defer lua.pop(1); // pop ly table
if (ly_type == .nil) return error.MissingLyTable;
inline for (struc.fields) |field| {
setField(allocator, lua, field, &data) catch {};
}
@ -151,11 +157,19 @@ pub fn LuaParser(comptime Struct: type) type {
if (global_errors.items.len != 0) {
maybe_load_error = error.InvalidConfig;
}
return .{ .structure = data, .errors = global_errors, .maybe_load_error = maybe_load_error };
// Parse custom binds and labels
try parseCustom(lua);
return .{
.structure = data,
.errors = global_errors,
.maybe_load_error = maybe_load_error,
.allocator = allocator,
};
}
pub fn setField(allocator: std.mem.Allocator, lua: *Lua, comptime field: std.builtin.Type.StructField, data: *Struct) !void {
_ = lua.getGlobal("ly");
defer lua.pop(1); // pop ly table
const type_info = @typeInfo(field.type);
const actual_type, const is_optional = blk: {
if (type_info == .optional) {
@ -240,8 +254,119 @@ pub fn LuaParser(comptime Struct: type) type {
} else unreachable;
}
pub fn parseCustom(lua: *Lua) !void {
_ = lua.getGlobal("ly");
defer lua.pop(1); // pop ly table
if (!lua.isTable(-1)) return error.MissingLyTable;
_ = lua.getField(-1, "custom_commands");
// custom_commands can be omitted or empty, so we just skip instead of erroring
binds: {
if (lua.isTable(-1)) {
const len = @as(usize, @intCast(lua.lenRaiseErr(-1)));
if (len == 0) break :binds;
for (1..len + 1) |i| {
// push i-th table to stack
const ith_table_type = lua.getIndex(-1, @intCast(i));
defer lua.pop(1); // i-th table in custom_commands
if (ith_table_type != .table) continue;
// skip command if binding isn't set or not a string
const binding_type = lua.getField(-1, "binding");
if (binding_type != .string) continue;
const binding = lua.toString(-1) catch continue;
const bindingZ = temporary_allocator.dupe(u8, binding) catch "";
lua.pop(1); // binding value
if (!custom.binds.contains(bindingZ)) {
custom.binds.put(temporary_allocator, bindingZ, .{}) catch {};
}
if (custom.binds.getPtr(bindingZ)) |command| {
// binding name
const name_type = lua.getField(-1, "name");
if (name_type != .string) continue;
const binding_name = lua.toString(-1) catch "";
command.name = temporary_allocator.dupe(u8, binding_name) catch "";
lua.pop(1); // name value
// binding command
const cmd_type = lua.getField(-1, "cmd");
if (cmd_type != .string) continue;
const binding_cmd = lua.toString(-1) catch "";
command.cmd = temporary_allocator.dupe(u8, binding_cmd) catch "";
lua.pop(1); // cmd value
}
}
}
}
lua.pop(1);
_ = lua.getField(-1, "custom_labels");
// custom_labels can be omitted, so we just skip instead of erroring
labels: {
if (lua.isTable(-1)) {
const len = @as(usize, @intCast(lua.lenRaiseErr(-1)));
if (len == 0) break :labels;
for (1..len + 1) |i| {
// push i-th table to stack
const ith_table_type = lua.getIndex(-1, @intCast(i));
defer lua.pop(1); // i-th table in custom_labels
if (ith_table_type != .table) continue;
// skip command if binding isn't set or not a string
const label_type = lua.getField(-1, "label");
if (label_type != .string) continue;
const label = lua.toString(-1) catch continue;
const labelZ = temporary_allocator.dupe(u8, label) catch "";
lua.pop(1); // label value
if (!custom.labels.contains(labelZ)) {
custom.labels.put(temporary_allocator, labelZ, .{ .name = labelZ }) catch {};
}
if (custom.labels.getPtr(labelZ)) |label_ptr| {
// label command
const cmd_type = lua.getField(-1, "cmd");
if (cmd_type != .string) continue;
const label_cmd = lua.toString(-1) catch "";
label_ptr.cmd = temporary_allocator.dupe(u8, label_cmd) catch "";
lua.pop(1); // cmd value
// label refresh
const name_type = lua.getField(-1, "refresh");
if (name_type != .number) continue;
const label_refresh: u32 = @intCast(lua.toInteger(-1) catch 0);
label_ptr.refresh = label_refresh;
lua.pop(1); // name value
}
}
}
}
lua.pop(1);
}
pub fn deinit(self: *Self) void {
_ = self; // autofix
inline for (@typeInfo(Struct).@"struct".fields) |field| {
const type_info = @typeInfo(field.type);
const actual_type = blk: {
if (type_info == .optional) {
break :blk type_info.optional.child;
}
break :blk field.type;
};
const value = @field(self.structure, field.name);
if (actual_type == []const u8 or actual_type == [:0]const u8) {
if (type_info == .optional) {
if (value) |inner| {
self.allocator.free(inner);
}
} else {
self.allocator.free(value);
}
}
}
for (0..global_errors.items.len) |i| {
const err = global_errors.items[i];
temporary_allocator.free(err.type_name);

View file

@ -274,7 +274,6 @@ pub fn main(init: std.process.Init) !void {
};
state.config = config_parser.structure();
std.debug.print("=== {}\n", .{state.config.gameoflife_fg});
var lang_buffer: [16]u8 = undefined;
const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang});
@ -292,7 +291,7 @@ pub fn main(init: std.process.Init) !void {
state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" });
}
if (config_parser.maybe_load_error() == null) {
if (config_parser.maybe_load_error() == null and config_parser == .ini) {
migrator.lateConfigFieldHandler(&state.config);
}