Files
factorio-scenario-ExpCluster/locale/ExpCore/server.lua
2017-12-11 18:31:38 +00:00

408 lines
16 KiB
Lua

--[[
Explosive Gaming
This file can be used with permission but this and the credit below must remain in the file.
Contact a member of management on our discord to seek permission to use our code.
Any changes that you may make to the code are yours but that does not make the script yours.
Discord: https://discord.gg/r6dC2uK
]]
--Please Only Edit Below This Line-----------------------------------------------------------
-- server allows control over threads and other features the devs missed out
local Server = {}
Server._thread = {}
--- Returns a un-used uuid (better system needed)
-- @usage obj.uuid = Server.new_uuid()
-- @treturn string the new uuid
function Server.new_uuid()
local uuid = tostring(Server._uuid()())
uuid = string.to_hex('uuid'..uuid)
return uuid
end
-- use this to change the location of the server uuids
function Server._uuid(reset)
global.exp_core = not reset and global.exp_core or {}
global.exp_core.uuids = not reset and global.exp_core.uuids or game.create_random_generator()
return global.exp_core.uuids
end
--- Returns either the number of threads or a able of threads
-- @usage Server.threads() -- return {...}
-- Server.threads(true) -- return int
-- @tparam[opt=nil] bolean count true to return the number of threads
-- @return either a list of threads or a number
function Server.threads(count)
return count and Server._threads().all._n or Server._threads().all
end
-- use this to change the location of the server threads
-- all stores the threads indexed uuid, the other three only store the uuid's to index in the all table
function Server._threads(reset)
global.exp_core = not reset and global.exp_core or {}
global.exp_core.threads = not reset and global.exp_core.threads or {queue={},tick={},timeout={},events={},all={_n=0},paused={},named={}}
return global.exp_core.threads
end
-- see thread:create (this was done so thread can remain local)
function Server.new_thread(obj)
return Server._thread:create(obj)
end
--- Used to get a thread via it's uuid or by name if one is given
-- @usage Server.get_thread('decon') -- return thread
-- @param mixed either a uuid or the name given to a thread
-- @treturn table the thread by that name or uuid
function Server.get_thread(mixed)
local threads = Server._threads()
if threads.named[mixed] then return threads.all[threads.named[mixed]]
elseif threads.paused[mixed] then return threads.all[threads.paused[mixed]]
elseif threads.all[mixed] then return threads.all[mixed]
else return false end
end
--- Adds a thread into the resolve queue, can be used to lower lag
-- @usage Server.queue_thread(thread) -- return true/false
-- @tparam table the thread to add to the queue must have a resolve function (must be open)
-- @treturn bolean was the thread added
function Server.queue_thread(thread_to_queue)
if not thread_to_queue and not thread_to_queue.valid and not thread_to_queue:valid() then return false end
if not thread_to_queue._resolve then return false end
table.insert(Server._threads().queue,thread_to_queue.uuid)
return true
end
--- Closes all active threads, can use force if it causes errors
-- @usage Server.close_all_threads()
-- Server.close_all_threads(true) -- use if no force makes errors
-- @tparam bolean with_force use force when closing
function Server.close_all_threads(with_force)
if not with_force then
for uuid,next_thread in pairs(Server.threads()) do
if uuid ~= '_n' then next_thread:close() end
end
else
Server._threads(true)
end
end
--- Runs all the theads which have opened with an on_tick event
-- @ussage Server.run_tick_threads()
function Server.run_tick_threads()
table.each(Server._threads().tick,function(uuid)
local next_thread = Server.get_thread(uuid)
if next_thread and next_thread:valid() and next_thread._tick then
local success, err = pcall(next_thread._tick,next_thread)
if not success then next_thread:error(err) end
end
end)
end
--- Checks the timeout on all active timeout threads
-- @ussage Server.check_timeouts()
function Server.check_timeouts()
table.each(Server._threads().timeout,function(uuid)
local next_thread = Server.get_thread(uuid)
if next_thread and next_thread:valid() then
next_thread:check_timeout()
end
end)
end
--- Calles all threads on a certain game event (used with script.on_event)
-- @tparam table event the event that is called
function Server._thread_handler(event)
local event_id = event.name
local threads = Server._threads().events[event_id]
if not threads then return end
table.each(threads,function(uuid)
local next_thread = Server.get_thread(uuid)
if next_thread and next_thread:valid() then
if is_type(next_thread._events[event_id],'function') then
local success, err = pcall(next_thread._events[event_id],next_thread,event)
if not success then next_thread:error(err) end
end
end
end)
end
for _,event in pairs(defines.events) do Event.register(event,Server._thread_handler) end
--[[ cant be used V
--- Adds a event handler to tell threads about events
-- @usage Server.add_thread_handler(defines.event)
-- @tparam number event the event to run the thread handler on
-- @treturn bolean if the handler was added
function Server.add_thread_handler(event)
if not is_type(event,'number') then return false end
local threads = Server._threads()
if not threads.events[event] then
threads.events[event] = {}
Event.register(event,Server._thread_handler)
return true
end
return false
end
]]
--- Given a string or function it will run that function and return any values
-- @usage Server.interface('local x = 1+1 print(x) return x') -- return 2
-- Server.interface('local x = 1+1 print(x)',thread) -- no return
-- @param callback either a function or string which will be ran via pcall
-- @param[opt] use_thread give a thread for the interface to run on (does not need to be open, but cant use on_resolve)
-- @param[opt] ... any args you want to pass to the function
function Server.interface(callback,use_thread,...)
if use_thread then
if use_thread == true then use_thread = Server.new_thread{data={callback,...}} end
use_thread:on_event('resolve',function(thread)
if is_type(thread.data[1],'function') then
local success, err = pcall(unpack(thread.data))
if not success then error(err) end
return err
else
local callback = table.remove(thread.data,1)
local success, err = pcall(loadstring(callback),unpack(thread.data))
if not success then error(err) end
return err
end
end)
use_thread:open()
Server.queue_thread(use_thread)
else
if is_type(callback,'function') then
local success, err = pcall(callback,...)
return success, err
else
local success, err = pcall(loadstring(callback),...)
return success, err
end
return false
end
end
if commands._expgaming then
commands.add_command('server-interface', 'Runs the given input from the script', {'code',true}, function(event,args)
local callback = args.code
if not string.find(callback,'%s') and not string.find(callback,'return') then callback = 'return '..callback end
if game.player then callback = 'local player, surface, force = game.player, game.player.surface, game.player.force '..callback end
if Ranking and Ranking.get_rank and game.player then callback = 'local rank = Ranking.get_rank(game.player) '..callback end
local success, err = Server.interface(callback)
player_return(err)
end)
end
-- thread allows you to run fuinction async to the main game
--- Returns a new thread object
-- @usage new_thread = thread:create()
-- @tparam[opt={}] table obj all are opt {timeout=int,name=str,data=any} advanced users can prefix with _function to avoid the on_function functions
-- @treturn table the new thread object
function Server._thread:create(obj)
local obj = obj or {}
setmetatable(obj,{__index=Server._thread})
obj.uuid = Server.new_uuid()
return obj
end
-- see Server.queue_thread - this just opens it first
function Server._thread:queue()
self:open()
return Server.queue_thread(self)
end
--- Test if the thread has all requied parts
-- @usage if thread:valid() then end
-- @tparam bolean skip_location_check true to skip the location check
-- @treturn bolean is the thread valid
function Server._thread:valid(skip_location_check)
local skip_location_check = skip_location_check or false
if is_type(self.uuid,'string') and
skip_location_check or is_type(self.opened,'number') and
skip_location_check or is_type(Server._threads().all[self.uuid],'table') and
is_type(self.timeout) or is_type(self.timeout,'number') and
is_type(self.name) or is_type(self.name,'string') and
is_type(self._close) or is_type(self._close,'function') and
is_type(self._timeout) or is_type(self._timeout,'function') and
is_type(self._tick) or is_type(self._tick,'function') and
is_type(self._resolve) or is_type(self._resolve,'function') and
is_type(self._success) or is_type(self._success,'function') and
is_type(self._error) or is_type(self._error,'function') then
return true
end
return false
end
--- Opens the thread by storing it in a place the server object can find it
-- @usage thread:open() -- return true
-- @treturn bolean if the thread was opened
function Server._thread:open()
if not self:valid(true) or self.opened then return false end
local threads = Server._threads()
local uuid = self.uuid
self.opened = game.tick
threads.all[uuid] = threads.all[uuid] or self
threads.all._n = threads.all._n+1
if is_type(self.timeout,'number') then table.insert(threads.timeout,uuid) end
if is_type(self._tick,'function') then table.insert(threads.tick,uuid) end
if is_type(self.name,'string') then threads.named[self.name] = threads.named[self.name] or self.uuid end
if is_type(self._events,'table') then
table.each(self._events,function(callback,event,threads,uuid)
-- cant be used V
--Server.add_thread_handler(event)
if not threads.events[event] then threads.events[event] = {} end
table.insert(threads.events[event],uuid)
end,threads,self.uuid)
end
return true
end
--- Inverse of thread:open() - it removes the thread and calles on_close
-- @usage thread:close() -- return true
-- @treturn bolean if the thread had a on_close function
function Server._thread:close()
local threads = Server._threads()
local uuid = self.uuid
local _return = false
if is_type(self._close,'function') then pcall(self._close,self) _return = true end
local value,key = table.find(threads.queue,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(threads.queue,key) end
local value,key = table.find(threads.timeout,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(threads.timeout,key) end
local value,key = table.find(threads.tick,function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(threads.tick,key) end
if is_type(self._events,'table') then
table.each(self._events,function(callback,event)
if threads.events[event] then
local value,key = table.find(threads.events[event],function(v,k,uuid) return v == uuid end,uuid)
if key then table.remove(threads.events[event],key) end
-- cant be used V
--if #threads.events[event] == 0 then Event.remove(event,Server.game_event) threads.events[event] = nil end
end
end)
end
if is_type(self.name,'string') then threads.paused[self.name] = self.uuid self.opened = nil
else threads.all[uuid] = nil threads.all._n = threads.all._n-1 end
return _return
end
--- Trigger the on_resolve function and closes the thread - error and success called based on result of pcall (useful for async)
-- @usage thread:resolve(x,y,z) -- return true
-- @param[opt] ... any arguments you want to pass to the resolve function
-- @treturn bolean true if the thread called on_success or on_error
function Server._thread:resolve(...)
local _return = false
if is_type(self._resolve,'function') then
local success, err = pcall(self._resolve,self,...)
if success then
if is_type(self._success,'function') then
Server.interface(function(thread,err)
local success,err = pcall(thread._success,thread,err)
if not success then thread:error(err) end
end,true,self,err)
_return = true
end
else
_return = self:error(err)
end
end
self:close()
return _return
end
--- Checks the timeout on a thread - if timedout then it calles on_timeout and closes
-- @usage thread:check_timeout() -- return true
-- @treturn bolean if the thread timedout
function Server._thread:check_timeout()
local _return = false
if not self:valid() then return false end
if is_type(self.timeout,'number') and game.tick >= (self.opened+self.timeout) then
if is_type(self._timeout,'function') then
pcall(self._timeout,self)
end
_return = true
self:close()
end
return _return
end
--- Rasies an error on this thread
-- @usage thread:error(err) -- return true
-- @param err the err to be rasied
-- @treturn bolean did the thread handdle the error
function Server._thread:error(err)
local _return = false
if is_type(self._error,'function') then
pcall(self._error,self,err)
_return = true
else
error(err)
end
return _return
end
--- Set function to run then an event is called on a thread, none of them are 'needed' but you are advised to have atleast one
-- @usage thread:on_event('close',function) -- return true
-- events = ['close','timeout','tick','resolve','success','error']
-- if event is a number then it is asumed to be a game event
-- @tparam string event the name of the event that it is called on
-- @tparam function callback the function which is called on the event
-- @treturn was the function added
function Server._thread:on_event(event,callback)
local events = {'close','timeout','tick','resolve','success','error'}
local value = table.find(events,function(v,k,find) return v == string.lower(find) end,event)
if value and is_type(callback,'function') then
self['_'..value] = callback
return true
elseif is_type(event,'number') and is_type(callback,'function') then
if not self._events then self._events = {} end
self._events[event] = callback
return true
end
return false
end
Event.register(defines.events.on_tick,function(event)
local threads = Server._threads()
if #threads.tick > 0 then Server.run_tick_threads() end
if #threads.timeout > 0 then Server.check_timeouts() end
if #threads.queue > 0 then
local current_thread = threads.all[threads.queue[1]]
if current_thread and current_thread:valid() then current_thread:resolve() end
end
end)
Event.register(-2,function(event)
local threads = Server.threads()
for uuid,thread in pairs(threads) do
if uuid ~= '_n' then setmetatable(thread,{__index=Server._thread}) end
end
end)
return Server
--[[
Thread Example:
local thread = Server.new_thread{name='tree-decon',data={}}
-- user thread:on_event('tick') rather than thread:on_event(defines.events.on_tick) as it makes less lag
thread:on_event('tick',function(self)
local trees = self.data
if #trees == 0 then return end
local tree = table.remove(trees,1)
if tree.valid then tree.destroy() end
end)
thread:on_event('error',function(self,err)
-- cant see how this can cause an error
-- but this is where error handling goes
-- any event including on_resolve and on_tick can raise this
end)
thread:on_event(defines.events.on_marked_for_deconstruction,function(self,event)
if event.entity.type == 'tree' then
table.insert(self.data,event.entity)
end
end)
thread:open()
local thread = Server.new_thread{name='print-place',data={}}
thread:on_event(defines.events.on_built_entity,function(self,event)
game.print('Events')
end)
thread:open()
]]