diff --git a/config/file_loader.lua b/config/file_loader.lua index 790aa6c6..744c058c 100644 --- a/config/file_loader.lua +++ b/config/file_loader.lua @@ -32,10 +32,13 @@ return { 'modules.addons.scorched-earth', 'modules.addons.pollution-grading', 'modules.addons.random-player-colours', + -- GUI + 'modules.commands.debug', -- Config Files 'config.command_auth_admin', -- commands tagged with admin_only are blocked for non admins 'config.command_auth_roles', -- commands must be allowed via the role config 'config.command_auth_runtime_disable', -- allows commands to be enabled and disabled during runtime 'config.permission_groups', -- loads some predefined permission groups 'config.roles', -- loads some predefined roles + 'expcore.gui.test' } \ No newline at end of file diff --git a/config/roles.lua b/config/roles.lua index 0383bd85..b1863ea7 100644 --- a/config/roles.lua +++ b/config/roles.lua @@ -44,6 +44,7 @@ Roles.new_role('Senior Administrator','SAdmin') :set_parent('Administrator') :allow{ 'command/interface', + 'command/debug', 'command/toggle-cheat-mode' } @@ -237,7 +238,7 @@ Roles.override_player_roles{ FlipHalfling90={'Moderator','Member'}, Gizan={'Pay to Win','Moderator','Member'}, Hobbitkicker={'Moderator','Member'}, - jess_gaming={'Trainee','Member'}, + jessi_gaming={'Trainee','Member'}, Koroto={'Moderator','Member'}, mafisch3={'Moderator','Member'}, maplesyrup01={'Moderator','Member'}, diff --git a/control.lua b/control.lua index 02ff8cec..48a4fbbc 100644 --- a/control.lua +++ b/control.lua @@ -4,7 +4,10 @@ -- all files which are loaded (including the config files) are present in ./config/file_loader.lua -- this file is the landing point for all scenarios please DO NOT edit directly, further comments are to aid development +log('[START] -----| Explosive Gaming Scenario Loader |-----') + -- Info on the data lifecycle and how we use it: https://github.com/Refactorio/RedMew/wiki/The-data-lifecycle +log('[INFO] Setting up lua environment') require 'resources.data_stages' _LIFECYCLE = _STAGE.control -- Control stage @@ -22,6 +25,7 @@ require 'resources.version' ext_require = require('expcore.common').ext_require -- Please go to config/file_loader.lua to edit the files that are loaded +log('[INFO] Getting file loader config') local files = require 'config.file_loader' -- Loads all files from the config and logs that they are loaded @@ -30,7 +34,7 @@ local errors = {} for index,path in pairs(files) do -- Loads the next file in the list - log(string.format('[INFO] Loading files %3d/%s',index,total_file_count)) + log(string.format('[INFO] Loading file %3d/%s (%s)',index,total_file_count,path)) local success,file = pcall(require,path) -- Error Checking @@ -49,4 +53,5 @@ end -- Logs all errors again to make it make it easy to find log('[INFO] All files loaded with '..#errors..' errors:') -for _,error in pairs(errors) do log(error) end \ No newline at end of file +for _,error in pairs(errors) do log(error) end +log('[END] -----| Explosive Gaming Scenario Loader |-----') \ No newline at end of file diff --git a/expcore/Gui/buttons.lua b/expcore/Gui/buttons.lua new file mode 100644 index 00000000..9e3ae887 --- /dev/null +++ b/expcore/Gui/buttons.lua @@ -0,0 +1,121 @@ +--- Gui class define for buttons and sprite buttons +--[[ +>>>> Functions + Button.new_button(name) --- Creates a new button element define + + Button._prototype:on_click(player,element) --- Registers a handler for when the button is clicked + Button._prototype:on_left_click(player,element) --- Registers a handler for when the button is clicked with the left mouse button + Button._prototype:on_right_click(player,element) --- Registers a handler for when the button is clicked with the right mouse button + + Button._prototype:set_sprites(sprite,hovered_sprite,clicked_sprite) --- Adds sprites to a button making it a spirte button + Button._prototype:set_click_filter(filter,...) --- Adds a click / mouse button filter to the button + Button._prototype:set_key_filter(filter,...) --- Adds a control key filter to the button + + Other functions present from expcore.gui.core +]] +local mod_gui = require 'mod-gui' +local Gui = require 'expcore.gui.core' + +local Button = { + _prototype=Gui._prototype_factory{ + on_click = Gui._event_factory('on_click'), + on_left_click = Gui._event_factory('on_left_click'), + on_right_click = Gui._event_factory('on_right_click'), + } +} + +--- Creates a new button element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new button element define +function Button.new_button(name) + + local self = Gui._define_factory(Button._prototype) + self.draw_data.type = 'button' + self.draw_data.style = mod_gui.button_style + + if name then + self:debug_name(name) + end + + Gui.on_click(self.name,function(event) + local mouse_button = event.button + local keys = {alt=event.alt,control=event.control,shift=event.shift} + event.keys = keys + + if self.post_authenticator then + if not self.post_authenticator(event.player,self.name) then return end + end + + if mouse_button == defines.mouse_button_type.left and self.events.on_left_click then + self.events.on_left_click(event.player,event.element) + elseif mouse_button == defines.mouse_button_type.right and self.events.on_right_click then + self.events.on_right_click(event.player,event.element) + end + + if self.mouse_button_filter and not self.mouse_button_filter[mouse_button] then return end + if self.key_button_filter then + for key,state in pairs(self.key_button_filter) do + if state and not keys[key] then return end + end + end + + if self.events.on_click then + self.events.on_click(event.player,event.element,event) + end + end) + + return self +end + +--- Adds sprites to a button making it a spirte button +-- @tparam sprite SpritePath the sprite path for the default sprite for the button +-- @tparam[opt] hovered_sprite SpritePath the sprite path for the sprite when the player hovers over the button +-- @tparam[opt] clicked_sprite SpritePath the sprite path for the sprite when the player clicks the button +-- @treturn self returns the button define to allow chaining +function Button._prototype:set_sprites(sprite,hovered_sprite,clicked_sprite) + self.draw_data.type = 'sprite-button' + self.draw_data.sprite = sprite + self.draw_data.hovered_sprite = hovered_sprite + self.draw_data.clicked_sprite = clicked_sprite + return self +end + +--- Adds a click / mouse button filter to the button +-- @tparam filter ?string|table either a table of mouse buttons or the first mouse button to filter, with a table true means allowed +-- @tparam[opt] ... when filter is not a table you can add the mouse buttons one after each other +-- @treturn self returns the button define to allow chaining +function Button._prototype:set_click_filter(filter,...) + if type(filter) == 'string' then + filter = {[filter]=true} + for _,v in pairs({...}) do + filter[v] = true + end + end + + for k,v in pairs(filter) do + if type(v) == 'string' then + filter[k] = defines.mouse_button_type[v] + end + end + + self.mouse_button_filter = filter + return self +end + +--- Adds a control key filter to the button +-- @tparam filter ?string|table either a table of control keys or the first control keys to filter, with a table true means allowed +-- @tparam[opt] ... when filter is not a table you can add the control keyss one after each other +-- @treturn self returns the button define to allow chaining +function Button._prototype:set_key_filter(filter,...) + if type(filter) == 'string' then + filter = {[filter]=true} + for _,v in pairs({...}) do + filter[v] = true + end + end + + self.key_button_filter = filter + return self +end + +return Button \ No newline at end of file diff --git a/expcore/Gui/center.lua b/expcore/Gui/center.lua new file mode 100644 index 00000000..b849075a --- /dev/null +++ b/expcore/Gui/center.lua @@ -0,0 +1,191 @@ +--- Gui structure define for center gui frames +--[[ +>>>> Functions + CenterFrames.get_flow(player) --- Gets the center flow for a player + CenterFrames.clear_flow(player) --- Clears the center flow for a player + CenterFrames.draw_frame(player,name) --- Draws the center frame for a player, if already open then will do nothing + CenterFrames.redraw_frame(player,name) --- Draws the center frame for a player, if already open then will destroy it and redraw + CenterFrames.toggle_frame(player,name,state) --- Toggles if the frame is currently open or not, will open if closed and close if open + + CenterFrames.new_frame(permision_name) --- Sets the frame to be the current active gui when opened and closes all other frames + CenterFrames._prototype:on_draw(player,frame) --- Use to draw your elements onto the new frame + CenterFrames._prototype:set_auto_focus(state) --- Sets the frame to be the current active gui when opened and closes all other frames + CenterFrames._prototype:draw_frame(player) --- Draws this frame to the player, if already open does nothing (will call on_draw to draw to the frame) + CenterFrames._prototype:redraw_frame(player) --- Draws this frame to the player, if already open it will remove it and redraw it (will call on_draw to draw to the frame) + CenterFrames._prototype:toggle_frame(player) --- Toggles if the frame is open, if open it will close it and if closed it will open it + CenterFrames._prototype:event_handler(action) --- Creates an event handler that will trigger one of its functions, use with Event.add +]] +local Gui = require 'expcore.gui.core' +local Toolbar = require 'expcore.gui.toolbar' +local Game = require 'utils.game' + +local CenterFrames = { + _prototype = Gui._prototype_factory{ + on_draw = Gui._event_factory('on_draw') + } +} + +--- Gets the center flow for a player +-- @tparam player LuapPlayer the player to get the flow for +-- @treturn LuaGuiElement the center flow +function CenterFrames.get_flow(player) + player = Game.get_player_from_any(player) + return player.gui.center +end + +--- Clears the center flow for a player +-- @tparam player LuapPlayer the player to clear the flow for +function CenterFrames.clear_flow(player) + local flow = CenterFrames.get_flow(player) + flow.clear() +end + +--- Draws the center frame for a player, if already open then will do nothing +-- @tparam player LuapPlayer the player that will have the frame drawn +-- @tparam name string the name of the hui that will drawn +-- @treturn LuaGuiElement the new frame that was made +function CenterFrames.draw_frame(player,name) + local define = Gui.get_define(name,true) + if define then + return define:draw_frame(player) + end +end + +--- Draws the center frame for a player, if already open then will destroy it and redraw +-- @tparam player LuapPlayer the player that will have the frame drawn +-- @tparam name string the name of the hui that will drawn +-- @treturn LuaGuiElement the new frame that was made +function CenterFrames.redraw_frame(player,name) + local define = Gui.get_define(name,true) + if define then + return define:draw_frame(player) + end +end + +--- Toggles if the frame is currently open or not, will open if closed and close if open +-- @tparam player LuapPlayer the player that will have the frame toggled +-- @tparam name string the name of the hui that will be toggled +-- @tparam[opt] state boolean when set will force a state for the frame +-- @treturn boolean if the frame if no open or closed +function CenterFrames.toggle_frame(player,name,state) + local define = Gui.get_define(name,true) + if define then + if state == true then + define:draw_frame(player) + return true + elseif state == false then + local flow = CenterFrames.get_flow(player) + if flow[define.name] then + flow[define.name].destroy() + end + return false + else + return define:toggle_frame(player) + end + end +end + +--- Creates a new center frame define +-- @tparam permision_name string the name that can be used with the permision system +-- @treturn table the new center frame define +function CenterFrames.new_frame(permision_name) + local self = Toolbar.new_button(permision_name) + + self:on_click(function(player,element) + self:toggle_frame(player) + end) + + local mt = getmetatable(self) + mt.__index = CenterFrames._prototype + mt.__call = self.event_handler + + Gui.on_custom_close(self.name,function(event) + local element = event.element + if element and element.valid then element.destroy() end + end) + + return self +end + +--- Sets the frame to be the current active gui when opened and closes all other frames +-- @tparam[opt=true] state boolean when true will auto close other frames and set this frame as player.opened +function CenterFrames._prototype:set_auto_focus(state) + if state == false then + self.auto_focus = false + else + self.auto_focus = true + end +end + +--- Draws this frame to the player, if already open does nothing (will call on_draw to draw to the frame) +-- @tparam player LuaPlayer the player to draw the frame for +-- @treturn LuaGuiElement the new frame that was drawn +function CenterFrames._prototype:draw_frame(player) + player = Game.get_player_from_any(player) + local flow = CenterFrames.get_flow(player) + + if flow[self.name] then + return flow[self.name] + end + + if self.auto_focus then + flow.clear() + end + + local frame = flow.add{ + type='frame', + name=self.name + } + + if self.auto_focus then + player.opened = frame + end + + if self.events.on_draw then + self.events.on_draw(player,frame) + end + + return frame +end + +--- Draws this frame to the player, if already open it will remove it and redraw it (will call on_draw to draw to the frame) +-- @tparam player LuaPlayer the player to draw the frame for +-- @treturn LuaGuiElement the new frame that was drawn +function CenterFrames._prototype:redraw_frame(player) + player = Game.get_player_from_any(player) + local flow = CenterFrames.get_flow(player) + + if flow[self.name] then + flow[self.name].destroy() + end + + return self:draw_frame(player) +end + +--- Toggles if the frame is open, if open it will close it and if closed it will open it +-- @tparam player LuaPlayer the player to draw the frame for +-- @treturn boolean with the gui frame is now open +function CenterFrames._prototype:toggle_frame(player) + player = Game.get_player_from_any(player) + local flow = CenterFrames.get_flow(player) + + if flow[self.name] then + flow[self.name].destroy() + return false + else + self:draw_frame(player) + return true + end +end + +--- Creates an event handler that will trigger one of its functions, use with Event.add +-- @tparam[opt=update] action string the action to take on this event +function CenterFrames._prototype:event_handler(action) + action = action or 'update' + return function(event) + local player = Game.get_player_by_index(event.player_index) + self[action](self,player) + end +end + +return CenterFrames \ No newline at end of file diff --git a/expcore/Gui/checkboxs.lua b/expcore/Gui/checkboxs.lua new file mode 100644 index 00000000..b31d9f6e --- /dev/null +++ b/expcore/Gui/checkboxs.lua @@ -0,0 +1,266 @@ +--- Gui class define for checkboxs and radiobuttons +--[[ +>>>> Using an option set + An option set is a set of radio buttons where only one of them can be active at a time, this means that when one + is clicked all the other ones are set to false, an option set must be defined before hand and will always store + its state but is not limited by how it can categorize the store. + + First you must register the store with a name and a update callback, and an optional function for categorize: + + local example_option_set = + Gui.new_option_set('example-option-set',function(value,category) + game.print('Example options set '..category..' is now: '..tostring(value)) + end,Gui.player_store) + + Then you must register some radiobutton defines and include them in the option set: + + local example_option_one = + Gui.new_radiobutton() + :set_caption('Option One') + :add_as_option(example_option_set,'One') + + local example_option_two = + Gui.new_radiobutton() + :set_caption('Option Two') + :add_as_option(example_option_set,'Two') + + Note that these radiobuttons can still have on_element_update events but this may result in a double trigger of events as + the option set update is always triggered; also add_store cant be used as the option set acts as the store however get + and set store will still work but will effect the option set rather than the indivual radiobuttons. + +>>>> Functions + Checkbox.new_checkbox(name) --- Creates a new checkbox element define + Checkbox._prototype_checkbox:on_element_update(callback) --- Registers a handler for when an element instance updates + Checkbox._prototype_checkbox:on_store_update(callback) --- Registers a handler for when the stored value updates + + Checkbox.new_radiobutton(name) --- Creates a new radiobutton element define + Checkbox._prototype_radiobutton:on_element_update(callback) --- Registers a handler for when an element instance updates + Checkbox._prototype_radiobutton:on_store_update(callback) --- Registers a handler for when the stored value updates + Checkbox._prototype_radiobutton:add_as_option(option_set,option_name) --- Adds this radiobutton to be an option in the given option set (only one can be true at a time) + + Checkbox.new_option_set(name,callback,categorize) --- Registers a new option set that can be linked to radiobutotns (only one can be true at a time) + Checkbox.draw_option_set(name,element) --- Draws all radiobuttons that are part of an option set at once (Gui.draw will not work) + + Checkbox.reset_radiobutton(element,exclude,recursive) --- Sets all radiobutotn in a element to false (unless excluded) and can act recursivly + + Other functions present from expcore.gui.core +]] +local Gui = require 'expcore.gui.core' +local Store = require 'expcore.store' +local Game = require 'utils.game' + +--- Event call for on_checked_state_changed and store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value boolean the new state of the checkbox +local function event_call(define,element,value) + if define.events.on_element_update then + local player = Game.get_player_by_index(element.player_index) + define.events.on_element_update(player,element,value) + end +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value boolean the new state of the checkbox +local function store_call(define,element,value) + element.state = value + event_call(define,element,value) +end + +local Checkbox = { + option_sets={}, + option_categorize={}, + _prototype_checkbox=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + }, + _prototype_radiobutton=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} + +--- Creates a new checkbox element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new checkbox element define +function Checkbox.new_checkbox(name) + + local self = Gui._define_factory(Checkbox._prototype_checkbox) + self.draw_data.type = 'checkbox' + self.draw_data.state = false + + if name then + self:debug_name(name) + end + + self.post_draw = function(element) + if self.store then + local category = self.categorize and self.categorize(element) or nil + local state = self:get_store(category,true) + if state then element.state = true end + end + end + + Gui.on_checked_state_changed(self.name,function(event) + local element = event.element + + if self.option_set then + local value = Checkbox.option_sets[self.option_set][element.name] + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + elseif self.store then + local value = element.state + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + else + local value = element.state + event_call(self,element,value) + + end + end) + + return self +end + +--- Creates a new radiobutton element define, has all functions checkbox has +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new button element define +function Checkbox.new_radiobutton(name) + local self = Checkbox.new_checkbox(name) + self.draw_data.type = 'radiobutton' + + local mt = getmetatable(self) + mt.__index = Checkbox._prototype_radiobutton + + return self +end + +--- Adds this radiobutton to be an option in the given option set (only one can be true at a time) +-- @tparam option_set string the name of the option set to add this element to +-- @tparam option_name string the name of this option that will be used to idenitife it +-- @tparam self the define to allow chaining +function Checkbox._prototype_radiobutton:add_as_option(option_set,option_name) + self.option_set = option_set + self.option_name = option_name or self.name + + Checkbox.option_sets[option_set][self.option_name] = self.name + Checkbox.option_sets[option_set][self.name] = self.option_name + + self:add_store(Checkbox.option_categorize[option_set]) + + return self +end + +--- Gets the stored value of the radiobutton or the option set if present +-- @tparam category[opt] string the category to get such as player name or force name +-- @treturn any the value that is stored for this define +function Checkbox._prototype_radiobutton:get_store(category,internal) + if not self.store then return end + local location = not internal and self.option_set or self.store + + if self.categorize then + return Store.get_child(location,category) + else + return Store.get(location) + end +end + +--- Sets the stored value of the radiobutton or the option set if present +-- @tparam category[opt] string the category to get such as player name or force name +-- @tparam value any the value to set for this define, must be valid for its type ie boolean for checkbox etc +-- @treturn boolean true if the value was set +function Checkbox._prototype_radiobutton:set_store(category,value,internal) + if not self.store then return end + local location = not internal and self.option_set or self.store + + if self.categorize then + return Store.set_child(location,category,value) + else + return Store.set(location,category) + end +end + +--- Registers a new option set that can be linked to radiobutotns (only one can be true at a time) +-- @tparam name string the name of the option set, must be unique +-- @tparam callback function the update callback when the value of the option set chagnes +-- callback param - value string - the new selected option for this option set +-- callback param - category string - the category that updated if categorize was used +-- @tpram categorize function the function used to convert an element into a string +-- @treturn string the name of this option set to be passed to add_as_option +function Checkbox.new_option_set(name,callback,categorize) + + Store.register(name,function(value,category) + local options = Checkbox.option_sets[name] + for opt_name,define_name in pairs(options) do + if Gui.defines[define_name] then + local define = Gui.get_define(define_name) + local state = opt_name == value + define:set_store(category,state,true) + end + end + callback(value,category) + end) + + Checkbox.option_categorize[name] = categorize + Checkbox.option_sets[name] = {} + + return name +end + +--- Draws all radiobuttons that are part of an option set at once (Gui.draw will not work) +-- @tparam name string the name of the option set to draw the radiobuttons of +-- @tparam element LuaGuiElement the parent element that the radiobuttons will be drawn to +function Checkbox.draw_option_set(name,element) + if not Checkbox.option_sets[name] then return end + local options = Checkbox.option_sets[name] + + for _,option in pairs(options) do + if Gui.defines[option] then + Gui.defines[option]:draw_to(element) + end + end + +end + +--- Sets all radiobutotn in a element to false (unless excluded) and can act recursivly +-- @tparam element LuaGuiElement the root gui element to start setting radio buttons from +-- @tparam[opt] exclude ?string|table the name of the radiobutton to exclude or a table of radiobuttons where true will set the state true +-- @tparam[opt=false] recursive boolean if true will recur as much as possible, if a number will recur that number of times +-- @treturn boolean true if successful +function Checkbox.reset_radiobuttons(element,exclude,recursive) + if not element or not element.valid then return end + exclude = type(exclude) == 'table' and exclude or exclude ~= nil and {[exclude]=true} or {} + recursive = type(recursive) == 'number' and recursive-1 or recursive + + for _,child in pairs(element.children) do + if child and child.valid and child.type == 'radiobutton' then + local state = exclude[child.name] or false + local define = Gui.defines[child.name] + + if define then + local category = define.categorize and define.categorize(child) or state + define:set_store(category,state) + + else + child.state = state + + end + + elseif child.children and (type(recursive) == 'number' and recursive >= 0 or recursive == true) then + Checkbox.reset_radiobutton(child,exclude,recursive) + + end + end + + return true +end + +return Checkbox \ No newline at end of file diff --git a/expcore/Gui/core.lua b/expcore/Gui/core.lua new file mode 100644 index 00000000..07d7e8f7 --- /dev/null +++ b/expcore/Gui/core.lua @@ -0,0 +1,513 @@ +--- Core gui file for making element defines and element classes (use require 'expcore.gui') +-- see utils.gui for event handlering +-- see expcore.gui.test for examples for element defines +--[[ +>>>> Basic useage with no defines + This module can be igroned if you are only wanting only event handlers as utils.gui adds the following: + + Gui.uid_name() --- Generates a unqiue name to register events to + Gui.on_checked_state_changed(callback) --- Register a handler for the on_gui_checked_state_changed event + Gui.on_click(callback) --- Register a handler for the on_gui_click event + Gui.on_elem_changed(callback) --- Register a handler for the on_gui_elem_changed + Gui.on_selection_state_changed(callback) --- Register a handler for the on_gui_selection_state_changed event + Gui.on_text_changed(callback) --- Register a handler for the on_gui_text_changed event + Gui.on_value_changed(callback) --- Register a handler for the on_gui_value_changed event + + Note that all event handlers will include event.player as a valid player and that if the player or the + element is not valid then the callback will not be run. + +>>>> Interal factory functions + There are a few factory function that are used by the class definations the use of these function are important to + know about but should only be used when making a new class deination rather than an element defination. See one of + the existing class definations for an example of when to use these. + +>>>> Basic prototype functions + Using a class defination you can create a new element dinfation in our examples we will be using the checkbox. + + local checkbox_example = Gui.new_checkbox() + + Although all class definations are stored in Gui.classes the main function used to make new element defination are + made aviable in the top level gui module. All functions which return a new element defination will accept a name argument + which is a name which is used while debuging and is not required to be used (has not been used in examples) + + Every element define will accept a caption and tooltip (although some may not show) and to do this you would use the two + set function provided for the element defines: + + checkbox_example:set_caption('Example Checkbox') + checkbox_example:set_tooltip('Example checkbox') + + Each element define can have event handlers set, for our example checkbox we only have access to on_change which will trigger + when the state of the checkbox changes; if we want to assign handlers using the utils.gui methods then we can get the uid by calling + the uid function on the element define; however, each element can only have one handler (of each event) so it is not possible to use + Gui.on_checked_state_changed and on_change at the same time in our example. + + checkbox_example:on_change(function(player,element,value) + player.print('Example checkbox is now: '..tostring(value)) + end) + + local checkbox_example_uid = checkbox_example:uid() + Gui.on_click(checkbox_example_uid,function(event) + event.player.print('You clicked the example checkbox!') + end) + + Finally you will want to draw your element defines for which you can call deirectly on the deinfe or use Gui.draw to do; when Gui.draw is + used it can be given either the element define, the define's uid or the debug name of the define (if set): + + checkbox_example:draw_to(parent_element) + Gui.draw(checkbox_example_uid,parent_element) + +>>>> Using authenticators with draw + When an element is drawn to its parent it can always be used but if you want to limit who can use it then you can use an authenticator. There + are two types which can be used: post and pre; using a pre authenticator will mean that the draw function is stoped before the element is added + to the parent element while using a post authenticator will draw the element to the parent but will disable the element from interaction. Both may + be used if you have use for such. + + -- unless global.checkbox_example_allow_pre_auth is true then the checkbox will not be drawn + checkbox_example:set_pre_authenticator(function(player,define_name) + player.print('Example checkbox pre auth callback ran') + return global.checkbox_example_allow_pre_auth + end) + + -- unless global.checkbox_example_allow_post_auth is true then the checkbox will be drawn but deactiveated (provided pre auth returns true) + checkbox_example:set_post_authenticator(function(player,define_name) + player.print('Example checkbox pre auth callback ran') + return global.checkbox_example_allow_post_auth + end) + +>>>> Using store + A powerful assept of this gui system is allowing an automatic store for the state of a gui element, this means that when a gui is closed and re-opened + the elements which have a store will retain they value even if the element was previously destroied. The store is not limited to only per player and can + be catergorised by any method you want such as one that is shared between all players or by all players on a force. Using a method that is not limited to + one player means that when one player changes the state of the element it will be automaticlly updated for all other player (even if the element is already drawn) + and so this is a powerful and easy way to sync gui elements. + + -- note the example below is the same as checkbox_example:add_store(Gui.player_store) + checkbox_example:add_store(function(element) + local player = Game.get_player_by_index(element.player_index) + return player.force.name + end) + + Of course this tool is not limited to only player interactions; the current satate of a define can be gotten using a number of methods and the value can + even be updated by the script and have all instances of the element define be updated. When you use a category then we must give a category to the get + and set functions; in our case we used Gui.player_store which uses the player's name as the category which is why 'Cooldude2606' is given as a argument, + if we did not set a function for add_store then all instances for all players have the same value and so a category is not required. + + checkbox_example:get_store('Cooldude2606') + Gui.get_store(name,'Cooldude2606') + + checkbox_example:set_store('Cooldude2606',true) + Gui.set_store(name,'Cooldude2606',true) + + These methods use the Store module which means that if you have the need to access these sotre location (for example if you want to add a watch function) then + you can get the store location of any define using checkbox_example.store + + Important note about event handlers: when the store is updated it will also trigger the event handlers (such as on_element_update) for that define but only + for the valid instances of the define which means if a player does not have the element drawn on a gui then it will not trigger the events; if you want a + trigger for all updates then you can use on_store_update however you will be required to parse the category which may or may not be a + player name (depends what store categorize function you use) + +>>>> Example formating + + local checkbox_example = + Gui.new_checkbox() + :set_caption('Example Checkbox') + :set_tooltip('Example checkbox') + :add_store(Gui.player_store) + :on_element_update(function(player,element,value) + player.print('Example checkbox is now: '..tostring(value)) + end) + +>>>> Functions + Gui._prototype_factory(tbl) --- Used internally to create new prototypes for element defines + Gui._event_factory(name) --- Used internally to create event handler adders for element defines + Gui._store_factory(callback) --- Used internally to create store adders for element defines + Gui._sync_store_factory(callback) --- Used internally to create synced store adders for element defines + Gui._define_factory(prototype) --- Used internally to create new element defines from a class prototype + + Gui._prototype:uid() --- Gets the uid for the element define + Gui._prototype:debug_name(name) --- Sets a debug alias for the define + Gui._prototype:set_caption(caption) --- Sets the caption for the element define + Gui._prototype:set_tooltip(tooltip) --- Sets the tooltip for the element define + Gui._prototype:on_element_update(callback) --- Add a hander to run on the general value update event, different classes will handle this event differently + + Gui._prototype:set_pre_authenticator(callback) --- Sets an authenticator that blocks the draw function if check fails + Gui._prototype:set_post_authenticator(callback) --- Sets an authenticator that disables the element if check fails + Gui._prototype:draw_to(element) --- Draws the element using what is in the draw_data table, allows use of authenticator if present, registers new instances if store present + Gui.draw(name,element) --- Draws a copy of the element define to the parent element, see draw_to + + Gui._prototype:add_store(categorize) --- Adds a store location for the define that will save the state of the element, categorize is a function that returns a string + Gui._prototype:add_sync_store(location,categorize) --- Adds a store location for the define that will sync between games, categorize is a function that returns a string + Gui._prototype:on_store_update(callback) --- Adds a event callback for when the store changes are other events are not gauenteted to be raised + Gui.player_store(element) --- A categorize function to be used with add_store, each player has their own value + Gui.force_store(element) --- A categorize function to be used with add_store, each force has its own value + Gui.surface_store(element) --- A categorize function to be used with add_store, each surface has its own value + + Gui._prototype:get_store(category) --- Gets the value in this elements store, category needed if categorize function used + Gui._prototype:set_store(category,value) --- Sets the value in this elements store, category needed if categorize function used + Gui.get_store(name,category) --- Gets the value that is stored for a given element define, category needed if categorize function used + Gui.set_store(name,category,value) --- Sets the value stored for a given element define, category needed if categorize function used + + Gui.toggle_enable(element) --- Will toggle the enabled state of an element + Gui.toggle_visible(element) --- Will toggle the visiblity of an element +]] +local Gui = require 'utils.gui' +local Game = require 'utils.game' +local Store = require 'expcore.store' +local Instances = require 'expcore.gui.instances' + +Gui._prototype = {} -- Stores the base prototype of all element defines +Gui.classes = {} -- Stores the class definations used to create element defines +Gui.defines = {} -- Stores the indivdual element definations +Gui.names = {} -- Stores debug names to link to gui uids + +--- Used internally to create new prototypes for element defines +-- @tparam tbl table a table that will have functions added to it +-- @treturn table the new table with the keys added to it +function Gui._prototype_factory(tbl) + for k,v in pairs(Gui._prototype) do + if not tbl[k] then tbl[k] = v end + end + return tbl +end + +--- Used internally to create event handler adders for element defines +-- @tparam name string the key that the event will be stored under, should be the same as the event name +-- @treturn function the function that can be used to add an event handler +function Gui._event_factory(name) + --- Gui._prototype:on_event(callback) + --- Add a hander to run on this event, replace event with the event, different classes have different events + -- @tparam callback function the function that will be called on the event + -- callback param - player LuaPlayer - the player who owns the gui element + -- callback param - element LuaGuiElement - the element that caused the event + -- callback param - value any - (not always present) the updated value for the element + -- callback param - ... any - other class defines may add more params + -- @treturn self the element define to allow chaining + return function(self,callback) + if type(callback) ~= 'function' then + return error('Event callback must be a function',2) + end + + self.events[name] = callback + return self + end +end + +--- Used internally to create store adders for element defines +-- @tparam callback a callback is called when there is an update to the stored value and stould set the state of the element +-- @treturn function the function that can be used to add a store the the define +function Gui._store_factory(callback) + --- Gui._prototype:add_store(categorize) + --- Adds a store location for the define that will save the state of the element, categorize is a function that returns a string + -- @tparam[opt] categorize function if present will be called to convert an element into a category string + -- categorize param - element LuaGuiElement - the element that needs to be converted + -- categorize return - string - a determistic string that referses to a category such as player name or force name + -- @treturn self the element define to allow chaining + return function(self,categorize) + if self.store then return end + + self.store = Store.uid_location() + self.categorize = categorize + + Instances.register(self.name,self.categorize) + + Store.register(self.store,function(value,category) + if self.events.on_store_update then + self.events.on_store_update(value,category) + end + + if Instances.is_registered(self.name) then + Instances.apply_to_elements(self.name,category,function(element) + callback(self,element,value) + end) + end + end) + + return self + end +end + +--- Used internally to create synced store adders for element defines +-- @tparam callback a callback is called when there is an update to the stored value and stould set the state of the element +-- @treturn function the function that can be used to add a sync store the the define +function Gui._sync_store_factory(callback) + --- Gui._prototype:add_sync_store(location,categorize) + --- Adds a store location for the define that will sync between games, categorize is a function that returns a string + -- @tparam location string a unique string location, unlike add_store a uid location should not be used to avoid migration problems + -- @tparam[opt] categorize function if present will be called to convert an element into a category string + -- categorize param - element LuaGuiElement - the element that needs to be converted + -- categorize return - string - a determistic string that referses to a category such as player name or force name + -- @treturn self the element define to allow chaining + return function(self,location,categorize) + if self.store then return end + + if Store.is_registered(location) then + return error('Location for store is already registered: '..location,2) + end + + self.store = location + self.categorize = categorize + + Instances.register(self.name,self.categorize) + + Store.register_synced(self.store,function(value,category) + if self.events.on_store_update then + self.events.on_store_update(value,category) + end + + if Instances.is_registered(self.name) then + Instances.apply_to_elements(self,category,function(element) + callback(self,element,value) + end) + end + end) + + return self + end +end + +--- Used internally to create new element defines from a class prototype +-- @tparam prototype table the class prototype that will be used for the element define +-- @treturn table the new element define with all functions accessed via __index metamethod +function Gui._define_factory(prototype) + local uid = Gui.uid_name() + local define = setmetatable({ + name=uid, + events={}, + draw_data={ + name=uid + } + },{ + __index=prototype, + __call=function(self,element,...) + return self:draw_to(element,...) + end + }) + Gui.defines[define.name] = define + return define +end + +--- Gets the uid for the element define +-- @treturn string the uid of this element define +function Gui._prototype:uid() + return self.name +end + +--- Sets a debug alias for the define +-- @tparam name string the debug name for the element define that can be used to get this element define +-- @treturn self the element define to allow chaining +function Gui._prototype:debug_name(name) + self.debug_name = name + return self +end + +--- Sets the caption for the element define +-- @tparam caption string the caption that will be drawn with the element +-- @treturn self the element define to allow chaining +function Gui._prototype:set_caption(caption) + self.draw_data.caption = caption + return self +end + +--- Sets the tooltip for the element define +-- @tparam tooltip string the tooltip that will be displayed for this element when drawn +-- @treturn self the element define to allow chaining +function Gui._prototype:set_tooltip(tooltip) + self.draw_data.tooltip = tooltip + return self +end + +--- Sets an authenticator that blocks the draw function if check fails +-- @tparam callback function the function that will be ran to test if the element should be drawn or not +-- callback param - player LuaPlayer - the player that the element is being drawn to +-- callback param - define_name string - the name of the define that is being drawn +-- callback return - boolean - false will stop the element from being drawn +-- @treturn self the element define to allow chaining +function Gui._prototype:set_pre_authenticator(callback) + if type(callback) ~= 'function' then + return error('Pre authenticator callback must be a function') + end + + self.pre_authenticator = callback + return self +end + +--- Sets an authenticator that disables the element if check fails +-- @tparam callback function the function that will be ran to test if the element should be enabled or not +-- callback param - player LuaPlayer - the player that the element is being drawn to +-- callback param - define_name string - the name of the define that is being drawn +-- callback return - boolean - false will disable the element +-- @treturn self the element define to allow chaining +function Gui._prototype:set_post_authenticator(callback) + if type(callback) ~= 'function' then + return error('Authenicater callback must be a function') + end + + self.post_authenticator = callback + return self +end + +--- Draws the element using what is in the draw_data table, allows use of authenticator if present, registers new instances if store present +-- the data with in the draw_data is set up through the use of all the other functions +-- @tparam element LuaGuiElement the element that the define will draw a copy of its self onto +-- @treturn LuaGuiElement the new element that was drawn so styles can be applied +function Gui._prototype:draw_to(element,...) + if element[self.name] then return end + local player = Game.get_player_by_index(element.player_index) + + if self.pre_authenticator then + if not self.pre_authenticator(player,self.name) then return end + end + + local new_element = element.add(self.draw_data) + + if self.post_authenticator then + new_element.enabled = self.post_authenticator(player,self.name) + end + + if Instances.is_registered(self.name) then + Instances.add_element(self.name,new_element) + end + + if self.post_draw then self.post_draw(new_element,...) end + + return new_element +end + +--- Gets the value in this elements store, category needed if categorize function used +-- @tparam category[opt] string the category to get such as player name or force name +-- @treturn any the value that is stored for this define +function Gui._prototype:get_store(category) + if not self.store then return end + if self.categorize then + return Store.get_child(self.store,category) + else + return Store.get(self.store) + end +end + +--- Sets the value in this elements store, category needed if categorize function used +-- @tparam category[opt] string the category to get such as player name or force name +-- @tparam value any the value to set for this define, must be valid for its type ie boolean for checkbox etc +-- @treturn boolean true if the value was set +function Gui._prototype:set_store(category,value) + if not self.store then return end + if self.categorize then + return Store.set_child(self.store,category,value) + else + return Store.set(self.store,category) + end +end + +--- Gets an element define give the uid, debug name or a copy of the element define +-- @tparam name ?string|table the uid, debug name or define for the element define to get +-- @tparam[opt] internal boolean when true the error trace is one level higher (used internally) +-- @treturn table the element define that was found or an error +function Gui.get_define(name,internal) + if type(name) == 'table' then + if name.name and Gui.defines[name.name] then + return Gui.defines[name.name] + end + end + + local define = Gui.defines[name] + + if not define and Gui.names[name] then + return Gui.defines[Gui.names[name]] + + elseif not define then + return error('Invalid name for element define, name not found.',internal and 3 or 2) or nil + + end + + return define +end + +--- Gets the value that is stored for a given element define, category needed if categorize function used +-- @tparam name ?string|table the uid, debug name or define for the element define to get +-- @tparam[opt] category string the category to get the value for +-- @treturn any the value that is stored for this define +function Gui.get_store(name,category) + local define = Gui.get_define(name,true) + return define:get_store(category) +end + +--- Sets the value stored for a given element define, category needed if categorize function used +-- @tparam name ?string|table the uid, debug name or define for the element define to set +-- @tparam[opt] category string the category to set the value for +-- @tparam value any the value to set for the define, must be valid for its type ie boolean for a checkbox +-- @treturn boolean true if the value was set +function Gui.set_store(name,category,value) + local define = Gui.get_define(name,true) + return define:get_store(category,value) +end + +--- A categorize function to be used with add_store, each player has their own value +-- @tparam element LuaGuiElement the element that will be converted to a string +-- @treturn string the player's name who owns this element +function Gui.player_store(element) + local player = Game.get_player_by_index(element.player_index) + return player.name +end + +--- A categorize function to be used with add_store, each force has its own value +-- @tparam element LuaGuiElement the element that will be converted to a string +-- @treturn string the player's force name who owns this element +function Gui.force_store(element) + local player = Game.get_player_by_index(element.player_index) + return player.force.name +end + +--- A categorize function to be used with add_store, each surface has its own value +-- @tparam element LuaGuiElement the element that will be converted to a string +-- @treturn string the player's surface name who owns this element +function Gui.surface_store(element) + local player = Game.get_player_by_index(element.player_index) + return player.surface.name +end + +--- Draws a copy of the element define to the parent element, see draw_to +-- @tparam name ?string|table the uid, debug name or define for the element define to draw +-- @tparam element LuaGuiEelement the parent element that it the define will be drawn to +-- @treturn LuaGuiElement the new element that was created +function Gui.draw(name,element,...) + local define = Gui.get_define(name,true) + return define:draw_to(element,...) +end + +--- Will toggle the enabled state of an element +-- @tparam element LuaGuiElement the gui element to toggle +function Gui.toggle_enable(element) + if not element or not element.valid then return end + if not element.enabled then + element.enabled = true + else + element.enabled = false + end +end + +--- Will toggle the visiblity of an element +-- @tparam element LuaGuiElement the gui element to toggle +function Gui.toggle_visible(element) + if not element or not element.valid then return end + if not element.visible then + element.visible = true + else + element.visible = false + end +end + +--- Sets the padding for a gui element +-- @tparam element LuaGuiElement the element to set the padding for +-- @tparam[opt=0] up number the amount of padding on the top +-- @tparam[opt=0] down number the amount of padding on the bottom +-- @tparam[opt=0] left number the amount of padding on the left +-- @tparam[opt=0] right number the amount of padding on the right +function Gui.set_padding(element,up,down,left,right) + local style = element.style + style.top_padding = up or 0 + style.bottom_padding = down or 0 + style.left_padding = left or 0 + style.right_padding = right or 0 +end + +return Gui \ No newline at end of file diff --git a/expcore/Gui/dropdown.lua b/expcore/Gui/dropdown.lua new file mode 100644 index 00000000..1737577e --- /dev/null +++ b/expcore/Gui/dropdown.lua @@ -0,0 +1,189 @@ +--- Gui class define for dropdowns and list boxs +--[[ +>>>> Functions + Dropdown.new_dropdown(name) --- Creates a new dropdown element define + Dropdown.new_list_box(name) --- Creates a new list box element define + + Dropdown._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + Dropdown._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + Dropdown._prototype:new_static_options(options,...) --- Adds new static options to the dropdown which will trigger the general callback + Dropdown._prototype:new_dynamic_options(callback) --- Adds a callback which should return a table of values to be added as options for the dropdown (appended after static options) + Dropdown._prototype:add_option_callback(option,callback) --- Adds a case specific callback which will only run when that option is selected (general case still triggered) + + Dropdown.select_value(element,value) --- Selects the option from a dropdown or list box given the value rather than key + Dropdown.get_selected_value(element) --- Returns the currently selected value rather than index + + Other functions present from expcore.gui.core +]] +local Gui = require 'expcore.gui.core' +local Game = require 'utils.game' + +--- Event call for on_selection_state_changed and store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new option for the dropdown +local function event_call(define,element,value) + local player = Game.get_player_by_index(element.player_index) + + if define.events.on_element_update then + define.events.on_element_update(player,element,value) + end + + if define.option_callbacks and define.option_callbacks[value] then + define.option_callbacks[value](player,element,value) + end +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new option for the dropdown +local _select_value +local function store_call(define,element,value) + _select_value(element,value) + event_call(define,element,value) +end + +local Dropdown = { + _prototype=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} + +--- Creates a new dropdown element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new dropdown element define +function Dropdown.new_dropdown(name) + + local self = Gui._define_factory(Dropdown._prototype) + self.draw_data.type = 'drop-down' + + if name then + self:debug_name(name) + end + + self.post_draw = function(element) + if self.dynamic_options then + local player = Game.get_player_by_index(element.player_index) + local dynamic_options = self.dynamic_options(player,element) + local items = element.items + for _,v in pairs(dynamic_options) do + table.insert(items,v) + end + element.items = items + end + + if self.store then + local category = self.categorize and self.categorize(element) or nil + local value = self:get_store(category) + if value then Dropdown.select_value(element,value) end + end + end + + Gui.on_selection_state_changed(self.name,function(event) + local element = event.element + local value = Dropdown.get_selected_value(element) + + if self.store then + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + else + event_call(self,element,value) + + end + + end) + + return self +end + +--- Creates a new list box element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new list box element define +function Dropdown.new_list_box(name) + local self = Dropdown.new_dropdown(name) + self.draw_data.type = 'list-box' + + return self +end + +--- Adds new static options to the dropdown which will trigger the general callback +-- @tparam options ?string|table either a table of option strings or the first option string, with a table values are the options +-- @tparam[opt] ... when options is not a table you can add the options one after each other +-- @tparam self the define to allow chaining +function Dropdown._prototype:new_static_options(options,...) + if type(options) == 'string' then + options = {options} + for _,v in pairs({...}) do + table.insert(options,v) + end + end + + self.options = options + self.draw_data.items = options + return self +end +Dropdown._prototype.add_options = Dropdown._prototype.new_static_options + +--- Adds a callback which should return a table of values to be added as options for the dropdown (appended after static options) +-- @tparam callback function the function that will run to get the options for the dropdown +-- callback param - player LuaPlayer - the player that the element is being drawn to +-- callback param - element LuaGuiElement - the element that is being drawn +-- callback return - table - the values of this table will be appended to the static options of the dropdown +-- @tparam self the define to allow chaining +function Dropdown._prototype:new_dynamic_options(callback) + if type(callback) ~= 'function' then + return error('Dynamic options callback must be a function',2) + end + self.dynamic_options = callback + return self +end +Dropdown._prototype.add_dynamic = Dropdown._prototype.new_dynamic_options + +--- Adds a case specific callback which will only run when that option is selected (general case still triggered) +-- @tparam option string the name of the option to trigger the callback on; if not already added then will be added as an option +-- @tparam callback function the function that will be called when that option is selected +-- callback param - player LuaPlayer - the player who owns the gui element +-- callback param - element LuaGuiElement - the element which is being effected +-- callback param - value string - the new option that has been selected +-- @tparam self the define to allow chaining +function Dropdown._prototype:add_option_callback(option,callback) + if not self.option_callbacks then self.option_callbacks = {} end + if not self.options then self.options = {} end + + self.option_callbacks[option] = callback + if not table.contains(self.options,option) then + table.insert(self.options,option) + end + + return self +end + +--- Selects the option from a dropdown or list box given the value rather than key +-- @tparam element LuaGuiElement the element that contains the option +-- @tparam value string the option to select from the dropdown +-- @treturn number the key where the value was +function Dropdown.select_value(element,value) + for k,item in pairs(element.items) do + if item == value then + element.selected_index = k + return k + end + end +end +_select_value = Dropdown.select_value + +--- Returns the currently selected value rather than index +-- @tparam element LuaGuiElement the gui element that you want to get the value of +-- @treturn string the value that is currently selected +function Dropdown.get_selected_value(element) + local index = element.selected_index + return element.items[index] +end + +return Dropdown \ No newline at end of file diff --git a/expcore/Gui/elem-button.lua b/expcore/Gui/elem-button.lua new file mode 100644 index 00000000..e976d3d5 --- /dev/null +++ b/expcore/Gui/elem-button.lua @@ -0,0 +1,111 @@ +--- Gui class defines for elem buttons +--[[ +>>>> Functions + ElemButton.new_elem_button(name) --- Creates a new elem button element define + + ElemButton._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + ElemButton._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + ElemButton._prototype:set_type(type) --- Sets the type of the elem button, the type is required so this must be called at least once + ElemButton._prototype:set_default(value) --- Sets the default value for the elem button, this may be a function or a string + + Other functions present from expcore.gui.core +]] +local Gui = require 'expcore.gui.core' +local Game = require 'utils.game' + +--- Event call for on_elem_changed and store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new value for the elem button +local function event_call(define,element,value) + local player = Game.get_player_by_index(element.player_index) + + if define.events.on_element_update then + define.events.on_element_update(player,element,value) + end + +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new value for the elem button +local function store_call(self,element,value) + element.elem_value = value + event_call(self,element,value) +end + +local ElemButton = { + _prototype=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} + +--- Creates a new elem button element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new elem button element define +function ElemButton.new_elem_button(name) + + local self = Gui._define_factory(ElemButton._prototype) + self.draw_data.type = 'choose-elem-button' + + if name then + self:debug_name(name) + end + + self.post_draw = function(element) + local player = Game.get_player_by_index(element.player_index) + + if type(self.default) == 'function' then + element.elem_value = self.default(player,element) + end + + if self.store then + local category = self.categorize and self.categorize(element) or nil + local value = self:get_store(category) + if value then element.elem_value = value end + end + end + + Gui.on_elem_changed(self.name,function(event) + local element = event.element + local value = element.elem_value + + if self.store then + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + else + event_call(self,element,value) + + end + + end) + + return self +end + +--- Sets the type of the elem button, the type is required so this must be called at least once +-- @tparam type string the type that this elem button is see factorio api +-- @treturn the element define to allow for chaining +function ElemButton._prototype:set_type(type) + self.draw_data.elem_type = type + return self +end + +--- Sets the default value for the elem button, this may be a function or a string +-- @tparam value ?string|function a string will be a static default and a function will be called when drawn to get the default +-- @treturn the element define to allow for chaining +function ElemButton._prototype:set_default(value) + self.default = value + if type(value) ~= 'function' then + self.draw_data[self.draw_data.elem_type] = value + end + return self +end + +return ElemButton \ No newline at end of file diff --git a/expcore/Gui/instances.lua b/expcore/Gui/instances.lua new file mode 100644 index 00000000..7e0fde9f --- /dev/null +++ b/expcore/Gui/instances.lua @@ -0,0 +1,227 @@ +--- This file is a breakout from core which forcues on instance management of defines +--[[ +>>>> Using registered instance groups + The main use of this module is to register a group of elements refered here as "instances of an element define" in which + is meant that you define the name of a group of drawn elements that are really just multiple versions of a single element. + For example this might be that you have one label in multiple places (either for one player or many) and you want to update + the caption of all of them at once; this is where this module comes it. + + First you must register the way that the intances are stored and under what name, using Instances.register you will give the + name of the collective group of instances followed by an optional categorise function which allows varients to be stored under one + name (like one for each force or player) + + -- categorise works in the same way as store categorise + -- so the function will worl here but no value is stored only gui elements + Instances.register('score',Gui.force_store) + + Then when you draw the new element to a gui you will want to add the element to the group: + + Instances.add_element('score',new_element) + + Then when you want to get the instances you have two options; Instances.get_elements or Instances.apply_to_elements when you want loop + over the elements it is more efficient to use apply_to_elements: + + Instances.get_elements('score','player') -- returns all elements that were added with the 'player' category + Instances.apply_to_elements('score','player',function(element) -- runs the function on every valid element + element.caption = 0 + end) + + Note that if you dont give a categorise function then you dont need to give a category when getting the elements. + +>>>> Using unregistered instance groups + When using a registered group and the functions that go with them it is much simpler to use and more importantly includes error checking + for valid instance group names; the down side is that the group must be registered which can only be done during startup and not during runtime. + To counter this there are two functions simlair to those above in order to add and get instances but may lead to errors not being noticed due to + the error interal error checking being skiped to allow it to work. + + The main difference between the two groups of functions is that the category must always be present even if is nil; example below shows how a + instance group would work when registered vs unregistered: + + -- Registered with category + Instances.register('score',Gui.force_store) -- force_store will return the force name of an element + Instances.add_element('score',new_element) -- the new element is added to the category based on in force + Instances.apply_to_elements('score','player',function(element) + element.caption = '0' + end) -- gets all instances from the player force and sets the caption to 0 + + -- Unregistered with category + Instances.unregistered_add_element('score','player',new_element) -- adds the new element to the player category + Instances.unregistered_apply_to_elements('score','player',function(element) + element.caption = '0' + end) -- gets all instances from the player force and sets the caption to 0 + + -- Registered without category; note that category can just be igroned + Instances.register('score') -- all instances will be under one group with no categories + Instances.add_element('score',new_element) -- adds the new element to the instance list + Instances.apply_to_elements('score',function(element) + element.caption = '0' + end) -- gets all instances and sets the element caption to 0 + + -- Unregistered without category; note that category must be given as nil + Instances.unregistered_add_element('score',nil,new_element) -- adds the new element to a single group with no categories + Instances.unregistered_apply_to_elements('score',nil,function(element) + element.caption = '0' + end) -- gets all instances and sets the element caption to 0 + +>>>> Functions + Instances.has_categories(name) --- Returns if a instnace group has a categorise function; must be registerd + Instances.is_registered(name) --- Returns if the given name is a registered instance group + Instances.register(name,categorise) --- Registers the name of an instance group to allow for storing element instances + + Instances.add_element(name,element) --- Adds an element to the instance group under the correct category; must be registered + Instances.get_elements_raw(name,category) --- Gets all element instances without first removing any invalid ones; used internally and must be registered + Instances.get_valid_elements(name,category,callback) --- Gets all valid element instances and has the option of running a callback on those that are valid + + Instances.unregistered_add_element(name,category,element) --- A version of add_element that does not require the group to be registered + Instances.unregistered_get_elements(name,category,callback) --- A version of get_elements that does not require the group to be registered +]] +local Global = require 'utils.global' + +local Instances = { + categorise={}, + data={} +} +Global.register(Instances.data,function(tbl) + Instances.data = tbl +end) + +--- Returns if a instnace group has a categorise function; must be registerd +-- @tparam name string the name of the instance group +-- @treturn boolean true if there is a categorise function +function Instances.has_categories(name) + return type(Instances.categorise[name]) == 'function' +end + +--- Returns if the given name is a registered instance group +-- @tparam name string the name of the instance group you are testing +-- @treturn boolean true if the name is registered +function Instances.is_registered(name) + return Instances.categorise[name] ~= nil +end + +--- Registers the name of an instance group to allow for storing element instances +-- @tparam name string the name of the instance group; must to unique +-- @tparam[opt] categorise function function used to turn the element into a string +-- categorise param - element LuaGuiElement - the gui element to be turned into a string +-- categorise return - string - the category that the element will be added to like the player's name or force's name +-- @treturn string the name that was added so it can be used as a varible +function Instances.register(name,categorise) + if _LIFECYCLE ~= _STAGE.control then + return error('Can only be called during the control stage', 2) + end + + if Instances.categorise[name] then + return error('Instances for '..name..' already exist.',2) + end + + categorise = type(categorise) == 'function' and categorise or true + + Instances.data[name] = {} + Instances.categorise[name] = categorise + + return name +end + +--- Adds an element to the instance group under the correct category; must be registered +-- @tparam name string the name of the instance group to add the element to +-- @tparam element LuaGuiElement the element to add the the instance group +function Instances.add_element(name,element) + if not Instances.categorise[name] then + return error('Inavlid name for instance group: '..name,2) + end + + if Instances.has_categories(name) then + local category = Instances.categorise[name](element) + if not Instances.data[name][category] then Instances.data[name][category] = {} end + table.insert(Instances.data[name][category],element) + else + table.insert(Instances.data[name],element) + end +end + +--- Gets all element instances without first removing any invalid ones; used internally and must be registered +-- @tparam name string the name of the instance group to get the instances of +-- @tparam[opt] category string the category to get the instance from, not needed when no categorise function +-- @treturn table the table of element instances of which some may be invalid +function Instances.get_elements_raw(name,category) + if not Instances.categorise[name] then + return error('Inavlid name for instance group: '..name,2) + end + + if Instances.has_categories(name) then + return Instances.data[name][category] or {} + else + return Instances.data[name] + end +end + +--- Gets all valid element instances and has the option of running a callback on those that are valid +-- @tparam name string the name of the instance group to get the instances of +-- @tparam[opt] category string the category to get the instances of, not needed when no categorise function +-- @tparan[opt] callback function when given the callback will be ran on all valid elements +-- callback param - element LuaGuiElement - the current valid element +-- @treturn table the table of element instances with all invalid ones removed +function Instances.get_valid_elements(name,category,callback) + if not Instances.categorise[name] then + return error('Inavlid name for instance group: '..name,2) + end + + category = category or callback + local elements = Instances.get_elements_raw(name,category) + local categorise = Instances.has_categories(name) + + for key,element in pairs(elements) do + if not element or not element.valid then + elements[key] = nil + else + if categorise and callback then callback(element) + elseif category then category(element) end + end + end + + return elements +end +Instances.get_elements = Instances.get_valid_elements +Instances.apply_to_elements = Instances.get_valid_elements + +--- A version of add_element that does not require the group to be registered +-- @tparam name string the name of the instance group to add the element to +-- @tparam category ?string|nil the category to add the element to, can be nil but must still be given +-- @tparam element LuaGuiElement the element to add to the instance group +function Instances.unregistered_add_element(name,category,element) + if not Instances.data[name] then Instances.data[name] = {} end + if category then + if not Instances.data[name][category] then Instances.data[name][category] = {} end + table.insert(Instances.data[name][category],element) + else + table.insert(Instances.data[name],element) + end +end + +--- A version of get_elements that does not require the group to be registered +-- @tparam name string the name of the instance group to get the instances of +-- @tparam category ?string|nil the category to get the instances of, can be nil but must still be given +-- @tparam[opt] callback function when given will be called on all valid instances +-- callback param - element LuaGuiElement - the current valid element +-- @treturn table the table of element instances with all invalid ones removed +function Instances.unregistered_get_elements(name,category,callback) + local elements = Instances.data[name] + if elements and category then + elements = elements[category] + end + + if not elements then return {} end + + for key,element in pairs(elements) do + if not element or not element.valid then + elements[key] = nil + else + if callback then callback(element) end + end + end + + return elements +end +Instances.unregistered_apply_to_elements = Instances.runtime_get_elements + +return Instances \ No newline at end of file diff --git a/expcore/Gui/left.lua b/expcore/Gui/left.lua new file mode 100644 index 00000000..da7f37e4 --- /dev/null +++ b/expcore/Gui/left.lua @@ -0,0 +1,290 @@ +--- Gui structure define for left frames +--[[ +>>>> Example formating + + -- first we add config that relates to the button on the toolbar, all normal button functions are present + local left_frame = + Gui.new_left_frame('test-left-frame') + :set_caption('Test Left Gui') + :set_post_authenticator(function(player,button_name) + return global.show_test_gui + end) + + -- then we add the config for the left frame, on_draw should draw the gui from an empty frame, on_update should take a frame from on_draw on edit it + :set_open_by_default() + :on_draw(function(_player,frame) + for _,player in pairs(game.connected_players) do + frame.add{ + type='label', + caption=player.name + } + end + end) + + -- now we can use the action factory to call events on the gui, actions are: 'update', 'update_all', 'redraw', 'redraw_all' + Event.add(defines.events.on_player_joined_game,left_frame 'update_all') + Event.add(defines.events.on_player_left_game,left_frame 'update_all') + +>>>> Functions + LeftFrames.get_flow(player) --- Gets the left frame flow for a player + LeftFrames.get_frame(name,player) --- Gets one frame from the left flow by its name + LeftFrames.get_open(player) --- Gets all open frames for a player, if non are open it will remove the close all button + LeftFrames.toggle_frame(name,player,state) --- Toggles the visiblty of a left frame, or sets its visiblty state + + LeftFrames.new_frame(permision_name) --- Creates a new left frame define + LeftFrames._prototype:set_open_by_default(state) --- Sets if the frame is visible when a player joins, can also be a function to return a boolean + LeftFrames._prototype:get_frame(player) --- Gets the frame for this define from the left frame flow + LeftFrames._prototype:is_open(player) --- Returns if the player currently has this define visible + LeftFrames._prototype:toggle(player) --- Toggles the visiblty of the left frame + + LeftFrames._prototype:update(player) --- Updates the contents of the left frame, first tries update callback, oter wise will clear and redraw + LeftFrames._prototype:update_all(update_offline) --- Updates the frame for all players, see update + LeftFrames._prototype:redraw(player) --- Redraws the frame by calling on_draw, will always clear the frame + LeftFrames._prototype:redraw_all(update_offline) --- Redraws the frame for all players, see redraw + + LeftFrames._prototype:on_draw(player,frame) --- Use to draw your elements to the new frame + LeftFrames._prototype:on_update(player,frame) --- Use to edit your frame when there is no need to redraw it + LeftFrames._prototype:event_handler(action) --- Creates an event handler that will trigger one of its functions, use with Event.add +]] +local Gui = require 'expcore.gui.core' +local Toolbar = require 'expcore.gui.toolbar' +local Buttons = require 'expcore.gui.buttons' +local mod_gui = require 'mod-gui' +local Game = require 'utils.game' +local Event = require 'utils.event' + +local LeftFrames = { + frames={}, + _prototype=Gui._prototype_factory{ + on_draw = Gui._event_factory('on_draw'), + on_update = Gui._event_factory('on_update') + } +} +setmetatable(LeftFrames._prototype, { + __index = Buttons._prototype +}) + +--- Gets the left frame flow for a player +-- @tparam player LuaPlayer the player to get the flow of +-- @treturn LuaGuiElement the left frame flow for the player +function LeftFrames.get_flow(player) + player = Game.get_player_from_any(player) + return mod_gui.get_frame_flow(player) +end + +--- Gets one frame from the left flow by its name +-- @tparam name string the name of the gui frame to get +-- @tparam player LuaPlayer the player to get the frame of +-- @treturn LuaGuiElement the frame in the left frame flow with that name +function LeftFrames.get_frame(name,player) + local define = LeftFrames.frames[name] + if not define then + return error('Left Frame '..name..' is not defined.',2) + end + return define:get_frame(player) +end + +--- Gets all open frames for a player, if non are open it will remove the close all button +-- @tparam player LuaPlayer the player to get the flow of +-- @treturn table contains all the open (and registered) frames for the player +function LeftFrames.get_open(player) + local open = {} + local flow = LeftFrames.get_flow(player) + + for _,define in pairs(LeftFrames.frames) do + if define:is_open(player) then + table.insert(open,define) + end + end + + flow[LeftFrames.toggle_button.name].visible = #open ~= 0 + + return open +end + +--- Toggles the visiblty of a left frame, or sets its visiblty state +-- @tparam name string the name of the gui frame to toggle +-- @tparam player LuaPlayer the player to get the frame of +-- @tparam[opt] state boolean when given will be the state that the visiblty is set to +-- @treturn boolean the new state of the visiblity +function LeftFrames.toggle_frame(name,player,state) + local define = LeftFrames.frames[name] + if not define then + return error('Left Frame '..name..' is not defined.',2) + end + + local frame = LeftFrames.get_frame(name,player) + if state ~= nil then + frame.visible = state + else + Gui.toggle_visible(frame) + end + + LeftFrames.get_open(player) + + return frame.visible +end + +--- Creates a new left frame define +-- @tparam permision_name string the name that can be used with the permision system +-- @treturn table the new left frame define +function LeftFrames.new_frame(permision_name) + + local self = Toolbar.new_button(permision_name) + + local mt = getmetatable(self) + mt.__index = LeftFrames._prototype + mt.__call = self.event_handler + + self:on_click(function(player,_element) + self:toggle(player) + end) + + LeftFrames.frames[self.name] = self + + return self +end + +--- Sets if the frame is visible when a player joins, can also be a function to return a boolean +-- @tparam[opt=true] state ?boolean|function the default state of the visiblty, can be a function +-- state param - player LuaPlayer - the player that has joined the game +-- state param - define_name string - the define name for the frame +-- state return - boolean - false will hide the frame +function LeftFrames._prototype:set_open_by_default(state) + if state == false then + self.open_by_default = false + else + self.open_by_default = state + end + return self +end + +--- Gets the frame for this define from the left frame flow +-- @tparam player LuaPlayer the player to get the frame of +-- @treturn LuaGuiElement the frame in the left frame flow for this define +function LeftFrames._prototype:get_frame(player) + local flow = LeftFrames.get_flow(player) + if flow[self.name] and flow[self.name].valid then + return flow[self.name] + end +end + +--- Returns if the player currently has this define visible +-- @tparam player LuaPlayer the player to get the frame of +-- @treturn boolean true if it is open/visible +function LeftFrames._prototype:is_open(player) + local frame = self:get_frame(player) + return frame and frame.visible or false +end + +--- Toggles the visiblty of the left frame +-- @tparam player LuaPlayer the player to toggle the frame of +-- @treturn boolean the new state of the visiblity +function LeftFrames._prototype:toggle(player) + local frame = self:get_frame(player) + Gui.toggle_visible(frame) + LeftFrames.get_open(player) + return frame.visible +end + +--- Updates the contents of the left frame, first tries update callback, oter wise will clear and redraw +-- @tparam player LuaPlayer the player to update the frame of +function LeftFrames._prototype:update(player) + local frame = self:get_frame(player) + if self.events.on_update then + self.events.on_update(player,frame) + elseif self.events.on_draw then + frame.clear() + self.events.on_draw(player,frame) + end +end + +--- Updates the frame for all players, see update +-- @tparam[opt=false] update_offline boolean when true will update the frame for offline players +function LeftFrames._prototype:update_all(update_offline) + local players = update_offline == true and game.players or game.connected_players + for _,player in pairs(players) do + self:update(player) + end +end + +--- Redraws the frame by calling on_draw, will always clear the frame +-- @tparam player LuaPlayer the player to update the frame of +function LeftFrames._prototype:redraw(player) + local frame = self:get_frame(player) + frame.claer() + if self.events.on_draw then + self.events.on_draw(player,frame) + end +end + +--- Redraws the frame for all players, see redraw +-- @tparam[opt=false] update_offline boolean when true will update the frame for offline players +function LeftFrames._prototype:redraw_all(update_offline) + local players = update_offline == true and game.players or game.connected_players + for _,player in pairs(players) do + self:redraw(player) + end +end + +--- Creates an event handler that will trigger one of its functions, use with Event.add +-- @tparam[opt=update] action string the action to take on this event +function LeftFrames._prototype:event_handler(action) + action = action or 'update' + return function(event) + local player = Game.get_player_by_index(event.player_index) + self[action](self,player) + end +end + +LeftFrames.toggle_button = +Buttons.new_button() +:set_tooltip('Close Windows') +:set_caption('<') +:on_click(function(player,element) + for _,define in pairs(LeftFrames.frames) do + local frame = LeftFrames.get_frame(define.name,player) + frame.visible = false + end + element.visible = false +end) + +Event.add(defines.events.on_player_created,function(event) + local player = Game.get_player_by_index(event.player_index) + local flow = LeftFrames.get_flow(player) + + local close_button = LeftFrames.toggle_button(flow) + Gui.set_padding(close_button) + local style = close_button.style + style.width = 18 + style.height = 36 + style.font = 'default-small-bold' + + for _,define in pairs(LeftFrames.frames) do + local frame = flow.add{ + type='frame', + name=define.name + } + + if define.events.on_draw then + define.events.on_draw(player,frame) + end + + if define.open_by_default == false then + frame.visible = false + elseif type(define.open_by_default) == 'function' then + if not define.open_by_default(player,define.name) then + frame.visible = false + end + end + + if not Toolbar.allowed(player,define.name) then + frame.visible = false + end + + end + + LeftFrames.get_open(player) +end) + +return LeftFrames \ No newline at end of file diff --git a/expcore/Gui/popups.lua b/expcore/Gui/popups.lua new file mode 100644 index 00000000..fa0c0668 --- /dev/null +++ b/expcore/Gui/popups.lua @@ -0,0 +1,228 @@ +--- Gui structure define for popup gui +--[[ +>>>> Functions + PopupFrames.get_flow(player) --- Gets the left flow that contains the popup frames + PopupFrames.open(define_name,player,open_time,...) --- Opens a popup for the player, can give the amount of time it is open as well as params for the draw function + + PopupFrames.close_progress --- Progress bar which when depleaded will close the popup frame + PopupFrames.close_button --- A button which can be used to close the gui before the timer runs out + + PopupFrames.new_popup(name) --- Creates a new popup frame define + PopupFrames._prototype:set_default_open_time(amount) --- Sets the default open time for the popup, will be used if non is provided with open + PopupFrames._prototype:open(player,open_time,...) --- Opens this define for a player, can be given open time and any other params for the draw function +]] +local Gui = require 'expcore.gui.core' +local Game = require 'utils.game' +local Event = require 'utils.event' +local ProgressBar = require 'expcore.gui.progress-bar' +local Button = require 'expcore.gui.buttons' +local mod_gui = require 'mod-gui' +local Color = require 'resources.color_presets' +local Global = require 'utils.global' + +local PopupFrames = { + paused_popups={}, + popup_flow_name = Gui.uid_name(), + main_frame_name = Gui.uid_name(), + close_frame_name = Gui.uid_name(), + _prototype = Gui._prototype_factory{ + on_draw = Gui._event_factory('on_draw') + } +} +Global.register(PopupFrames.paused_popups,function(tbl) + PopupFrames.paused_popups = tbl +end) + +--- Sets the state of the element in the pasued list, nil or true +-- @tparam element LuaGuiElement the element to set the state of +-- @tparam[opt] state boolean the state to set it to, true will pause the the progress bar +local function set_pasued_state(element,state) + local name = element.player_index..':'..element.index + PopupFrames.paused_popups[name] = state +end + +--- Gets the state of the element in the pasued list, nil or true +-- @tparam element LuaGuiElement the element to get the state of +local function get_pasued_state(element) + local name = element.player_index..':'..element.index + return PopupFrames.paused_popups[name] +end + +--- Gets the left flow that contains the popup frames +-- @tparam player LuaPlayer the player to get the flow for +-- @treturn LuaGuiElement the left flow that contains the popup frames +function PopupFrames.get_flow(player) + player = Game.get_player_from_any(player) + local flow = mod_gui.get_frame_flow(player) + return flow[PopupFrames.popup_flow_name] +end + +--- Opens a popup for the player, can give the amount of time it is open as well as params for the draw function +-- @tparam define_name string the name of the define that you want to open for the player +-- @tparam player LuaPlayer the player to open the popup for +-- @tparam[opt] open_time number the minimum number of ticks you want the popup open for, 0 means no limit, nil will take default +-- @tparam ... any the other params that you want to pass to your on_draw event +-- @treturn LuaGuiElement the frame that was drawn, the inner gui flow which contains the content +function PopupFrames.open(define_name,player,open_time,...) + local define = Gui.get_define(define_name,true) + player = Game.get_player_from_any(player) + return define:open(player,open_time,...) +end + +--- Closes the popup, is called by progress bar and close button +-- @tparam element LuaGuiElement either the progress bar or the close button +local function close_popup(element) + local frame = element.parent.parent.parent + if not frame or not frame.valid then return end + set_pasued_state(element.parent[PopupFrames.close_progress:uid()]) + frame.destroy() +end + +--- Progress bar which when depleaded will close the popup frame +PopupFrames.close_progress = +ProgressBar.new_progressbar() +:use_count_down() +:set_tooltip('Pause/Resume Auto-close') +:on_complete(function(player,element) + close_popup(element) +end) + +--- A button which can be used to close the gui before the timer runs out +PopupFrames.close_button = +Button.new_button() +:set_sprites('utility/close_white') +:set_tooltip('Close Popup') +:on_click(function(player,element) + close_popup(element) +end) + +--- When the progress bar is clicked it will pause its progress, or resume if previously paused +Gui.on_click(PopupFrames.close_progress:uid(),function(event) + local element = event.element + if get_pasued_state(element) then + set_pasued_state(element) + else + set_pasued_state(element,true) + end +end) + +--- When the parent flow of the progress bar is clicked it will pause its progress, or resume if previously paused +Gui.on_click(PopupFrames.close_frame_name,function(event) + local element = event.element[PopupFrames.close_progress:uid()] + if get_pasued_state(element) then + set_pasued_state(element) + else + set_pasued_state(element,true) + end +end) + +--- Creates a new popup frame define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new popup frame define +function PopupFrames.new_popup(name) + local self = Gui._define_factory(PopupFrames._prototype) + self.draw_data.type = 'flow' + self.draw_data.direction = 'vertical' + + if name then + self:debug_name(name) + end + + local mt = getmetatable(self) + mt.__call = function(tbl,player,open_time,...) + return tbl:open(player,open_time,...) + end + + self.post_draw = function(element,maximum,...) + -- main content frame + local frame = element.add{ + type='flow', + name=PopupFrames.main_frame_name + } + frame.style.horizontally_stretchable = true + + -- flow for progress bar and close button + local close_flow = element.add{ + type='flow', + name=PopupFrames.close_frame_name + } + close_flow.style.horizontally_stretchable = true + + -- progress bar, when 0 then a static full one is drawn + local progress_style + if maximum == 0 then + progress_style = close_flow.add{ + type='progressbar', + tooltip='No Auto-close', + value=1 + }.style + else + progress_style = PopupFrames.close_progress(close_flow,maximum).style + end + progress_style.top_padding = 6 + progress_style.bottom_padding = 3 + progress_style.height = 11 + progress_style.color = Color.grey + + -- close button, will close the popup when clicked + local close_button = PopupFrames.close_button(close_flow) + Gui.set_padding(close_button) + local close_button_style = close_button.style + close_button_style.width = 20 + close_button_style.height = 20 + + -- event trigger to draw the gui content + if self.events.on_draw then + local player = Game.get_player_by_index(element.player_index) + self.events.on_draw(player,frame,...) + end + end + + return self +end + +--- Sets the default open time for the popup, will be used if non is provided with open +-- @tparam amount number the number of ticks, by default, the popup will be open for +-- @treturn table the define to allow for chaining +function PopupFrames._prototype:set_default_open_time(amount) + self.default_open_time = amount + return self +end + +--- Opens this define for a player, can be given open time and any other params for the draw function +-- @tparam player LuaPlayer the player to open the popup for +-- @tparam[opt] open_time number the minimum number of ticks you want the popup open for, 0 means no limit, nil will take default +-- @tparam ... any the other params that you want to pass to your on_draw event +-- @treturn LuaGuiElement the frame that was drawn, the inner gui flow which contains the content +function PopupFrames._prototype:open(player,open_time,...) + open_time = open_time or self.default_open_time or 0 + player = Game.get_player_from_any(player) + + local flow = PopupFrames.get_flow(player) + local frame = flow.add{ + type='frame', + style='blurry_frame' + } + + Gui.set_padding(frame,3,3,4,4) + return self:draw_to(frame,open_time,...)[PopupFrames.main_frame_name] +end + +--- When player is first created the popup flow is added to they left flow +Event.add(defines.events.on_player_created,function(event) + local player = Game.get_player_by_index(event.player_index) + local flow = mod_gui.get_frame_flow(player) + + flow.add{ + type='flow', + direction='vertical', + name=PopupFrames.popup_flow_name + } +end) + +--- Every tick any, not pasued, progress bars will go down by one tick +Event.add(defines.events.on_tick,PopupFrames.close_progress:event_countdown(function(element) + return not get_pasued_state(element) +end)) + +return PopupFrames \ No newline at end of file diff --git a/expcore/Gui/progress-bar.lua b/expcore/Gui/progress-bar.lua new file mode 100644 index 00000000..6944fb8e --- /dev/null +++ b/expcore/Gui/progress-bar.lua @@ -0,0 +1,390 @@ +--- Gui element define for progess bars +--[[ +>>>> Functions + ProgressBar.set_maximum(element,amount,count_down) --- Sets the maximum value that represents the end value of the progress bar + ProgressBar.increment(element,amount) --- Increases the value of the progressbar, if a define is given all of its instances are incremented + ProgressBar.decrement(element,amount) --- Decreases the value of the progressbar, if a define is given all of its instances are decresed + + ProgressBar.new_progressbar(name) --- Creates a new progressbar element define + ProgressBar._prototype:set_maximum(amount,count_down) --- Sets the maximum value that represents the end value of the progress bar + ProgressBar._prototype:use_count_down(state) --- Will set the progress bar to start at 1 and trigger when it hits 0 + ProgressBar._prototype:increment(amount,category) --- Increases the value of the progressbar + ProgressBar._prototype:increment_filtered(amount,filter) --- Increases the value of the progressbar, if the filter condition is met, does not work with store + ProgressBar._prototype:decrement(amount,category) --- Decreases the value of the progressbar + ProgressBar._prototype:decrement_filtered(amount,filter) --- Decreases the value of the progressbar, if the filter condition is met, does not work with store + ProgressBar._prototype:add_element(element,maximum) --- Adds an element into the list of instances that will are waiting to complete, does not work with store + ProgressBar._prototype:reset_element(element) --- Resets an element, or its store, to be back at the start, either 1 or 0 + + ProgressBar._prototype:on_complete(callback) --- Triggers when a progress bar element compeltes (hits 0 or 1) + ProgressBar._prototype:on_complete(callback) --- Triggers when a store value completes (hits 0 or 1) + ProgressBar._prototype:event_counter(filter) --- Event handler factory that counts up by 1 every time the event triggeres, can filter which elements are incremented + ProgressBar._prototype:event_countdown(filter) --- Event handler factory that counts down by 1 every time the event triggeres, can filter which elements are decremented +]] +local Gui = require 'expcore.gui.core' +local Global = require 'utils.global' +local Game = require 'utils.game' + +--- Event call for when the value is outside the range 0-1 +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +local function event_call(define,element) + local player = Game.get_player_by_index(element.player_index) + + if define.events.on_complete then + define.events.on_complete(player,element,function() + define:add_element(element) + define:reset_element(element) + end) + end +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +local function store_call(define,element,value) + if value then + element.value = value + if define.count_down and value <= 0 + or not define.count_down and value >= 1 then + event_call(define,element) + end + end +end + +local ProgressBar = { + unregistered={}, -- elements with no callbacks + independent={}, -- elements with a link to a deinfe + _prototype=Gui._prototype_factory{ + -- note both events will recive a reset function that can be used to reset the progress of the element/store + on_complete = Gui._event_factory('on_complete'), + on_store_complete = Gui._event_factory('on_store_complete'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} +Global.register({ + ProgressBar.unregistered, + ProgressBar.independent +},function(tbl) + ProgressBar.unregistered = tbl[1] + ProgressBar.independent = tbl[2] +end) + +--- Gets the define data, cant use Gui.get_define as it would error +-- @tparam define ?table|string the define to get +-- @treturn table the define or nil +local function get_define(define) + if type(define) == 'table' then + if define.name and Gui.defines[define.name] then + return Gui.defines[define.name] + end + end + + return Gui.defines[define] +end + +--- Gets the element data, used when there is no define +-- @tparam element LuaGuiElement +-- @treturn table the element data simialr to define +local function get_element(element) + if not element.valid then return end + local name = element.player_index..':'..element.index + + if ProgressBar.unregistered[name] then + return ProgressBar.unregistered[name] + end +end + +--- Sets the maximum value that represents the end value of the progress bar +-- @tparam element ?LuaGuiElement|string either a gui element or a registered define +-- @tparam amount number the amount to have set as the maximum +function ProgressBar.set_maximum(element,amount) + amount = amount > 0 and amount or error('amount must be greater than 0') + + local define = get_define(element) + if define then + define:set_deafult_maximum(amount) + + else + local element_data = get_element(element) + + if element_data then + element_data.maximum = amount + + else + local name = element.player_index..':'..element.index + ProgressBar.unregistered[name] = { + element=element, + maximum=amount or 1 + } + + end + + end +end + +--- Increases the value of the progressbar, if a define is given all of its instances are incremented +-- @tapram element ?LuaGuiElement|string either a gui element or a registered define +-- @tparam[opt=1] amount number the amount to increase the progressbar by +function ProgressBar.increment(element,amount) + amount = type(amount) == 'number' and amount or 1 + + local define = get_define(element) + if define then + define:increment(amount) + + else + local element_data = get_element(element) + + if element_data then + local real_amount = amount/element_data.maximum + element.value = element.value + real_amount + + if element.value >= 1 then + local name = element.player_index..':'..element.index + ProgressBar.unregistered[name] = nil + return true + end + end + + end +end + +--- Decreases the value of the progressbar, if a define is given all of its instances are decresed +-- @tapram element ?LuaGuiElement|string either a gui element or a registered define +-- @tparam[opt=1] amount number the amount to decrease the progressbar by +function ProgressBar.decrement(element,amount) + amount = type(amount) == 'number' and amount or 1 + + local define = get_define(element) + if define then + define:decrement(amount) + + else + local element_data = get_element(element) + + if element_data then + local real_amount = amount/element_data.maximum + element.value = element.value - real_amount + + if element.value <= 0 then + local name = element.player_index..':'..element.index + ProgressBar.unregistered[name] = nil + return true + end + end + + end +end + +--- Creates a new progressbar element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new progressbar elemente define +function ProgressBar.new_progressbar(name) + local self = Gui._define_factory(ProgressBar._prototype) + self.draw_data.type = 'progressbar' + + if name then + self:debug_name(name) + end + + self.post_draw = function(element,maximum) + if self.store then + local category = self.categorize and self.categorize(element) or nil + local value = self:get_store(category) + if not value then + value = self.count_down and 1 or 0 + self:set_store(category,value) + end + element.value = value + + else + if self.count_down then + element.value = 1 + end + + if not ProgressBar.independent[self.name] then + ProgressBar.independent[self.name] = {} + end + + table.insert(ProgressBar.independent[self.name],{ + element = element, + maximum = maximum + }) + + end + + end + + return self +end + +--- Sets the maximum value that represents the end value of the progress bar +-- @tparam amount number the amount to have set as the maximum +-- @treturn table the define to allow chaining +function ProgressBar._prototype:set_default_maximum(amount) + amount = amount > 0 and amount or error('amount must be greater than 0') + self.default_maximum = amount + return self +end + +--- Will set the progress bar to start at 1 and trigger when it hits 0 +-- @tparam[opt=true] state boolean when true the bar will start filled, to be used with decrease +-- @treturn table the define to allow chaining +function ProgressBar._prototype:use_count_down(state) + if state == false then + self.count_down = false + else + self.count_down = true + end + return self +end + +--- Main logic for changing the value of a progress bar, this only applies when its a registered define +-- @tparam self table the define that is being changed +-- @tparam amount number the amount which it is being changed by, may be negative +-- @tparam[opt] category string the category to use with store +local function change_value_prototype(self,amount,category,filter) + + local function reset_store() + local value = self.count_down and 1 or 0 + local _category = category or value + self:set_store(_category,value) + end + + if self.store then + local value = self:get_store(category) or self.count_down and 1 or 0 + local maximum = self.default_maximum or 1 + local new_value = value + (amount/maximum) + + if self.count_down and new_value <= 0 + or not self.count_down and new_value >= 1 then + self:set_store(category) + + if self.events.on_store_complete then + category = category or reset_store + self.events.on_store_complete(category,reset_store) + end + end + + category = category or new_value + self:set_store(category,new_value) + + return + end + + if ProgressBar.independent[self.name] then + for key,element_data in pairs(ProgressBar.independent[self.name]) do + local element = element_data.element + if not element or not element.valid then + ProgressBar.independent[self.name][key] = nil + + else + if not filter or filter(element) then + local maximum = element_data.maximum or self.default_maximum or 1 + element.value = element.value + (amount/maximum) + + if self.count_down and element.value <= 0 + or not self.count_down and element.value >= 1 then + ProgressBar.independent[self.name][key] = nil + event_call(self,element) + end + end + + end + end + end + +end + +--- Increases the value of the progressbar +-- @tparam[opt=1] amount number the amount to increase the progressbar by +-- @tparam[opt] category string the category that is used with a store +function ProgressBar._prototype:increment(amount,category) + amount = type(amount) == 'number' and amount or 1 + change_value_prototype(self,amount,category) +end + +--- Increases the value of the progressbar, if the filter condition is met, does not work with store +-- @tparam[opt=1] amount number the amount to increase the progressbar by +-- @tparam[opt] category string the category that is used with a store +function ProgressBar._prototype:increment_filtered(amount,filter) + amount = type(amount) == 'number' and amount or 1 + change_value_prototype(self,amount,nil,filter) +end + +--- Decreases the value of the progressbar +-- @tparam[opt=1] amount number the amount to decrease the progressbar by +-- @tparam[opt] category string the category that is used with a store +function ProgressBar._prototype:decrement(amount,category) + amount = type(amount) == 'number' and amount or 1 + change_value_prototype(self,-amount,category) +end + +--- Decreases the value of the progressbar, if the filter condition is met, does not work with store +-- @tparam[opt=1] amount number the amount to decrease the progressbar by +-- @tparam[opt] category string the category that is used with a store +function ProgressBar._prototype:decrement_filtered(amount,filter) + amount = type(amount) == 'number' and amount or 1 + change_value_prototype(self,-amount,nil,filter) +end + +--- Adds an element into the list of instances that will are waiting to complete, does not work with store +-- note use store if you want persistent data, this only stores the elements not the values which they have +-- @tparam element LuaGuiElement the element that you want to add into the waiting to complete list +-- @tparam[opt] maximum number the maximum for this element if not given the default for this define is used +function ProgressBar._prototype:add_element(element,maximum) + if self.store then return end + if not ProgressBar.independent[self.name] then + ProgressBar.independent[self.name] = {} + end + table.insert(ProgressBar.independent[self.name],{ + element = element, + maximum = maximum + }) +end + +--- Resets an element, or its store, to be back at the start, either 1 or 0 +-- @tparam element LuaGuiElement the element that you want to reset the progress of +function ProgressBar._prototype:reset_element(element) + if not element or not element.valid then return end + local value = self.count_down and 1 or 0 + if self.store then + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + else + element.value = value + end +end + +--- Event handler factory that counts up by 1 every time the event triggeres, can filter which elements are incremented +-- @tparam[opt] filter function when given will use filtered incerement +-- @treturn function the event handler +function ProgressBar._prototype:event_counter(filter) + if type(filter) == 'function' then + return function() + self:increment_filtered(1,filter) + end + else + return function() + self:increment() + end + end +end + +--- Event handler factory that counts down by 1 every time the event triggeres, can filter which elements are decremented +-- @tparam[opt] filter function when given will use filtered decerement +-- @treturn function the event handler +function ProgressBar._prototype:event_countdown(filter) + if type(filter) == 'function' then + return function() + self:decrement_filtered(1,filter) + end + else + return function() + self:decrement() + end + end +end + +return ProgressBar \ No newline at end of file diff --git a/expcore/Gui/slider.lua b/expcore/Gui/slider.lua new file mode 100644 index 00000000..7c0df3ee --- /dev/null +++ b/expcore/Gui/slider.lua @@ -0,0 +1,187 @@ +--- Gui class define for silders +--[[ +>>>> Functions + Slider.new_slider(name) --- Creates a new slider element define + + Slider._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + Slider._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + Slider._prototype:use_notches(state) --- Adds notches to the slider + Slider._prototype:set_range(min,max) --- Sets the range of a slider, if not used will use default values for a slider + Slider._prototype:draw_label(element) --- Draws a new label and links its value to the value of this slider, if no store then it will only show one value per player + Slider._prototype:enable_auto_draw_label(state) --- Enables auto draw of the label, the label will share the same parent element as the slider + + Other functions present from expcore.gui.core +]] +local Gui = require 'expcore.gui.core' +local Instances = require 'expcore.gui.instances' +local Game = require 'utils.game' + +--- Event call for on_value_changed and store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value number the new value for the slider +local function event_call(define,element,value) + local player = Game.get_player_by_index(element.player_index) + + local min,max = element.get_slider_minimum(),element.get_slider_maximum() + local delta = max-min + local percent = delta == 0 and 0 or (value-min)/delta + + if define.events.on_element_update then + define.events.on_element_update(player,element,value,percent) + end + + local category = player.name + if define.categorize then + category = define.categorize(element) + end + + Instances.unregistered_get_elements(define.name..'-label',category,function(label) + label.caption = tostring(math.round(value,2)) + end) +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value number the new value for the slider +local function store_call(define,element,value) + element.slider_value = value + event_call(define,element,value) +end + +local Slider = { + _prototype=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} + +--- Creates a new slider element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new slider element define +function Slider.new_slider(name) + + local self = Gui._define_factory(Slider._prototype) + self.draw_data.type = 'slider' + + if name then + self:debug_name(name) + end + + self.post_draw = function(element) + local player = Game.get_player_by_index(element.player_index) + local min,max = element.get_slider_minimum(),element.get_slider_maximum() + + if type(self.min) == 'function' then + min = self.min(player,element) + end + + if type(self.max) == 'function' then + max = self.max(player,element) + end + + element.set_slider_minimum_maximum(min,max) + + if self.store then + local category = self.categorize and self.categorize(element) or nil + local value = self:get_store(category) + if value then element.slider_value = value end + end + + if self.auto_label then + self:draw_label(element.parent) + end + end + + Gui.on_value_changed(self.name,function(event) + local element = event.element + local value = element.slider_value + + if self.store then + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + else + event_call(self,element,value) + + end + + end) + + return self +end + +--- Adds notches to the slider +-- @tparam[opt] state boolean when true will draw notches onto the slider +function Slider._prototype:use_notches(state) + if state == false then + self.draw_data.style = nil + else + self.draw_data.style = 'notched_slider' + end + return self +end + +--- Sets the range of a slider, if not used will use default values for a slider +-- @tparam[opt] min number the minimum value that the slider can take +-- @tparam[opt] max number the maximum value that the slider can take +-- @treturn self the define to allow chaining +function Slider._prototype:set_range(min,max) + self.min = min + self.max = max + + if type(min) == 'number' then + self.draw_data.minimum_value = min + end + + if type(max) == 'number' then + self.draw_data.maximum_value = max + end + + return self +end + +--- Draws a new label and links its value to the value of this slider, if no store then it will only show one value per player +-- @tparam element LuaGuiElement the parent element that the lable will be drawn to +-- @treturn LuaGuiElement the new label element so that styles can be applied +function Slider._prototype:draw_label(element) + local name = self.name..'-label' + if element[name] then return end + + local value = 0 + if self.store then + local category = self.categorize and self.categorize(element) or value + value = self:get_store(category) or 0 + end + + local new_element = element.add{ + name=name, + type='label', + caption=tostring(math.round(value,2)) + } + + local categorise = self.categorise or Gui.player_store + local category = categorise(new_element) + + Instances.unregistered_add_element(name,category,new_element) + + return new_element +end + +--- Enables auto draw of the label, the label will share the same parent element as the slider +-- @tparam[opt=true] state boolean when false will disable the auto draw of the label +-- @treturn self the define to allow chaining +function Slider._prototype:enable_auto_draw_label(state) + if state == false then + self.auto_label = false + else + self.auto_label = true + end + return self +end + +return Slider \ No newline at end of file diff --git a/expcore/Gui/test.lua b/expcore/Gui/test.lua new file mode 100644 index 00000000..4de0a048 --- /dev/null +++ b/expcore/Gui/test.lua @@ -0,0 +1,664 @@ +--- This file creates a teste gui that is used to test every input method +-- note that this does not cover every permutation only features in indepentance +-- for example store in most cases is just by player name, but other store methods are tested with checkbox +local Gui = require 'expcore.gui' +local format_chat_colour,table_keys = ext_require('expcore.common','format_chat_colour','table_keys') +local Colors = require 'resources.color_presets' +local Event = require 'utils.event' +local Store = require 'expcore.store' + +local tests = {} + +--[[ + Toolbar Tests + > No display - Toolbar button with no display + > With caption - Toolbar button with a caption display + > With icons - Toolbar button with an icon +]] + +Gui.new_toolbar_button('click-1') +:set_post_authenticator(function(player,button_name) + return global.click_one +end) +:on_click(function(player,element) + player.print('CLICK 1') +end) + +Gui.new_toolbar_button('click-2') +:set_caption('Click Two') +:set_post_authenticator(function(player,button_name) + return global.click_two +end) +:on_click(function(player,element) + player.print('CLICK 2') +end) + +Gui.new_toolbar_button('click-3') +:set_sprites('utility/questionmark') +:set_post_authenticator(function(player,button_name) + return global.click_three +end) +:on_click(function(player,element) + player.print('CLICK 3') +end) + +--[[ + Center Frame Tests + > Main test gui - Main test gui triggers all other tests +]] + +local test_gui = +Gui.new_center_frame('gui-test-open') +:set_caption('Open Test Gui') +:set_tooltip('Main test gui triggers all other tests') +:set_post_authenticator(function(player,button_name) + return global.show_test_gui +end) + +:on_draw(function(player,frame) + for test_group_name,test_group in pairs(tests) do + + player.print('Starting tests for: '..format_chat_colour(test_group_name,Colors.cyan)) + + local pass_count = 0 + local test_count = 0 + + local flow = frame.add{ + type='flow', + name=test_group_name, + direction='vertical' + } + + for test_name,test in pairs(test_group) do + local test_function = type(test) == 'function' and test or test.draw_to + test_count = test_count+1 + + local success,err = pcall(test_function,test,flow) + if success then + pass_count = pass_count+1 + else + player.print('Failed Test: '..format_chat_colour(test_name,Colors.red)) + log('Gui Test Failed: '..test_name..' stacktrace:\n'..err) + end + + end + + if pass_count == test_count then + player.print('All tests '..format_chat_colour('passed',Colors.green)..' ('..test_group_name..')') + else + player.print('Passed '..format_chat_colour(pass_count..'/'..test_count,Colors.cyan)..' ('..test_group_name..')') + end + + end +end) + +--[[ + Left Frame Test + > Left frame which holds all online player names, updates when player leaves or joins +]] + +local left_frame = +Gui.new_left_frame('test-left-frame') +:set_caption('Test Left Gui') +:set_tooltip('Left frame which holds all online player names, updates when player leaves or joins') +:set_post_authenticator(function(player,button_name) + return global.show_test_gui +end) + +:set_open_by_default() +:on_draw(function(_player,frame) + for _,player in pairs(game.connected_players) do + frame.add{ + type='label', + caption=player.name + } + end +end) + +Event.add(defines.events.on_player_joined_game,left_frame 'update_all') +Event.add(defines.events.on_player_left_game,left_frame 'update_all') + +--[[ + Popup Test + > Allows opening a popup which contains the players name and tick it was opened +]] + +local test_popup = +Gui.new_popup('test-popup') +:on_draw(function(player,frame) + frame.add{ + type='label', + caption=player.name + } + frame.add{ + type='label', + caption=game.tick + } +end) + +Gui.new_toolbar_button('test-popup-open') +:set_caption('Test Popup') +:set_tooltip('Allows opening a popup which contains the players name and tick it was opened') +:set_post_authenticator(function(player,button_name) + return global.show_test_gui +end) +:on_click(function(player,element) + test_popup(player,300) +end) + +--[[ + Button Tests + > No display - Simple button which has no display + > Caption - Simple button but has a caption on it + > Icons - Button with an icon display plus two icons for hover and select + > Auth - Button which can only be passed when auth is true (press no display to toggle; needs reopen) +]] + +local button_no_display = +Gui.new_button('test-button-no-display') +:set_tooltip('Button no display') +:on_click(function(player,element) + player.print('Button no display') + global.test_auth_button = not global.test_auth_button + player.print('Auth Button auth state: '..tostring(global.test_auth_button)) +end) + +local button_with_caption = +Gui.new_button('test-button-with-caption') +:set_tooltip('Button with caption') +:set_caption('Button Caption') +:on_click(function(player,element) + player.print('Button with caption') +end) + +local button_with_icon = +Gui.new_button('test-button-with-icon') +:set_tooltip('Button with icons') +:set_sprites('utility/warning_icon','utility/warning','utility/warning_white') +:on_click(function(player,element) + player.print('Button with icons') +end) + +local button_with_auth = +Gui.new_button('test-button-with-auth') +:set_tooltip('Button with auth') +:set_post_authenticator(function(player,button_name) + return global.test_auth_button +end) +:on_click(function(player,element) + player.print('Button with auth') +end) + +tests.Buttons = { + ['No display']=button_no_display, + ['Caption']=button_with_caption, + ['Icons']=button_with_icon, + ['Auth']=button_with_auth +} + +--[[ + Checkbox Test + > Local -- Simple checkbox that can toggle + > Game store -- Checkbox which syncs its state between all players + > Force store -- Checkbox which syncs its state with all players on the same force + > Player store -- Checkbox that stores its state between re-draws +]] + +local checkbox_local = +Gui.new_checkbox('test-checkbox-local') +:set_tooltip('Checkbox local') +:set_caption('Checkbox Local') +:on_element_update(function(player,element,state) + player.print('Checkbox local: '..tostring(state)) +end) + +local checkbox_game = +Gui.new_checkbox('test-checkbox-store-game') +:set_tooltip('Checkbox store game') +:set_caption('Checkbox Store Game') +:add_store() +:on_element_update(function(player,element,state) + player.print('Checkbox store game: '..tostring(state)) +end) + +local checkbox_force = +Gui.new_checkbox('test-checkbox-store-force') +:set_tooltip('Checkboc store force') +:set_caption('Checkbox Store Force') +:add_store(Gui.force_store) +:on_element_update(function(player,element,state) + player.print('Checkbox store force: '..tostring(state)) +end) + +local checkbox_player = +Gui.new_checkbox('test-checkbox-store-player') +:set_tooltip('Checkbox store player') +:set_caption('Checkbox Store Player') +:add_store(Gui.player_store) +:on_element_update(function(player,element,state) + player.print('Checkbox store player: '..tostring(state)) +end) + +tests.Checkboxs = { + ['Local']=checkbox_local, + ['Game store']=checkbox_game, + ['Force store']=checkbox_force, + ['Player store']=checkbox_player +} + +--[[ + Radiobutton Tests + > Local -- Simple radiobutton that can only be toggled true + > Player store -- Radio button that saves its state between re-draws + > Option set -- A set of radio buttons where only one can be true at a time +]] + +local radiobutton_local = +Gui.new_radiobutton('test-radiobutton-local') +:set_tooltip('Radiobutton local') +:set_caption('Radiobutton Local') +:on_element_update(function(player,element,state) + player.print('Radiobutton local: '..tostring(state)) +end) + +local radiobutton_player = +Gui.new_radiobutton('test-radiobutton-store') +:set_tooltip('Radiobutton store') +:set_caption('Radiobutton Store') +:add_store(Gui.player_store) +:on_element_update(function(player,element,state) + player.print('Radiobutton store: '..tostring(state)) +end) + +local radiobutton_option_set = +Gui.new_radiobutton_option_set('gui.test.share',function(value,category) + game.print('Radiobutton option set for: '..category..' is now: '..tostring(value)) +end,Gui.player_store) + +local radiobutton_option_one = +Gui.new_radiobutton('test-radiobutton-option-one') +:set_tooltip('Radiobutton option set') +:set_caption('Radiobutton Option One') +:add_as_option(radiobutton_option_set,'One') +:on_element_update(function(player,element,state) + player.print('Radiobutton option one: '..tostring(state)) +end) + +local radiobutton_option_two = +Gui.new_radiobutton('test-radiobutton-option-two') +:set_tooltip('Radiobutton option set') +:set_caption('Radiobutton Option Two') +:add_as_option(radiobutton_option_set,'Two') +:on_element_update(function(player,element,state) + player.print('Radiobutton option two: '..tostring(state)) +end) + +local radiobutton_option_three = +Gui.new_radiobutton('test-radiobutton-option-three') +:set_tooltip('Radiobutton option set') +:set_caption('Radiobutton Option Three') +:add_as_option(radiobutton_option_set,'Three') +:on_element_update(function(player,element,state) + player.print('Radiobutton option three: '..tostring(state)) +end) + +tests.Radiobuttons = { + ['Local']=radiobutton_local, + ['Player store']=radiobutton_player, + ['Option set']=function(self,frame) + Gui.draw_option_set(radiobutton_option_set,frame) + end +} + +--[[ + Dropdown Test + > Local static general -- Simple dropdown with all static options and general handler + > Player startic general -- Dropdown with all static options and general handler and stores option between re-draws + > Local static case -- Dropdown with all static options but case handlers and a general handler + > Player static case -- Dropdown with all static options but case handlers and a general handler and stores option between re-draws + > Local dynamic -- Dropdown with one static option with the reset generated by a function + > Player dynamic -- Dropdown with one static option with the reset generated by a function and stores option between re-draws +]] + +local dropdown_local_static_general = +Gui.new_dropdown('test-dropdown-local-static-general') +:set_tooltip('Dropdown local static general') +:add_options('One','Two','Three','Four') +:on_element_update(function(player,element,value) + player.print('Dropdown local static general: '..tostring(value)) +end) + +local dropdown_player_static_general = +Gui.new_dropdown('test-dropdown-store-static-general') +:set_tooltip('Dropdown store static general') +:add_options('One','Two','Three','Four') +:add_store(Gui.player_store) +:on_element_update(function(player,element,value) + player.print('Dropdown store static general: '..tostring(value)) +end) + +local function print_option_selected_1(player,element,value) + player.print('Dropdown local static case (case): '..tostring(value)) +end + +local dropdown_local_static_case = +Gui.new_dropdown('test-dropdown-local-static-case') +:set_tooltip('Dropdown local static case') +:add_options('One','Two') +:add_option_callback('One',print_option_selected_1) +:add_option_callback('Two',print_option_selected_1) +:add_option_callback('Three',print_option_selected_1) +:add_option_callback('Four',print_option_selected_1) +:on_element_update(function(player,element,value) + player.print('Dropdown local static case (general): '..tostring(value)) +end) + +local function print_option_selected_2(player,element,value) + player.print('Dropdown store static case (case): '..tostring(value)) +end + +local dropdown_player_static_case = +Gui.new_dropdown('test-dropdown-store-static-case') +:set_tooltip('Dropdown store static case') +:add_store(Gui.player_store) +:add_options('One','Two') +:add_option_callback('One',print_option_selected_2) +:add_option_callback('Two',print_option_selected_2) +:add_option_callback('Three',print_option_selected_2) +:add_option_callback('Four',print_option_selected_2) +:on_element_update(function(player,element,value) + player.print('Dropdown store static case (general): '..tostring(value)) +end) + +local dropdown_local_dynamic = +Gui.new_dropdown('test-dropdown-local-dynamic') +:set_tooltip('Dropdown local dynamic') +:add_options('Static') +:add_dynamic(function(player,element) + return table_keys(Colors) +end) +:on_element_update(function(player,element,value) + player.print('Dropdown local dynamic: '..tostring(value)) +end) + +local dropdown_player_dynamic = +Gui.new_dropdown('test-dropdown-store-dynamic') +:set_tooltip('Dropdown store dynamic') +:add_options('Static') +:add_dynamic(function(player,element) + return table_keys(Colors) +end) +:add_store(Gui.player_store) +:on_element_update(function(player,element,value) + player.print('Dropdown store dynamic: '..tostring(value)) +end) + +tests.Dropdowns = { + ['Local static general']=dropdown_local_static_general, + ['Player startic general']=dropdown_player_static_general, + ['Local static case']=dropdown_local_static_case, + ['Player static case']=dropdown_player_static_case, + ['Local dynamic general']=dropdown_local_dynamic, + ['Player dynamic general']=dropdown_player_dynamic +} + +--[[ + List Box Tests + > Local -- A list box with all static options and general handler + > Store -- A list box with all static options and general handler and stores options between re-draws +]] + +local list_box_local = +Gui.new_list_box('test-list-box-local') +:set_tooltip('List box local') +:add_options('One','Two','Three','Four') +:on_element_update(function(player,element,value) + player.print('Dropdown local: '..tostring(value)) +end) + +local list_box_player = +Gui.new_list_box('test-list-box-store') +:set_tooltip('List box store') +:add_options('One','Two','Three','Four') +:add_store(Gui.player_store) +:on_element_update(function(player,element,value) + player.print('Dropdown store: '..tostring(value)) +end) + +tests["List Boxs"] = { + ['Local']=list_box_local, + ['Player']=list_box_player +} + +--[[ + Slider Tests + > Local default -- Simple slider with default range + > Local notched -- Simple slider with notches + > Store default -- Slider with default range that stores value between re-draws + > Static range -- Simple slider with a static range + > Dynamic range -- Slider with a dynamic range + > Local label -- Simple slider with default range which has a label + > Store label -- Slider with default range which has a label and stores value between re-draws +]] + +local slider_local_default = +Gui.new_slider('test-slider-local-default') +:set_tooltip('Silder local default') +:on_element_update(function(player,element,value,percent) + player.print('Slider local default: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local slider_notched_default = +Gui.new_slider('test-slider-notched-default') +:set_tooltip('Silder notched default') +:use_notches() +:on_element_update(function(player,element,value,percent) + player.print('Slider notched default: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local slider_player_default = +Gui.new_slider('test-slider-store-default') +:set_tooltip('Silder store default') +:add_store(Gui.player_store) +:on_element_update(function(player,element,value,percent) + player.print('Slider store default: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local slider_static = +Gui.new_slider('test-slider-static-range') +:set_tooltip('Silder static range') +:set_range(5,50) +:on_element_update(function(player,element,value,percent) + player.print('Slider static range: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local slider_dynamic = +Gui.new_slider('test-slider-dynamic-range') +:set_tooltip('Silder dynamic range') +:set_range(function(player,element) + return player.index - 5 +end,function(player,element) + return player.index + 4 +end) +:on_element_update(function(player,element,value,percent) + player.print('Slider dynamic range: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local label_slider_local = +Gui.new_slider('test-slider-local-label') +:set_tooltip('Silder local label') +:enable_auto_draw_label() +:on_element_update(function(player,element,value,percent) + player.print('Slider local label: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +local label_slider_player = +Gui.new_slider('test-slider-store-label') +:set_tooltip('Silder store label') +:enable_auto_draw_label() +:add_store(Gui.player_store) +:on_element_update(function(player,element,value,percent) + player.print('Slider store label: '..tostring(math.round(value))..' '..tostring(math.round(percent,1))) +end) + +tests.Sliders = { + ['Local default']=slider_local_default, + ['Local notched']=slider_notched_default, + ['Player default']=slider_player_default, + ['Static range']=slider_static, + ['Dynamic range']=slider_dynamic, + ['Local label']=function(self,frame) + local flow = frame.add{type='flow'} + label_slider_local:draw_to(flow) + end, + ['Player label']=function(self,frame) + local flow = frame.add{type='flow'} + label_slider_player:draw_to(flow) + end +} + +--[[ + Text Tests + > Local field -- Simple text field + > Store field -- Test field that stores text between re-draws + > Local box -- Simple text box + > Wrap box -- Text box which has word wrap and selection disabled +]] + +local text_filed_local = +Gui.new_text_filed('test-text-field-local') +:set_tooltip('Text field local') +:on_element_update(function(player,element,value) + player.print('Text field local: '..value) +end) + +local text_filed_store = +Gui.new_text_filed('test-text-field-store') +:set_tooltip('Text field store') +:add_store(Gui.player_store) +:on_element_update(function(player,element,value) + player.print('Text field store: '..value) +end) + +local text_box_local = +Gui.new_text_box('test-text-box-local') +:set_tooltip('Text box local') +:on_element_update(function(player,element,value) + player.print('Text box local: '..value) +end) + +local text_box_wrap = +Gui.new_text_box('test-text-box-wrap') +:set_tooltip('Text box wrap') +:set_selectable(false) +:set_word_wrap() +:on_element_update(function(player,element,value) + player.print('Text box wrap: '..value) +end) + +tests.Texts = { + ['Local field']=text_filed_local, + ['Store field']=text_filed_store, + ['Local box']=text_box_local, + ['Wrap box']=text_box_wrap +} + +--[[ + Elem Button Tests + > Local -- Simple elem button + > Default -- Simple elem button which has a default value + > Function -- Elem button which has a dynamic default + > Store -- Elem button which stores its value between re-draws +]] + +local elem_local = +Gui.new_elem_button('test-elem-local') +:set_tooltip('Elem') +:set_type('item') +:on_element_update(function(player,element,value) + player.print('Elem: '..value) +end) + +local elem_default = +Gui.new_elem_button('test-elem-default') +:set_tooltip('Elem default') +:set_type('item') +:set_default('iron-plate') +:on_element_update(function(player,element,value) + player.print('Elem default: '..value) +end) + +local elem_function = +Gui.new_elem_button('test-elem-function') +:set_tooltip('Elem function') +:set_type('item') +:set_default(function(player,element) + return 'iron-plate' +end) +:on_element_update(function(player,element,value) + player.print('Elem function: '..value) +end) + +local elem_store = +Gui.new_elem_button('test-elem-store') +:set_tooltip('Elem store') +:set_type('item') +:add_store(Gui.player_store) +:on_element_update(function(player,element,value) + player.print('Elem store: '..value) +end) + +tests["Elem Buttons"] = { + ['Local']=elem_local, + ['Default']=elem_default, + ['Function']=elem_function, + ['Store']=elem_store +} + +--[[ + Progress bar tests + > Simple -- Progress bar that fills every 2 seconds + > Store -- Progress bar that fills every 5 seconds with synced value + > Reverce -- Progress bar that decreases every 2 seconds +]] + +local progressbar_one = +Gui.new_progressbar('test-prog-one') +:set_default_maximum(120) +:on_complete(function(player,element,reset_element) + reset_element() +end) + +local progressbar_two = +Gui.new_progressbar('test-prog-one') +:set_default_maximum(300) +:add_store(Gui.force_store) +:on_complete(function(player,element,reset_element) + reset_element() +end) +:on_store_complete(function(category,reset_store) + reset_store() +end) + +local progressbar_three = +Gui.new_progressbar('test-prog-one') +:set_default_maximum(120) +:use_count_down() +:on_complete(function(player,element,reset_element) + reset_element() +end) + +Event.add(defines.events.on_tick,function() + progressbar_one:increment() + progressbar_three:decrement() + local categories = Store.get_children(progressbar_two.store) + for category,_ in pairs(categories) do + progressbar_two:increment(1,category) + end +end) + +tests["Progress Bars"] = { + ['Simple']=progressbar_one, + ['Store']=progressbar_two, + ['Reverce']=progressbar_three +} \ No newline at end of file diff --git a/expcore/Gui/text.lua b/expcore/Gui/text.lua new file mode 100644 index 00000000..3a06e439 --- /dev/null +++ b/expcore/Gui/text.lua @@ -0,0 +1,156 @@ +--- Gui class define for text fields and text boxs +--[[ +>>>> Functions + Text.new_text_field(name) --- Creates a new text field element define + Text._prototype_field:on_element_update(callback) --- Registers a handler for when an element instance updates + Text._prototype_field:on_store_update(callback) --- Registers a handler for when the stored value updates + + Text.new_text_box(name) --- Creates a new text box element define + Text._prototype_field:on_element_update(callback) --- Registers a handler for when an element instance updates + Text._prototype_field:on_store_update(callback) --- Registers a handler for when the stored value updates + Text._prototype_box:set_selectable(state) --- Sets the text box to be selectable + Text._prototype_box:set_word_wrap(state) --- Sets the text box to have word wrap + Text._prototype_box:set_read_only(state) --- Sets the text box to be read only + + Other functions present from expcore.gui.core +]] +local Gui = require 'expcore.gui.core' +local Game = require 'utils.game' + +--- Event call for on_text_changed and store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new text for the text field +local function event_call(define,element,value) + local player = Game.get_player_by_index(element.player_index) + + if define.events.on_element_update then + define.events.on_element_update(player,element,value) + end + +end + +--- Store call for store update +-- @tparam define table the define that this is acting on +-- @tparam element LuaGuiElement the element that triggered the event +-- @tparam value string the new text for the text field +local function store_call(self,element,value) + element.text = value + event_call(self,element,value) +end + +local Text = { + _prototype_field=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + }, + _prototype_box=Gui._prototype_factory{ + on_element_update = Gui._event_factory('on_element_update'), + on_store_update = Gui._event_factory('on_store_update'), + add_store = Gui._store_factory(store_call), + add_sync_store = Gui._sync_store_factory(store_call) + } +} + +--- Creates a new text field element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new text field element define +function Text.new_text_field(name) + + local self = Gui._define_factory(Text._prototype_field) + self.draw_data.type = 'textfield' + + if name then + self:debug_name(name) + end + + self.post_draw = function(element) + if self.selectable then + element.selectable = true + end + + if self.word_wrap then + element.word_wrap = true + end + + if self.read_only then + element.read_only = true + end + + if self.store then + local category = self.categorize and self.categorize(element) or nil + local value = self:get_store(category) + if value then element.text = value end + end + end + + Gui.on_text_changed(self.name,function(event) + local element = event.element + local value = element.text + + if self.store then + local category = self.categorize and self.categorize(element) or value + self:set_store(category,value) + + else + event_call(self,element,value) + + end + + end) + + return self +end + +--- Creates a new text box element define +-- @tparam[opt] name string the optional debug name that can be added +-- @treturn table the new text box element define +function Text.new_text_box(name) + local self = Text.new_text_field(name) + self.draw_data.type = 'text-box' + + local mt = getmetatable(self) + mt.__index = Text._prototype_box + + return self +end + +--- Sets the text box to be selectable +-- @tparam[opt=true] state boolean when false will set the state to false +-- @treturn self table the define to allow for chaining +function Text._prototype_box:set_selectable(state) + if state == false then + self.selectable = false + else + self.selectable = true + end + return self +end + +--- Sets the text box to have word wrap +-- @tparam[opt=true] state boolean when false will set the state to false +-- @treturn self table the define to allow for chaining +function Text._prototype_box:set_word_wrap(state) + if state == false then + self.word_wrap = false + else + self.word_wrap = true + end + return self +end + +--- Sets the text box to be read only +-- @tparam[opt=true] state boolean when false will set the state to false +-- @treturn self table the define to allow for chaining +function Text._prototype_box:set_read_only(state) + if state == false then + self.read_only = false + else + self.read_only = true + end + return self +end + +return Text \ No newline at end of file diff --git a/expcore/Gui/toolbar.lua b/expcore/Gui/toolbar.lua new file mode 100644 index 00000000..87843f5a --- /dev/null +++ b/expcore/Gui/toolbar.lua @@ -0,0 +1,101 @@ +--- Gui structure for the toolbar (top left) +--[[ +>>>> Example format + -- this is the same as any other button define, this just automatically draws it + -- you can use add_button if you already defined the button + local toolbar_button = + Toolbar.new_button('print-click') + :on_click(function(player,_element) + player.print('You clicked a button!') + end) + +>>>> Functions + Toolbar.new_button(name) --- Adds a new button to the toolbar + Toolbar.add_button(button) --- Adds an existing buttton to the toolbar + Toolbar.update(player) --- Updates the player's toolbar with an new buttons or expected change in auth return +]] +local Buttons = require 'expcore.gui.buttons' +local Gui = require 'expcore.gui.core' +local Roles = require 'expcore.roles' +local Event = require 'utils.event' +local Game = require 'utils.game' + +local Toolbar = { + permisison_names = {}, + buttons = {} +} + +function Toolbar.allowed(player,define_name) + local permisison_name = Toolbar.permisison_names[define_name] or define_name + return Roles.player_allowed(player,permisison_name) +end + +function Toolbar.permission_alias(define_name,permisison_name) + Toolbar.permisison_names[define_name] = permisison_name +end + +--- Adds a new button to the toolbar +-- @tparam[opt] name string when given allows an alias to the button for the permission system +-- @treturn table the button define +function Toolbar.new_button(name) + local button = Buttons.new_button() + button:set_post_authenticator(Toolbar.allowed) + Toolbar.add_button(button) + Toolbar.permission_alias(button.name,name) + return button +end + +--- Adds an existing buttton to the toolbar +-- @tparam button table the button define for the button to be added +function Toolbar.add_button(button) + table.insert(Toolbar.buttons,button) + Gui.allow_player_to_toggle_top_element_visibility(button.name) + Gui.on_player_show_top(button.name,function(event) + if not button.post_authenticator(event.player,button.name) then + event.element.visible = false + end + end) + if not button.post_authenticator then + button:set_post_authenticator(function() return true end) + end +end + +--- Updates the player's toolbar with an new buttons or expected change in auth return +-- @tparam player LuaPlayer the player to update the toolbar for +function Toolbar.update(player) + local top = Gui.get_top_element_flow(player) + if not top then return end + local visible = top[Gui.top_toggle_button_name].caption == '<' + for _,button in pairs(Toolbar.buttons) do + local element + if top[button.name] then element = top[button.name] + else element = button:draw_to(top) end + if button.post_authenticator(player,button.name) then + element.visible = visible + element.enabled = true + else + element.visible = false + element.enabled = false + end + end +end + +--- When there is a new player they will have the toolbar update +Event.add(defines.events.on_player_created,function(event) + local player = Game.get_player_by_index(event.player_index) + Toolbar.update(player) +end) + +--- When a player gets a new role they will have the toolbar updated +Event.add(Roles.player_role_assigned,function(event) + local player = Game.get_player_by_index(event.player_index) + Toolbar.update(player) +end) + +--- When a player loses a role they will have the toolbar updated +Event.add(Roles.player_role_unassigned,function(event) + local player = Game.get_player_by_index(event.player_index) + Toolbar.update(player) +end) + +return Toolbar \ No newline at end of file diff --git a/expcore/commands.lua b/expcore/commands.lua index c96dcce4..c7698802 100644 --- a/expcore/commands.lua +++ b/expcore/commands.lua @@ -218,7 +218,7 @@ ]] local Game = require 'utils.game' -local player_return = ext_require('expcore.common','player_return') +local player_return,write_json = ext_require('expcore.common','player_return','write_json') local Commands = { defines={ -- common values are stored error like signals @@ -580,14 +580,14 @@ end -- logs command usage to file local function command_log(player,command,comment,params,raw,details) local player_name = player and player.name or '' - game.write_file('log/commands.log',game.table_to_json{ + write_json('log/commands.log',{ player_name=player_name, command_name=command.name, comment=comment, details=details, params=params, raw=raw - }..'\n',true,0) + }) end --- Main event function that is ran for all commands, used internally please avoid direct use diff --git a/expcore/common.lua b/expcore/common.lua index 481fc4b1..8e8c6e72 100644 --- a/expcore/common.lua +++ b/expcore/common.lua @@ -7,12 +7,34 @@ Public.type_check_error(value,test_type,error_message,level) --- Raises an error if the value is of the incorrect type Public.param_check(value,test_type,param_name,param_number) --- Raises an error when the value is the incorrect type, uses a consistent error message format - Public.extract_keys(tbl,...) --- Extracts certain keys from a table - Public.player_return(value,colour,player) --- Will return a value of any type to the player/server console, allows colour for in-game players + Public.write_json(path,tbl) --- Writes a table object to a file in json format Public.opt_require(path) --- Calls a require that will not error if the file is not found Public.ext_require(path,...) --- Calls a require and returns only the keys given, file must return a table + + Public.format_time(ticks,options) --- Formats tick into a clean format, denominations from highest to lowest + + Public.move_items(items,surface,position,radius,chest_type) --- Moves items to the position and stores them in the closest entity of the type given + + Public.print_grid_value(value, surface, position, scale, offset, immutable) --- Prints a colored value on a location. + Public.print_colored_grid_value(value, surface, position, offset, immutable, + color_value, base_color, delta_color, under_bound, over_bound) --- Prints a colored value on a location. with extra settings. + Public.clear_flying_text(surface) --- Clears all flying text entites on a surface + + Public.string_contains(s, contains) --- Tests if a string contains a given substring. + + Public.extract_keys(tbl,...) --- Extracts certain keys from a table + Public.enum(tbl) --- Converts a table to an enum + Public.auto_complete(options,input,use_key,rtn_key) --- Returns the closest match to the input + Public.table_keys(tbl) --- Returns all the keys of a table + Public.table_values(tbl) --- Returns all the values of a table + Public.table_alphanumsort(tbl) --- Returns the list is a sorted way that would be expected by people (this is by key) + Public.table_keysort(tbl) --- Returns the list is a sorted way that would be expected by people (this is by key) (faster alterative than above) + + Public.format_chat_colour(message,color) --- Returns a message with valid chat tags to change its colour + Public.format_chat_colour_localized(message,color) --- Returns a message with valid chat tags to change its colour, using localization + Public.format_chat_player_name(player,raw_string) --- Returns the players name in the players color ]] local Colours = require 'resources.color_presets' @@ -61,19 +83,6 @@ function Public.param_check(value,test_type,param_name,param_number) return true end ---- Extracts certain keys from a table --- @usage local key_three, key_one = extract({key_one='foo',key_two='bar',key_three=true},'key_three','key_one') --- @tparam tbl table the table which contains the keys --- @tparam ... string the names of the keys you want extracted --- @return the keys in the order given -function Public.extract_keys(tbl,...) - local values = {} - for _,key in pairs({...}) do - table.insert(values,tbl[key]) - end - return unpack(values) -end - --- Will return a value of any type to the player/server console, allows colour for in-game players -- @usage player_return('Hello, World!') -- returns 'Hello, World!' to game.player or server console -- @usage player_return('Hello, World!','green') -- returns 'Hello, World!' to game.player with colour green or server console @@ -86,8 +95,8 @@ function Public.player_return(value,colour,player) player = player or game.player -- converts the value to a string local returnAsString - if Public.type_check(value,'table') then - if Public.type_check(value.__self,'userdata') then + if Public.type_check(value,'table') or type(value) == 'userdata' then + if Public.type_check(value.__self,'userdata') or type(value) == 'userdata' then -- value is userdata returnAsString = 'Cant Display Userdata' elseif Public.type_check(value[1],'string') and string.find(value[1],'.+[.].+') and not string.find(value[1],'%s') then @@ -115,6 +124,13 @@ function Public.player_return(value,colour,player) else rcon.print(returnAsString) end end +--- Writes a table object to a file in json format +-- @tparam path string the path of the file to write include / to use dir +-- @tpatam tbl table the table that will be converted to a json string and wrote to file +function Public.write_json(path,tbl) + game.write_file(path,game.table_to_json(tbl)..'\n',true,0) +end + --- Calls a require that will not error if the file is not found -- @usage local file = opt_require('file.not.present') -- will not cause any error -- @tparam path string the path that you want to require @@ -413,6 +429,8 @@ function Public.print_colored_grid_value(value, surface, position, offset, immut }.active = false end +--- Clears all flying text entites on a surface +-- @tparam surface LuaSurface the surface to clear function Public.clear_flying_text(surface) local entities = surface.find_entities_filtered{name ='flying-text'} for _,entity in pairs(entities) do @@ -430,7 +448,41 @@ function Public.string_contains(s, contains) return s and string.find(s, contains) ~= nil end ---- Returns the closest match to a key +--- Extracts certain keys from a table +-- @usage local key_three, key_one = extract({key_one='foo',key_two='bar',key_three=true},'key_three','key_one') +-- @tparam tbl table the table which contains the keys +-- @tparam ... string the names of the keys you want extracted +-- @return the keys in the order given +function Public.extract_keys(tbl,...) + local values = {} + for _,key in pairs({...}) do + table.insert(values,tbl[key]) + end + return unpack(values) +end + +--- Converts a table to an enum +-- @tparam tbl table the table that will be converted +-- @treturn table the new table that acts like an enum +function Public.enum(tbl) + local rtn = {} + for k,v in pairs(tbl) do + if type(k) ~= 'number' then + rtn[v]=k + end + end + for k,v in pairs(tbl) do + if type(k) == 'number' then + table.insert(rtn,v) + end + end + for k,v in pairs(rtn) do + rtn[v]=k + end + return rtn +end + +--- Returns the closest match to the input -- @tparam options table a table of options for the auto complete -- @tparam input string the input string that will be completed -- @tparam[opt=false] use_key boolean when true the keys of options will be used as the options diff --git a/expcore/gui.lua b/expcore/gui.lua new file mode 100644 index 00000000..5831acf8 --- /dev/null +++ b/expcore/gui.lua @@ -0,0 +1,252 @@ +--- This file is used to require all the different elements of the gui module +-- each module has an outline here but for more details see their seperate files in ./gui + +local Gui = require 'expcore.gui.core' +--[[ + Gui._prototype_factory(tbl) --- Used internally to create new prototypes for element defines + Gui._event_factory(name) --- Used internally to create event handler adders for element defines + Gui._store_factory(callback) --- Used internally to create store adders for element defines + Gui._sync_store_factory(callback) --- Used internally to create synced store adders for element defines + Gui._define_factory(prototype) --- Used internally to create new element defines from a class prototype + + Gui._prototype:uid() --- Gets the uid for the element define + Gui._prototype:debug_name(name) --- Sets a debug alias for the define + Gui._prototype:set_caption(caption) --- Sets the caption for the element define + Gui._prototype:set_tooltip(tooltip) --- Sets the tooltip for the element define + Gui._prototype:on_element_update(callback) --- Add a hander to run on the general value update event, different classes will handle this event differently + + Gui._prototype:set_pre_authenticator(callback) --- Sets an authenticator that blocks the draw function if check fails + Gui._prototype:set_post_authenticator(callback) --- Sets an authenticator that disables the element if check fails + Gui._prototype:draw_to(element) --- Draws the element using what is in the draw_data table, allows use of authenticator if present, registers new instances if store present + Gui.draw(name,element) --- Draws a copy of the element define to the parent element, see draw_to + + Gui._prototype:add_store(categorize) --- Adds a store location for the define that will save the state of the element, categorize is a function that returns a string + Gui._prototype:add_sync_store(location,categorize) --- Adds a store location for the define that will sync between games, categorize is a function that returns a string + Gui._prototype:on_store_update(callback) --- Adds a event callback for when the store changes are other events are not gauenteted to be raised + Gui.player_store(element) --- A categorize function to be used with add_store, each player has their own value + Gui.force_store(element) --- A categorize function to be used with add_store, each force has its own value + Gui.surface_store(element) --- A categorize function to be used with add_store, each surface has its own value + + Gui._prototype:get_store(category) --- Gets the value in this elements store, category needed if categorize function used + Gui._prototype:set_store(category,value) --- Sets the value in this elements store, category needed if categorize function used + Gui.get_store(name,category) --- Gets the value that is stored for a given element define, category needed if categorize function used + Gui.set_store(name,category,value) --- Sets the value stored for a given element define, category needed if categorize function used + + Gui.toggle_enable(element) --- Will toggle the enabled state of an element + Gui.toggle_visible(element) --- Will toggle the visiblity of an element +]] + +local Instances = require 'expcore.gui.instances' +Gui.new_instance_group = Instances.registers +Gui.get_instances = Instances.get_elements +Gui.add_instance = Instances.get_elements +Gui.update_instances = Instances.apply_to_elements +Gui.classes.instances = Instances + +local Button = require 'expcore.gui.buttons' +Gui.new_button = Button.new_button +Gui.classes.button = Button +--[[ + Button.new_button(name) --- Creates a new button element define + + Button._prototype:on_click(player,element) --- Registers a handler for when the button is clicked + Button._prototype:on_left_click(player,element) --- Registers a handler for when the button is clicked with the left mouse button + Button._prototype:on_right_click(player,element) --- Registers a handler for when the button is clicked with the right mouse button + + Button._prototype:set_sprites(sprite,hovered_sprite,clicked_sprite) --- Adds sprites to a button making it a spirte button + Button._prototype:set_click_filter(filter,...) --- Adds a click / mouse button filter to the button + Button._prototype:set_key_filter(filter,...) --- Adds a control key filter to the button +]] + +local Checkbox = require 'expcore.gui.checkboxs' +Gui.new_checkbox = Checkbox.new_checkbox +Gui.new_radiobutton = Checkbox.new_radiobutton +Gui.new_radiobutton_option_set = Checkbox.new_option_set +Gui.draw_option_set = Checkbox.draw_option_set +Gui.classes.checkbox = Checkbox +--[[ + Checkbox.new_checkbox(name) --- Creates a new checkbox element define + Checkbox._prototype_checkbox:on_element_update(callback) --- Registers a handler for when an element instance updates + Checkbox._prototype_checkbox:on_store_update(callback) --- Registers a handler for when the stored value updates + + Checkbox.new_radiobutton(name) --- Creates a new radiobutton element define + Checkbox._prototype_radiobutton:on_element_update(callback) --- Registers a handler for when an element instance updates + Checkbox._prototype_radiobutton:on_store_update(callback) --- Registers a handler for when the stored value updates + Checkbox._prototype_radiobutton:add_as_option(option_set,option_name) --- Adds this radiobutton to be an option in the given option set (only one can be true at a time) + + Checkbox.new_option_set(name,callback,categorize) --- Registers a new option set that can be linked to radiobutotns (only one can be true at a time) + Checkbox.draw_option_set(name,element) --- Draws all radiobuttons that are part of an option set at once (Gui.draw will not work) + + Checkbox.reset_radiobutton(element,exclude,recursive) --- Sets all radiobutotn in a element to false (unless excluded) and can act recursivly +]] + +local Dropdown = require 'expcore.gui.dropdown' +Gui.new_dropdown = Dropdown.new_dropdown +Gui.new_list_box = Dropdown.new_list_box +Gui.classes.dropdown = Dropdown +--[[ + Dropdown.new_dropdown(name) --- Creates a new dropdown element define + Dropdown.new_list_box(name) --- Creates a new list box element define + + Dropdown._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + Dropdown._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + Dropdown._prototype:new_static_options(options,...) --- Adds new static options to the dropdown which will trigger the general callback + Dropdown._prototype:new_dynamic_options(callback) --- Adds a callback which should return a table of values to be added as options for the dropdown (appended after static options) + Dropdown._prototype:add_option_callback(option,callback) --- Adds a case specific callback which will only run when that option is selected (general case still triggered) + + Dropdown.select_value(element,value) --- Selects the option from a dropdown or list box given the value rather than key + Dropdown.get_selected_value(element) --- Returns the currently selected value rather than index +]] + +local Slider = require 'expcore.gui.slider' +Gui.new_slider = Slider.new_slider +Gui.classes.slider = Slider +--[[ + Slider.new_slider(name) --- Creates a new slider element define + + Slider._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + Slider._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + Slider._prototype:use_notches(state) --- Adds notches to the slider + Slider._prototype:set_range(min,max) --- Sets the range of a slider, if not used will use default values for a slider + Slider._prototype:draw_label(element) --- Draws a new label and links its value to the value of this slider, if no store then it will only show one value per player + Slider._prototype:enable_auto_draw_label(state) --- Enables auto draw of the label, the label will share the same parent element as the slider +]] + +local Text = require 'expcore.gui.text' +Gui.new_text_filed = Text.new_text_field +Gui.new_text_box = Text.new_text_box +Gui.classes.text = Text +--[[ + Text.new_text_field(name) --- Creates a new text field element define + Text._prototype_field:on_element_update(callback) --- Registers a handler for when an element instance updates + Text._prototype_field:on_store_update(callback) --- Registers a handler for when the stored value updates + + Text.new_text_box(name) --- Creates a new text box element define + Text._prototype_field:on_element_update(callback) --- Registers a handler for when an element instance updates + Text._prototype_field:on_store_update(callback) --- Registers a handler for when the stored value updates + Text._prototype_box:set_selectable(state) --- Sets the text box to be selectable + Text._prototype_box:set_word_wrap(state) --- Sets the text box to have word wrap + Text._prototype_box:set_read_only(state) --- Sets the text box to be read only +]] + +local ElemButton = require 'expcore.gui.elem-button' +Gui.new_elem_button = ElemButton.new_elem_button +Gui.classes.elem_button = ElemButton +--[[ + ElemButton.new_elem_button(name) --- Creates a new elem button element define + + ElemButton._prototype:on_element_update(callback) --- Registers a handler for when an element instance updates + ElemButton._prototype:on_store_update(callback) --- Registers a handler for when the stored value updates + + ElemButton._prototype:set_type(type) --- Sets the type of the elem button, the type is required so this must be called at least once + ElemButton._prototype:set_default(value) --- Sets the default value for the elem button, this may be a function or a string +]] + +local ProgressBar = require 'expcore.gui.progress-bar' +Gui.new_progressbar = ProgressBar.new_progressbar +Gui.set_progressbar_maximum = ProgressBar.set_maximum +Gui.increment_progressbar = ProgressBar.increment +Gui.decrement_progressbar = ProgressBar.decrement +Gui.classes.progressbar = ProgressBar +--[[ + ProgressBar.set_maximum(element,amount,count_down) --- Sets the maximum value that represents the end value of the progress bar + ProgressBar.increment(element,amount) --- Increases the value of the progressbar, if a define is given all of its instances are incremented + ProgressBar.decrement(element,amount) --- Decreases the value of the progressbar, if a define is given all of its instances are decresed + + ProgressBar.new_progressbar(name) --- Creates a new progressbar element define + ProgressBar._prototype:set_maximum(amount,count_down) --- Sets the maximum value that represents the end value of the progress bar + ProgressBar._prototype:use_count_down(state) --- Will set the progress bar to start at 1 and trigger when it hits 0 + ProgressBar._prototype:increment(amount,category) --- Increases the value of the progressbar + ProgressBar._prototype:increment_filtered(amount,filter) --- Increases the value of the progressbar, if the filter condition is met, does not work with store + ProgressBar._prototype:decrement(amount,category) --- Decreases the value of the progressbar + ProgressBar._prototype:decrement_filtered(amount,filter) --- Decreases the value of the progressbar, if the filter condition is met, does not work with store + ProgressBar._prototype:add_element(element,maximum) --- Adds an element into the list of instances that will are waiting to complete, does not work with store + ProgressBar._prototype:reset_element(element) --- Resets an element, or its store, to be back at the start, either 1 or 0 + + ProgressBar._prototype:on_complete(callback) --- Triggers when a progress bar element compeltes (hits 0 or 1) + ProgressBar._prototype:on_complete(callback) --- Triggers when a store value completes (hits 0 or 1) + ProgressBar._prototype:event_counter(filter) --- Event handler factory that counts up by 1 every time the event triggeres, can filter which elements are incremented + ProgressBar._prototype:event_countdown(filter) --- Event handler factory that counts down by 1 every time the event triggeres, can filter which elements are decremented +]] + +local Toolbar = require 'expcore.gui.toolbar' +Gui.new_toolbar_button = Toolbar.new_button +Gui.add_button_to_toolbar = Toolbar.add_button +Gui.update_toolbar = Toolbar.update +Gui.classes.toolbar = Toolbar +--[[ + Toolbar.new_button(name) --- Adds a new button to the toolbar + Toolbar.add_button(button) --- Adds an existing buttton to the toolbar + Toolbar.update(player) --- Updates the player's toolbar with an new buttons or expected change in auth return +]] + +local LeftFrames = require 'expcore.gui.left' +Gui.get_left_frame_flow = LeftFrames.get_flow +Gui.toggle_left_frame = LeftFrames.toggle_frame +Gui.new_left_frame = LeftFrames.new_frame +Gui.classes.left_frames = LeftFrames +--[[ + LeftFrames.get_flow(player) --- Gets the left frame flow for a player + LeftFrames.get_frame(name,player) --- Gets one frame from the left flow by its name + LeftFrames.get_open(player) --- Gets all open frames for a player, if non are open it will remove the close all button + LeftFrames.toggle_frame(name,player,state) --- Toggles the visiblty of a left frame, or sets its visiblty state + + LeftFrames.new_frame(permision_name) --- Creates a new left frame define + LeftFrames._prototype:set_open_by_default(state) --- Sets if the frame is visible when a player joins, can also be a function to return a boolean + LeftFrames._prototype:get_frame(player) --- Gets the frame for this define from the left frame flow + LeftFrames._prototype:is_open(player) --- Returns if the player currently has this define visible + LeftFrames._prototype:toggle(player) --- Toggles the visiblty of the left frame + + LeftFrames._prototype:update(player) --- Updates the contents of the left frame, first tries update callback, oter wise will clear and redraw + LeftFrames._prototype:update_all(update_offline) --- Updates the frame for all players, see update + LeftFrames._prototype:redraw(player) --- Redraws the frame by calling on_draw, will always clear the frame + LeftFrames._prototype:redraw_all(update_offline) --- Redraws the frame for all players, see redraw + + LeftFrames._prototype:on_draw(player,frame) --- Use to draw your elements to the new frame + LeftFrames._prototype:on_update(player,frame) --- Use to edit your frame when there is no need to redraw it + LeftFrames._prototype:event_handler(action) --- Creates an event handler that will trigger one of its functions, use with Event.add +]] + +local CenterFrames = require 'expcore.gui.center' +Gui.get_center_flow = CenterFrames.get_flow +Gui.toggle_left_frame = CenterFrames.toggle_frame +Gui.draw_center_frame = CenterFrames.draw_frame +Gui.redraw_center_frame = CenterFrames.redraw_frames +Gui.new_center_frame = CenterFrames.new_frame +Gui.classes.center_frames = CenterFrames +--[[ + CenterFrames.get_flow(player) --- Gets the center flow for a player + CenterFrames.clear_flow(player) --- Clears the center flow for a player + CenterFrames.draw_frame(player,name) --- Draws the center frame for a player, if already open then will do nothing + CenterFrames.redraw_frame(player,name) --- Draws the center frame for a player, if already open then will destory it and redraw + CenterFrames.toggle_frame(player,name,state) --- Toggles if the frame is currently open or not, will open if closed and close if open + + CenterFrames.new_frame(permision_name) --- Sets the frame to be the current active gui when opened and closes all other frames + CenterFrames._prototype:on_draw(player,frame) --- Use to draw your elements onto the new frame + CenterFrames._prototype:set_auto_focus(state) --- Sets the frame to be the current active gui when opened and closes all other frames + CenterFrames._prototype:draw_frame(player) --- Draws this frame to the player, if already open does nothing (will call on_draw to draw to the frame) + CenterFrames._prototype:redraw_frame(player) --- Draws this frame to the player, if already open it will remove it and redraw it (will call on_draw to draw to the frame) + CenterFrames._prototype:toggle_frame(player) --- Toggles if the frame is open, if open it will close it and if closed it will open it + CenterFrames._prototype:event_handler(action) --- Creates an event handler that will trigger one of its functions, use with Event.add +]] + +local PopupFrames = require 'expcore.gui.popups' +Gui.get_popup_flow = PopupFrames.get_flow +Gui.open_popup = PopupFrames.open +Gui.new_popup = PopupFrames.new_popup +Gui.classes.popup_frames = PopupFrames +--[[ + PopupFrames.get_flow(player) --- Gets the left flow that contains the popup frames + PopupFrames.open(define_name,player,open_time,...) --- Opens a popup for the player, can give the amount of time it is open as well as params for the draw function + + PopupFrames.close_progress --- Progress bar which when depleaded will close the popup frame + PopupFrames.close_button --- A button which can be used to close the gui before the timer runs out + + PopupFrames.new_popup(name) --- Creates a new popup frame define + PopupFrames._prototype:set_default_open_time(amount) --- Sets the default open time for the popup, will be used if non is provided with open + PopupFrames._prototype:open(player,open_time,...) --- Opens this define for a player, can be given open time and any other params for the draw function +]] + +return Gui \ No newline at end of file diff --git a/expcore/locale/en.cfg b/expcore/locale/en.cfg index 82d68681..931ce162 100644 --- a/expcore/locale/en.cfg +++ b/expcore/locale/en.cfg @@ -24,4 +24,9 @@ error-log-format-promote=[ERROR] rolePromote/__1__ :: __2__ game-message-assign=__1__ has been assigned to __2__ by __3__ game-message-unassign=__1__ has been unassigned from __2__ by __3__ reject-role=Invalid Role Name. -reject-player-role=Player has a higher role. \ No newline at end of file +reject-player-role=Player has a higher role. + +[gui_util] +button_tooltip=Shows / hides the Toolbar Gui buttons. + +[expcore-gui] diff --git a/expcore/roles.lua b/expcore/roles.lua index 11df21dc..a2dca09e 100644 --- a/expcore/roles.lua +++ b/expcore/roles.lua @@ -160,6 +160,7 @@ local Global = require 'utils.global' local Event = require 'utils.event' local Groups = require 'expcore.permission_groups' local Colours = require 'resources.color_presets' +local write_json = ext_require('expcore.common','write_json') local Roles = { config={ @@ -223,12 +224,12 @@ local function emit_player_roles_updated(player,type,roles,by_player_name,skip_g by_player_index=by_player_index, roles=roles }) - game.write_file('log/roles.log',game.table_to_json{ + write_json('log/roles.log',{ player_name=player.name, by_player_name=by_player_name, type=type, roles_changed=role_names - }..'\n',true,0) + }) end --- Returns a string which contains all roles in index order displaying all data for them diff --git a/expcore/store.lua b/expcore/store.lua new file mode 100644 index 00000000..f391dbd2 --- /dev/null +++ b/expcore/store.lua @@ -0,0 +1,306 @@ +--- Adds an easy way to store and watch for updates to a value +--[[ +>>>> Basic Use + At the most basic level this allows for the naming of locations to store in the global table, the second feature is that you are + able to listen for updates of this value, which means that when ever the set function is called it will trigger the update callback. + + This may be useful when storing config values and when they get set you want to make sure it is taken care of, or maybe you want + to have a value that you can trigger an update of from different places. + + -- this will register a new location called 'scenario.dificutly' and the start value is 'normal' + -- note that setting a start value is optional and we could take nil to mean normal + Store.register('scenario.dificutly',function(value) + game.print('The scenario dificulty has be set to: '..value) + end,'normal') + + -- this will return 'normal' as we have not set the value anywhere else + Store.get('scenario.dificutly') + + -- this will set the value in the store to 'hard' and will trigger the update callback which will print a message to the game + Store.set('scenario.dificutly','hard') + +>>>> Using Children + One limitation of store is that all lcoations must be registered to avoid desyncs, to get round this issue "children" can be used. + When you set the value of a child it does not have its own update callback so rather the "partent" location which has been registerd + will have its update value called with a second param of the name of that child. + + This may be useful when you want a value of each player or force and since you cant regisier every player at the start you must use + the players name as the child name. + + -- this will register the lcoation 'scenario.score' where we plan to use force names as the child + -- here we have not set a start value since it will be an empty location + Store.register('scenario.score',function(value,child) + game.print(child..' now has a score of '..value) + end) + + -- this will return nil, but will not error as children dont need to be registerd + Store.get_child('scenario.score','player') + + -- this will set 'player' to have a value of 10 for 'scenario.score' and trigger the game message print + Store.set_child('scenario.score','player',10) + + -- this would be the same as Store.get however this will return an empty table rather than nil + Store.get_children('scenario.score') + +>>>> Using Sync + There is the option to use Store.register_synced which is the same as Store.register however you can combine this with an external script + which can read the output from 'script-output/log/store.log' and have it send rcon commands back to the game allowing for cross instance + syncing of values. + + This may be useful when you want to have a value change effect multiple instances or even if you just want a database to store values so + you can sync data between map resets. + + -- this example will register the location 'stastics.total-play-time' where we plan to use plan names as the child + -- note that the location must be the same across instances + Store.register_synced('stastics.total-play-time',function(value,child) + game.print(child..' now has now played for '..value) + end) + + -- use of set,get,set_child and get_chlid are all the same as non synced + +>>>> Using a watch function + Some times the value that you want is not some made up value that you have but rather a factorio value or something similar, in order to recive + updates on these values (if the factorio api does not provide an event for it) you will need to add a watch function to update the store when the + values changes. You will want to keep these watch functions small since they run every tick. + + -- this will register a location 'game.speed', note that the lcoation can be anything but we chose 'game.speed' to match what we are watching + -- also note that we do not need a start value here since it will be set on the first tick, but you may want a start value to avoid a trigger of the callback + Store.register('game.speed',function(value) + game.print('The game speed has been set to: '..value) + end) + + -- this will add the watch function to the lcoation, every tick the function is ran and the value returned in compeared to the stored value + -- if the two values are different then the store is overriden and the update function is called + Store.add_watch('game.speed',function() + return game.speed + end) + +>>>> Alternative method + Some people may prefer to use a varible rather than a string for formating reasons here is an example. Also for any times when + there will be little external input Store.uid_location() can be used to generate non conflicting locations, use of register_synced will + still require a name other wise there may be mirgration issuses. + + local store_game_speed = Store.uid_location() + + Store.register(store_game_speed,function(value) + game.print('The game speed has been set to: '..value) + end) + + Store.add_watch(store_game_speed,function() + return game.speed + end) + +]] + +local Global = require 'utils.global' +local Event = require 'utils.event' +local write_json = ext_require('expcore.common','write_json','table_keys') +local Token = require 'utils.token' + +local Store = { + data={}, + callbacks={}, + synced={}, + watchers={} +} +Global.register(Store.data,function(tbl) + Store.data = tbl +end) + +--- Check for if a lcoation is registered +-- @tparam location string the location to test for +-- @treturn boolean true if registered +function Store.is_registered(location) + return not not Store.callbacks[location] +end + +--- Returns a unqiue name that can be used for a store +-- @treturn string a unqiue name +function Store.uid_location() + return tostring(Token.uid()) +end + +--- Registers a new location with an update callback which is triggered when the value updates +-- @tparam location string a unique string that points to the data, string used rather than token to allow migration +-- @tparam callback function this callback will be called when the stored value is set to a new value +-- @tparam[opt] start_value any this value will be the inital value that is stored at this location +function Store.register(location,callback,start_value) + if _LIFECYCLE ~= _STAGE.control then + return error('Can only be called during the control stage', 2) + end + + if Store.callbacks[location] then + return error('Location is already registered', 2) + end + + if type(callback) ~= 'function' then + return error('Callback must be a function', 2) + end + + Store.data[location] = start_value + Store.callbacks[location] = callback + + return location +end + +--- Registers a new cross server synced location with an update callback, and external script is required for cross server +-- @tparam location string a unique string that points to the data, string used rather than token to allow migration +-- @tparam callback function this callback will be called when the stored value is set to a new value +-- @tparam[opt] start_value any this value will be the inital value that is stored at this location +function Store.register_synced(location,callback,start_value) + if _LIFECYCLE ~= _STAGE.control then + return error('Can only be called during the control stage', 2) + end + + if Store.callbacks[location] then + return error('Location is already registered', 2) + end + + if type(callback) ~= 'function' then + return error('Callback must be a function', 2) + end + + Store.data[location] = start_value + Store.callbacks[location] = callback + Store.synced[location] = true +end + +--- Adds a function that will be checked every tick for a change in the returned value, when the value changes it will be saved in the store +-- @tparam location string the location where the data will be saved and compeared to, must already be a registered location +-- @tparam callback function this function will be called every tick to check for a change in value +function Store.add_watch(location,callback) + if _LIFECYCLE ~= _STAGE.control then + return error('Can only be called during the control stage', 2) + end + + if Store.callbacks[location] then + return error('Location is already being watched', 2) + end + + if type(callback) ~= 'function' then + return error('Callback must be a function', 2) + end + + Store.watchers[location] = callback +end + +--- Gets the value stored at a location, this location must be registered +-- @tparam location string the location to get the data from +-- @tparam[opt=false] no_error boolean when true no error is returned if the location is not registered +-- @treturn any the data which was stored at the location +function Store.get(location,no_error) + if not Store.callbacks[location] and not no_error then + return error('Location is not registered', 2) + end + + return Store.data[location] +end + +--- Sets the value at a location, this location must be registered, if server synced it will emit the change to file +-- @tparam location string the location to set the data to +-- @tparam value any the new value to set at the location, value may be reverted if there is a watch callback +-- @treturn boolean true if it was successful +function Store.set(location,value) + if not Store.callbacks[location] then + return error('Location is not registered', 2) + end + + Store.data[location] = value + Store.callbacks[location](value) + + if Store.synced[location] then + write_json('log/store.log',{ + location=location, + value=value + }) + end + + return true +end + +--- Gets all non nil children at a location, children can be added and removed during runtime +-- this is similar to Store.get but will always return a table even if it is empty +-- @tparam location string the location to get the children of +-- @treturn table a table containg all the children and they values +function Store.get_children(location) + local store = Store.get(location) + + if type(store) ~= 'table' and store ~= nil then + return error('Location has a non table value', 2) + end + + return store or {} +end + +--- Gets the value of the child to a location, children can be added and removed during runtime +-- @tparam location string the location of which the child is located +-- @tparam child string the child element to get the value of +-- @treturn any the value which was stored at that location +function Store.get_child(location,child) + local store = Store.get(location) + + if type(store) ~= 'table' and store ~= nil then + return error('Location has a non table value', 2) + end + + return store and store[child] +end + +--- Sets the value of the chlid to a location, children can be added and removed during runtime +-- when a child is set it will call the update handler of the parent allowing children be to added at runtime +-- this may be used when a player joins the game and the child is the players name +-- @tparam location string the location of which the child is located +-- @tparam child string the child element to set the value of +-- @tparam value any the value to set at this location +-- @treturn boolean true if it was successful +function Store.set_child(location,child,value) + local store = Store.get(location) + + if type(store) ~= 'table' and store ~= nil then + return error('Location has a non table value', 2) + end + + if not store then + Store.data[location] = {} + end + + Store.data[location][child] = value + Store.callbacks[location](value,child) + + if Store.synced[location] then + write_json('log/store.log',{ + location=location, + child=child, + value=value + }) + end + + return true +end + +-- Event handler for the watcher callbacks +Event.add(defines.events.on_tick,function() + local errors = {} + + for location,callback in pairs(Store.watchers) do + local store_old = Store.data[location] + local success,store_new = pcall(callback) + + if not success then + table.insert(errors,store_new) + else + if type(store_old) ~= type(store_new) + or type(store_old) == 'table' and not table.compare(store_new,store_new) + or store_old ~= store_new then + Store.data[location] = store_new + Store.callbacks[location](store_new) + end + end + end + + if #errors > 0 then + error(table.concat(errors,'; ')) + end +end) + +return Store \ No newline at end of file diff --git a/locale/en/expcore.cfg b/locale/en/expcore.cfg index e725bd91..931ce162 100644 --- a/locale/en/expcore.cfg +++ b/locale/en/expcore.cfg @@ -1,5 +1,4 @@ time-symbol-days-short=__1__d -color-tag=[color=__1__]__2__[/color] [expcore-commands] unauthorized=Unauthorized, Access is denied due to invalid credentials @@ -25,4 +24,9 @@ error-log-format-promote=[ERROR] rolePromote/__1__ :: __2__ game-message-assign=__1__ has been assigned to __2__ by __3__ game-message-unassign=__1__ has been unassigned from __2__ by __3__ reject-role=Invalid Role Name. -reject-player-role=Player has a higher role. \ No newline at end of file +reject-player-role=Player has a higher role. + +[gui_util] +button_tooltip=Shows / hides the Toolbar Gui buttons. + +[expcore-gui] diff --git a/modules/commands/debug.lua b/modules/commands/debug.lua new file mode 100644 index 00000000..5e9b1496 --- /dev/null +++ b/modules/commands/debug.lua @@ -0,0 +1,7 @@ +local DebugView = require 'modules.gui.debug.main_view' +local Commands = require 'expcore.commands' + +Commands.new_command('debug','Opens the debug pannel for viewing tables.') +:register(function(player,raw) + DebugView.open_dubug(player) +end) \ No newline at end of file diff --git a/modules/commands/interface.lua b/modules/commands/interface.lua index 6e5b89b0..03604435 100644 --- a/modules/commands/interface.lua +++ b/modules/commands/interface.lua @@ -9,13 +9,15 @@ local interface_modules = { ['Commands']=Commands, ['output']=Common.player_return, ['Group']='expcore.permission_groups', - ['Roles']='expcore.roles' + ['Roles']='expcore.roles', + ['Store']='expcore.store', + ['Gui']='expcore.gui' } -- loads all the modules given in the above table for key,value in pairs(interface_modules) do if type(value) == 'string' then - interface_modules[key] = require(value) + interface_modules[key] = Common.opt_require(value) end end @@ -94,5 +96,6 @@ add_interface_callback('tile',function(player) return player.surface.get_tile(pl return { add_interface_callback=add_interface_callback, interface_env=interface_env, - interface_callbacks=interface_callbacks + interface_callbacks=interface_callbacks, + clean_stack_trace=function(str) return str:gsub('%.%.%..-/temp/currently%-playing','') end } \ No newline at end of file diff --git a/modules/gui/debug/_g_view.lua b/modules/gui/debug/_g_view.lua new file mode 100644 index 00000000..3cf4bbd2 --- /dev/null +++ b/modules/gui/debug/_g_view.lua @@ -0,0 +1,114 @@ +local Gui = require 'utils.gui' +local Model = require 'modules.gui.debug.model' +local Color = require 'resources.color_presets' + +local dump = Model.dump + +local Public = {} + +local ignore = { + _G = true, + assert = true, + collectgarbage = true, + error = true, + getmetatable = true, + ipairs = true, + load = true, + loadstring = true, + next = true, + pairs = true, + pcall = true, + print = true, + rawequal = true, + rawlen = true, + rawget = true, + rawset = true, + select = true, + setmetatable = true, + tonumber = true, + tostring = true, + type = true, + xpcall = true, + _VERSION = true, + module = true, + require = true, + package = true, + unpack = true, + table = true, + string = true, + bit32 = true, + math = true, + debug = true, + serpent = true, + log = true, + table_size = true, + global = true, + remote = true, + commands = true, + settings = true, + rcon = true, + script = true, + util = true, + mod_gui = true, + game = true, + rendering = true +} + +local header_name = Gui.uid_name() +local left_panel_name = Gui.uid_name() +local right_panel_name = Gui.uid_name() + +Public.name = '_G' + +function Public.show(container) + local main_flow = container.add {type = 'flow', direction = 'horizontal'} + + local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name} + local left_panel_style = left_panel.style + left_panel_style.width = 300 + + for key, value in pairs(_G) do + if not ignore[key] then + local header = + left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = tostring(key)} + Gui.set_data(header, value) + end + end + + local right_panel = main_flow.add {type = 'text-box', name = right_panel_name} + right_panel.read_only = true + right_panel.selectable = true + + local right_panel_style = right_panel.style + right_panel_style.vertically_stretchable = true + right_panel_style.horizontally_stretchable = true + right_panel_style.maximal_width = 1000 + right_panel_style.maximal_height = 1000 + + Gui.set_data(left_panel, {right_panel = right_panel, selected_header = nil}) +end + +Gui.on_click( + header_name, + function(event) + local element = event.element + local value = Gui.get_data(element) + + local left_panel = element.parent.parent + local left_panel_data = Gui.get_data(left_panel) + local right_panel = left_panel_data.right_panel + local selected_header = left_panel_data.selected_header + + if selected_header then + selected_header.style.font_color = Color.white + end + + element.style.font_color = Color.orange + left_panel_data.selected_header = element + + local content = dump(value) + right_panel.text = content + end +) + +return Public diff --git a/modules/gui/debug/event_view.lua b/modules/gui/debug/event_view.lua new file mode 100644 index 00000000..27f5ac82 --- /dev/null +++ b/modules/gui/debug/event_view.lua @@ -0,0 +1,117 @@ +local Event = require 'utils.event' +local table = require 'utils.table' +local Gui = require 'utils.gui' +local Model = require 'modules.gui.debug.model' + +local format = string.format +local insert = table.insert + +local events = defines.events + +-- Constants +local events_to_keep = 10 + +-- Local vars +local Public = { + name = 'Events' +} +local name_lookup = {} + +-- GUI names +local checkbox_name = Gui.uid_name() + +-- global tables +local enabled = {} +local last_events = {} +global.debug_event_view = { + enabled = enabled, + last_events = last_events +} + +function Public.on_open_debug() + local tbl = global.debug_event_view + if tbl then + enabled = tbl.enabled + last_events = tbl.last_events + else + enabled = {} + last_events = {} + + global.debug_event_view = { + enabled = enabled, + last_events = last_events + } + end + + Public.on_open_debug = nil +end + +-- Local functions +local function event_callback(event) + local id = event.name + if not enabled[id] then + return + end + local name = name_lookup[id] + + if not last_events[name] then + last_events[name] = {} + end + + insert(last_events[name], 1, event) + last_events[name][events_to_keep + 1] = nil + event.name = nil + + local str = format('%s (id = %s): %s', name, id, Model.dump(event)) + game.print(str) + log(str) +end + +local function on_gui_checked_state_changed(event) + local element = event.element + local name = element.caption + local id = events[name] + local state = element.state and true or false + element.state = state + if state then + enabled[id] = true + else + enabled[id] = false + end +end + +-- GUI + +-- Create a table with events sorted by their names +local grid_builder = {} +for name, _ in pairs(events) do + grid_builder[#grid_builder + 1] = name +end + +table.sort(grid_builder) + +function Public.show(container) + local main_frame_flow = container.add({type = 'flow', direction = 'vertical'}) + local scroll_pane = main_frame_flow.add({type = 'scroll-pane'}) + local gui_table = scroll_pane.add({type = 'table', column_count = 3, draw_horizontal_lines = true}) + + for _, event_name in pairs(grid_builder) do + local index = events[event_name] + gui_table.add({type = 'flow'}).add { + name = checkbox_name, + type = 'checkbox', + state = enabled[index] or false, + caption = event_name + } + end +end + +Gui.on_checked_state_changed(checkbox_name, on_gui_checked_state_changed) + +-- Event registers (TODO: turn to removable hooks.. maybe) +for name, id in pairs(events) do + name_lookup[id] = name + Event.add(id, event_callback) +end + +return Public diff --git a/modules/gui/debug/global_view.lua b/modules/gui/debug/global_view.lua new file mode 100644 index 00000000..6a888377 --- /dev/null +++ b/modules/gui/debug/global_view.lua @@ -0,0 +1,133 @@ +local Gui = require 'utils.gui' +local Model = require 'modules.gui.debug.model' +local Color = require 'resources.color_presets' + +local dump = Model.dump +local dump_text = Model.dump_text +local concat = table.concat + +local Public = {} + +local ignore = {tokens = true} + +local header_name = Gui.uid_name() +local left_panel_name = Gui.uid_name() +local right_panel_name = Gui.uid_name() +local input_text_box_name = Gui.uid_name() +local refresh_name = Gui.uid_name() + +Public.name = 'global' + +function Public.show(container) + local main_flow = container.add {type = 'flow', direction = 'horizontal'} + + local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name} + local left_panel_style = left_panel.style + left_panel_style.width = 300 + + for key, _ in pairs(global) do + if not ignore[key] then + local header = + left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = tostring(key)} + Gui.set_data(header, key) + end + end + + local right_flow = main_flow.add {type = 'flow', direction = 'vertical'} + + local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'} + + local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name} + local input_text_box_style = input_text_box.style + input_text_box_style.horizontally_stretchable = true + input_text_box_style.height = 32 + input_text_box_style.maximal_width = 1000 + + local refresh_button = + right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'} + local refresh_button_style = refresh_button.style + refresh_button_style.width = 32 + refresh_button_style.height = 32 + + local right_panel = right_flow.add {type = 'text-box', name = right_panel_name} + right_panel.read_only = true + right_panel.selectable = true + + local right_panel_style = right_panel.style + right_panel_style.vertically_stretchable = true + right_panel_style.horizontally_stretchable = true + right_panel_style.maximal_width = 1000 + right_panel_style.maximal_height = 1000 + + local data = { + right_panel = right_panel, + input_text_box = input_text_box, + selected_header = nil, + selected_token_id = nil + } + + Gui.set_data(input_text_box, data) + Gui.set_data(left_panel, data) + Gui.set_data(refresh_button, data) +end + +Gui.on_click( + header_name, + function(event) + local element = event.element + local key = Gui.get_data(element) + + local left_panel = element.parent.parent + local data = Gui.get_data(left_panel) + local right_panel = data.right_panel + local selected_header = data.selected_header + local input_text_box = data.input_text_box + + if selected_header then + selected_header.style.font_color = Color.white + end + + element.style.font_color = Color.orange + data.selected_header = element + + input_text_box.text = concat {"global['", key, "']"} + input_text_box.style.font_color = Color.black + + local content = dump(global[key]) or 'nil' + right_panel.text = content + end +) + +local function update_dump(text_input, data, player) + local suc, ouput = dump_text(text_input.text, player) + if not suc then + text_input.style.font_color = Color.red + else + text_input.style.font_color = Color.black + data.right_panel.text = ouput + end +end + +Gui.on_text_changed( + input_text_box_name, + function(event) + local element = event.element + local data = Gui.get_data(element) + + update_dump(element, data, event.player) + end +) + +Gui.on_click( + refresh_name, + function(event) + local element = event.element + local data = Gui.get_data(element) + + local input_text_box = data.input_text_box + + update_dump(input_text_box, data, event.player) + end +) + +return Public diff --git a/modules/gui/debug/main_view.lua b/modules/gui/debug/main_view.lua new file mode 100644 index 00000000..bac9ffb5 --- /dev/null +++ b/modules/gui/debug/main_view.lua @@ -0,0 +1,103 @@ +local Gui = require 'utils.gui' +local Color = require 'resources.color_presets' + +local Public = {} + +local pages = { + require 'modules.gui.debug.redmew_global_view', + require 'modules.gui.debug.global_view', + require 'modules.gui.debug.package_view', + require 'modules.gui.debug._g_view', + require 'modules.gui.debug.event_view' +} + +local main_frame_name = Gui.uid_name() +local close_name = Gui.uid_name() +local tab_name = Gui.uid_name() + +function Public.open_dubug(player) + for i = 1, #pages do + local page = pages[i] + local callback = page.on_open_debug + if callback then + callback() + end + end + + local center = player.gui.center + local frame = center[main_frame_name] + if frame then + return + end + + frame = center.add {type = 'frame', name = main_frame_name, caption = 'Debuggertron 3001', direction = 'vertical'} + local frame_style = frame.style + frame_style.height = 600 + frame_style.width = 900 + + local tab_flow = frame.add {type = 'flow', direction = 'horizontal'} + local container = frame.add {type = 'flow'} + container.style.vertically_stretchable = true + + local data = {} + + for i = 1, #pages do + local page = pages[i] + local tab_button = tab_flow.add({type = 'flow'}).add {type = 'button', name = tab_name, caption = page.name} + local tab_button_style = tab_button.style + + Gui.set_data(tab_button, {index = i, frame_data = data}) + + if i == 1 then + tab_button_style.font_color = Color.orange + + data.selected_index = i + data.selected_tab_button = tab_button + data.container = container + + Gui.set_data(frame, data) + page.show(container) + end + end + + frame.add {type = 'button', name = close_name, caption = 'Close'} +end + +Gui.on_click( + tab_name, + function(event) + local element = event.element + local data = Gui.get_data(element) + + local index = data.index + local frame_data = data.frame_data + local selected_index = frame_data.selected_index + + if selected_index == index then + return + end + + local selected_tab_button = frame_data.selected_tab_button + selected_tab_button.style.font_color = Color.black + + frame_data.selected_tab_button = element + frame_data.selected_index = index + element.style.font_color = Color.orange + + local container = frame_data.container + Gui.clear(container) + pages[index].show(container) + end +) + +Gui.on_click( + close_name, + function(event) + local frame = event.player.gui.center[main_frame_name] + if frame then + Gui.destroy(frame) + end + end +) + +return Public diff --git a/modules/gui/debug/model.lua b/modules/gui/debug/model.lua new file mode 100644 index 00000000..bf04eac9 --- /dev/null +++ b/modules/gui/debug/model.lua @@ -0,0 +1,147 @@ +local Gui = require 'utils.gui' +local table = require 'utils.table' + +local gui_names = Gui.names +local type = type +local concat = table.concat +local inspect = table.inspect +local pcall = pcall +local loadstring = loadstring +local rawset = rawset + +local Public = {} + +local luaObject = {'{', nil, ", name = '", nil, "'}"} +local luaPlayer = {"{LuaPlayer, name = '", nil, "', index = ", nil, '}'} +local luaEntity = {"{LuaEntity, name = '", nil, "', unit_number = ", nil, '}'} +local luaGuiElement = {"{LuaGuiElement, name = '", nil, "'}"} + +local function get(obj, prop) + return obj[prop] +end + +local function get_name_safe(obj) + local s, r = pcall(get, obj, 'name') + if not s then + return 'nil' + else + return r or 'nil' + end +end + +local function get_lua_object_type_safe(obj) + local s, r = pcall(get, obj, 'help') + + if not s then + return + end + + return r():match('Lua%a+') +end + +local function inspect_process(item) + if type(item) ~= 'table' or type(item.__self) ~= 'userdata' then + return item + end + + local suc, valid = pcall(get, item, 'valid') + if not suc then + -- no 'valid' property + return get_lua_object_type_safe(item) or '{NoHelp LuaObject}' + end + + if not valid then + return '{Invalid LuaObject}' + end + + local obj_type = get_lua_object_type_safe(item) + if not obj_type then + return '{NoHelp LuaObject}' + end + + if obj_type == 'LuaPlayer' then + luaPlayer[2] = item.name or 'nil' + luaPlayer[4] = item.index or 'nil' + + return concat(luaPlayer) + elseif obj_type == 'LuaEntity' then + luaEntity[2] = item.name or 'nil' + luaEntity[4] = item.unit_number or 'nil' + + return concat(luaEntity) + elseif obj_type == 'LuaGuiElement' then + local name = item.name + luaGuiElement[2] = gui_names and gui_names[name] or name or 'nil' + + return concat(luaGuiElement) + else + luaObject[2] = obj_type + luaObject[4] = get_name_safe(item) + + return concat(luaObject) + end +end + +local inspect_options = {process = inspect_process} +function Public.dump(data) + return inspect(data, inspect_options) +end +local dump = Public.dump + +function Public.dump_ignore_builder(ignore) + local function process(item) + if ignore[item] then + return nil + end + + return inspect_process(item) + end + + local options = {process = process} + return function(data) + return inspect(data, options) + end +end + +function Public.dump_function(func) + local res = {'upvalues:\n'} + + local i = 1 + while true do + local n, v = debug.getupvalue(func, i) + + if n == nil then + break + elseif n ~= '_ENV' then + res[#res + 1] = n + res[#res + 1] = ' = ' + res[#res + 1] = dump(v) + res[#res + 1] = '\n' + end + + i = i + 1 + end + + return concat(res) +end + +function Public.dump_text(text, player) + local func = loadstring('return ' .. text) + if not func then + return false + end + + rawset(game, 'player', player) + + local suc, var = pcall(func) + + rawset(game, 'player', nil) + + if not suc then + return false + end + + return true, dump(var) +end + +return Public diff --git a/modules/gui/debug/package_view.lua b/modules/gui/debug/package_view.lua new file mode 100644 index 00000000..479fb369 --- /dev/null +++ b/modules/gui/debug/package_view.lua @@ -0,0 +1,161 @@ +local Gui = require 'utils.gui' +local Color = require 'resources.color_presets' +local Model = require 'modules.gui.debug.model' + +local dump_function = Model.dump_function +local loaded = _G.package.loaded + +local Public = {} + +local ignore = { + _G = true, + package = true, + coroutine = true, + table = true, + string = true, + bit32 = true, + math = true, + debug = true, + serpent = true, + ['utils.math'] = true, + util = true, + ['utils.inspect'] = true, + ['mod-gui'] = true +} + +local file_label_name = Gui.uid_name() +local left_panel_name = Gui.uid_name() +local breadcrumbs_name = Gui.uid_name() +local top_panel_name = Gui.uid_name() +local variable_label_name = Gui.uid_name() +local text_box_name = Gui.uid_name() + +Public.name = 'package' + +function Public.show(container) + local main_flow = container.add {type = 'flow', direction = 'horizontal'} + + local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name} + local left_panel_style = left_panel.style + left_panel_style.width = 300 + + for name, file in pairs(loaded) do + if not ignore[name] then + local file_label = + left_panel.add({type = 'flow'}).add {type = 'label', name = file_label_name, caption = name} + Gui.set_data(file_label, file) + end + end + + local right_flow = main_flow.add {type = 'flow', direction = 'vertical'} + + local breadcrumbs = right_flow.add {type = 'label', name = breadcrumbs_name} + + local top_panel = right_flow.add {type = 'scroll-pane', name = top_panel_name} + local top_panel_style = top_panel.style + top_panel_style.height = 200 + top_panel_style.maximal_width = 1000 + top_panel_style.horizontally_stretchable = true + + local text_box = right_flow.add {type = 'text-box', name = text_box_name} + text_box.read_only = true + text_box.selectable = true + + local text_box_style = text_box.style + text_box_style.vertically_stretchable = true + text_box_style.horizontally_stretchable = true + text_box_style.maximal_width = 1000 + text_box_style.maximal_height = 1000 + + local data = { + left_panel = left_panel, + breadcrumbs = breadcrumbs, + top_panel = top_panel, + text_box = text_box, + selected_file_label = nil, + selected_variable_label = nil + } + + Gui.set_data(left_panel, data) + Gui.set_data(top_panel, data) +end + +Gui.on_click( + file_label_name, + function(event) + local element = event.element + local file = Gui.get_data(element) + + local left_panel = element.parent.parent + local data = Gui.get_data(left_panel) + + local selected_file_label = data.selected_file_label + + if selected_file_label then + selected_file_label.style.font_color = Color.white + end + + element.style.font_color = Color.orange + data.selected_file_label = element + + local top_panel = data.top_panel + local text_box = data.text_box + + Gui.clear(top_panel) + + local file_type = type(file) + + if file_type == 'table' then + for k, v in pairs(file) do + local label = + top_panel.add({type = 'flow'}).add {type = 'label', name = variable_label_name, caption = k} + Gui.set_data(label, v) + end + elseif file_type == 'function' then + text_box.text = dump_function(file) + else + text_box.text = tostring(file) + end + end +) + +Gui.on_click( + variable_label_name, + function(event) + local element = event.element + local variable = Gui.get_data(element) + + local top_panel = element.parent.parent + local data = Gui.get_data(top_panel) + local text_box = data.text_box + + local variable_type = type(variable) + + if variable_type == 'table' then + Gui.clear(top_panel) + for k, v in pairs(variable) do + local label = + top_panel.add({type = 'flow'}).add {type = 'label', name = variable_label_name, caption = k} + Gui.set_data(label, v) + end + return + end + + local selected_label = data.selected_variable_label + + if selected_label and selected_label.valid then + selected_label.style.font_color = Color.white + end + + element.style.font_color = Color.orange + data.selected_variable_label = element + + if variable_type == 'function' then + text_box.text = dump_function(variable) + else + text_box.text = tostring(variable) + end + end +) + +return Public diff --git a/modules/gui/debug/redmew_global_view.lua b/modules/gui/debug/redmew_global_view.lua new file mode 100644 index 00000000..51204150 --- /dev/null +++ b/modules/gui/debug/redmew_global_view.lua @@ -0,0 +1,129 @@ +local Gui = require 'utils.gui' +local Global = require 'utils.global' +local Token = require 'utils.token' +local Color = require 'resources.color_presets' +local Model = require 'modules.gui.debug.model' + +local dump = Model.dump +local dump_text = Model.dump_text +local concat = table.concat + +local Public = {} + +local header_name = Gui.uid_name() +local left_panel_name = Gui.uid_name() +local right_panel_name = Gui.uid_name() +local input_text_box_name = Gui.uid_name() +local refresh_name = Gui.uid_name() + +Public.name = 'Global' + +function Public.show(container) + local main_flow = container.add {type = 'flow', direction = 'horizontal'} + + local left_panel = main_flow.add {type = 'scroll-pane', name = left_panel_name} + local left_panel_style = left_panel.style + left_panel_style.width = 300 + + for token_id, token_name in pairs(Global.names) do + local header = left_panel.add({type = 'flow'}).add {type = 'label', name = header_name, caption = token_name} + Gui.set_data(header, token_id) + end + + local right_flow = main_flow.add {type = 'flow', direction = 'vertical'} + + local right_top_flow = right_flow.add {type = 'flow', direction = 'horizontal'} + + local input_text_box = right_top_flow.add {type = 'text-box', name = input_text_box_name} + local input_text_box_style = input_text_box.style + input_text_box_style.horizontally_stretchable = true + input_text_box_style.height = 32 + input_text_box_style.maximal_width = 1000 + + local refresh_button = + right_top_flow.add {type = 'sprite-button', name = refresh_name, sprite = 'utility/reset', tooltip = 'refresh'} + local refresh_button_style = refresh_button.style + refresh_button_style.width = 32 + refresh_button_style.height = 32 + + local right_panel = right_flow.add {type = 'text-box', name = right_panel_name} + right_panel.read_only = true + right_panel.selectable = true + + local right_panel_style = right_panel.style + right_panel_style.vertically_stretchable = true + right_panel_style.horizontally_stretchable = true + right_panel_style.maximal_width = 1000 + right_panel_style.maximal_height = 1000 + + local data = { + right_panel = right_panel, + input_text_box = input_text_box, + selected_header = nil + } + + Gui.set_data(input_text_box, data) + Gui.set_data(left_panel, data) + Gui.set_data(refresh_button, data) +end + +Gui.on_click( + header_name, + function(event) + local element = event.element + local token_id = Gui.get_data(element) + + local left_panel = element.parent.parent + local data = Gui.get_data(left_panel) + local right_panel = data.right_panel + local selected_header = data.selected_header + local input_text_box = data.input_text_box + + if selected_header then + selected_header.style.font_color = Color.white + end + + element.style.font_color = Color.orange + data.selected_header = element + + input_text_box.text = concat {'global.tokens[', token_id, ']'} + input_text_box.style.font_color = Color.black + + local content = dump(Token.get_global(token_id)) or 'nil' + right_panel.text = content + end +) + +local function update_dump(text_input, data, player) + local suc, ouput = dump_text(text_input.text, player) + if not suc then + text_input.style.font_color = Color.red + else + text_input.style.font_color = Color.black + data.right_panel.text = ouput + end +end + +Gui.on_text_changed( + input_text_box_name, + function(event) + local element = event.element + local data = Gui.get_data(element) + + update_dump(element, data, event.player) + end +) + +Gui.on_click( + refresh_name, + function(event) + local element = event.element + local data = Gui.get_data(element) + + local input_text_box = data.input_text_box + + update_dump(input_text_box, data, event.player) + end +) + +return Public diff --git a/old/modules/ExpGamingCore/Gui/center/control.lua b/old/modules/DONE/Core/Gui/center/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/center/control.lua rename to old/modules/DONE/Core/Gui/center/control.lua diff --git a/old/modules/ExpGamingCore/Gui/center/softmod.json b/old/modules/DONE/Core/Gui/center/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/center/softmod.json rename to old/modules/DONE/Core/Gui/center/softmod.json diff --git a/old/modules/ExpGamingCore/Gui/control.lua b/old/modules/DONE/Core/Gui/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/control.lua rename to old/modules/DONE/Core/Gui/control.lua diff --git a/old/modules/ExpGamingCore/Gui/inputs/control.lua b/old/modules/DONE/Core/Gui/inputs/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/inputs/control.lua rename to old/modules/DONE/Core/Gui/inputs/control.lua diff --git a/old/modules/ExpGamingCore/Gui/inputs/softmod.json b/old/modules/DONE/Core/Gui/inputs/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/inputs/softmod.json rename to old/modules/DONE/Core/Gui/inputs/softmod.json diff --git a/old/modules/ExpGamingCore/Gui/left/control.lua b/old/modules/DONE/Core/Gui/left/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/left/control.lua rename to old/modules/DONE/Core/Gui/left/control.lua diff --git a/old/modules/ExpGamingCore/Gui/left/order_config.lua b/old/modules/DONE/Core/Gui/left/order_config.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/left/order_config.lua rename to old/modules/DONE/Core/Gui/left/order_config.lua diff --git a/old/modules/ExpGamingCore/Gui/left/softmod.json b/old/modules/DONE/Core/Gui/left/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/left/softmod.json rename to old/modules/DONE/Core/Gui/left/softmod.json diff --git a/old/modules/ExpGamingCore/Gui/locale/de.cfg b/old/modules/DONE/Core/Gui/locale/de.cfg similarity index 100% rename from old/modules/ExpGamingCore/Gui/locale/de.cfg rename to old/modules/DONE/Core/Gui/locale/de.cfg diff --git a/old/modules/ExpGamingCore/Gui/locale/en.cfg b/old/modules/DONE/Core/Gui/locale/en.cfg similarity index 100% rename from old/modules/ExpGamingCore/Gui/locale/en.cfg rename to old/modules/DONE/Core/Gui/locale/en.cfg diff --git a/old/modules/ExpGamingCore/Gui/locale/fr.cfg b/old/modules/DONE/Core/Gui/locale/fr.cfg similarity index 100% rename from old/modules/ExpGamingCore/Gui/locale/fr.cfg rename to old/modules/DONE/Core/Gui/locale/fr.cfg diff --git a/old/modules/ExpGamingCore/Gui/locale/nl.cfg b/old/modules/DONE/Core/Gui/locale/nl.cfg similarity index 100% rename from old/modules/ExpGamingCore/Gui/locale/nl.cfg rename to old/modules/DONE/Core/Gui/locale/nl.cfg diff --git a/old/modules/ExpGamingCore/Gui/locale/sv-SE.cfg b/old/modules/DONE/Core/Gui/locale/sv-SE.cfg similarity index 100% rename from old/modules/ExpGamingCore/Gui/locale/sv-SE.cfg rename to old/modules/DONE/Core/Gui/locale/sv-SE.cfg diff --git a/old/modules/ExpGamingCore/Gui/popup/control.lua b/old/modules/DONE/Core/Gui/popup/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/popup/control.lua rename to old/modules/DONE/Core/Gui/popup/control.lua diff --git a/old/modules/ExpGamingCore/Gui/popup/softmod.json b/old/modules/DONE/Core/Gui/popup/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/popup/softmod.json rename to old/modules/DONE/Core/Gui/popup/softmod.json diff --git a/old/modules/ExpGamingCore/Gui/softmod.json b/old/modules/DONE/Core/Gui/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/softmod.json rename to old/modules/DONE/Core/Gui/softmod.json diff --git a/old/modules/ExpGamingCore/Gui/src/server.lua b/old/modules/DONE/Core/Gui/src/server.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/src/server.lua rename to old/modules/DONE/Core/Gui/src/server.lua diff --git a/old/modules/ExpGamingCore/Gui/src/test.lua b/old/modules/DONE/Core/Gui/src/test.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/src/test.lua rename to old/modules/DONE/Core/Gui/src/test.lua diff --git a/old/modules/ExpGamingCore/Gui/toolbar/control.lua b/old/modules/DONE/Core/Gui/toolbar/control.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/toolbar/control.lua rename to old/modules/DONE/Core/Gui/toolbar/control.lua diff --git a/old/modules/ExpGamingCore/Gui/toolbar/order_config.lua b/old/modules/DONE/Core/Gui/toolbar/order_config.lua similarity index 100% rename from old/modules/ExpGamingCore/Gui/toolbar/order_config.lua rename to old/modules/DONE/Core/Gui/toolbar/order_config.lua diff --git a/old/modules/ExpGamingCore/Gui/toolbar/softmod.json b/old/modules/DONE/Core/Gui/toolbar/softmod.json similarity index 100% rename from old/modules/ExpGamingCore/Gui/toolbar/softmod.json rename to old/modules/DONE/Core/Gui/toolbar/softmod.json diff --git a/utils/core.lua b/utils/core.lua index d8bdc6b3..92fc031f 100644 --- a/utils/core.lua +++ b/utils/core.lua @@ -3,13 +3,11 @@ -- Dependencies local Game = require 'utils.game' local Color = require 'resources.color_presets' -local Server = require 'features.server' -- localized functions local random = math.random local sqrt = math.sqrt local floor = math.floor -local format = string.format local match = string.match local insert = table.insert local concat = table.concat @@ -201,27 +199,6 @@ function Module.set_and_return(tbl, key, value) return value end ---- Takes msg and prints it to all players. Also prints to the log and discord --- @param msg The message to print --- @param warning_prefix The name of the module/warning -function Module.action_warning(warning_prefix, msg) - game.print(prefix .. msg, Color.yellow) - msg = format('%s %s', warning_prefix, msg) - log(msg) - Server.to_discord_bold(msg) -end - ---- Takes msg and prints it to all players except provided player. Also prints to the log and discord --- @param msg The message to print --- @param warning_prefix The name of the module/warning --- @param player the player not to send the message to -function Module.silent_action_warning(warning_prefix, msg, player) - Module.print_except(prefix .. msg, player, Color.yellow) - msg = format('%s %s', warning_prefix, msg) - log(msg) - Server.to_discord_bold(msg) -end - -- add utility functions that exist in base factorio/util require 'util' diff --git a/utils/event_core.lua b/utils/event_core.lua index 6ea0dac0..364e3134 100644 --- a/utils/event_core.lua +++ b/utils/event_core.lua @@ -40,6 +40,7 @@ end local function on_init() _LIFECYCLE = 5 -- on_init + log('[INFO] Entering on_init') local handlers = event_handlers[init_event_name] call_handlers(handlers) @@ -47,10 +48,12 @@ local function on_init() event_handlers[load_event_name] = nil _LIFECYCLE = 8 -- Runtime + log('[INFO] Entering runtime') end local function on_load() _LIFECYCLE = 6 -- on_load + log('[INFO] Entering on_load') local handlers = event_handlers[load_event_name] call_handlers(handlers) @@ -58,6 +61,7 @@ local function on_load() event_handlers[load_event_name] = nil _LIFECYCLE = 8 -- Runtime + log('[INFO] Entering runtime') end local function on_nth_tick_event(event) diff --git a/utils/global.lua b/utils/global.lua index 4d23bd43..a2689e9a 100644 --- a/utils/global.lua +++ b/utils/global.lua @@ -36,7 +36,7 @@ function Global.register_init(tbl, init_handler, callback) ) end -if _DEBUG then +if _DEBUG or true then local concat = table.concat local names = {} diff --git a/utils/gui.lua b/utils/gui.lua index 7ff901aa..8a765dff 100644 --- a/utils/gui.lua +++ b/utils/gui.lua @@ -2,6 +2,7 @@ local Token = require 'utils.token' local Event = require 'utils.event' local Game = require 'utils.game' local Global = require 'utils.global' +local mod_gui = require 'mod-gui' local Gui = {} @@ -179,7 +180,7 @@ Gui.on_player_show_top = custom_handler_factory(on_visible_handlers) Gui.on_pre_player_hide_top = custom_handler_factory(on_pre_hidden_handlers) --- Allows the player to show / hide this element. --- The element must be part in gui.top. +-- The element must be in Gui.get_top_element_flow(player) -- This function must be called in the control stage, i.e not inside an event. -- @param element_name This name must be globally unique. function Gui.allow_player_to_toggle_top_element_visibility(element_name) @@ -189,7 +190,17 @@ function Gui.allow_player_to_toggle_top_element_visibility(element_name) top_elements[#top_elements + 1] = element_name end +--- Returns the flow where top elements can be added and will be effected by google visibility +-- For the toggle to work it must be registed with Gui.allow_player_to_toggle_top_element_visibility(element_name) +-- @tparam player LuaPlayer pointer to the player who has the gui +-- @treturn LuaGuiEelement the top element flow +function Gui.get_top_element_flow(player) + player = Game.get_player_from_any(player) + return mod_gui.get_button_flow(player) +end + local toggle_button_name = Gui.uid_name() +Gui.top_toggle_button_name = toggle_button_name Event.add( defines.events.on_player_created, @@ -200,16 +211,18 @@ Event.add( return end - local b = - player.gui.top.add { + local top = Gui.get_top_element_flow(player) + + local b = top.add { type = 'button', name = toggle_button_name, + style = mod_gui.button_style, caption = '<', - tooltip = 'Shows / hides the Redmew Gui buttons.' + tooltip = {'gui_util.button_tooltip'} } local style = b.style style.width = 18 - style.height = 38 + style.height = 36 style.left_padding = 0 style.top_padding = 0 style.right_padding = 0 @@ -223,21 +236,16 @@ Gui.on_click( function(event) local button = event.element local player = event.player - local top = player.gui.top + local top = Gui.get_top_element_flow(player) if button.caption == '<' then for i = 1, #top_elements do local name = top_elements[i] local ele = top[name] if ele and ele.valid then - local style = ele.style - - -- if visible is not set it has the value of nil. - -- Hence nil is treated as is visible. - local v = style.visible - if v or v == nil then + if ele.visible then custom_raise(on_pre_hidden_handlers, ele, player) - style.visible = false + ele.visible = false end end end @@ -249,17 +257,15 @@ Gui.on_click( local name = top_elements[i] local ele = top[name] if ele and ele.valid then - local style = ele.style - - if not style.visible then - style.visible = true + if not ele.visible then + ele.visible = true custom_raise(on_visible_handlers, ele, player) end end end button.caption = '<' - button.style.height = 38 + button.style.height = 36 end end ) @@ -284,4 +290,4 @@ if _DEBUG then end end -return Gui +return Gui \ No newline at end of file