Refactor of commands

This commit is contained in:
Cooldude2606
2019-03-01 20:24:23 +00:00
parent e547f76d6f
commit 62dcfe8694
288 changed files with 5364 additions and 1067 deletions

View File

@@ -1,754 +0,0 @@
--- Factorio Softmod Manager
-- @module FSM
-- @alias Manager
-- @author Cooldude2606
-- @usage Manager = require("FactorioSoftmodManager")
local moduleIndex = require("/modules/index")
local Manager = {}
-- this is a constant that is used to represent the server
SERVER = setmetatable({index=0,name='<server>',online_time=0,afk_time=0,print=print,admin=true,valid=true,__self={}},{__index=function(tbl,key) if type(game.players[1][key]) == 'function' then return function() end else return nil end end})
--- Setup for metatable of the Manager to force read only nature
-- @usage Manager() -- runs Manager.loadModdules()
-- @usage Manager[name] -- returns module by that name
-- @usage tostring(Manager) -- returns formated list of loaded modules
local ReadOnlyManager = setmetatable({},{
__metatable=false,
__index=function(tbl,key)
-- first looks in manager and then looks in mander.loadModules
return rawget(Manager,key) ~= nil and rawget(Manager,key) or rawget(Manager.loadModules,key)
end,
__call=function(tbl)
-- if there are no modules loaded then it loads them
if #tbl.loadModules == 0 then
tbl.loadModules()
end
end,
__newindex=function(tbl,key,value)
-- provents the changing of any key that is not currentState
if key == 'currentState' then
-- provides a verbose that is always emited describing the change in state
Manager.verbose(string.rep('__',10)..'| Start: '..value..' |'..string.rep('__',10),true)
Manager.verbose('The verbose state is now: '..tostring(Manager.setVerbose[value]),true)
rawset(Manager,key,value)
else error('Manager is read only please use included methods',2) end
end,
__tostring=function(tbl)
-- acts as a redirect
return tostring(Manager.loadModules)
end
})
local function setupModuleName(name)
-- creates a table that acts like a string but is read only
return setmetatable({},{
__index=function(tbl,key) return name end,
__newindex=function(tbl,key,value) error('Module Name Is Read Only') end,
__tostring=function(tbl) return name end,
__concat=function(val1,val2) return type(val1) == 'string' and val1..name or name..val2 end,
__metatable=false,
})
end
Manager.currentState = 'selfInit'
-- selfInit > moduleLoad > moduleInit > modulePost > moduleEnv
--- Default output for the verbose
-- @usage Manager.verbose('Hello, World!')
-- @tparam string rtn the value that will be returned though verbose output
Manager._verbose = function(rtn)
-- creates one file per game, ie clears file on reset
if game and Manager.setVerbose._output ~= true then Manager.setVerbose._output=true game.write_file('verbose.log',rtn)
elseif game then game.write_file('verbose.log','\n'..rtn,true) end
-- standard print and log, _log is a version of log which is ln1 of control.lua for shorter log lines
if print then print(rtn) end
if _log then _log(rtn) end
end
--- Used to call the output of the verbose when the current state allows it
-- @usage Manager.verbose('Hello, World!')
-- @tparam string rtn the value that will be returned though verbose output
-- @tparam string action is used to decide which verbose this is error || event etc
Manager.verbose = function(rtn,action)
local settings = Manager.setVerbose
local state = Manager.currentState
if Manager.error and state == Manager.error.__crash then return end
-- if ran in a module the the global moduleName is present
local rtn = type(rtn) == table and serpent.line(rtn) or tostring(rtn)
if moduleName then rtn='['..moduleName..'] '..rtn
else rtn='[FSM] '..rtn end
-- module_verbose is a local override for a file, action is used in the manager to describe an extra type, state is the current state
-- if action is true then it will always trigger verbose
if module_verbose or (action and (action == true or settings[action])) or (not action and settings[state]) then
if type(settings.output) == 'function' then
-- calls the output function, not pcalled as if this fails some thing is very wrong
settings.output(rtn)
else
error('Verbose set for: '..state..' but output can not be called',2)
end
end
end
--- Main logic for allowing verbose at different stages though out the script
-- @function Manager.setVerbose
-- @usage Manager.setVerbose{output=log}
-- @tparam newTbl settings the table that will be searched for settings to be updated
-- @usage Manager.setVerbose[setting] -- returns the value of that setting
-- @usage tostring(Manager.setVerbose) -- returns a formated list of the current settings
Manager.setVerbose = setmetatable(
--- Different verbose settings used for setVerbose
-- @table Manager.verboseSettings
-- @tfield boolean selfInit called while the manager is being set up
-- @tfield boolean moduleLoad when a module is required by the manager
-- @tfield boolean moduleInit when and within the initation of a module
-- @tfield boolean modulePost when and within the post of a module
-- @tfield boolean moduleEnv during module runtime, this is a global option set within each module(module_verbose=true ln:1) for fine control
-- @tfield boolean eventRegistered when a module registers its event handlers
-- @tfield boolean errorCaught when an error is caught during runtime
-- @tfield function output can be: print || log || or other function
-- @field _output a constant value that can used to store output data
{
selfInit=true,
moduleLoad=false,
moduleInit=false,
modulePost=false,
moduleEnv=false,
eventRegistered=false,
errorCaught=true,
output=Manager._verbose,
_output={}
},
{
__metatable=false,
__call=function(tbl,settings)
-- does not allow any new keys, but will override any existing ones
for key,value in pairs(settings) do
if rawget(tbl,key) ~= nil then
Manager.verbose('Verbose for: "'..key..'" has been set to: '..tostring(value))
rawset(tbl,key,value)
end
end
end,
__newindex=function(tbl,key,value)
-- stops creationg of new keys
error('New settings cannot be added during runtime',2)
end,
__index=function(tbl,key)
-- will always return a value, never nil
return rawget(tbl,key) or false
end,
__tostring=function(tbl)
-- a simple concat function for the settings
local rtn = ''
for key,value in pairs(tbl) do
if type(value) == 'boolean' then
rtn=rtn..key..': '..tostring(value)..', '
end
end
return rtn:sub(1,-3)
end
}
)
-- call to verbose to show start up, will always be present
Manager.verbose(string.rep('__',10)..'| Start: selfInit |'..string.rep('__',10),true)
Manager.verbose('The verbose state is: '..tostring(Manager.setVerbose.selfInit),true)
--- Used to avoid conflicts in the global table
-- @usage global[key] -- used like the normal global table
-- @usage global{'foo','bar'} -- sets the default value
-- @usage global(true) -- restores global to default
-- @usage global(mopdule_name) -- returns that module's global
-- @tparam[opt={}] ?table|string|true if table then the default for the global, if a string then the module to get the global of, if true then reset the global to default
-- @treturn table the new global table for that module
Manager.global=setmetatable({__defaults={},__global={
__call=function(tbl,default) return Manager.global(default) end,
__index=function(tbl,key) return rawget(Manager.global(),key) or tbl(key) end,
__newindex=function(tbl,key,value) rawset(Manager.global(),key,value) end,
__pairs=function(tbl)
local tbl = Manager.global()
local function next_pair(tbl,k)
k, v = next(tbl, k)
if type(v) ~= nil then return k,v end
end
return next_pair, tbl, nil
end
}},{
__call=function(tbl,default,metatable_src)
-- creates varible link to global and module information, use of a metatable is for already formed globals
local Global = _G.global
local metatable = getmetatable(metatable_src)
local moduleName = type(default) == 'string' and default or metatable and metatable._moduleName or moduleName
local module_path = type(default) == 'string' and Manager.loadModules.__load[default] or metatable and metatable._module_path or module_path
-- if there is no name or path then it will return and unedited version of global
if not module_path or not moduleName then return _G.global end
-- edits the link to global to be the corrected dir, path varible is also created
local path = 'global'
for dir in module_path:gmatch('%a+') do
path = path..'.'..dir
if not rawget(Global,dir) then Manager.verbose('Added Global Dir: '..path) rawset(Global,dir,{}) end
Global = rawget(Global,dir)
end
-- the default value is set if there was a default given
if type(default) == 'table' then Manager.verbose('Default global has been set for: global'..string.sub(module_path:gsub('/','.')),2) rawset(rawget(tbl,'__defaults'),tostring(moduleName),default) end
-- if the default value is true then it will reset the global to its default
if default == true and rawget(rawget(tbl,'__defaults'),tostring(moduleName)) then
Manager.verbose('Reset Global Dir to default: '..path)
-- cant set it to be equle otherwise it will lose its global propeity
local function deepcopy(tbl) if type(tbl) ~= 'table' then return tbl end local rtn = {} for key,value in pairs(tbl) do rtn[key] = deepcopy(value) end return rtn end
for key,value in pairs(Global) do rawset(Global,key,nil) end
for key,value in pairs(rawget(rawget(tbl,'__defaults'),tostring(moduleName))) do rawset(Global,key,deepcopy(value)) end
end
-- the metatable is remade if not already present
metatable = metatable or {
__call=function(tbl,default) return Manager.global(default,tbl) end,
__index=function(tbl,key) return rawget(Manager.global(nil,tbl),key) or moduleIndex[key] and Manager.global(key) end,
__newindex=function(tbl,key,value) rawset(Manager.global(nil,tbl),key,value) end,
__pairs=function(tbl)
local tbl = Manager.global(nil,tbl)
local function next_pair(tbl,k)
k, v = next(tbl, k)
if type(v) ~= nil then return k,v end
end
return next_pair, tbl, nil
end,
_module_path=module_path,_moduleName=moduleName
}
return setmetatable(Global,metatable)
end,
__index=function(tbl,key) return rawget(tbl(),key) or rawget(_G.global,key) or moduleIndex[key] and Manager.global(key) end,
__newindex=function(tbl,key,value) rawset(tbl(),key,value) end,
__pairs=function(tbl)
local tbl = Manager.global()
local function next_pair(tbl,k)
k, v = next(tbl, k)
if type(v) ~= nil then return k,v end
end
return next_pair, tbl, nil
end
})
setmetatable(global,Manager.global.__global)
--- Creates a sand box envorment and runs a callback in that sand box; provents global pollution
-- @function Manager.sandbox
-- @usage Manager.sandbox(callback) -- return sandbox, success, other returns from callback
-- @tparam function callback the function that will be ran in the sandbox
-- @param[opt] env any other params that the function will use
-- @usage Manager.sandbox() -- returns and empty sandbox
-- @usage Manager.sandbox[key] -- returns the sand box value in that key
Manager.sandbox = setmetatable({
-- can not use existing keys of _G
verbose=Manager.verbose,
loaded_modules={}, -- this is over riden later
module_verbose=false,
module_exports=false,
_no_error_verbose=true
},{
__metatable=false,
__index=ReadOnlyManager,
__call=function(tbl,callback,env,...)
if type(callback) == 'function' then
-- creates a new sandbox env
local sandbox = tbl()
local env = type(env) == 'table' and env or {}
local _G_mt = getmetatable(_G)
-- creates a new ENV where it will look in the provided env then the sand box and then _G, new indexes saved to sandbox
local tmp_env = setmetatable({},{__index=function(tbl,key) return env[key] or sandbox[key] or rawget(_G,key) end,newindex=sandbox})
tmp_env._ENV = tmp_env
tmp_env._G_mt = _G_mt
-- sets the upvalues for the function
local i = 1
while true do
local name, value = debug.getupvalue(callback,i)
if not name then break else if not value and tmp_env[name] then debug.setupvalue(callback,i,tmp_env[name]) end end
i=i+1
end
-- runs the callback
setmetatable(_G,{__index=tmp_env,newindex=sandbox})
local rtn = {pcall(callback,...)}
local success = table.remove(rtn,1)
setmetatable(_G,_G_mt)
-- this is to allow modules to be access with out the need of using Mangaer[name] also keeps global clean
if success then return success, rtn, sandbox
else return success, rtn[1], sandbox end
else return setmetatable({},{__index=tbl}) end
end
})
--- Allows access to modules via require and collections are returned as one object
-- @function Manager.require
-- @usage local Module = Manager.require(ModuleName)
-- @usage local Module = Manager.require[ModuleName]
-- @usage local SrcData = Manager.require(path)
-- @treturn table the module that was required, one object containg submodules for a
Manager.require = setmetatable({
__require=require
},{
__metatable=false,
__index=function(tbl,key) return tbl(key,nil,true) end,
__call=function(tbl,path,env,mute,noLoad)
local raw_require = rawget(tbl,'__require')
local env = env or {}
-- runs in a sand box becuase sandbox everything
local success, data = Manager.sandbox(raw_require,env,path)
-- if there was no error then it assumed the path existed and returns the data
if success then return unpack(data)
else
if type(path) ~= 'string' then error('Path supplied must be a string; got: '..type(path),2) return end
local override = {}
local softmod = override
local path = path:find('@') and path:sub(1,path:find('@')-1) or path
-- tries to load the module from the modeul index
if moduleIndex[path] and not noLoad or Manager.loadModules.__load[path] then softmod = Manager.loadModules[path] end
-- will then look for any submodules if there are any; only once every module is loaded
for moduleName,subpath in pairs(moduleIndex) do
if moduleName:find(path) == 1 and moduleName ~= path then
local start, _end = moduleName:find(path)
local subname = moduleName:sub(_end+2)
-- does not add the module if it is a subsubmodule; or the key already exitsts
if not softmod then softmod = {} end
if not subname:find('.',nil,true) and not softmod[subname] then softmod[subname] = Manager.require(moduleName,nil,true,true) end
end
end
-- if there is any keys in the softmod it is returned else the errors with the require error
if override ~= softmod then return softmod end
if mute then return false else error(data,2) end
end
end
})
require = Manager.require
--- Loads the modules that are present in the index list
-- @function Manager.loadModules
-- @usage Manager.loadModules() -- loads all moddules in the index list
-- @usage #Manager.loadModules -- returns the number of modules loaded
-- @usage tostring(Manager.loadModules) -- returns a formatted list of all modules loaded
-- @usage pairs(Manager.loadModules) -- loops over the loaded modules moduleName, module
Manager.loadModules = setmetatable({
__load=setmetatable({},{__call=function(self,moduleName)
-- check to provent multiple calls
if self[moduleName] then return end
self[moduleName] = true
self = Manager.loadModules
-- loads the module and its dependices if there are not loaded
local load = moduleIndex[moduleName]
if not load then return end
local path = table.remove(load,1)
Manager.verbose('Loading module: "'..moduleName..'"; path: '..path)
-- loads the parent module
if moduleName:find('.',nil,true) then
local revModuleName = moduleName:reverse()
local start, _end = revModuleName:find('.',nil,true)
local parentName = revModuleName:sub(_end+1):reverse()
Manager.verbose('Loading module parent: "'..parentName..'" for: "'..moduleName..'"; path: '..path)
self.__load(parentName)
end
-- loads the dependices
Manager.verbose('Loading module dependices for: "'..moduleName..'"; path: '..path)
for _,depName in pairs(load) do self.__load(depName) end
self.__load[moduleName] = path
-- runs the module in a sandbox env
local success, module, sandbox = Manager.sandbox(Manager.require.__require,{moduleName=setupModuleName(moduleName),module_path=path},path..'/control')
-- extracts the module into a global index table for later use
if success then
-- verbose to notifie of any globals that were attempted to be created
local globals = ''
for key,value in pairs(sandbox) do globals = globals..key..', ' end
if globals ~= '' then Manager.verbose('Globals caught in "'..moduleName..'": '..globals:sub(1,-3),'errorCaught') end
Manager.verbose('Successfully loaded: "'..moduleName..'"; path: '..path)
-- if it is not a table or nil then it will set up a metatable on it
local currentType = type(rawget(self,moduleName))
if currentType ~= 'nil' and currentType ~= 'table' then
-- if it is a function then it is still able to be called even if more keys are going to be added
-- if it is a string then it will act like one; if it is a number well thats too many metatable indexs
self[moduleName] = setmetatable({__old=self[moduleName]},{
__call=function(self,...) if type(self.__old) == 'function' then self.__old(...) else return self.__old end end,
__tostring=function(self) return self.__old end,
__concat=function(self,val) return self.__old..val end
})
end
-- if you prefere module_exports can be used rather than returning the module
local appendAs = sandbox.module_exports or table.remove(module,1)
if not self[moduleName] then self[moduleName] = appendAs -- if nil it just sets the value
else for key,value in pairs(appendAs) do self[moduleName][key] = value end end -- else it appends the new values
-- if there is a module by this name in _G ex table then it will be indexed to the new module
if rawget(_G,moduleName) and type(rawget(self,moduleName)) == 'table' then setmetatable(rawget(_G,moduleName),{__index=self[moduleName]}) end
if type(rawget(self,moduleName)) == 'table' then self[moduleName]._module_path = path self[moduleName]._moduleName = moduleName end
-- loads the submodule for this softmod
Manager.verbose('Loading submodules for: "'..moduleName..'"; path: '..path)
for subModName,_ in pairs(moduleIndex) do
if subModName:find(moduleName) == 1 and subModName ~= moduleName then self.__load(subModName) end
end
else
Manager.verbose('Failed load: "'..moduleName..'"; path: '..path..' ('..module..')','errorCaught')
for event_name,callbacks in pairs(Manager.event) do Manager.verbose('Removed Event Handler: "'..moduleName..'/'..Manager.event.names[event_name],'eventRegistered') callbacks[moduleName] = nil end
end
end}),
__init=setmetatable({},{__call=function(self,moduleName)
-- check to provent multiple calls
if self[moduleName] or not Manager.loadModules.__load[moduleName] then return end
self[moduleName] = true
self = Manager.loadModules
-- calls on_init for each module
-- looks for init so that init or on_init can be used
local data = self[moduleName]
if type(data) == 'table' and data.init and data.on_init == nil then data.on_init = data.init data.init = nil end
if type(data) == 'table' and data.on_init and type(data.on_init) == 'function' then
Manager.verbose('Initiating module: "'..moduleName..'"')
local success, err = Manager.sandbox(data.on_init,{moduleName=setupModuleName(moduleName),module_path=Manager.loadModules.__load[tostring(moduleName)]},data)
if success then
Manager.verbose('Successfully Initiated: "'..moduleName..'"')
else
Manager.verbose('Failed Initiation: "'..moduleName..'" ('..err..')','errorCaught')
end
-- clears the init function so it cant be used in runtime
data.on_init = nil
end
end}),
__post=setmetatable({},{__call=function(self,moduleName)
-- check to provent multiple calls
if self[moduleName] or not Manager.loadModules.__init[moduleName] then return end
self[moduleName] = true
self = Manager.loadModules
-- calls on_post for each module
-- looks for post so that post or on_post can be used
local data = self[moduleName]
if type(data) == 'table' and data.post and data.on_post == nil then data.on_post = data.post data.post = nil end
if type(data) == 'table' and data.on_post and type(data.on_post) == 'function' then
Manager.verbose('Post for module: "'..moduleName..'"')
local success, err = Manager.sandbox(data.on_post,{moduleName=setupModuleName(moduleName),module_path=Manager.loadModules.__load[tostring(moduleName)]},data)
if success then
Manager.verbose('Successful post: "'..moduleName..'"')
else
Manager.verbose('Failed post: "'..moduleName..'" ('..err..')','errorCaught')
end
-- clears the post function so it cant be used in runtime
data.on_post = nil
end
end})
},
{
__metatable=false,
__index=function(self,moduleName)
-- will load one module if it is not already loaded, will not init during load state or post
self.__load(moduleName)
if (ReadOnlyManager.currentState == 'moduleLoad') then return end
self.__init(moduleName)
if (ReadOnlyManager.currentState == 'moduleInit') then return end
self.__post(moduleName)
return rawget(self,moduleName)
end,
__call=function(self)
-- goes though the index looking for modules to load
ReadOnlyManager.currentState = 'moduleLoad'
for moduleName,path in pairs(moduleIndex) do self.__load(moduleName) end
-- runs though all loaded modules looking for on_init function; all other modules have been loaded use this to load extra code based on opttial dependies
ReadOnlyManager.currentState = 'moduleInit'
for moduleName,path in pairs(self) do
if moduleName ~= '__load' and moduleName ~= '__init' and moduleName ~= '__post' then self.__init(moduleName) end
end
-- runs though all loaded modules looking for on_post function; all other modules have been loaded and inited, do not load extra code in this time only altar your own data
ReadOnlyManager.currentState = 'modulePost'
for moduleName,path in pairs(self) do
if moduleName ~= '__load' and moduleName ~= '__init' and moduleName ~= '__post' then self.__post(moduleName) end
end
ReadOnlyManager.currentState = 'moduleEnv'
end,
__len=function(tbl)
-- counts the number of loaded modules
local rtn = 0
for key,value in pairs(tbl) do
rtn = rtn + 1
end
return rtn-3
end,
__tostring=function(tbl)
-- a concat of all the loaded modules
local rtn = 'Load Modules: '
for key,value in pairs(tbl) do
if key ~= '__load' and key ~= '__init' and key ~= '__post' then rtn=rtn..key..', ' end
end
return rtn:sub(1,-3)
end
}
)
Manager.sandbox.loaded_modules = Manager.loadModules
--- A more detailed replacement for the lua error function to allow for handlers to be added; repleaces default error so error can be used instead of Manager.error
-- @function Manager.error
-- @usage Manager.error(err) -- calls all error handlers that are set or if none then prints to game and if that fails crashs game
-- @usage Manager.error() -- returns an error constant that can be used to crash game
-- @usage Manager.error(Manager.error()) -- crashs the game
-- @usage Manager.error.addHandler(name,callback) -- adds a new handler if handler returns Manager.error() then game will crash
-- @tparam[2] ?string|fucntion err the string to be passed to handlers; if a function it will register a handler
-- @tparam[2] function callback if given the err param will be used to given the handler a name
-- @usage Manager.error[name] -- returns the handler of that name if present
-- @usage #Manager.error -- returns the number of error handlers that are present
-- @usage pairs(Manager.error) -- loops over only the error handlers handler_name,hander
Manager.error = setmetatable({
__crash=false,
__error_call=error,
__error_const={},
__error_handler=function(handler_name,callback)
-- when handler_name is a string it is expeced that callback is a function; other wise handler_name must be a function
if type(handler_name) == 'string' and type(callback) == 'function' then Manager.error[handler_name]=callback
elseif type(handler_name) == 'function' then table.insert(Manager.error,handler_name)
else Manager.error('Handler is not a function',2) end
end,
in_pcall=function(level)
local level = level and level+1 or 2
while true do
if not debug.getinfo(level) then return false end
if debug.getinfo(level).name == 'pcall' then return level end
level=level+1
end
end
},{
__metatalbe=false,
__call=function(tbl,err,...)
-- if no params then return the error constant
if err == nil then return rawget(tbl,'__error_const') end
-- if the error constant is given crash game
if err == rawget(tbl,'__error_const') then Manager.verbose('Force Crash','errorCaught') rawset(tbl,'__crash',true) rawget(tbl,'__error_call')('Force Crash',2) end
-- other wise treat the call as if its been passed an err string
if not _no_error_verbose or Manager.currentState ~= 'moduleEnv' then Manager.verbose('An error has occurred: '..err,'errorCaught') end
if #tbl > 0 then
-- there is at least one error handler loaded; loops over the error handlers
for handler_name,callback in pairs(tbl) do
local success, err = pcall(callback,err,...)
if not success then Manager.verbose('Error handler: "'..handler_name..'" failed to run ('..err..')','errorCaught') end
-- if the error constant is returned from the handler then crash the game
if err == rawget(tbl,'__error_const') then Manager.verbose('Force Stop by: '..handler_name,'errorCaught') rawset(tbl,'__crash',true) rawget(tbl,'__error_call')('Force Stop by: '..handler_name) end
end
elseif game then
-- there are no handlers loaded so it will print to the game if loaded
Manager.verbose('No error handlers loaded; Default game print used','errorCaught')
game.print(err)
else
-- all else fails it will crash the game with the error code
Manager.verbose('No error handlers loaded; Game not loaded; Forced crash: '..err,'errorCaught')
rawset(tbl,'__crash',true)
rawget(tbl,'__error_call')(err,2)
end
local args = {...}
local trace = args[1] and type(args[1]) == 'number' and args[1]+1 or 2
if tbl.in_pcall(2) then rawget(tbl,'__error_call')(err,trace) end
end,
__index=function(tbl,key)
-- this allows the __error_handler to be called from many different names
if type(key) ~= 'string' then return end
if key:lower() == 'addhandler' or key:lower() == 'sethandler' or key:lower() == 'handler' or key:lower() == 'register' then return rawget(tbl,'__error_handler')
else rawget(tbl,'__error_call')('Invalid index for error handler; please use build in methods.') end
end,
__newindex=function(tbl,key,value)
-- making a new index adds it as a handler
if type(value) == 'function' then
Manager.verbose('Added Error Handler: "'..key..'"','eventRegistered')
rawset(tbl,key,value)
end
end,
__len=function(tbl)
-- counts the number of handlers there are
local rtn=0
for handler_name,callback in pairs(tbl) do
rtn=rtn+1
end
return rtn
end,
__pairs=function(tbl)
-- will not return any of the three core values as part of pairs
local function next_pair(tbl,k)
local v
k, v = next(tbl, k)
if k == '__error_call' or k == '__error_const' or k == '__error_handler' or k == '__crash' or k == 'in_pcall' then return next_pair(tbl,k) end
if type(v) == 'function' then return k,v end
end
return next_pair, tbl, nil
end
})
-- overrides the default error function
error=Manager.error
-- event does work a bit differnt from error, and if event breaks error is the fallback
--- Event handler that modules can use, each module can register one function per event
-- @function Manager.event
-- @usage Manager.event[event_name] = callback -- sets the callback for that event
-- @usage Manager.event[event_name] = nil -- clears the callback for that event
-- @usage Manager.event(event_name,callback) -- sets the callback for that event
-- @usage Manager.event[event_name] -- returns the callback for that event or the event id if not registered
-- @usage Manager.event(event_name) -- runs all the call backs for that event
-- @tparam ?int|string event_name that referes to an event
-- @tparam function callback the function that will be set for that event
-- @usage Manager.event() -- returns the stop value for the event proccessor, if returned during an event will stop all other callbacks
-- @usage #Manager.event -- returns the number of callbacks that are registered
-- @usage pairs(Manager.events) -- returns event_id,table of callbacks
Manager.event = setmetatable({
__stop={},
__events={},
__event=script.on_event,
__generate=script.generate_event_name,
__get_handler=script.get_event_handler,
__raise=script.raise_event,
__init=script.on_init,
__load=script.on_load,
__config=script.on_configuration_changed,
events=defines.events,
error_cache={}
},{
__metatable=false,
__call=function(tbl,event_name,new_callback,...)
if Manager.error.__crash then Manager.error.__error_call('No error handlers loaded; Game not loaded; Forced crash: '..tostring(Manager.error.__crash)) end
-- if no params then return the stop constant
if event_name == nil then return rawget(tbl,'__stop') end
-- if the event name is a table then loop over each value in that table
if type(event_name) == 'table' then
for key,_event_name in pairs(event_name) do tbl(_event_name,new_callback,...) end return
end
-- convert the event name to a number index
event_name = tonumber(event_name) or tbl.names[event_name]
-- if there is a callback present then set new callback rather than raise the event
if type(new_callback) == 'function' then
Manager.event[event_name] = new_callback return
end
-- other wise raise the event and call every callback; no use of script.raise_event due to override
local event_functions = tbl.__events[event_name]
if type(event_functions) == 'table' then
for moduleName,callback in pairs(event_functions) do
-- loops over the call backs and which module it is from
if type(callback) ~= 'function' then error('Invalid Event Callback: "'..event_name..'/'..moduleName..'"') end
local success, err = Manager.sandbox(callback,{moduleName=setupModuleName(moduleName),module_path=Manager.loadModules.__load[tostring(moduleName)]},new_callback,...)
if not success then
local cache = tbl.error_cache
local error_message = 'Event Failed: "'..moduleName..'/'..tbl.names[event_name]..'" ('..err..')'
if not cache[error_message] then Manager.verbose(error_message,'errorCaught') error(error_message) end
if tbl.names[event_name] == 'on_tick' then
if not cache[error_message] then cache[error_message] = {game.tick,1} end
if cache[error_message][1] >= game.tick-10 then cache[error_message] = {game.tick,cache[error_message][2]+1}
else cache[error_message] = nil end
if cache[error_message] and cache[error_message][2] > 100 then
Manager.verbose('There was an error happening every tick for 100 ticks, the event handler has been removed!','errorCaught')
event_functions[moduleName] = nil
end
end
end
-- if stop constant is returned then stop further processing
if err == rawget(tbl,'__stop') then Manager.verbose('Event Haulted By: "'..moduleName..'"','errorCaught') break end
end
end
end,
__newindex=function(tbl,key,value)
-- handles the creation of new event handlers
if type(value) ~= 'function' and type(value) ~= nil then error('Attempted to set a non function value to an event',2) end
-- checks for a global module name that is present
local moduleName = moduleName or 'FSM'
-- converts the key to a number index for the event
Manager.verbose('Added Handler: "'..tbl.names[key]..'"','eventRegistered')
-- checks that the event has a valid table to store callbacks; if its not valid it will creat it and register a real event handler
if not rawget(rawget(tbl,'__events'),key) then
if key == -1 or key == -2 then -- this already has a handler
elseif key < 0 then rawget(tbl,tbl.names[key])(function(...) tbl(key,...) end)
else rawget(tbl,'__event')(key,function(...) tbl(key,...) end) end
rawset(rawget(tbl,'__events'),key,{}) end
-- adds callback to Manager.event.__events[event_id][moduleName]
rawset(rawget(rawget(tbl,'__events'),key),tostring(moduleName),value)
end,
__index=function(tbl,key)
-- few redirect key
local redirect={register=tbl,dispatch=tbl,remove=function(event_id) tbl[event_name]=nil end}
if rawget(redirect,key) then return rawget(redirect,key) end
-- proforms different look ups depentding weather the current module has an event handler registered
if moduleName then
-- first looks for the event callback table and then under the module name; does same but converts the key to a number; no handler regisered so returns the converted event id
return rawget(rawget(tbl,'__events'),key) and rawget(rawget(rawget(tbl,'__events'),key),tostring(moduleName))
or rawget(rawget(tbl,'__events'),rawget(tbl,'names')[key]) and rawget(rawget(rawget(tbl,'__events'),rawget(tbl,'names')[key]),tostring(moduleName))
or rawget(tbl,'names')[key]
else
-- if there is no module present then it will return the full list of regisered handlers; or other wise the converted event id
return rawget(rawget(tbl,'__events'),key) or rawget(rawget(tbl,'__events'),rawget(tbl,'names')[key]) or rawget(tbl,'names')[key]
end
end,
__len=function(tbl)
-- counts every handler that is regised not just the the number of events with handlers
local rtn=0
for event,callbacks in pairs(tbl) do
for module,callback in pairs(callbacks) do
rtn=rtn+1
end
end
return rtn
end,
__pairs=function(tbl)
-- will loops over the event handlers and not Manager.event
local function next_pair(tbl,k)
k, v = next(rawget(tbl,'__events'), k)
if type(v) == 'table' then return k,v end
end
return next_pair, tbl, nil
end
})
--- Sub set to Manger.event and acts as a coverter between event_name and event_id
-- @table Manager.event.names
-- @usage Manager.event[event_name]
rawset(Manager.event,'names',setmetatable({},{
__index=function(tbl,key)
if type(key) == 'number' or tonumber(key) then
-- if it is a number then it will first look in the cache
if rawget(tbl,key) then return rawget(tbl,key) end
-- if it is a core event then it will simply return
if key == -1 then rawset(tbl,key,'__init')
elseif key == -2 then rawset(tbl,key,'__load')
elseif key == -3 then rawset(tbl,key,'__config')
else
-- if it is not a core event then it does a value look up on Manager.events aka defines.events
for event,id in pairs(rawget(Manager.event,'events')) do
if id == key then rawset(tbl,key,event) end
end
end
-- returns the value from the cache after being loaded in
return rawget(tbl,key)
-- if it is a string then no reverse look up is required
else
if key == 'on_init' or key == 'init' or key == '__init' then return -1
elseif key == 'on_load' or key == 'load' or key == '__load' then return -2
elseif key == 'on_configuration_changed' or key == 'configuration_changed' or key == '__config' then return -3
else return rawget(rawget(Manager.event,'events'),key) end
end
end
}))
script.on_init(function(...)
Manager.verbose('____________________| SubStart: script.on_init |____________________')
setmetatable(global,Manager.global.__global)
local names = {}
for name,default in pairs(Manager.global.__defaults) do table.insert(names,name) end
Manager.verbose('Global Tables: '..table.concat(names,', '))
for name,default in pairs(Manager.global.__defaults) do global(name)(true) end
Manager.event(-1,...)
Manager.verbose('____________________| SubStop: script.on_init |____________________')
end)
script.on_load(function(...)
Manager.verbose('____________________| SubStart: script.on_load |____________________')
setmetatable(global,Manager.global.__global)
local names = {}
for name,default in pairs(Manager.global.__defaults) do table.insert(names,name) end
Manager.verbose('Global Tables: '..table.concat(names,', '))
--for name,default in pairs(Manager.global.__defaults) do Manager.verbose('Global '..name..' = '..serpent.line(Manager.global(name))) end
Manager.event(-2,...)
Manager.verbose('____________________| SubStop: script.on_load |____________________')
end)
--over rides for the base values; can be called though Event
Event=setmetatable({},{__call=Manager.event,__index=function(tbl,key) return Manager.event[key] or script[key] or error('Invalid Index To Table Event') end})
script.mod_name = setmetatable({},{__index=_G.moduleName})
script.on_event=Manager.event
script.raise_event=Manager.event
script.on_init=function(callback) Manager.event(-1,callback) end
script.on_load=function(callback) Manager.event(-2,callback) end
script.on_configuration_changed=function(callback) Manager.event(-3,callback) end
script.get_event_handler=function(event_name) return type(Manager.event[event_name]) == 'function' and Manager.event[event_name] or nil end
script.generate_event_name=function(event_name) local event_id = Manager.event.__generate() local event_name = event_name or event_id Manager.event.events[event_name]=event_id return event_id end
-- to do set up nth tick
return ReadOnlyManager

View File

@@ -1,212 +0,0 @@
local Container = {
files={}, -- file paths which get loaded
-- these will become globals that are used to keep softmod modules working together
handlers={
--event
--global
--error=error
--logging=log
--debug
--tableToString=serpent.line
},
_raw={}, -- any values that are replaced by handlers are moved here
_loaded={},
defines={
errorLoad='ERRLOAD', -- error when loading a file
errorNotFound='ERRNOTFOUND', -- error when file not found
logAlways=0, -- will always be logged
logBasic=1, -- should be logged but not required
logDebug=2, -- longer logs of debugging
logEvents=3, -- logs which take place very often such as frequent event triggers if no other filters
logVerbose=4, -- basically a log of any thing useful
logAll=5 -- what ever is left to log weather you see a current need or not
},
-- to prevent desyncs during on_load any change to the following must be updated
-- example: runtime change to logLevel must be applied during on_load to avoid desyncs
safeError=true, -- when true then errors are logged not raised
debug=false, -- if debug functions are triggered see Container.inDebug
logLevel=1 -- what level of details is given by logs
}
function Container.log(level,...)
if level <= Container.logLevel then Container.stdout(...) end
end
function Container.stdout(...)
local msg = ''
for _,value in pairs({...}) do
msg = msg..' '..Container.tostring(value)
end
if Container.handlers.logging then
Container.handlers.logging(msg)
else
log(msg)
end
end
function Container.error(...)
if Container.safeError then Container.stdout('ERROR',...) else Container.stderr(...) end
end
function Container.stderr(type,...)
local msg = 'ERROR: '..tostring(type)
for _,value in pairs({...}) do
msg = msg..' '..Container.tostring(value)
end
if Container.handlers.error then
Container.handlers.error(msg)
else
error(msg)
end
end
function Container.type(value,test)
if not test then return type(value) end
return value and type(value) == test
end
function Container.isLocaleString(locale)
if Container.type(locale,'table') then
local _string = locale[1]
-- '.+[.].+' this is a check for the key value pair
-- '%s' this is a check for any white space
return Container.type(_string,'string') and _string:find('.+[.].+') and not _string:find('%s')
end
return false
end
function Container.isUserdata(userdata)
if Container.type(userdata,'table') then
return Container.type(userdata.__self,'userdata')
end
return false
end
function Container.tostring(value)
local _type = type(value)
if _type == 'table' then
if Container.isUserdata(value) then
-- the value is userdata
return '<USERDATA>'
elseif getmetatable(rtn) ~= nil and not tostring(rtn):find('table: 0x') then
-- the value is a table but contains the metamethod __tostring
return tostring(value)
else
-- the value is a table
if Container.handlers.tableToString then
return Container.handlers.tableToString(value)
else
return serpent.line(value)
end
end
elseif Container.type(value,'function') then
-- the value is a function and the function name is given
local name = debug.getinfo(value,'n').name or 'ANON'
return '<FUNCTION:'..name..'>'
else
-- all other values: number, string and boolean tostring is save to use
return tostring(value)
end
end
--- Sandboxs a function into the container and the given env, will load upvalues if provied in the given env
-- @usage container:sandbox(print,{},'hello from the sandbox')
-- @tparam callback function the function that will be run in the sandbox
-- @tparam env table the env which the function will run in, place upvalues in this table
-- @param[opt] any args you want to pass to the function
-- @treturn boolean did the function run without error
-- @treturn string|table returns error message or the returns from the function
-- @treturn table returns back the env as new values may have been saved
function Container.sandbox(callback,env,...)
-- creates a sandbox env which will later be loaded onto _G
local sandbox_env = setmetatable(env,{
__index=function(tbl,key)
return rawget(_G,key)
end
})
sandbox_env._ENV = sandbox_env
sandbox_env._MT_G = getmetatable(_G)
-- sets any upvalues on the callback
local i = 1
while true do
local name, value = debug.getupvalue(callback,i)
if not name then break end
if not value and sandbox_env[name] then
debug.setupvalue(callback,i,sandbox_env[name])
end
i=i+1
end
-- adds the sandbox to _G
setmetatable(_G,{__index=sandbox_env,__newindex=sandbox_env})
local rtn = {pcall(callback,...)}
local success = table.remove(rtn,1)
setmetatable(_G,_MT_G)
-- returns values from the callback, if error then it returns the error
if success then return success, rtn, sandbox_env
else return success, rtn[1], sandbox_env end
end
function Container.loadFile(filePath)
if Container._loaded[filePath] then return Container._loaded[filePath] end
local success,file = pcall(require,filePath)
if not success then return Container.error(Container.defines.errorLoad,filePath,file) end
-- if the file was not found then it returns an error from require which does not trip pcall, tested for here
if Container.type(file,'string') and file:find('no such file') then
-- tries with modules. appended to the front of the path and .control on the end
local success,_file = pcall(require,'modules.'..filePath..'.control')
if not success then return Container.error(Container.defines.errorLoad,filePath,_file) end
-- again tests for the error not caught by pcall
if Container.type(_file,'string') and _file:find('no such file') then return Container.error(Container.defines.errorNotFound,filePath) end
Container.log(Container.defines.logDebug,'Loaded file:',filePath)
Container._loaded[filePath] = _file
return _file
end
Container.log(Container.defines.logDebug,'Loaded file:',filePath)
Container._loaded[filePath] = file
return file
end
function Container.loadHandlers()
Container.log(Container.defines.logAlways,'Loading Container Handlers')
for key,value in pairs(Container.handlers) do
if Container.type(value,'string') then
-- if it is a string then it is treated as a file path
Container.handlers[key] = Container.loadFile(value)
end
if _G[key] then
-- if the key exists then it is moved to _raw before being over ridden
Container._raw[key] = _G[key]
-- it is also moved to _R for global access
if not _R then _R = {} end
_R[key] = _G[key]
end
rawset(_G,key,Container.handlers[key])
end
end
function Container.loadFiles()
Container.log(Container.defines.logAlways,'Loading Container Files')
for _,filePath in pairs(Container.files) do
Container.loadFile(filePath)
end
end
function Container.initFiles()
Container.log(Container.defines.logAlways,'Initiating Container Files')
for filePath,file in pairs(Container._loaded) do
if file.on_init then
file.on_init()
Container.log(Container.defines.logDebug,'Initiated file:',filePath)
end
end
end
function Container.postFiles()
Container.log(Container.defines.logAlways,'POSTing Container Files')
for filePath,file in pairs(Container._loaded) do
if file.on_post then
file.on_post()
Container.log(Container.defines.logDebug,'POSTed file:',filePath)
end
end
end
return Container

View File

@@ -1,103 +1,38 @@
--[[ not_luadoc=true
function _log(...) log(...) end -- do not remove this is used for smaller verbose lines
Manager = require("FactorioSoftmodManager")
Manager.setVerbose{
selfInit=true, -- called while the manager is being set up
moduleLoad=false, -- when a module is required by the manager
moduleInit=false, -- when and within the initation of a module
modulePost=false, -- when and within the post of a module
moduleEnv=false, -- during module runtime, this is a global option set within each module for fine control
eventRegistered=false, -- when a module registers its event handlers
errorCaught=true, -- when an error is caught during runtime
output=Manager._verbose -- can be: can be: print || log || other function
}
Manager() -- can be Manager.loadModules() if called else where
]]
-- If you're looking to configure anything, you want config.lua. Nearly everything in this file is dictated by the config.
-- Info on the data lifecycle and how we use it: https://github.com/Refactorio/RedMew/wiki/The-data-lifecycle
require 'resources.data_stages'
_LIFECYCLE = _STAGE.control -- Control stage
require 'utils.data_stages'
Container = require 'container'
Container.debug=false
Container.logLevel=Container.defines.logAll
Container.safeError=true
Container.handlers = {
require=function(path,env,...)
env = env or {}
local success, rtn, sandbox_env = Container.sandbox(_R.require,env,path,...)
return rtn
end,
Event='utils.event',
Global='utils.global',
--error=error,
logging=function(...) log(...) end,
tableToString=serpent.line
-- Overrides the _G.print function
require 'utils.print_override'
-- Omitting the math library is a very bad idea
require 'utils.math'
-- Global Debug and make sure our version file is registered
Debug = require 'utils.debug'
require 'resources.version'
local files = {
'modules.test'
}
Container.loadHandlers()
Container.files = {
'AdvancedStartingItems',
'ChatPopup',
'DamagePopup',
'DeathMarkers',
'DeconControl',
'ExpGamingAdmin',
'ExpGamingBot',
'ExpGamingCommands',
'ExpGamingCore',
'ExpGamingInfo',
'ExpGamingLib',
'ExpGamingPlayer',
'FactorioStdLib',
'GameSettingsGui',
'GuiAnnouncements',
'PlayerAutoColor',
'SpawnArea',
'WarpPoints',
'WornPaths',
'ExpGamingAdmin.Gui',
'ExpGamingAdmin.Ban',
'ExpGamingAdmin.Reports',
'ExpGamingAdmin.ClearInventory',
'ExpGamingAdmin.TempBan',
'ExpGamingAdmin.Teleport',
'ExpGamingAdmin.Commands',
'ExpGamingAdmin.Jail',
'ExpGamingAdmin.Warnings',
'ExpGamingAdmin.Kick',
'ExpGamingBot.autoMessage',
'ExpGamingBot.discordAlerts',
'ExpGamingBot.autoChat',
'ExpGamingCommands.cheatMode',
'ExpGamingCommands.repair',
'ExpGamingCommands.tags',
'ExpGamingCommands.home',
'ExpGamingCore.Command',
'ExpGamingCommands.teleport',
'ExpGamingCommands.bonus',
'ExpGamingCommands.kill',
'ExpGamingCore.Server',
'ExpGamingCore.Gui',
'ExpGamingInfo.Science',
'ExpGamingPlayer.playerList',
'ExpGamingCore.Sync',
'ExpGamingCore.Role',
'ExpGamingInfo.Readme',
'ExpGamingInfo.Rockets',
'ExpGamingCore.Group',
'ExpGamingInfo.Tasklist',
'ExpGamingPlayer.playerInfo',
'ExpGamingPlayer.afkKick',
'FactorioStdLib.Table',
'ExpGamingPlayer.polls',
'FactorioStdLib.Color',
'FactorioStdLib.Game',
'FactorioStdLib.String',
'ExpGamingPlayer.inventorySearch',
'ExpGamingCore.Gui.center',
'ExpGamingCore.Gui.popup',
'ExpGamingCore.Gui.toolbar',
'ExpGamingCore.Gui.left',
'ExpGamingCore.Gui.inputs'
}
Container.loadFiles()
Container.initFiles()
Container.postFiles()
-- Loads all files in array above and logs progress
local total_files = string.format('%3d',#files)
local errors = {}
for index,path in pairs(files) do
log(string.format('[INFO] Loading files %3d/%s',index,total_files))
local success,file = pcall(require,path)
-- error checking
if not success then
log('[ERROR] Failed to load file: '..path)
log('[ERROR] '..file)
table.insert(errors,'[ERROR] '..path..' :: '..file)
elseif type(file) == 'string' and file:find('not found') then
log('[ERROR] File not found: '..path)
table.insert(errors,'[ERROR] '..path..' :: Not Found')
end
end
log('[INFO] All files loaded with '..#errors..' errors:')
for _,error in pairs(errors) do log(error) end -- logs all errors again to make it make it easy to find

548
expcore/commands.lua Normal file
View File

@@ -0,0 +1,548 @@
--- Factorio command making module that makes commands with better parse and more modularity
-- @author Cooldude2606
-- @module Commands
--[[
>>>>Example Authenticator
-- adds an admin only authenticator where if a command has the tag admin_only: true
-- then will only allow admins to use this command
Commands.add_authenticator(function(player,command,tags,reject)
if tags.admin_only then -- the command has the tag admin_only set to true
if player.admin then -- the player is an admin
return true -- no return is needed for success but is useful to include
else -- the player is not admin
-- you must return to block a command, they are a few ways to do this:
-- return false -- most basic and has no custom error message
-- return reject -- sill no error message and is here in case people dont know its a function
-- reject() -- rejects the player, return not needed but please return if possible
-- return reject() -- rejects the player and has a failsafe return to block command
-- reject('This command is for admins only!') -- reject but with custom error message, return not needed but please return if possible
return reject('This command is for admins only!') -- reject but with custom error message and has return failsafe
end
else -- command does not require admin
return true -- no return is needed for success but is useful to include
end
end)
>>>>Example Parse
-- adds a parse that will cover numbers within the given range
-- input, player and reject are common to all parse functions
-- range_min and range_max are passed to the function from add_param
Commands.add_parse('number_range_int',function(input,player,reject,range_min,range_max)
local rtn = tonumber(input) or nil -- converts input to number
rtn = type(rtn) == 'number' and math.floor(rtn) or nil -- floor the number
if not rtn or rtn < range_min or rtn > range_max then -- check if it is nil or out of the range
-- invalid input for we will reject the input, they are a few ways to do this:
-- dont return anything -- will print generic input error
-- return false -- this WILL NOT reject the input as false can be a valid output
-- return reject -- will print generic input error
-- return reject() -- will print generic input error with no please check type message
-- reject() -- if you do not return the value then they will be a duplicate message
return reject('Number entered is not in range: '..range_min..', '..range_max) -- reject with custom error
else
return rtn -- returns the number value this will be passed to the command callback
end
end)
>>>>Example Command
-- adds a command that will print the players name a given number of times
-- and can only be used by admin to show how auth works
Commands.add_command('repeat-name','Will repeat you name a number of times in chat.') -- creates the new command with the name "repeat-name" and a help message
:add_param('repeat-count',false,'number_range_int',1,5) -- adds a new param called "repeat-count" that is required and is type "number_range_int" the name can be used here as add_parse was used
:add_param('smiley',true,function(input,player,reject) -- this param is optional and has a custom parse function where add_parse was not used before hand
if not input then return false end -- here you can see the default check
if input:lower() == 'true' or input:lower() == 'yes' then
return true -- the value is truthy so true is returned
else
-- it should be noted that this function will be ran even when the param is not present
-- in this case input is nil and so a default can be returned, see above
return false -- false is returned other wise
end
end)
:add_tag('admin_only',true) -- adds the tag admin_only: true which because of the above authenticator means you must be added to use this command
:add_alias('name','rname') -- adds two aliases "name" and "rname" for this command which will work as if the ordinal name was used
--:auto_concat() -- cant be used due to optional param here, but this will make all user input params after the last expected one be added to the last expected one
:register(function(player,repeat_count,smiley,raw) -- this registers the command to the game, notice the params are what were defined above
-- prints the raw input to show that it can be used
game.print(player.name..' used a command with input: '..raw)
-- some smiley logic
local msg
if smiley then
msg = ':) '..player.name
else
msg = ') '..player.name
end
-- prints your name alot
for i = 1,repeat_count do
Commands.print(i..msg) -- this command is an alias for ("expcore.common").player_return it will print any value to the player/server not just strings
end
-- if you wanted to you can return some values here
-- no return -- only success message is printed
-- Commands.error('optional message here') -- prints an error message
-- return Commands.error('optional message here') -- prints an error message, and stops success message being printed
-- Commands.success('optional message here') -- same as below but success message is printed twice DONT DO this
-- return Commands.success('optional message here') -- prints your message and then the success message
end)
>>>>Examples With No Comments (for example formatting)
Commands.add_authenticator(function(player,command,tags,reject)
if tags.admin_only then
if player.admin then
return true
else
return reject('This command is for admins only!')
end
else
return true
end
end)
Commands.add_parse('number_range_int',function(input,player,reject,range_min,range_max)
local rtn = tonumber(input) or nil
rtn = type(rtn) == 'number' and math.floor(rtn) or nil
if not rtn or rtn < range_min or rtn > range_max then
return reject('Number entered is not in range: '..range_min..', '..range_max)
else
return rtn
end
end)
Commands.add_command('repeat-name','Will repeat you name a number of times in chat.')
:add_param('repeat-count',false,'number_range_int',1,5)
:add_param('smiley',true,function(input,player,reject)
if not input then return false end
if input:lower() == 'true' or input:lower() == 'yes' then
return true
else
return false
end
end)
:add_tag('admin_only',true)
:add_alias('name','rname')
:register(function(player,repeat_count,smiley,raw)
game.print(player.name..' used a command with input: '..raw)
local msg = ') '..player.name
if smiley then
msg = ':'..msg
end
for i = 1,repeat_count do
Commands.print(i..msg)
end
end)
]]
local Game = require 'utils.game'
local player_return = require('expcore.common').player_return
local Commands = {
defines={
-- common values are stored error like signals
error='CommandError',
unauthorized='CommandErrorUnauthorized',
success='CommandSuccess'
},
commands={
-- custom command data will be stored here
},
authorization_fail_on_error=false, -- set due to have authorize fail if a callback fails to run, more secure
authorization={
-- custom function are stored here which control who can use what commands
},
_prototype={
-- used to store functions which gets added to new custom commands
},
parse={
-- used to store default functions which are common parse function such as player or number in range
},
print=player_return -- short cut so player_return does not need to be required in every module
}
--- Adds an authorization callback, function used to check if a player if allowed to use a command
-- @see Commands.authorize
-- @tparam callback function the callback you want to register as an authenticator
-- callback param - player: LuaPlayer - the player who is trying to use the command
-- callback param - command: string - the name of the command which is being used
-- callback param - tags: table - any tags which have been set for the command
-- callback param - reject: function(error_message?: string) - call to fail authorize with optional error message
-- @treturn number the index it was inserted at use to remove the callback, if anon function used
function Commands.add_authenticator(callback)
return table.insert(Commands.authorization,callback)
end
--- Removes an authorization callback
-- @see Commands.add_authenticator
-- @tparam callback function|number the callback to remove, an index returned by add_authenticator can be passed
-- @treturn boolean was the callback found and removed
function Commands.remove_authenticator(callback)
if type(callback) == 'number' then
-- if a number is passed then it is assumed to be the index
if Commands.authorization[callback] then
table.remove(Commands.authorization,callback)
return true
end
else
-- will search the array and remove the key
local index
for key,value in pairs(Commands.authorization) do
if value == callback then
index = key
break
end
end
-- if the function was found it is removed
if index then
table.remove(Commands.authorization,index)
return true
end
end
return false
end
--- Mostly used internally, calls all authorization callbacks, returns if the player is authorized
-- @tparam player LuaPlayer the player that is using the command, passed to callbacks
-- @tparam command_name string the command that is being used, passed to callbacks
-- @treturn[1] boolean true player is authorized
-- @treturn[1] string commands const for success
-- @treturn[2] boolean false player is unauthorized
-- @treturn[2] string|locale_string the reason given by the authenticator
function Commands.authorize(player,command_name)
local failed
local command_data = Commands.commands[command_name]
if not command_data then return false end
-- function passed to authorization callback to make it simpler to use
local auth_fail = function(error_message)
failed = error_message or {'ExpGamingCore_Command.unauthorized'}
return Commands.defines.unauthorized
end
-- loops over each authorization callback if any return false or unauthorized command will fail
for _,callback in pairs(Commands.authorization) do
-- callback(player: LuaPlayer, command: string, tags: table, reject: function(error_message?: string))
local success, rtn = pcall(callback,player,command_name,command_data.tags,auth_fail)
-- error handler
if not success then
-- the callback failed to run
log('[ERROR] Authorization failed: '..rtn)
if Commands.authorization_fail_on_error then
failed = 'Internal Error'
end
elseif rtn == false or rtn == Commands.defines.unauthorized or rtn == auth_fail or failed then
-- the callback returned unauthorized, failed be now be set if no value returned
failed = failed or {'ExpGamingCore_Command.unauthorized'}
break
end
end
-- checks if the authorization failed
if failed then
return false, failed
else
return true, Commands.defines.success
end
end
--- Adds a common parse that can be called by name when it wants to be used
-- nb: this is not needed as you can use the callback directly this just allows it to be called by name
-- @tparam name string the name of the parse, should be the type like player or player_alive, must be unique
-- @tparam callback function the callback that is ran to prase the input
-- parse param - input: string - the input given by the user for this param
-- parse param - player: LuaPlayer - the player who is using the command
-- parse param - reject: function(error_message) - use this function to send a error to the user and fail running
-- parse return - the value that will be passed to the command callback, must not be nil and if reject then command is not run
-- @treturn boolean was the parse added will be false if the name is already used
function Commands.add_parse(name,callback)
if Commands.parse[name] then
return false
else
Commands.parse[name] = callback
return true
end
end
--- Creates a new command object to added details to, note this does not register the command to the game
-- @tparam name string the name of the command to be created
-- @tparam help string the help message for the command
-- @treturn Commands._prototype this will be used with other functions to generate the command functions
function Commands.add_command(name,help)
local command = setmetatable({
name=name,
help=help,
callback=function() Commands.internal_error(false,name,'No callback registered') end,
auto_concat=false,
min_param_count=0,
max_param_count=0,
tags={
-- stores tags that can be used by auth
},
aliases={
-- n = name: string
},
params={
-- [param_name] = {optional: boolean, parse: function}
}
}, {
__index= Commands._prototype
})
Commands.commands[name] = command
return command
end
--- Adds a new param to the command this will be displayed in the help and used to parse the input
-- @tparam name string the name of the new param that is being added to the command
-- @tparam[opt=true] optional is this param required for this command, these must be after all required params
-- @tparam[opt=pass through] parse function this function will take the input and return a new (or same) value
-- @param[opt] ... extra args you want to pass to the parse function; for example if the parse is general use
-- parse param - input: string - the input given by the user for this param
-- parse param - player: LuaPlayer - the player who is using the command
-- parse param - reject: function(error_message) - use this function to send a error to the user and fail running
-- parse return - the value that will be passed to the command callback, must not be nil and if reject then command is not run
-- @treturn Commands._prototype pass through to allow more functions to be called
function Commands._prototype:add_param(name,optional,parse,...)
if optional ~= false then optional = true end
parse = parse or function(string) return string end
self.params[name] = {
optional=optional,
parse=parse,
parse_args={...}
}
self.max_param_count = self.max_param_count+1
if not optional then
self.min_param_count = self.min_param_count+1
end
return self
end
--- Adds a tag to the command which is passed via the tags param to the authenticators, can be used to assign command roles or type
-- @tparam name string the name of the tag to be added; used to keep tags separate
-- @tparam value any the tag that you want can be anything that the authenticators are expecting
-- nb: if value is nil then name will be assumed as the value and added at a numbered index
-- @treturn Commands._prototype pass through to allow more functions to be called
function Commands._prototype:add_tag(name,value)
if not value then
-- value not given so name is the value
table.insert(self.tags,name)
else
-- name is given so its key: value
self.tags[name] = value
end
return self
end
--- Adds an alias or multiple that will also be registered with the same callback, eg /teleport can be /tp with both working
-- @usage command:add_alias('aliasOne','aliasTwo','etc')
-- @tparam ... string any amount of aliases that you want this command to be callable with
-- @treturn Commands._prototype pass through to allow more functions to be called
function Commands._prototype:add_alias(...)
for _,alias in pairs({...}) do
table.insert(self.aliases,alias)
--Commands.alias_map[alias] = self.name
end
return self
end
--- Enables auto concatenation of any params on the end so quotes are not needed for last param
-- nb: this will disable max param checking as they will be concated onto the end of that last param
-- this can be useful for reasons or longs text, can only have one per command
-- @treturn Commands._prototype pass through to allow more functions to be called
function Commands._prototype:auto_concat()
self.auto_concat = true
return self
end
--- Adds the callback to the command and registers all aliases, params and help message with the game
-- nb: this must be the last function ran on the command and must be done for the command to work
-- @tparam callback function the callback for the command, will receive the player running command, and params added with add_param
-- callback param - player: LuaPlayer - the player who used the command
-- callback param - ... - any params which were registered with add_param in the order they where registered
-- callback param - raw: string - the raw input from the user, comes after every param added with add_param
function Commands._prototype:register(callback)
-- generates a description to be used
self.callback = callback
local params = self.params
local description = ''
for param_name,param_details in pairs(self.params) do
if param_details.optional then
description = string.format('%s [%s]',description,param_name)
else
description = string.format('%s <%s>',description,param_name)
end
end
self.description = description
-- registers the command under its own name
commands.add_command(self.name,description..' '..self.help,function(command_event)
local success, err = pcall(Commands.run_command,command_event)
if not success then log('[ERROR] command/'..self.name..' :: '..err) end
end)
-- adds any aliases that it has
for _,alias in pairs(self.aliases) do
if not commands.commands[alias] and not commands.game_commands[alias] then
commands.add_command(alias,description..' '..self.help,function(command_event)
command_event.name = self.name
local success, err = pcall(Commands.run_command,command_event)
Commands.internal_error(success,self.name,err)
end)
end
end
end
--- Sends an error message to the player and returns a constant to return to command handler to exit execution
-- nb: this is for non fatal errors meaning there is no log of this event
-- nb: if reject is giving as a param to the callback use that instead
-- @usage return Commands.error()
-- @tparam[opt] error_message string an optional error message that can be sent to the user
-- @tparam[opt] play_sound string the sound to play for the error
-- @treturn Commands.defines.error return this to command handler to exit execution
function Commands.error(error_message,play_sound)
error_message = error_message or ''
player_return({'ExpGamingCore_Command.command-fail',error_message},'orange_red')
if play_sound ~= false then
play_sound = play_sound or 'utility/wire_pickup'
if game.player then game.player.play_sound{path=play_sound} end
end
return Commands.defines.error
end
--- Sends an error to the player and logs the error, used with pcall within command handler please avoid direct use
-- nb: use error(error_message) within your callback to trigger do not trigger directly as the handler may still continue
-- @tparam success boolean the success value returned from pcall, or just false to trigger error
-- @tparam command_name string the name of the command this is used within the log
-- @tparam error_message string the error returned by pcall or some other error, this is logged and not returned to player
-- @treturn boolean the opposite of success so true means to cancel execution, used internally
function Commands.internal_error(success,command_name,error_message)
if not success then
Commands.error('Internal Error, Please contact an admin','utility/cannot_build')
log('[ERROR] command/'..command_name..' :: '..error_message)
end
return not success
end
--- Sends a value to the player, followed by a command complete message
-- nb: either return a value from your callback to trigger or return the return of this to prevent two messages
-- @tparam[opt] value any the value to return to the player, if nil then only success message returned
-- @treturn Commands.defines.success return this to the command handler to prevent two success messages
function Commands.success(value)
if value then player_return(value) end
player_return({'ExpGamingCore_Command.command-ran'},'cyan')
return Commands.defines.success
end
--- Main event function that is ran for all commands, used internally please avoid direct use
-- @tparam command_event table passed directly from command event from the add_command function
function Commands.run_command(command_event)
local command_data = Commands.commands[command_event.name]
local player = Game.get_player_by_index(command_event.player_index)
-- checks if player is allowed to use the command
local authorized, auth_fail = Commands.authorize(player,command_data.name)
if not authorized then
Commands.error(auth_fail,'utility/cannot_build')
return
end
-- null param check
if command_data.min_param_count > 0 and not command_event.parameter then
Commands.error({'ExpGamingCore_Command.invalid-inputs',command_data.name,command_data.description})
return
end
-- splits the arguments
local input_string = command_event.parameter
local quote_params = {} -- stores any " " params
input_string = input_string:gsub('"[^"]-"',function(w)
-- finds all " " params are removes spaces for the next part
local no_qoutes = w:sub(2,-2)
local no_spaces = no_qoutes:gsub('%s','_')
quote_params[no_spaces]=no_qoutes
if command_data.auto_concat then
-- if auto concat then dont remove quotes as it should be included later
quote_params[w:gsub('%s','_')]=w
end
return no_spaces
end)
local raw_params = {} -- stores all params
local param_number = 0
local last_index = 0
for word in input_string:gmatch('%S+') do
param_number = param_number + 1
if param_number > command_data.max_param_count then
-- there are too many params given to the command
if not command_data.auto_concat then
-- error as they should not be more
Commands.error({'ExpGamingCore_Command.invalid-inputs',command_data.name,command_data.description})
return
else
-- concat to the last param
if quote_params[word] then
-- if it was a " " param then the spaces are re added now
raw_params[last_index]=raw_params[last_index]..' '..quote_params[word]
else
raw_params[last_index]=raw_params[last_index]..' '..word
end
end
else
-- new param that needs to be added
-- all words are added to an array
if quote_params[word] then
-- if it was a " " param then the spaces are re added now
last_index = table.insert(raw_params,quote_params[word])
else
last_index = table.insert(raw_params,word)
end
end
end
-- checks param count
local param_count = #raw_params
if param_count < command_data.min_param_count then
Commands.error({'ExpGamingCore_Command.invalid-inputs',command_data.name,command_data.description})
return
end
-- parses the arguments
local index = 1
local params = {}
for param_name, param_data in pairs(command_data.params) do
local parse_callback = param_data.parse
if type(parse_callback) == 'string' then
-- if its a string this allows it to be pulled from the common store
parse_callback = Commands.parse[parse_callback]
end
if not type(parse_callback) == 'function' then
-- if its not a function throw and error
Commands.internal_error(success,command_data.name,'Invalid param parse '..tostring(param_data.parse))
return
end
-- used below as the reject function
local parse_fail = function(error_message)
error_message = error_message or ''
Commands.error('Invalid Param "'..param_name..'"; '..error_message)
return
end
-- input: string, player: LuaPlayer, reject: function, ... extra args
local success,param_parsed = pcall(parse_callback,raw_params[index],player,parse_fail,unpack(param_data.parse_args))
if Commands.internal_error(success,command_data.name,param_parsed) then return end
-- param_data.optional == false is so that optional parses are still ran even when not present
if (param_data.optional == false and param_parsed == nil) or param_parsed == Commands.defines.error or param_parsed == parse_fail then
-- no value was returned or error was returned, if nil then give error
if not param_parsed == Commands.defines.error then Commands.error('Invalid Param "'..param_name..'"; please make sure it is the correct type') end
return
end
-- adds the param to the table to be passed to the command callback
table.insert(params,param_parsed)
index=index+1
end
-- runs the command
-- player: LuaPlayer, ... command params, raw: string
table.insert(params,input_string)
local success, err = pcall(command_data.callback,player,unpack(params))
if Commands.internal_error(success,command_data.name,err) then return end
if err ~= Commands.defines.error and err ~= Commands.defines.success then Commands.success(err) end
end
return Commands

57
expcore/common.lua Normal file
View File

@@ -0,0 +1,57 @@
local Colours = require 'resources.color_presets'
local Game = require 'utils.game'
local Public = {}
--- Compare types faster for faster validation of prams
-- @usage is_type('foo','string') -- return true
-- @usage is_type('foo') -- return false
-- @param v the value to be tested
-- @tparam[opt=nil] string test_type the type to test for if not given then it tests for nil
-- @treturn boolean is v of type test_type
function Public.type_check(value,test_type)
return test_type and value and type(value) == test_type or not test_type and not value or false
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
-- @usage player_return('Hello, World!',nil,player) -- returns 'Hello, World!' to the given player
-- @param value any value of any type that will be returned to the player or console
-- @tparam[opt=defines.colour.white] ?defines.color|string colour the colour of the text for the player, ignored when printing to console
-- @tparam[opt=game.player] LuaPlayer player the player that return will go to, if no game.player then returns to server
function Public.player_return(value,colour,player)
colour = Public.type_check(colour,'table') and colour or Colours[colour] ~= Colours.white and Colours[colour] or Colours.white
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
-- 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
-- value is a locale string
returnAsString = value
elseif getmetatable(value) ~= nil and not tostring(value):find('table: 0x') then
-- value has a tostring meta method
returnAsString = tostring(value)
else
-- value is a table
returnAsString = serpent.block(value)
end
elseif Public.type_check(value,'function') then
-- value is a function
returnAsString = 'Cant Display Functions'
else returnAsString = tostring(value) end
-- returns to the player or the server
if player then
-- allows any valid player identifier to be used
player = Game.get_player_from_any(player)
if not player then error('Invalid Player given to player_return',2) end
-- plays a nice sound that is different to normal message sound
player.play_sound{path='utility/scenario_message'}
player.print(returnAsString,colour)
else rcon.print(returnAsString) end
end
return Public

View File

@@ -1,3 +1,5 @@
local Event = require 'utils.event'
function thisIsATestFunction(...)
game.print(serpent.line({...}))
end
@@ -5,3 +7,85 @@ end
Event.add(defines.events.on_console_chat,function(event)
if event.player_index then game.print('Message: '..event.message) end
end)
local Commands = require 'expcore.commands' -- require the Commands module
-- adds an admin only authenticator where if a command has the tag admin_only: true
-- then will only allow admins to use this command
Commands.add_authenticator(function(player,command,tags,reject)
if tags.admin_only then -- the command has the tag admin_only set to true
if player.admin then -- the player is an admin
return true -- no return is needed for success but is useful to include
else -- the player is not admin
-- you must return to block a command, they are a few ways to do this:
-- return false -- most basic and has no custom error message
-- return reject -- sill no error message and is here in case people dont know its a function
-- reject() -- rejects the player, return not needed but please return if possible
-- return reject() -- rejects the player and has a failsafe return to block command
-- reject('This command is for admins only!') -- reject but with custom error message, return not needed but please return if possible
return reject('This command is for admins only!') -- reject but with custom error message and has return failsafe
end
else -- command does not require admin
return true -- no return is needed for success but is useful to include
end
end)
-- adds a parse that will cover numbers within the given range
-- input, player and reject are common to all parse functions
-- range_min and range_max are passed to the function from add_param
Commands.add_parse('number_range_int',function(input,player,reject,range_min,range_max)
local rtn = tonumber(input) or nil -- converts input to number
rtn = type(rtn) == 'number' and math.floor(rtn) or nil -- floor the number
if not rtn or rtn < range_min or rtn > range_max then -- check if it is nil or out of the range
-- invalid input for we will reject the input, they are a few ways to do this:
-- dont return anything -- will print generic input error
-- return false -- this WILL NOT reject the input as false can be a valid output
-- return reject -- will print generic input error
-- return reject() -- will print generic input error with no please check type message
-- reject() -- if you do not return the value then they will be a duplicate message
return reject('Number entered is not in range: '..range_min..', '..range_max) -- reject with custom error
else
return rtn -- returns the number value this will be passed to the command callback
end
end)
-- adds a command that will print the players name a given number of times
-- and can only be used by admin to show how auth works
Commands.add_command('repeat-name','Will repeat you name a number of times in chat.') -- creates the new command with the name "repeat-name" and a help message
:add_param('repeat-count',false,'number_range_int',1,5) -- adds a new param called "repeat-count" that is required and is type "number_range_int" the name can be used here as add_parse was used
:add_param('smiley',true,function(input,player,reject) -- this param is optional and has a custom parse function where add_parse was not used before hand
if not input then return false end -- here you can see the default check
if input:lower() == 'true' or input:lower() == 'yes' then
return true -- the value is truthy so true is returned
else
-- it should be noted that this function will be ran even when the param is not present
-- in this case input is nil and so a default can be returned, see above
return false -- false is returned other wise
end
end)
:add_tag('admin_only',true) -- adds the tag admin_only: true which because of the above authenticator means you must be added to use this command
:add_alias('name','rname') -- adds two aliases "name" and "rname" for this command which will work as if the ordinal name was used
--:auto_concat() -- cant be used due to optional param here, but this will make all user input params after the last expected one be added to the last expected one
:register(function(player,repeat_count,smiley,raw) -- this registers the command to the game, notice the params are what were defined above
-- prints the raw input to show that it can be used
game.print(player.name..' used a command with input: '..raw)
-- some smiley logic
local msg
if smiley then
msg = ':) '..player.name
else
msg = ') '..player.name
end
-- prints your name alot
for i = 1,repeat_count do
Commands.print(i..msg) -- this command is an alias for ("expcore.common").player_return it will print any value to the player/server not just strings
end
-- if you wanted to you can return some values here
-- no return -- only success message is printed
-- Commands.error('optional message here') -- prints an error message
-- return Commands.error('optional message here') -- prints an error message, and stops success message being printed
-- Commands.success('optional message here') -- same as below but success message is printed twice DONT DO this
-- return Commands.success('optional message here') -- prints your message and then the success message
end)

Some files were not shown because too many files have changed in this diff Show More