Files
factorio-scenario-ExpCluster/expcore/store.lua
2019-09-22 17:35:32 +01:00

351 lines
11 KiB
Lua

--[[-- Core Module - Store
- Adds an easy way to store and watch for updates to a value
@core Store
@alias Store
@usage
-- The data store module is designed to be an alterative way to store data in the global table
-- each piece of data is stored at a location and optional key of that location
-- it is recomented that you use a local varible to store the location
local scenario_difficuly = Store.uid_location()
local team_scores = 'team-scores'
-- Setting and getting data is then as simple as
-- note that when storing a table you must use Store.update
Store.set(scenario_difficuly,'Hard')
Store.set(team_scores,game.player.force.name,20)
Store.get(scenario_difficuly) -- returns 'Hard'
Store.get(team_scores,game.player.force.name) -- returns 20
Store.update(team_scores,game.player.force.name,function(value,key)
return value + 10 -- add 10 to the score
end)
-- The reason for using stores over global is the abilty to watch for updates
-- for stores to work you must register them, often at the end of the file
Store.register(scenario_difficuly,function(value)
game.print('Scenario difficulty has been set to: '..value)
end)
Store.register(team_scores,function(value,key)
game.print('Team '..key..' now has a score of '..value)
end)
-- This can be very powerful when working with data that can be changed for a number of locations
-- with this module you can enable any location to output its changes to a file
-- say we wanted team scores to be synced across servers or between saves
-- although you will need to set up a method of storing the data outside the game
Store.register(team_scores,true,function(value,key)
game.print('Team '..key..' now has a score of '..value)
end)
-- If you want multiple handlers on one store location then you can register to the raw event
Event.add(Store.events.on_value_changed,function(event)
game.print('Store '..event.location..'/'..event.key..' was updated to: '..event.value)
end)
]]
local Global = require 'utils.global' --- @dep utils.global
local Event = require 'utils.event' --- @dep utils.event
local table_keys,write_json,get_file_path = ext_require('expcore.common','table_keys','write_json','get_file_path') --- @dep expcore.common
local Token = require 'utils.token' --- @dep utils.token
local Store = {
registered={},
synced={},
callbacks={},
events = {
on_value_changed=script.generate_event_name()
}
}
local store_data = {}
Global.register(Store.data,function(tbl)
store_data = tbl
end)
local function error_not_table(value)
if type(value) ~= 'table' then
error('Location is not a table can not use key locations',3)
end
end
--[[-- Registers a new location with an update callback which is triggered when the value updates
@tparam[opt] string location string a unique that points to the data, string used rather than token to allow migration
@tparam[opt=false] boolean synced when true will output changes to a file so it can be synced
@tparam[opt] function callback when given the callback will be automatically registered to the update of the value
@treturn string the location that is being used
@usage-- Registering a new store location
local store_id = Store.register()
@usage-- Registering a new store location, with custom update callback
local store_id = Store.uid_location()
Store.register(store_id,function(value,key)
game.print('Store '..store_id..'/'..key..' was updated to: '..value)
end)
]]
function Store.register(location,synced,callback)
if _LIFECYCLE ~= _STAGE.control then
return error('Can only be called during the control stage', 2)
end
if type(location) ~= 'string' then
callback = synced
synced = location
end
if type(synced) ~= 'boolean' then
callback = synced
end
location = type(location) == 'string' and location or Store.uid_location()
if Store.registered[location] then
return error('Location '..location..' is already registered by '..Store.registered[location], 2)
end
Store.registered[location] = get_file_path(1)
Store.synced[location] = synced and true or nil
Store.callbacks[location] = callback or nil
return location
end
--[[-- Gets the value stored at a location, this location must be registered
@tparam string location the location to get the data from
@tparam[opt] string key the key location if used
@treturn any the data which was stored at the location
@usage-- Getting the data at a store location
local data = Store.get(store_id_no_keys)
local data = Store.get(store_id_with_keys,'key_one')
]]
function Store.get(location,key)
if not Store.registered[location] then
return error('Location is not registered', 2)
end
local data = store_data[location]
if key and data then
error_not_table(data)
return data[key]
end
return data
end
--[[-- Sets the value at a location, this location must be registered
@tparam string location the location to set the data to
@tparam[opt] string key the key location if used
@tparam any value the new value to set at the location, value may be reverted if there is a watch callback, cant be nil
@tparam[opt=false] boolean from_sync set this true to avoid an output to the sync file
@tparam[opt=false] boolean from_internal set this true to add one to the error stack offset
@treturn boolean true if it was successful
@usage-- Setting the data at a store location
Store.set(store_id_no_keys,'Hello, World!')
Store.set(store_id_with_keys,'key_one','Hello, World!')
]]
function Store.set(location,key,value,from_sync,from_internal)
if not Store.registered[location] then
return error('Location is not registered', from_internal and 3 or 2)
end
if value == nil then
value = key
key = nil
end
if key then
local data = store_data[location]
if not data then
data = {}
store_data[location] = data
end
error_not_table(data)
data[key] = value
else
store_data[location] = value
end
script.raise_event(Store.events.on_value_changed,{
tick=game.tick,
location=location,
key=key,
value=value,
from_sync=from_sync or false
})
return true
end
--[[-- Allows for updating a value based on the current value; only valid way to change tables in a store
@tparam string location the location to set the data to
@tparam[opt] string key the key location if required
@tparam[opt] function update_callback the function called to update the value stored, rtn value to set new value
@usage-- Updating a value stored at a location
Store.update(store_id_no_keys,function(value)
return value + 1
end)
Store.update(store_id_with_keys,'key_one',function(value)
return value + 1
end)
@usage-- Updating a table stored at a location
Store.update(store_id_no_keys,function(value)
value.ctn = value.ctn + 1
end)
Store.update(store_id_with_keys,'key_one',function(value)
value.ctn = value.ctn + 1
end)
]]
function Store.update(location,key,update_callback,...)
local value = Store.get(location,key)
local args
if type(key) == 'function' then
args = {update_callback,...}
update_callback = key
key = nil
end
local rtn
if update_callback and type(update_callback) == 'function' then
if args then
rtn = update_callback(value,key,unpack(args))
else
rtn = update_callback(value,key,...)
end
end
if rtn then
Store.set(location,key,rtn,nil,true)
else
script.raise_event(Store.events.on_value_changed,{
tick=game.tick,
location=location,
key=key,
value=value,
from_sync=false
})
end
end
--[[-- Allows for updating all values at a location based on the current value; only valid way to change tables in a store
@tparam string location the location to set the data to
@tparam[opt] function update_callback the function called to update the value stored
@usage-- Updating all values at a location
Store.update(store_id_with_keys,function(value)
return value + 1
end)
@usage-- Updating all tables at a location
Store.update(store_id_with_keys,function(value)
value.ctn = value.ctn + 1
end)
]]
function Store.update_all(location,update_callback,...)
local data = Store.get(location)
error_not_table(data)
for key,value in pairs(data) do
local rtn
if update_callback and type(update_callback) == 'function' then
rtn = update_callback(value,key,...)
end
if rtn then
Store.set(location,key,rtn,nil,true)
else
script.raise_event(Store.events.on_value_changed,{
tick=game.tick,
location=location,
key=key,
value=value,
from_sync=false
})
end
end
end
--[[-- Sets the value at a location to nil, this location must be registered
@tparam string location the location to set the data to
@tparam[opt] string key the key location if used
@tparam[opt=false] boolean from_sync set this true to avoid an output to the sync file
@treturn boolean true if it was successful
@usage-- Clear the data at a location
Store.clear(store_id_no_keys)
Store.clear(store_id_with_keys,'key_one')
]]
function Store.clear(location,key,from_sync)
if not Store.callbacks[location] then
return error('Location is not registered', 2)
end
if key then
local data = store_data[location]
if not data then return end
error_not_table(data)
data[key] = nil
else
store_data[location] = nil
end
script.raise_event(Store.events.on_value_changed,{
tick=game.tick,
location=location,
key=key,
from_sync=from_sync or false
})
return true
end
--[[-- Gets all non nil keys at a location, keys 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 string location the location to get the keys of
@treturn table a table containing all the keys names
@usage-- Get all keys at a store location
local keys = Store.get_keys(store_id_with_keys)
]]
function Store.get_keys(location)
local data = Store.get(location)
return type(data) == 'table' and table_keys(data) or {}
end
--[[-- Check for if a location is registered
@tparam string location the location to test for
@treturn boolean true if registered
@usage-- Check that a store is registered
local registerd = Store.is_registered(store_id)
]]
function Store.is_registered(location)
return Store.registered[location]
end
--[[-- Returns a unique name that can be used for a store
@treturn string a unique name
@usage-- Get a new unique store id
local store_id = Store.uid_location()
]]
function Store.uid_location()
return tostring(Token.uid())
end
-- Handles syncing
Event.add(Store.events.on_value_changed,function(event)
if Store.callbacks[event.location] then
Store.callbacks[event.location](event.value,event.key)
end
if not event.from_sync and Store.synced[event.location] then
write_json('log/store.log',{
tick=event.tick,
location=event.location,
key=event.key,
value=event.value,
})
end
end)
return Store