local sharedstate={} local logging=require((...):gsub("(.)$", "%1.") .. 'logging') -- function names local is_initialized, callback_value, set_value, get_value, resolve_key, resolve_index, get_table --schema: similar to state_table, nested table only contains value local state_queue={} local function ternary(condition, val_if_true, val_if_false) if condition then return val_if_true else return val_if_false end end -- we're protecting internal variables here, i'm *that* scared of writing bad -- code again. -- this section is purely for making sure the tables are in a consistent state -- maybe this should be a separate library for general state tracking, then i -- could use it in other parts of my code for local state tracking -- (aka how to unnecessarily increase complexity, if i want to do something i -- can just make a function for it) do -- schema: {key: {value: val, old_value: val, callback: function, -- index: integer}} -- wrapping the value in a table should allow the user to store nil local state_table={} -- schema: {1: key_name_for_val_1, ...} -- this is to save bandwidth in pings local state_map={} --- Check if initialized ---Check if a key is initialized ---@param key string key ---@return true if initialized, else false function is_initialized(key) logging.trace('is_initialized', key) return state_table[key] ~= nil end --- Internal; ensure key is initialized ---Properly initialize a key in all tables. ---already initialized ---@param key string key name ---@return boolean if key has been initialized local function init_key(key) logging.trace('init_key', key) if key == nil then local errormsg="sharedstate: A key name is required" error(errormsg) end if is_initialized(key) then return false end -- lua is fucking asinine and starts tables at 1 local index=#state_map+1 state_map[index]=key local tbl={} tbl["index"]=index state_table[key]=tbl return true end --- Run callback function ---Run the callback function of the given key ---@param key string Key function callback_value(key) logging.trace("callback_value", key) local new_value=state_table[key]["value"] local old_value=state_table[key]["old_value"] if type(state_table[key]["callback"]) == "function" and old_value ~= new_value then state_table[key]["callback"](new_value, old_value) end end --- Set value and run callback -- Sets a value in the state table, initializing it if neede, and runs -- the callback if it has been previously set. This function should -- only be called within ping receiver functions ---@param key string key ---@param value any value ---@param callback? function callback function function set_value(key, value, callback) logging.debug("sharedstate: key " .. tostring(key) .. " set to " .. tostring(value)) local initialized=init_key(key) local entry=state_table[key] if initialized then entry["value"]=value entry["old_value"]=value entry["callback"]=callback else entry["value"]=ternary(value ~= nil, value, entry["value"]) callback_value(key) entry["callback"]=callback or entry["callback"] entry["old_value"]=value end end --- Get value ---Get a value from the state table ---@param key string key ---@return any value from state table if set, else nil function get_value(key) logging.trace("get_value", key) if not is_initialized(key) then return nil end return state_table[key]["value"] end --- Resolve key ---Resolve key of given index ---@param index integer index ---@return any key function resolve_key(index) logging.trace("resolve_key", index) return state_map[index] end --- Resolve index ---Resolve index of given key ---@param key string key ---@return integer index function resolve_index(key) logging.trace("resolve_index", key) return state_table[key]["index"] end --- Get table --Internal; Gets a copy of the shared state table with internal data --removed function get_table() local t={} for k, v in pairs(state_table) do t[k]={ ["value"]=v["value"] } end return t end end ---Add an item to the shared state store. ---Adds an item to the shared state store, as well as initializes it with a ---value. This does not send a ping and should only be used during avatar ---initialization. ---@param key string key name ---@param value any initial value ---@param callback? function Callback function to run on value change function sharedstate.init(key, value, callback) logging.trace("sharedstate.init", key, value, callback) set_value(key, value, callback) end ---@deprecated use sharedstate.init instead sharedstate.add=sharedstate.init --- Internal; transfer value over network ---Ping used to transfer a value over the network, should not be called ---directly ---@param index integer Index of key ---@param value any New value function pings.sharedstate_recv(index, value) logging.trace("pings.sharedstate_recv", index, value) set_value(resolve_key(index), value) end --- Internal; transfer value over network using key names ---Ping used to transfer a value over the network, should not be called ---directly ---@param key string Key ---@param value any New value function pings.sharedstate_recv_named(key, value) logging.trace("pings.sharedstate_recv_named", key, value) set_value(key, value) end --- Internal; unpack a table of state into the local state store ---@param tbl table Table to unpack function pings.sharedstate_recv_table(tbl) logging.debug("Table sync received") for k, v in pairs(tbl) do set_value(k, v["value"]) end end --- Set shared value ---Sets a shared value. This sends a ping to transfer it over the network. ---@param key string key name ---@param value any value function sharedstate.set(key, value) logging.trace("sharedstate.set", key, value) if not is_initialized(key) then -- this makes the error traceback less confusing local errormsg="sharedstate: Key " .. key .. " has not been initialized." error(errormsg) end -- don't bother sending unchanged values if value ~= get_value(key) then -- pings.sharedstate_recv(resolve_index(key), value) pings.sharedstate_recv_named(key, value) end end ---Queue entries for sending --Queues entries for sending over a single ping, as to prevent being rate --limited. Values will not be accessible until sharedstate.commit() is run. ---@param key string key ---@param value any value function sharedstate.queue(key, value) local t={ ["value"] = value } state_queue[key]=t end ---Commit queued entries --Commit queued table entries and send in one ping. function sharedstate.commit() --schema: similar to state_table, nested table only contains value -- local send_queue={} -- -- this is intentional values in state_queued are key names -- for _, key in pairs(state_queued) do -- local t={ -- ["value"]=get_value(key) -- } -- send_queue[key]=t -- end pings.sharedstate_recv_table(state_queue) sharedstate.clear() end --- Clear a value from the shared state queue --Clears a value from the shared state queue. If no key is specified, clears --all queued values. ---@param key? string Key to clear function sharedstate.clear(key) if key ~= nil then state_queue[key] = nil else state_queue={} end end --- Sync full state --Syncs the full shared state table. This should be used sparsely function sharedstate.sync() if host:isHost() then ping.sharedstate_recv_table(get_table()) end end -- this can be copied directly from the internal functions as it is read only sharedstate.get=get_value return sharedstate