package config import ( "errors" "fmt" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" ) // Config represents a configuration loaded from a Lua file type Config struct { values map[string]any } // New creates a new empty configuration func New() *Config { return &Config{ values: make(map[string]any), } } // Load loads configuration from a Lua file func Load(filePath string) (*Config, error) { // Create a new Lua state state := luajit.New() if state == nil { return nil, errors.New("failed to create Lua state") } defer state.Close() // Execute the Lua file if err := state.DoFile(filePath); err != nil { return nil, fmt.Errorf("failed to load config file: %w", err) } // Create the config instance config := New() // Extract values from the Lua state if err := extractGlobals(state, config.values); err != nil { return nil, err } return config, nil } // extractGlobals extracts global variables from the Lua state func extractGlobals(state *luajit.State, values map[string]any) error { // Get the globals table (_G) state.GetGlobal("_G") if !state.IsTable(-1) { state.Pop(1) return errors.New("failed to get globals table") } // Pre-populate with standard globals for reference checking stdGlobals := map[string]bool{ "_G": true, "_VERSION": true, "assert": true, "collectgarbage": true, "coroutine": true, "debug": true, "dofile": true, "error": true, "getmetatable": true, "io": true, "ipairs": true, "load": true, "loadfile": true, "loadstring": true, "math": true, "next": true, "os": true, "package": true, "pairs": true, "pcall": true, "print": true, "rawequal": true, "rawget": true, "rawset": true, "require": true, "select": true, "setmetatable": true, "string": true, "table": true, "tonumber": true, "tostring": true, "type": true, "unpack": true, "xpcall": true, // LuaJIT specific globals "jit": true, "bit": true, "ffi": true, "bit32": true, } // First, let's get the original globals to compare with user globals originalGlobals := make(map[string]bool) // Execute empty Lua state to get standard globals emptyState := luajit.New() if emptyState != nil { defer emptyState.Close() emptyState.GetGlobal("_G") emptyState.PushNil() // Start iteration for emptyState.Next(-2) { if emptyState.IsString(-2) { key := emptyState.ToString(-2) originalGlobals[key] = true } emptyState.Pop(1) // Pop value, leave key for next iteration } emptyState.Pop(1) // Pop _G } // Iterate through the globals table state.PushNil() // Start iteration for state.Next(-2) { // Stack now has key at -2 and value at -1 // Get key as string if !state.IsString(-2) { state.Pop(1) // Pop value, leave key for next iteration continue } key := state.ToString(-2) // Skip standard Lua globals, but only if they're not overridden by user // (standard globals will be functions or tables, user values usually aren't) valueType := state.GetType(-1) // Skip functions, userdata, and threads regardless of origin if valueType == luajit.TypeFunction || valueType == luajit.TypeUserData || valueType == luajit.TypeThread { state.Pop(1) continue } // For known Lua globals, we need to see if they're the original or user-defined if stdGlobals[key] { // For simple value types, assume user-defined if valueType == luajit.TypeBoolean || valueType == luajit.TypeNumber || valueType == luajit.TypeString { // These are probably user values with standard names } else if originalGlobals[key] { // If it's in the original globals and not a simple type, skip it state.Pop(1) continue } } // Handle primitive types directly switch valueType { case luajit.TypeBoolean: values[key] = state.ToBoolean(-1) state.Pop(1) continue case luajit.TypeNumber: values[key] = state.ToNumber(-1) state.Pop(1) continue case luajit.TypeString: values[key] = state.ToString(-1) state.Pop(1) continue case luajit.TypeTable: // For tables, use the existing conversion logic default: // Skip unsupported types state.Pop(1) continue } // Handle tables (arrays and maps) if valueType == luajit.TypeTable { // Check if it looks like an array first arrLen := state.GetTableLength(-1) if arrLen > 0 { // Process as array arr := make([]any, arrLen) for i := 1; i <= arrLen; i++ { state.PushNumber(float64(i)) state.GetTable(-2) // Get t[i] switch state.GetType(-1) { case luajit.TypeBoolean: arr[i-1] = state.ToBoolean(-1) case luajit.TypeNumber: arr[i-1] = state.ToNumber(-1) case luajit.TypeString: arr[i-1] = state.ToString(-1) default: // For complex elements, try to convert if val, err := state.ToValue(-1); err == nil { arr[i-1] = val } } state.Pop(1) // Pop value } values[key] = arr state.Pop(1) continue } // Try normal table conversion for non-array tables if table, err := state.ToTable(-1); err == nil { values[key] = table } } // Pop value, leave key for next iteration state.Pop(1) } // Pop the globals table state.Pop(1) return nil } // Get returns a configuration value by key func (c *Config) Get(key string) any { return c.values[key] } // GetString returns a string configuration value func (c *Config) GetString(key string, defaultValue string) string { value, ok := c.values[key] if !ok { return defaultValue } str, ok := value.(string) if !ok { return defaultValue } return str } // GetInt returns an integer configuration value func (c *Config) GetInt(key string, defaultValue int) int { value, ok := c.values[key] if !ok { return defaultValue } // Handle both int and float64 (which is what Lua numbers become in Go) switch v := value.(type) { case int: return v case float64: return int(v) default: return defaultValue } } // GetFloat returns a float configuration value func (c *Config) GetFloat(key string, defaultValue float64) float64 { value, ok := c.values[key] if !ok { return defaultValue } // Handle both float64 and int switch v := value.(type) { case float64: return v case int: return float64(v) default: return defaultValue } } // GetBool returns a boolean configuration value func (c *Config) GetBool(key string, defaultValue bool) bool { value, ok := c.values[key] if !ok { return defaultValue } boolValue, ok := value.(bool) if !ok { return defaultValue } return boolValue } // GetMap returns a map configuration value func (c *Config) GetMap(key string) map[string]any { value, ok := c.values[key] if !ok { return nil } table, ok := value.(map[string]any) if !ok { return nil } return table } // GetArray returns an array of values from a Lua array func (c *Config) GetArray(key string) []any { value := c.Get(key) if value == nil { return nil } // Direct array if arr, ok := value.([]any); ok { return arr } // Arrays in Lua might also be represented as maps with an empty string key valueMap, ok := value.(map[string]any) if !ok { return nil } // The array data is stored with an empty key arr, ok := valueMap[""] if !ok { return nil } // Check if it's a float64 array (common for Lua numeric arrays) if floatArr, ok := arr.([]float64); ok { // Convert to []any result := make([]any, len(floatArr)) for i, v := range floatArr { result[i] = v } return result } // Otherwise, try to return as is anyArr, ok := arr.([]any) if !ok { return nil } return anyArr } // GetIntArray returns an array of integers from a Lua array func (c *Config) GetIntArray(key string) []int { value := c.Get(key) if value == nil { return nil } // Direct array case if arr, ok := value.([]any); ok { result := make([]int, 0, len(arr)) for _, v := range arr { if num, ok := v.(float64); ok { result = append(result, int(num)) } } return result } // Arrays in Lua might also be represented as maps with an empty string key valueMap, ok := value.(map[string]any) if !ok { return nil } // The array data is stored with an empty key arr, ok := valueMap[""] if !ok { return nil } // For numeric arrays, LuaJIT returns []float64 floatArr, ok := arr.([]float64) if !ok { return nil } // Convert to int slice result := make([]int, len(floatArr)) for i, v := range floatArr { result[i] = int(v) } return result } // GetStringArray returns an array of strings from a Lua array func (c *Config) GetStringArray(key string) []string { arr := c.GetArray(key) if arr == nil { return nil } result := make([]string, 0, len(arr)) for _, v := range arr { if str, ok := v.(string); ok { result = append(result, str) } } return result } // Values returns all configuration values // Note: The returned map should not be modified func (c *Config) Values() map[string]any { return c.values } // Set sets a configuration value func (c *Config) Set(key string, value any) { c.values[key] = value }