forked from Simnation/Main
279 lines
8.8 KiB
Lua
279 lines
8.8 KiB
Lua
![]() |
---@class CacheEntry
|
||
|
---@field Name string
|
||
|
---@field Compare fun(): any
|
||
|
---@field WaitTime integer | nil
|
||
|
---@field LastChecked integer|nil
|
||
|
---@field OnChange fun(new:any, old:any)[]
|
||
|
---@field Value any
|
||
|
|
||
|
---@class CacheModule
|
||
|
---@field Caches table<string, CacheEntry>
|
||
|
---@field LoopRunning boolean
|
||
|
---@field Create fun(name:string, compare:fun():any, waitTime:integer | nil): CacheEntry
|
||
|
---@field Get fun(name:string): any
|
||
|
---@field OnChange fun(name:string, onChange:fun(new:any, old:any))
|
||
|
---@field Remove fun(name:string)
|
||
|
Cache = Cache or {} ---@type CacheModule -- <-- we use Cache as a global variable so that if we dont required it more than once
|
||
|
|
||
|
Table = Table or Require('lib/utility/shared/tables.lua')
|
||
|
Id = Id or Require('lib/utility/shared/ids.lua')
|
||
|
|
||
|
local Config = Require("settings/sharedConfig.lua")
|
||
|
local max = 5000
|
||
|
local CreateThread = CreateThread
|
||
|
local Wait = Wait
|
||
|
local GetGameTimer = GetGameTimer
|
||
|
|
||
|
local resourceCallbacks = {} -- Add callbacks from resources
|
||
|
local resourceTracker = {
|
||
|
caches = {},
|
||
|
callbacks = {},
|
||
|
initialized = {}
|
||
|
}
|
||
|
|
||
|
Cache.Caches = Cache.Caches or {}
|
||
|
Cache.LoopRunning = Cache.LoopRunning or false
|
||
|
|
||
|
---@param ... any
|
||
|
local function debugPrint(...)
|
||
|
if Config.DebugLevel == 0 then return end
|
||
|
print("^2[Cache]^0", ...)
|
||
|
end
|
||
|
|
||
|
local function HasActiveCaches()
|
||
|
for _, cache in pairs(Cache.Caches) do
|
||
|
if cache.WaitTime ~= nil then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
local function processCacheEntry(now, cache)
|
||
|
cache.LastChecked = cache.LastChecked or now
|
||
|
cache.WaitTime = tonumber(cache.WaitTime) or max
|
||
|
local elapsed = now - cache.LastChecked
|
||
|
local remaining = cache.WaitTime - elapsed
|
||
|
|
||
|
if remaining <= 0 then
|
||
|
local oldValue = cache.Value
|
||
|
cache.Value = cache.Compare()
|
||
|
if not Table.Compare(cache.Value, oldValue) and cache.OnChange then
|
||
|
for i, onChange in pairs(cache.OnChange) do
|
||
|
onChange(cache.Value, oldValue)
|
||
|
end
|
||
|
end
|
||
|
cache.LastChecked = now
|
||
|
remaining = cache.WaitTime
|
||
|
end
|
||
|
|
||
|
return remaining
|
||
|
end
|
||
|
|
||
|
local function getNextWait(now)
|
||
|
local minWait = nil
|
||
|
for name, cache in pairs(Cache.Caches) do
|
||
|
if cache.Compare and cache.WaitTime ~= nil then
|
||
|
local remaining = processCacheEntry(now, cache)
|
||
|
if not minWait or remaining < minWait then
|
||
|
minWait = remaining
|
||
|
if minWait <= 0 then break end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return minWait
|
||
|
end
|
||
|
|
||
|
local function StartLoop()
|
||
|
if Cache.LoopRunning then return end
|
||
|
if not HasActiveCaches() then return end
|
||
|
Cache.LoopRunning = true
|
||
|
CreateThread(function()
|
||
|
while Cache.LoopRunning do
|
||
|
local now = GetGameTimer()
|
||
|
local minWait = getNextWait(now)
|
||
|
if minWait then
|
||
|
Wait(math.max(0, minWait))
|
||
|
else
|
||
|
Wait(max)
|
||
|
end
|
||
|
if not HasActiveCaches() then
|
||
|
Cache.LoopRunning = false
|
||
|
end
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
local function trackResource(resourceName, type, data)
|
||
|
if not resourceName then return end
|
||
|
|
||
|
-- Initialize resource tracking if needed
|
||
|
if not resourceTracker.initialized[resourceName] then
|
||
|
resourceTracker.initialized[resourceName] = true
|
||
|
resourceTracker.caches[resourceName] = resourceTracker.caches[resourceName] or {}
|
||
|
resourceTracker.callbacks[resourceName] = resourceTracker.callbacks[resourceName] or {}
|
||
|
|
||
|
AddEventHandler('onResourceStop', function(stoppingResource)
|
||
|
if stoppingResource ~= resourceName then return end
|
||
|
|
||
|
-- Clean up caches
|
||
|
for _, cacheName in ipairs(resourceTracker.caches[resourceName] or {}) do
|
||
|
if Cache.Caches[cacheName] then
|
||
|
Cache.Caches[cacheName] = nil
|
||
|
debugPrint(("Removed cache '%s' - resource '%s' stopped"):format(cacheName, resourceName))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Clean up callbacks
|
||
|
for _, cb in ipairs(resourceTracker.callbacks[resourceName] or {}) do
|
||
|
local targetCache = Cache.Caches[cb.cacheName]
|
||
|
if targetCache then
|
||
|
table.remove(targetCache.OnChange, cb.index)
|
||
|
debugPrint(("Removed OnChange callback from cache '%s' - resource '%s' stopped"):format(
|
||
|
cb.cacheName,
|
||
|
resourceName
|
||
|
))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Clear tracking data
|
||
|
resourceTracker.caches[resourceName] = nil
|
||
|
resourceTracker.callbacks[resourceName] = nil
|
||
|
resourceTracker.initialized[resourceName] = nil
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
-- Track the new item
|
||
|
if type == "cache" then
|
||
|
table.insert(resourceTracker.caches[resourceName], data)
|
||
|
elseif type == "callback" then
|
||
|
table.insert(resourceTracker.callbacks[resourceName], data)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---@param name string
|
||
|
---@param compare fun():any
|
||
|
---@param waitTime integer | nil
|
||
|
---@return CacheEntry | nil
|
||
|
function Cache.Create(name, compare, waitTime)
|
||
|
assert(name, "Cache name is required.")
|
||
|
assert(compare, "Comparison function is required.")
|
||
|
if type(waitTime) ~= "number" then
|
||
|
waitTime = nil
|
||
|
end
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
if cache and cache.Compare == compare then
|
||
|
debugPrint(_name .. " already exists with the same comparison function.")
|
||
|
return cache
|
||
|
end
|
||
|
local ok, result = pcall(compare)
|
||
|
if not ok then
|
||
|
debugPrint("Error creating cache '" .. _name .. "': " .. tostring(result))
|
||
|
return nil
|
||
|
end
|
||
|
---@type CacheEntry
|
||
|
local newCache = {
|
||
|
Name = _name,
|
||
|
Compare = compare,
|
||
|
WaitTime = waitTime, -- can be nil
|
||
|
LastChecked = nil,
|
||
|
OnChange = {},
|
||
|
Value = result
|
||
|
}
|
||
|
|
||
|
-- Track the cache with its resource
|
||
|
trackResource(GetInvokingResource(), "cache", _name)
|
||
|
|
||
|
Cache.Caches[_name] = newCache
|
||
|
|
||
|
debugPrint(_name .. " created with initial value: " .. tostring(result))
|
||
|
for _, onChange in pairs(newCache.OnChange) do
|
||
|
onChange(newCache.Value, 0)
|
||
|
end
|
||
|
StartLoop()
|
||
|
return newCache
|
||
|
end
|
||
|
|
||
|
---@param name string
|
||
|
---@return any
|
||
|
function Cache.Get(name)
|
||
|
assert(name, "Cache name is required.")
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
return cache and cache.Value or nil
|
||
|
end
|
||
|
|
||
|
--- This add a callback to the cache entry that will be called when the value changes.
|
||
|
--- The callback will be called with the new value and the old value.
|
||
|
--- you can call the value again to delete the callback.
|
||
|
---@param name string
|
||
|
---@param onChange fun(new:any, old:any)
|
||
|
function Cache.OnChange(name, onChange)
|
||
|
assert(name, "Cache name is required.")
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||
|
|
||
|
local id = Id.CreateUniqueId(Cache.Caches[_name]?.OnChange)
|
||
|
-- Figure out which resource is trying to register this callback
|
||
|
local invokingResource = GetInvokingResource()
|
||
|
if not invokingResource then return end
|
||
|
-- Add the new callback to our list
|
||
|
local callbackIndex = id
|
||
|
cache.OnChange[callbackIndex] = onChange
|
||
|
|
||
|
-- Track the callback with its resource
|
||
|
trackResource(GetInvokingResource(), "callback", {
|
||
|
cacheName = _name,
|
||
|
index = callbackIndex
|
||
|
})
|
||
|
|
||
|
debugPrint(("Added new OnChange callback to cache '%s' from resource '%s'"):format(_name, invokingResource))
|
||
|
return callbackIndex
|
||
|
end
|
||
|
|
||
|
function Cache.RemoveOnChange(name, id)
|
||
|
assert(name, "Cache name is required.")
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||
|
if cache.OnChange[id] then
|
||
|
cache.OnChange[id] = nil
|
||
|
debugPrint("Removed OnChange callback from cache '" .. _name .. "' with ID: " .. id)
|
||
|
else
|
||
|
debugPrint("No OnChange callback found with ID: " .. id .. " in cache '" .. _name .. "'")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---@param name string
|
||
|
function Cache.Remove(name)
|
||
|
assert(name, "Cache name is required.")
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
if cache then
|
||
|
Cache.Caches[_name] = nil
|
||
|
debugPrint(_name .. " removed from cache.")
|
||
|
if next(Cache.Caches) == nil then
|
||
|
Cache.LoopRunning = false
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---@param name string
|
||
|
---@param newValue any
|
||
|
function Cache.Update(name, newValue)
|
||
|
assert(name, "Cache name is required.")
|
||
|
local _name = tostring(name)
|
||
|
local cache = Cache.Caches[_name]
|
||
|
assert(cache, "Cache with name '" .. _name .. "' does not exist.")
|
||
|
local oldValue = cache.Value
|
||
|
if oldValue ~= newValue then
|
||
|
cache.Value = newValue
|
||
|
for _, onChange in pairs(cache.OnChange) do
|
||
|
onChange(newValue, oldValue)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return Cache
|