Files
factorio-scenario-ExpCluster/expcore/store.lua
2019-05-05 13:35:48 +01:00

190 lines
11 KiB
Lua

--- This module is for storing and watching values for updates, useful for config settings or limiting what can be changed
--[[
>>>> When to use this system
This system is to be used when you want to store a value and watch when it is changed or watch any value for changes.
Examples would include runtime config settings where something needs to change when the value is updated or when you have
values entered in a gui and you want them to be persistent between players like a force modifer gui
>>>> What store type to use
There are different types of store that can be used each is designed to be used in a certain situation:
local - this store type doesnt actually store any data and it has its use in only triggering the setter function when you use
the set function rather than watching for updates, this might be used as an interface between modules where when you change the
local varible you dont want it to trigger but when an outside source uses set it will trigger the setter.
player - this will use the sub_location as a player so each player will have they own entry in the store location, this can be used
with player modifiers where even if set is not used the update will still be detected.
force - this will use the sub_location as a force so each force will have its own entry in the store location, this can be used to store
custom settings for a force where if a player uses a gui to edit the setting it will detect the update and call the setter where you
can update the value on the gui for other players.
surface - this will use the sub_location as a surface so each surface will have its own entry in the store location, this will have the
same use case as force but for a surface rather than a force.
game - this will store all a single value so any sub_location string can be used, this is the general case so you really can store what
ever values you want to in this and watch for external updates, this would be used when its not a local varible for example if you are
watching the number of online players.
global - this will store all of its data in an external source indepentent of the lua code, this means that you can store data between
maps and even instances, when the value is updated it will trigger an emit where some external code should send a message to the other
connected instances to update they value. lcoal set -> emit update -> local setter -> remote set -> remote setter
>>>> Force mining speed example:
For this will print a message when the force mining speed has been updated, we will use the force type since each force will have its own
mining speed and our getter will just return the current minning speed of the force.
Store.register('force.mining_speed','force',function(force)
return force.manual_mining_speed_modifier
end,function(force,value)
force.manual_mining_speed_modifier = value
game.print(force.name..' how has '..value..' mining speed')
end)
Note that because we used type force the getter and setter are passed the force which the current check/update effects; if we used player or surface
the same would be true. However for local, game and global they are passed the sub_location string which allows you to store multiple things in the same
location; however one limitation is that a sub_location is required even if you only plan to store one value.
Store.set('force.mining_speed','player',2)
game.forces.player.manual_mining_speed_modifier = 2
The two cases above will have the effect of both setting the minning speed and outputing the update message. This can be quite useful when you start to
indroduce custom settings or do more than just output that the value was updated.
Store.get('force.mining_speed','player')
In a similar way get can be used to get the current value that is stored, if no value is stored then the getter function is called to get the value, this
function is more useful when you have custom settings since they would be no other way to access them.
>>>> Functions:
Store.register(location,store_type,getter,setter,no_error) --- Register a new location to store a value, the valu returned from getter will be watched for updates
Store.set(location,sub_location,value) --- Sets the stored values at the location, will call the setter function
Store.get(location,sub_location) --- Gets the value at the location, if the value is nil then the getter function is called
Store.check(location,sub_location) --- Checks if the store value needs updating, and if true will update it calling the setter function
]]
local Global = require 'utils.global'
local Event = require 'utils.event'
local Game = require 'utils.game'
local Enum,write_json = ext_require('expcore.common','enum','write_json')
local Store = {
data={},
locations={},
types = Enum{
'local', -- data is not stored with any sub_location, updates caused only by set
'player', -- data is stroed per player, updates caused by watch and set
'force', -- data is stroed per force, updates caused by watch and set
'surface', -- data is stroed per surface, updates caused by watch and set
'game', -- data is stored with any sub_location, updates caused by watch and set
'global' -- data is stored externaly with any sub_location, updates casued by watch, set and the external source
}
}
Global.register(Store.data,function(tbl)
Store.data = table
end)
--- Returns a factorio object for the sub_location
local function get_sub_location_object(store_type,sub_location)
if store_type == Store.types.player then
sub_location = Game.get_player_from_any(sub_location)
if not sub_location then return error('Invalid player for sub_location',3) end
return sub_location
elseif store_type == Store.types.force then
sub_location = type(sub_location) == 'table' and type(sub_location.__self) == 'userdata' and sub_location or game.forces[sub_location]
if not sub_location then return error('Invalid force for sub_location',3) end
return sub_location
elseif store_type == Store.types.surface then
sub_location = type(sub_location) == 'table' and type(sub_location.__self) == 'userdata' and sub_location or game.surfaces[sub_location]
if not sub_location then return error('Invalid surface for sub_location',3) end
return sub_location
end
end
--- Emits an event to the external store that a value was updated
local function set_global_location_value(location,sub_location,value)
write_json('log/store.log',{
location=location,
sub_location=sub_location,
value=value
})
end
--- Register a new location to store a value, the valu returned from getter will be watched for updates
-- @tparam location string the location path for the data must be unqiue
-- @tparam store_type string the type of store this is, see Store.types
-- @tparam getter function will be called to get the value for the store, the value is watched for updates
-- @tparam setter function when the store value changes the setter will be called
-- @tparam[opt=false] no_error boolean when true will skip check for location already registered
function Store.register(location,store_type,getter,setter,no_error)
if not no_error and Store.locations[location] then
return error('The location is already registed: '..location,2)
end
store_type = type(store_type) == 'string' and Store.types[store_type] or store_type
Store.locations[location] = {
location=location,
store_type=store_type,
getter=getter,
setter=setter
}
end
--- Sets the stored values at the location, will call the setter function
-- @tparam location string the location to be updated, must be registed
-- @tparam sub_location string sub_location to set, either string,player,force or surface depending on store type
-- @tparam value any the value to set at the location
function Store.set(location,sub_location,value)
if not Store.locations[location] then
return error('The location is not registed: '..location)
end
location = Store.locations[location]
local sub_location_object = get_sub_location_object(location.store_type,sub_location)
if location.store_type ~= Store.types['local'] then
if not Store.data[location.location] then Store.data[location.location] = {} end
Store.data[location.location][sub_location] = value
end
if location.store_type == Store.types.global then
set_global_location_value(location.location,value)
end
location.setter(sub_location_object or sub_location,value)
end
--- Gets the value at the location, if the value is nil then the getter function is called
-- @tparam location string the location to be returned, must be registed
-- @tparam sub_location string sub_location to get, either string,player,force or surface depending on store type
-- @treturn any the value that was at this location
function Store.get(location,sub_location)
if not Store.locations[location] then return end
location = Store.locations[location]
local sub_location_object = get_sub_location_object(location.store_type,sub_location)
local rtn = Store.data[location.location][sub_location]
if rtn == nil then rtn = location.getter(sub_location_object or sub_location) end
return rtn
end
--- Checks if the store value needs updating, and if true will update it calling the setter function
-- @tparam location string the location to be check, must be registed
-- @tparam sub_location string sub_location to check, either string,player,force or surface depending on store type
-- @treturn boolean if the value was updated and setter function called
function Store.check(location,sub_location)
if not Store.locations[location] then return false end
location = Store.locations[location]
local sub_location_object = get_sub_location_object(location.store_type,sub_location)
local store,getter = Store.data[location.location][sub_location],location.getter(sub_location_object or sub_location)
if store ~= getter then
if not Store.data[location.location] then Store.data[location.location] = {} end
Store.data[location.location][sub_location] = getter
location.setter(sub_location_object or sub_location,getter)
return true
end
return false
end
--- Checks once per second for changes to the store values
Event.on_nth_tick(60,function()
for _,location in pairs(Store.locations) do
if location.store_type ~= Store.types['local'] then
if not Store.data[location.location] then Store.data[location.location] = {} end
for sub_location,_ in pairs(Store.data[location.location]) do
Store.check(location,sub_location)
end
end
end
end)
return Store