mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-28 03:55:23 +09:00
306 lines
12 KiB
Lua
306 lines
12 KiB
Lua
--- Adds an easy way to store and watch for updates to a value
|
|
--[[
|
|
>>>> Basic Use
|
|
At the most basic level this allows for the naming of locations to store in the global table, the second feature is that you are
|
|
able to listen for updates of this value, which means that when ever the set function is called it will trigger the update callback.
|
|
|
|
This may be useful when storing config values and when they get set you want to make sure it is taken care of, or maybe you want
|
|
to have a value that you can trigger an update of from different places.
|
|
|
|
-- this will register a new location called 'scenario.dificutly' and the start value is 'normal'
|
|
-- note that setting a start value is optional and we could take nil to mean normal
|
|
Store.register('scenario.dificutly',function(value)
|
|
game.print('The scenario dificulty has be set to: '..value)
|
|
end,'normal')
|
|
|
|
-- this will return 'normal' as we have not set the value anywhere else
|
|
Store.get('scenario.dificutly')
|
|
|
|
-- this will set the value in the store to 'hard' and will trigger the update callback which will print a message to the game
|
|
Store.set('scenario.dificutly','hard')
|
|
|
|
>>>> Using Children
|
|
One limitation of store is that all lcoations must be registered to avoid desyncs, to get round this issue "children" can be used.
|
|
When you set the value of a child it does not have its own update callback so rather the "partent" location which has been registerd
|
|
will have its update value called with a second param of the name of that child.
|
|
|
|
This may be useful when you want a value of each player or force and since you cant regisier every player at the start you must use
|
|
the players name as the child name.
|
|
|
|
-- this will register the lcoation 'scenario.score' where we plan to use force names as the child
|
|
-- here we have not set a start value since it will be an empty location
|
|
Store.register('scenario.score',function(value,child)
|
|
game.print(child..' now has a score of '..value)
|
|
end)
|
|
|
|
-- this will return nil, but will not error as children dont need to be registerd
|
|
Store.get_child('scenario.score','player')
|
|
|
|
-- this will set 'player' to have a value of 10 for 'scenario.score' and trigger the game message print
|
|
Store.set_child('scenario.score','player',10)
|
|
|
|
-- this would be the same as Store.get however this will return an empty table rather than nil
|
|
Store.get_children('scenario.score')
|
|
|
|
>>>> Using Sync
|
|
There is the option to use Store.register_synced which is the same as Store.register however you can combine this with an external script
|
|
which can read the output from 'script-output/log/store.log' and have it send rcon commands back to the game allowing for cross instance
|
|
syncing of values.
|
|
|
|
This may be useful when you want to have a value change effect multiple instances or even if you just want a database to store values so
|
|
you can sync data between map resets.
|
|
|
|
-- this example will register the location 'stastics.total-play-time' where we plan to use plan names as the child
|
|
-- note that the location must be the same across instances
|
|
Store.register_synced('stastics.total-play-time',function(value,child)
|
|
game.print(child..' now has now played for '..value)
|
|
end)
|
|
|
|
-- use of set,get,set_child and get_chlid are all the same as non synced
|
|
|
|
>>>> Using a watch function
|
|
Some times the value that you want is not some made up value that you have but rather a factorio value or something similar, in order to recive
|
|
updates on these values (if the factorio api does not provide an event for it) you will need to add a watch function to update the store when the
|
|
values changes. You will want to keep these watch functions small since they run every tick.
|
|
|
|
-- this will register a location 'game.speed', note that the lcoation can be anything but we chose 'game.speed' to match what we are watching
|
|
-- also note that we do not need a start value here since it will be set on the first tick, but you may want a start value to avoid a trigger of the callback
|
|
Store.register('game.speed',function(value)
|
|
game.print('The game speed has been set to: '..value)
|
|
end)
|
|
|
|
-- this will add the watch function to the lcoation, every tick the function is ran and the value returned in compeared to the stored value
|
|
-- if the two values are different then the store is overriden and the update function is called
|
|
Store.add_watch('game.speed',function()
|
|
return game.speed
|
|
end)
|
|
|
|
>>>> Alternative method
|
|
Some people may prefer to use a varible rather than a string for formating reasons here is an example. Also for any times when
|
|
there will be little external input Store.uid_location() can be used to generate non conflicting locations, use of register_synced will
|
|
still require a name other wise there may be mirgration issuses.
|
|
|
|
local store_game_speed = Store.uid_location()
|
|
|
|
Store.register(store_game_speed,function(value)
|
|
game.print('The game speed has been set to: '..value)
|
|
end)
|
|
|
|
Store.add_watch(store_game_speed,function()
|
|
return game.speed
|
|
end)
|
|
|
|
]]
|
|
|
|
local Global = require 'utils.global'
|
|
local Event = require 'utils.event'
|
|
local write_json = ext_require('expcore.common','write_json','table_keys')
|
|
local Token = require 'utils.token'
|
|
|
|
local Store = {
|
|
data={},
|
|
callbacks={},
|
|
synced={},
|
|
watchers={}
|
|
}
|
|
Global.register(Store.data,function(tbl)
|
|
Store.data = tbl
|
|
end)
|
|
|
|
--- Check for if a lcoation is registered
|
|
-- @tparam location string the location to test for
|
|
-- @treturn boolean true if registered
|
|
function Store.is_registered(location)
|
|
return not not Store.callbacks[location]
|
|
end
|
|
|
|
--- Returns a unqiue name that can be used for a store
|
|
-- @treturn string a unqiue name
|
|
function Store.uid_location()
|
|
return tostring(Token.uid())
|
|
end
|
|
|
|
--- Registers a new location with an update callback which is triggered when the value updates
|
|
-- @tparam location string a unique string that points to the data, string used rather than token to allow migration
|
|
-- @tparam callback function this callback will be called when the stored value is set to a new value
|
|
-- @tparam[opt] start_value any this value will be the inital value that is stored at this location
|
|
function Store.register(location,callback,start_value)
|
|
if _LIFECYCLE ~= _STAGE.control then
|
|
return error('Can only be called during the control stage', 2)
|
|
end
|
|
|
|
if Store.callbacks[location] then
|
|
return error('Location is already registered', 2)
|
|
end
|
|
|
|
if type(callback) ~= 'function' then
|
|
return error('Callback must be a function', 2)
|
|
end
|
|
|
|
Store.data[location] = start_value
|
|
Store.callbacks[location] = callback
|
|
|
|
return location
|
|
end
|
|
|
|
--- Registers a new cross server synced location with an update callback, and external script is required for cross server
|
|
-- @tparam location string a unique string that points to the data, string used rather than token to allow migration
|
|
-- @tparam callback function this callback will be called when the stored value is set to a new value
|
|
-- @tparam[opt] start_value any this value will be the inital value that is stored at this location
|
|
function Store.register_synced(location,callback,start_value)
|
|
if _LIFECYCLE ~= _STAGE.control then
|
|
return error('Can only be called during the control stage', 2)
|
|
end
|
|
|
|
if Store.callbacks[location] then
|
|
return error('Location is already registered', 2)
|
|
end
|
|
|
|
if type(callback) ~= 'function' then
|
|
return error('Callback must be a function', 2)
|
|
end
|
|
|
|
Store.data[location] = start_value
|
|
Store.callbacks[location] = callback
|
|
Store.synced[location] = true
|
|
end
|
|
|
|
--- Adds a function that will be checked every tick for a change in the returned value, when the value changes it will be saved in the store
|
|
-- @tparam location string the location where the data will be saved and compeared to, must already be a registered location
|
|
-- @tparam callback function this function will be called every tick to check for a change in value
|
|
function Store.add_watch(location,callback)
|
|
if _LIFECYCLE ~= _STAGE.control then
|
|
return error('Can only be called during the control stage', 2)
|
|
end
|
|
|
|
if Store.callbacks[location] then
|
|
return error('Location is already being watched', 2)
|
|
end
|
|
|
|
if type(callback) ~= 'function' then
|
|
return error('Callback must be a function', 2)
|
|
end
|
|
|
|
Store.watchers[location] = callback
|
|
end
|
|
|
|
--- Gets the value stored at a location, this location must be registered
|
|
-- @tparam location string the location to get the data from
|
|
-- @tparam[opt=false] no_error boolean when true no error is returned if the location is not registered
|
|
-- @treturn any the data which was stored at the location
|
|
function Store.get(location,no_error)
|
|
if not Store.callbacks[location] and not no_error then
|
|
return error('Location is not registered', 2)
|
|
end
|
|
|
|
return Store.data[location]
|
|
end
|
|
|
|
--- Sets the value at a location, this location must be registered, if server synced it will emit the change to file
|
|
-- @tparam location string the location to set the data to
|
|
-- @tparam value any the new value to set at the location, value may be reverted if there is a watch callback
|
|
-- @treturn boolean true if it was successful
|
|
function Store.set(location,value)
|
|
if not Store.callbacks[location] then
|
|
return error('Location is not registered', 2)
|
|
end
|
|
|
|
Store.data[location] = value
|
|
Store.callbacks[location](value)
|
|
|
|
if Store.synced[location] then
|
|
write_json('log/store.log',{
|
|
location=location,
|
|
value=value
|
|
})
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Gets all non nil children at a location, children can be added and removed during runtime
|
|
-- this is similar to Store.get but will always return a table even if it is empty
|
|
-- @tparam location string the location to get the children of
|
|
-- @treturn table a table containg all the children and they values
|
|
function Store.get_children(location)
|
|
local store = Store.get(location)
|
|
|
|
if type(store) ~= 'table' and store ~= nil then
|
|
return error('Location has a non table value', 2)
|
|
end
|
|
|
|
return store or {}
|
|
end
|
|
|
|
--- Gets the value of the child to a location, children can be added and removed during runtime
|
|
-- @tparam location string the location of which the child is located
|
|
-- @tparam child string the child element to get the value of
|
|
-- @treturn any the value which was stored at that location
|
|
function Store.get_child(location,child)
|
|
local store = Store.get(location)
|
|
|
|
if type(store) ~= 'table' and store ~= nil then
|
|
return error('Location has a non table value', 2)
|
|
end
|
|
|
|
return store and store[child]
|
|
end
|
|
|
|
--- Sets the value of the chlid to a location, children can be added and removed during runtime
|
|
-- when a child is set it will call the update handler of the parent allowing children be to added at runtime
|
|
-- this may be used when a player joins the game and the child is the players name
|
|
-- @tparam location string the location of which the child is located
|
|
-- @tparam child string the child element to set the value of
|
|
-- @tparam value any the value to set at this location
|
|
-- @treturn boolean true if it was successful
|
|
function Store.set_child(location,child,value)
|
|
local store = Store.get(location)
|
|
|
|
if type(store) ~= 'table' and store ~= nil then
|
|
return error('Location has a non table value', 2)
|
|
end
|
|
|
|
if not store then
|
|
Store.data[location] = {}
|
|
end
|
|
|
|
Store.data[location][child] = value
|
|
Store.callbacks[location](value,child)
|
|
|
|
if Store.synced[location] then
|
|
write_json('log/store.log',{
|
|
location=location,
|
|
child=child,
|
|
value=value
|
|
})
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Event handler for the watcher callbacks
|
|
Event.add(defines.events.on_tick,function()
|
|
local errors = {}
|
|
|
|
for location,callback in pairs(Store.watchers) do
|
|
local store_old = Store.data[location]
|
|
local success,store_new = pcall(callback)
|
|
|
|
if not success then
|
|
table.insert(errors,store_new)
|
|
else
|
|
if type(store_old) ~= type(store_new)
|
|
or type(store_old) == 'table' and not table.compare(store_new,store_new)
|
|
or store_old ~= store_new then
|
|
Store.data[location] = store_new
|
|
Store.callbacks[location](store_new)
|
|
end
|
|
end
|
|
end
|
|
|
|
if #errors > 0 then
|
|
error(table.concat(errors,'; '))
|
|
end
|
|
end)
|
|
|
|
return Store |