--[[-- ExpGui - GuiData Provides a method of storing data for elements, players, and forces under a given scope. This is not limited to GUI element definitions but this is the most common use case. ]] local ExpUtil = require("modules/exp_util") local Storage = require("modules/exp_util/storage") --- @type table local scope_data = {} --- @type table local registered_scopes = {} --- @type table Reg -> Player Index local registration_numbers = {} local reg_obj = script.register_on_object_destroyed --- @alias DataKey LuaGuiElement | LuaPlayer | LuaForce --- @alias DataKeys "element_data" | "player_data" | "force_data" | "global_data" local DataKeys = ExpUtil.enum{ "element_data", "player_data", "force_data", "global_data" } local DataKeysConcat = table.concat(DataKeys, ", ") Storage.register({ scope_data = scope_data, registration_numbers = registration_numbers, }, function(tbl) scope_data = tbl.scope_data registration_numbers = tbl.registration_numbers for scope, data in pairs(tbl.scope_data) do local proxy = registered_scopes[scope] if proxy then rawset(proxy, "_raw", data) for k, v in pairs(data) do rawset(proxy, k, v) end end end end) --- @class ExpGui_GuiData local GuiData = { _scopes = registered_scopes, } --- @class ExpGui.GuiData_Raw --- @field element_data table?>? --- @field player_data table? --- @field force_data table? --- @field global_data table? -- This class has no prototype methods -- Keep this in sync with DataKeys to block arbitrary strings --- @class ExpGui.GuiData_Internal --- @field _scope string --- @field _raw ExpGui.GuiData_Raw -- This class has no prototype methods -- Do add keys to _raw without also referencing scope_data --- @class ExpGui.GuiData: ExpGui.GuiData_Internal --- @field element_data table> --- @field player_data table --- @field force_data table --- @field global_data table -- This class has no prototype methods -- Same as raw but __index ensures the values exist GuiData._metatable = { __class = "GuiData", } --- Return the index for a given key --- @param self ExpGui.GuiData_Internal --- @param key DataKeys | DataKey --- @return any function GuiData._metatable.__index(self, key) if type(key) == "string" then -- This is only called when the key does not exist, ie it is not in storage -- Once created, these tables are never removed as they are likely to be used again assert(DataKeys[key], "Valid keys are: " .. DataKeysConcat) local value = {} self._raw[key] = value rawset(self, key, value) scope_data[self._scope] = self._raw return value end -- Check a given child table based on the object type assert(type(key) == "userdata", "Index type '" .. ExpUtil.get_class_name(key) .. "' given to GuiData. Must be of type userdata.") local object_name = key.object_name --- @diagnostic disable-line assign-type-mismatch if object_name == "LuaGuiElement" then local data = self._raw.element_data local player_elements = data and data[key.player_index] return player_elements and player_elements[key.index] elseif object_name == "LuaPlayer" then local data = self._raw.player_data return data and data[key.index] elseif object_name == "LuaForce" then local data = self._raw.force_data return data and data[key.index] else error("Unsupported object class '" .. object_name .. "' given as index to GuiData.") end end --- Set the value index of a given key -- Internal type is not used here to allow for creation of storage --- @param self ExpGui.GuiData --- @param key DataKey --- @param value unknown function GuiData._metatable.__newindex(self, key, value) assert(type(key) == "userdata", "Index type '" .. ExpUtil.get_class_name(key) .. "' given to GuiData. Must be of type userdata.") local object_name = key.object_name if object_name == "LuaGuiElement" then --- @cast key LuaGuiElement local data = self.element_data local player_elements = data[key.player_index] if not player_elements then player_elements = {} data[key.player_index] = player_elements end player_elements[key.index] = value registration_numbers[reg_obj(key)] = key.player_index elseif object_name == "LuaPlayer" then --- @cast key LuaPlayer self.player_data[key.index] = value elseif object_name == "LuaForce" then --- @cast key LuaForce self.force_data[key.index] = value else error("Unsupported object class '" .. object_name .. "' given as index to GuiData.") end end --- Create the data object for a given scope --- @param scope string --- @return ExpGui.GuiData function GuiData.create(scope) ExpUtil.assert_not_runtime() assert(GuiData._scopes[scope] == nil, "Scope already exists with name: " .. scope) local instance = { _scope = scope, _raw = {}, } GuiData._scopes[scope] = instance --- @cast instance ExpGui.GuiData return setmetatable(instance, GuiData._metatable) end --- Get the link to an existing data scope --- @param scope string --- @return ExpGui.GuiData function GuiData.get(scope) return GuiData._scopes[scope] --[[ @as ExpGui.GuiData ]] end --- Used to clean up data from destroyed elements --- @param event EventData.on_object_destroyed local function on_object_destroyed(event) local player_index = registration_numbers[event.registration_number] if not player_index then return end local element_index = event.useful_id registration_numbers[event.registration_number] = nil for _, scope in pairs(registered_scopes) do local data = scope._raw.element_data local player_elements = data and data[player_index] if player_elements then player_elements[element_index] = nil if not next(player_elements) then data[player_index] = nil end end end end --- Used to clean up data from destroyed players --- @param event EventData.on_player_removed local function on_player_removed(event) local player_index = event.player_index for _, scope in pairs(registered_scopes) do local data = scope._raw.player_data if data then data[player_index] = nil end end end --- Used to clean up data from destroyed forces --- @param event EventData.on_forces_merged local function on_forces_merged(event) local force_index = event.source_index for _, scope in pairs(registered_scopes) do local data = scope._raw.force_data if data then data[force_index] = nil end end end local e = defines.events local events = { [e.on_object_destroyed] = on_object_destroyed, [e.on_player_removed] = on_player_removed, [e.on_forces_merged] = on_forces_merged, } GuiData.events = events return GuiData