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 -- If you're looking to configure anything, you want config.lua. Nearly everything in this file is dictated by the config.
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
]]
-- 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' -- Overrides the _G.print function
Container = require 'container' require 'utils.print_override'
Container.debug=false
Container.logLevel=Container.defines.logAll -- Omitting the math library is a very bad idea
Container.safeError=true require 'utils.math'
Container.handlers = {
require=function(path,env,...) -- Global Debug and make sure our version file is registered
env = env or {} Debug = require 'utils.debug'
local success, rtn, sandbox_env = Container.sandbox(_R.require,env,path,...) require 'resources.version'
return rtn
end, local files = {
Event='utils.event', 'modules.test'
Global='utils.global',
--error=error,
logging=function(...) log(...) end,
tableToString=serpent.line
} }
Container.loadHandlers()
Container.files = { -- Loads all files in array above and logs progress
'AdvancedStartingItems', local total_files = string.format('%3d',#files)
'ChatPopup', local errors = {}
'DamagePopup', for index,path in pairs(files) do
'DeathMarkers', log(string.format('[INFO] Loading files %3d/%s',index,total_files))
'DeconControl', local success,file = pcall(require,path)
'ExpGamingAdmin', -- error checking
'ExpGamingBot', if not success then
'ExpGamingCommands', log('[ERROR] Failed to load file: '..path)
'ExpGamingCore', log('[ERROR] '..file)
'ExpGamingInfo', table.insert(errors,'[ERROR] '..path..' :: '..file)
'ExpGamingLib', elseif type(file) == 'string' and file:find('not found') then
'ExpGamingPlayer', log('[ERROR] File not found: '..path)
'FactorioStdLib', table.insert(errors,'[ERROR] '..path..' :: Not Found')
'GameSettingsGui', end
'GuiAnnouncements', end
'PlayerAutoColor', log('[INFO] All files loaded with '..#errors..' errors:')
'SpawnArea', for _,error in pairs(errors) do log(error) end -- logs all errors again to make it make it easy to find
'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()

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,7 +1,91 @@
local Event = require 'utils.event'
function thisIsATestFunction(...) function thisIsATestFunction(...)
game.print(serpent.line({...})) game.print(serpent.line({...}))
end end
Event.add(defines.events.on_console_chat,function(event) Event.add(defines.events.on_console_chat,function(event)
if event.player_index then game.print('Message: '..event.message) end 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) end)

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