mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 11:35:22 +09:00
523 lines
19 KiB
Lua
523 lines
19 KiB
Lua
--[[-- Core Module - Store
|
|
- Used to store and watch for updates for values in the global table
|
|
@core Store
|
|
@alias Store
|
|
|
|
@usage-- Require the module and add a store with no keys
|
|
-- Store with no keys does not need a serializer
|
|
local Store = require 'expcore.store' --- @dep expcore.store
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- When the store is changed this function will trigger
|
|
Store.watch(scenario_diffculty, function(value)
|
|
game.print('The scenario diffculty has been set to '..value)
|
|
end)
|
|
|
|
Store.set(scenario_diffculty, 'hard') -- Set the value stored to 'hard'
|
|
Store.get(scenario_diffculty) -- Returns 'hard'
|
|
Store.update(scenario_diffculty, function(value) -- Will set value to 'normal' if no value is present
|
|
return not value and 'normal'
|
|
end)
|
|
|
|
@usage-- Require the module and add a store with keys
|
|
-- Store with keys does not require a serializer but it can be helpful
|
|
local Store = require 'expcore.store' --- @dep expcore.store
|
|
local player_scores = Store.register(function(player) -- Use player name as the key
|
|
return player.name
|
|
end)
|
|
|
|
-- When any key in the store is changed this function will trigger
|
|
Store.watch(player_scores, function(value, key, old_value)
|
|
game.print(key..' now has a score of '..value)
|
|
end)
|
|
|
|
Store.set(player_scores, game.player, 10) -- Set your score to 10
|
|
Store.get(scenario_diffculty, game.player) -- Returns 10
|
|
Store.update(scenario_diffculty, game.player, function(value) -- Add 1 to your score
|
|
return value + 1
|
|
end)
|
|
|
|
]]
|
|
|
|
local Event = require 'utils.event' --- @dep utils.event
|
|
|
|
local Store = {
|
|
--- The current highest uid that is being used, will not increase during runtime
|
|
-- @field uid
|
|
uid = 0,
|
|
--- An array of the serializers that stores are using, key is store uids
|
|
-- @table serializers
|
|
serializers = {},
|
|
--- An array of watchers that stores will trigger, key is store uids
|
|
-- @table watchers
|
|
watchers = {},
|
|
--- An index used for debuging to find the file where different stores where registered
|
|
-- @table file_paths
|
|
file_paths = {}
|
|
}
|
|
|
|
-- All data is stored in global.data_store and is accessed here with data_store
|
|
local data_store = {}
|
|
global.data_store = data_store
|
|
Event.on_load(function()
|
|
data_store = global.data_store
|
|
end)
|
|
|
|
--- Store Setup.
|
|
-- @section setup
|
|
|
|
--[[-- An error checking and serializing function for checking store uids and keys, note key is not required
|
|
@tparam number store the uid of the store that you want to check is valid
|
|
@tparam[opt] ?string|any key the key that you want to serialize or check is a string
|
|
@tparam[opt=1] number error_stack the position in the stack relative to the current function (1) to raise this error on
|
|
@treturn string if key is given and a serializer is registered, or key was already a string, then the key is returned
|
|
|
|
@usage-- Registering a new store and checking that it is valid
|
|
-- New store will use player names as the keys
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- player_scores is a valid store and key will be your player name
|
|
local key = Store.validate(player_scores, game.player)
|
|
|
|
]]
|
|
function Store.validate(store, key, error_stack)
|
|
error_stack = error_stack or 1
|
|
|
|
if type(store) ~= 'number' then
|
|
-- Store is not a number and so if not valid
|
|
error('Store uid given is not a number; recived type '..type(store), error_stack+1)
|
|
elseif store > Store.uid then
|
|
-- Store is a number but it is out of range, ie larger than the current highest uid
|
|
error('Store uid is out of range; recived '..tostring(store), error_stack+1)
|
|
elseif key ~= nil and type(key) ~= 'string' and Store.serializers[store] == nil then
|
|
-- Key is present but is not a string and there is no serializer registered
|
|
error('Store key is not a string and no serializer has been registered; recived '..type(key), error_stack+1)
|
|
elseif key ~= nil then
|
|
-- Key is present and so it is serialized and returned
|
|
local serializer = Store.serializers[store]
|
|
if type(key) ~= 'string' then
|
|
local success, serialized_key = pcall(serializer, key)
|
|
|
|
if not success then
|
|
-- Serializer casued an error while serializing the key
|
|
error('Store watcher casued an error:\n\t'..key, error_stack+1)
|
|
elseif type(serialized_key) ~= 'string' then
|
|
-- Serializer was successful but failed to return a string value
|
|
error('Store key serializer did not return a string; recived type '..type(key), error_stack+1)
|
|
end
|
|
|
|
return serialized_key
|
|
end
|
|
|
|
return key
|
|
end
|
|
|
|
end
|
|
|
|
--[[-- Required to create new stores and register an serializer to a store, serializer not required
|
|
@tparam[opt] function serializer the function used to convert non string keys into strings to be used in the store
|
|
@treturn number the uid for the new store that you have created, use this as the first param to all other functions
|
|
|
|
@usage-- Creating a store with no serializer
|
|
local scenario_diffculty = Store.register()
|
|
|
|
@usage-- Creating a store which can take LuaPlayer
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
]]
|
|
function Store.register(serializer)
|
|
if _LIFECYCLE ~= _STAGE.control then
|
|
-- Only allow this function to be called during the control stage
|
|
error('Store can not be registered durring runtime', 2)
|
|
end
|
|
|
|
-- Increment the uid counter
|
|
local uid = Store.uid + 1
|
|
Store.uid = uid
|
|
|
|
-- Register the serializer if given
|
|
if serializer then
|
|
Store.serializers[uid] = serializer
|
|
end
|
|
|
|
-- Add entry in the debug table
|
|
local file_path = debug.getinfo(2, 'S').source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
|
Store.file_paths[uid] = file_path
|
|
|
|
-- Return the new uid
|
|
return uid
|
|
end
|
|
|
|
--[[-- Register a watch function to a store that is called when the value in the store is changed, triggers for any key
|
|
@tparam number store the uid of the store that you want to watch for changes to
|
|
@tparam function watcher the function that will be called when there is a change to the store
|
|
|
|
@usage-- Printing the changed value to all players, no keys
|
|
-- Register the new store, we are not using keys so we dont need a serializer
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Register the watcher so that when we change the value the message is printed
|
|
Store.watch(scenario_diffculty, function(value)
|
|
game.print('The scenario diffculty has been set to '..value)
|
|
end)
|
|
|
|
-- Set a new value for the diffculty and see that it has printed to the game
|
|
Store.set(scenario_diffculty, 'hard')
|
|
|
|
@usage-- Printing the changed value to all players, with keys
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Register the watcher so that when we change the value the message is printed
|
|
Store.watch(player_scores, function(value, key, old_value)
|
|
game.print(key..' now has a score of '..value)
|
|
end)
|
|
|
|
-- Set a new value for your score and see that it has printed to the game
|
|
Store.set(player_scores, game.player, 10)
|
|
|
|
]]
|
|
function Store.watch(store, watcher)
|
|
if _LIFECYCLE ~= _STAGE.control then
|
|
-- Only allow this function to be called during the control stage
|
|
error('Store watcher can not be registered durring runtime', 2)
|
|
end
|
|
|
|
Store.validate(store, nil, 2)
|
|
|
|
-- Add the watchers table if it does not exist
|
|
local watchers = Store.watchers[store]
|
|
if not watchers then
|
|
watchers = {}
|
|
Store.watchers[store] = watchers
|
|
end
|
|
|
|
-- Append the new watcher function
|
|
watchers[#watchers+1] = watcher
|
|
end
|
|
|
|
--- Store Data Management.
|
|
-- @section data
|
|
|
|
--[[-- Used to retrive the current data that is stored, key is optional depending on if you are using them
|
|
@tparam number store the uid of the store that you want to get the value from
|
|
@tparam[opt] ?string|any key the key that you want to get the value of, must be a string unless you have a serializer
|
|
@treturn any the data that is stored
|
|
|
|
@usage-- Getting the value of a store with no keys
|
|
-- Register the new store, we are not using keys so we dont need a serializer
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Get the current diffculty for the scenario
|
|
local diffculty = Store.get(scenario_diffculty)
|
|
|
|
@usage-- Getting the data from a store with keys
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Get your current score
|
|
local my_score = Store.get(player_scores, game.player)
|
|
|
|
-- Get all scores
|
|
lcoal scores = Store.get(player_scores)
|
|
|
|
]]
|
|
function Store.get(store, key)
|
|
key = Store.validate(store, key, 2)
|
|
|
|
-- Get the data from the data store
|
|
local data = data_store[store]
|
|
if key then
|
|
if type(data) ~= 'table' then
|
|
data_store[store] = {_value = data_store[store]}
|
|
return nil
|
|
else
|
|
return data[key]
|
|
end
|
|
end
|
|
|
|
-- Return all data if there is no key
|
|
return data
|
|
end
|
|
|
|
--[[-- Used to clear the data in a store, will trigger any watchers, key is optional depending on if you are using them
|
|
@tparam number store the uid of the store that you want to clear
|
|
@tparam[opt] ?string|any key the key that you want to clear, must be a string unless you have a serializer
|
|
|
|
@usage-- Clear a store which does not use keys
|
|
-- Register the new store, we are not using keys so we dont need a serializer
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Clear the scenario diffculty
|
|
Store.clear(scenario_diffculty)
|
|
|
|
@usage-- Clear data that is in a store with keys
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Clear your score
|
|
Store.clear(player_scores, game.player)
|
|
|
|
-- Clear all scores
|
|
Store.clear(player_scores)
|
|
|
|
]]
|
|
function Store.clear(store, key)
|
|
key = Store.validate(store, key, 2)
|
|
local old_value
|
|
|
|
-- Check if there is a key being used
|
|
if key then
|
|
if type(data_store[store]) == 'table' then
|
|
old_value = data_store[store][key]
|
|
data_store[store][key] = nil
|
|
end
|
|
else
|
|
old_value = data_store[store]
|
|
data_store[store] = nil
|
|
end
|
|
|
|
-- Trigger any watch functions
|
|
Store.raw_trigger(store, key, nil, old_value)
|
|
end
|
|
|
|
--[[-- Used to set the data in a store, will trigger any watchers, key is optional depending on if you are using them
|
|
@tparam number store the uid of the store that you want to set
|
|
@tparam[opt] ?string|any key the key that you want to set, must be a string unless you have a serializer
|
|
@tparam any value the value that you want to set
|
|
|
|
@usage-- Setting a store which does not use keys
|
|
-- Register the new store, we are not using keys so we dont need a serializer
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Set the new scenario diffculty
|
|
Store.set(scenario_diffculty, 'hard')
|
|
|
|
@usage-- Set data in a store with keys
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_scores = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Set your current score
|
|
Store.set(player_scores, game.player, 10)
|
|
|
|
-- Set all scores, note this might not have much use
|
|
Store.set(player_scores, {
|
|
[game.player.name] = 10,
|
|
['SomeOtherPlayer'] = 0
|
|
})
|
|
|
|
]]
|
|
function Store.set(store, key, value)
|
|
-- Allow for key to be optional
|
|
if value == nil then
|
|
value = key
|
|
key = nil
|
|
end
|
|
|
|
-- Check the store is valid
|
|
key = Store.validate(store, key, 2)
|
|
local old_value
|
|
|
|
-- If there is a key being used then the store must be a able
|
|
if key then
|
|
if type(data_store[store]) ~= 'table' then
|
|
data_store[store] = {_value = data_store[store]}
|
|
end
|
|
old_value = data_store[store][key]
|
|
data_store[store][key] = value
|
|
else
|
|
old_value = data_store[store]
|
|
data_store[store] = value
|
|
end
|
|
|
|
-- Trigger any watchers
|
|
Store.raw_trigger(store, key, value, old_value)
|
|
end
|
|
|
|
--[[-- Used to update the data in a store, use this with tables, will trigger any watchers, key is optional depending on if you are using them
|
|
@tparam number store the uid of the store that you want to update
|
|
@tparam[opt] ?string|any key the key that you want to update, must be a string unless you have a serializer
|
|
@tparam function updater the function which is called to make changes to the value, such as changing table keys, if a value is returned it will replace the current value in the store
|
|
|
|
@usage-- Incrementing a global score
|
|
-- Because we are only going to have one score so we will not need keys or a serializer
|
|
local game_score = Store.register()
|
|
|
|
-- Setting a default value
|
|
Store.set(game_score, 0)
|
|
|
|
-- We now will update the game score by one, we return the value so that it is set as the new value in the store
|
|
Store.update(game_score, function(value)
|
|
return value + 1
|
|
end)
|
|
|
|
@usage-- Updating keys in a table of data
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_data = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Setting a default value for your player, used to show the table structure
|
|
Store.set(player_data, game.player, {
|
|
group = 'Admin',
|
|
role = 'Owner',
|
|
show_group_config = false
|
|
})
|
|
|
|
-- Updating the show_group_config key in your player data, note that it would be harder to call set every time
|
|
-- We do not need to return anything in this case as we are not replacing all the data
|
|
Store.update(player_data, game.player, function(data)
|
|
data.show_group_config = not data.show_group_config
|
|
end)
|
|
|
|
]]
|
|
function Store.update(store, key, updater)
|
|
-- Allow for key to be nil
|
|
if updater == nil then
|
|
updater = key
|
|
key = nil
|
|
end
|
|
|
|
-- Check the store is valid
|
|
key = Store.validate(store, key, 2)
|
|
local value, old_value
|
|
|
|
-- If a key is used then the store must be a table
|
|
if key then
|
|
if type(data_store[store]) ~= 'table' then
|
|
data_store[store] = {_value = data_store[store]}
|
|
end
|
|
|
|
-- Call the updater and if it returns a value then set this value
|
|
local rtn = updater(data_store[store][key])
|
|
if rtn then
|
|
old_value = data_store[store][key]
|
|
data_store[store][key] = rtn
|
|
end
|
|
value = data_store[store][key]
|
|
|
|
else
|
|
-- Call the updater and if it returns a value then set this value
|
|
local rtn = updater(data_store[store])
|
|
if rtn then
|
|
old_value = data_store[store][key]
|
|
data_store[store] = rtn
|
|
end
|
|
value = data_store[store]
|
|
|
|
end
|
|
|
|
-- Trigger any watchers
|
|
Store.raw_trigger(store, key, value, old_value)
|
|
end
|
|
|
|
--[[-- Used to update all values that are in a store, similar to Store.update but acts on all keys at once, will trigger watchers for every key present
|
|
@tparam number store the uid of the store that you want to map
|
|
@tparam function updater the function that is called on every key in this store
|
|
|
|
@usage-- Updating keys in a table of data
|
|
-- Register the new store, we are not using player names as the keys so it would be useful to accept LuaPlayer objects
|
|
local player_data = Store.register(function(player)
|
|
return player.name
|
|
end)
|
|
|
|
-- Setting a default value for your player, used to show the table structure
|
|
Store.set(player_data, game.player, {
|
|
group = 'Admin',
|
|
role = 'Owner',
|
|
show_group_config = false
|
|
})
|
|
|
|
-- Updating the show_group_config key for all players, note that it would be harder to call set every time
|
|
-- We do not need to return anything in this case as we are not replacing all the data
|
|
-- We also have access to the current key being updated if needed
|
|
Store.map(player_data, function(data, key)
|
|
data.show_group_config = not data.show_group_config
|
|
end)
|
|
|
|
]]
|
|
function Store.map(store, updater)
|
|
Store.validate(store, nil, 2)
|
|
|
|
-- Get all that data in the store and check its a table
|
|
local data = data_store[store]
|
|
if type(data) ~= 'table' then
|
|
return
|
|
end
|
|
|
|
-- Loop over all the keys and call the updater, setting value if returned, and calling watcher functions
|
|
for key, value in pairs(data) do
|
|
local rtn = updater(value, key)
|
|
if rtn then
|
|
data[key] = rtn
|
|
end
|
|
Store.raw_trigger(store, key, data[key], value)
|
|
end
|
|
end
|
|
|
|
--[[-- Used to trigger watcher functions, this may be used to trigger them if you did not use Store.update or Store.set
|
|
@tparam number store the uid of the store that you want to trigger
|
|
@tparam[opt] ?string|any key the key that you want to trigger, must be a string unless you have a serializer
|
|
@usage-- Faking the update to a store
|
|
-- The type of store we use does not really matter for this as long as you pass it what you watchers are expecting
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Trigger the watchers with a fake change of diffculty
|
|
Store.trigger(scenario_diffculty)
|
|
|
|
]]
|
|
function Store.trigger(store, key)
|
|
key = Store.validate(store, key, 2)
|
|
|
|
-- Get the data from the data store
|
|
local data = data_store[store]
|
|
if key then
|
|
data = data[key]
|
|
Store.raw_trigger(store, key, data, data)
|
|
else
|
|
Store.raw_trigger(store, key, data, data)
|
|
end
|
|
end
|
|
|
|
--[[-- Used to trigger watcher functions, the value and key are passed directly to the watchers regardless if the value is correct
|
|
@tparam number store the uid of the store that you want to trigger
|
|
@tparam[opt] ?string|any key the key that you want to trigger, must be a string unless you have a serializer
|
|
@tparam[opt] any value the new value that is at this key or store, passed directly to the watcher
|
|
@tparam[opt] any old_value the old value that was at this key or store often the same if value is a table, passed directly to the watcher
|
|
|
|
@usage-- Triggering a manule call of the watchers
|
|
-- The type of store we use does not really matter for this as long as you pass it what you watchers are expecting
|
|
local scenario_diffculty = Store.register()
|
|
|
|
-- Trigger the watchers with a fake change of diffculty
|
|
-- This is mostly used internally but it can be useful in other cases
|
|
Store.raw_trigger(scenario_diffculty, nil, 'normal', 'normal')
|
|
|
|
]]
|
|
function Store.raw_trigger(store, key, value, old_value)
|
|
key = Store.validate(store, key, 2)
|
|
|
|
-- Get the watchers and then loop over them
|
|
local watchers = Store.watchers[store] or {}
|
|
for _, watcher in pairs(watchers) do
|
|
local success, err = pcall(watcher, value, key, old_value)
|
|
if not success then
|
|
error('Store watcher casued an error:\n\t'..err)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Module return
|
|
return Store |