Files
OpinionatedFirearms/42/media/lua/server/distribution/OFDistributionBlocker.lua
2026-02-11 18:22:33 -05:00

528 lines
14 KiB
Lua

require 'Items/ProceduralDistributions'
require 'Items/SuburbsDistributions'
require 'Items/ItemPicker'
pcall(require, 'Vehicles/VehicleDistributions')
if _G.__OF_DISTRO_BLOCKER_REGISTERED then
return
end
_G.__OF_DISTRO_BLOCKER_REGISTERED = true
local function safeRequire(moduleName)
local ok, result = pcall(require, moduleName)
if ok and type(result) == "table" then
return result
end
return {}
end
local config = safeRequire("OFBlockConfig")
local spawnProfile = safeRequire("OFSpawnProfile")
local function trim(value)
if type(value) ~= "string" then
return nil
end
return (value:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function normalizeItemType(value)
local s = trim(value)
if not s or s == "" then
return nil
end
if s:sub(1, 1) == "@" then
return s
end
if s:find("%.") then
return s
end
return "Base." .. s
end
local function normalizePrefix(value)
local s = trim(value)
if not s or s == "" then
return nil
end
if s:sub(-1) == "*" then
s = s:sub(1, -2)
end
return s:lower()
end
local function normalizeAliasName(value)
local s = trim(value)
if not s or s == "" then
return nil
end
if s:sub(1, 1) == "@" then
s = s:sub(2)
end
return s:lower()
end
local function addAliasItems(outItems, outSeen, aliasMap, aliasName, visiting)
local key = normalizeAliasName(aliasName)
if not key or visiting[key] then
return
end
local values = aliasMap[key]
if type(values) ~= "table" then
return
end
visiting[key] = true
for _, entry in ipairs(values) do
if type(entry) == "string" and entry:sub(1, 1) == "@" then
addAliasItems(outItems, outSeen, aliasMap, entry, visiting)
else
local normalized = normalizeItemType(entry)
if normalized and normalized:sub(1, 1) ~= "@" then
local lowered = normalized:lower()
if not outSeen[lowered] then
outSeen[lowered] = true
outItems[#outItems + 1] = lowered
end
end
end
end
visiting[key] = nil
end
local function expandItems(rawItems, aliasMap)
local result = {}
local seen = {}
if type(rawItems) ~= "table" then
return result
end
for _, entry in ipairs(rawItems) do
if type(entry) == "string" and entry:sub(1, 1) == "@" then
addAliasItems(result, seen, aliasMap, entry, {})
else
local normalized = normalizeItemType(entry)
if normalized then
local lowered = normalized:lower()
if not seen[lowered] then
seen[lowered] = true
result[#result + 1] = lowered
end
end
end
end
return result
end
local function compileAliasMap(rawAliases)
local aliasMap = {}
if type(rawAliases) ~= "table" then
return aliasMap
end
for aliasName, list in pairs(rawAliases) do
local key = normalizeAliasName(aliasName)
if key then
aliasMap[key] = list
end
end
return aliasMap
end
local function compileBlockMatcher(spec, aliasMap)
local matcher = {
items = {},
prefixes = {},
}
if type(spec) ~= "table" then
return matcher
end
local expandedItems = expandItems(spec.items, aliasMap)
for _, lowered in ipairs(expandedItems) do
matcher.items[lowered] = true
end
if type(spec.prefixes) == "table" then
for _, rawPrefix in ipairs(spec.prefixes) do
local prefix = normalizePrefix(rawPrefix)
if prefix then
matcher.prefixes[#matcher.prefixes + 1] = prefix
end
end
end
return matcher
end
local function itemMatches(itemTypeLower, matcher)
if matcher.items[itemTypeLower] then
return true
end
for _, prefix in ipairs(matcher.prefixes) do
if itemTypeLower:sub(1, #prefix) == prefix then
return true
end
end
return false
end
local function compileListPatterns(raw)
if raw == nil or raw == "*" then
return nil
end
local patterns = {}
local function addPattern(value)
local s = trim(value)
if not s or s == "" then
return
end
if s == "*" then
patterns = nil
return true
end
patterns[#patterns + 1] = s:lower()
return false
end
if type(raw) == "string" then
addPattern(raw)
elseif type(raw) == "table" then
local source = raw.lists or raw
if type(source) == "string" then
addPattern(source)
elseif type(source) == "table" then
for _, value in ipairs(source) do
if addPattern(value) then
break
end
end
end
end
return patterns
end
local function listMatchesPattern(listNameLower, patternLower)
if not patternLower or patternLower == "" then
return false
end
if patternLower:sub(-1) == "*" then
local prefix = patternLower:sub(1, -2)
return listNameLower:sub(1, #prefix) == prefix
end
if listNameLower == patternLower then
return true
end
return listNameLower:sub(-#patternLower - 1) == ("." .. patternLower)
end
local function listMatches(listNameLower, patterns)
if not patterns then
return true
end
for _, pattern in ipairs(patterns) do
if listMatchesPattern(listNameLower, pattern) then
return true
end
end
return false
end
local function compileRules(rawRules, aliasMap)
local compiled = {}
if type(rawRules) ~= "table" then
return compiled
end
for _, rule in ipairs(rawRules) do
if type(rule) == "table" then
local whenBlock = rule.when or {}
compiled[#compiled + 1] = {
id = trim(rule.id) or "unnamed",
enabled = rule.enabled ~= false,
startsAt = tonumber(rule.startsAt or whenBlock.startsAt),
endsAt = tonumber(rule.endsAt or whenBlock.endsAt),
listPatterns = compileListPatterns(rule.where),
matcher = compileBlockMatcher(rule.block or rule, aliasMap),
}
end
end
return compiled
end
local aliasMap = compileAliasMap(config.aliases)
local globalMatcher = compileBlockMatcher(config.global, aliasMap)
local byListMatchers = {}
if type(config.byList) == "table" then
for listPattern, spec in pairs(config.byList) do
byListMatchers[#byListMatchers + 1] = {
pattern = trim(listPattern) and trim(listPattern):lower() or "",
matcher = compileBlockMatcher(spec, aliasMap),
}
end
end
local ruleMatchers = compileRules(config.rules, aliasMap)
local function compileSpawnProfile(rawProfile)
local managedItemSet = {}
local placementsByList = {}
local managedCount = 0
local function addPlacement(listName, itemType, rawWeight)
local cleanList = trim(listName)
local weight = tonumber(rawWeight)
if not cleanList or cleanList == "" or not weight or weight <= 0 then
return
end
if not placementsByList[cleanList] then
placementsByList[cleanList] = {}
end
placementsByList[cleanList][itemType] = weight
end
local function addEntry(itemTypeRaw, entry)
local normalized = normalizeItemType(itemTypeRaw)
if not normalized or normalized:sub(1, 1) == "@" then
return
end
local lowered = normalized:lower()
if not managedItemSet[lowered] then
managedItemSet[lowered] = true
managedCount = managedCount + 1
end
if type(entry) ~= "table" then
return
end
if entry.enabled == false then
return
end
if type(entry.placements) == "table" then
if entry.placements[1] then
for _, row in ipairs(entry.placements) do
if type(row) == "table" then
addPlacement(row.list, normalized, row.weight)
end
end
else
for listName, weight in pairs(entry.placements) do
addPlacement(listName, normalized, weight)
end
end
end
end
if type(rawProfile) == "table" then
if type(rawProfile.items) == "table" then
for itemType, entry in pairs(rawProfile.items) do
addEntry(itemType, entry)
end
end
if type(rawProfile.entries) == "table" then
for _, entry in ipairs(rawProfile.entries) do
if type(entry) == "table" then
addEntry(entry.item, entry)
end
end
end
end
return managedItemSet, placementsByList, managedCount
end
local managedSpawnItems, profilePlacementsByList, managedSpawnItemCount = compileSpawnProfile(spawnProfile)
local function isRuleActive(rule, nowEpoch)
if not rule.enabled then
return false
end
if (rule.startsAt or rule.endsAt) and not nowEpoch then
return false
end
if rule.startsAt and nowEpoch < rule.startsAt then
return false
end
if rule.endsAt and nowEpoch > rule.endsAt then
return false
end
return true
end
local function shouldBlock(listName, itemType, nowEpoch)
local listLower = (trim(listName) or "unknown"):lower()
local itemLower = normalizeItemType(itemType)
if not itemLower then
return false
end
itemLower = itemLower:lower()
if managedSpawnItems[itemLower] then
return true
end
if itemMatches(itemLower, globalMatcher) then
return true
end
for _, entry in ipairs(byListMatchers) do
if entry.pattern ~= "" and listMatchesPattern(listLower, entry.pattern) and itemMatches(itemLower, entry.matcher) then
return true
end
end
for _, rule in ipairs(ruleMatchers) do
if isRuleActive(rule, nowEpoch) and listMatches(listLower, rule.listPatterns) and itemMatches(itemLower, rule.matcher) then
return true
end
end
return false
end
local function removeBlockedEntries(items, listName, nowEpoch)
if type(items) ~= "table" then
return 0
end
local removed = 0
local i = 1
while i <= #items do
local itemType = items[i]
if type(itemType) == "string" then
if shouldBlock(listName, itemType, nowEpoch) then
table.remove(items, i)
if i <= #items then
table.remove(items, i)
end
removed = removed + 1
else
i = i + 2
end
else
i = i + 1
end
end
return removed
end
local function patchProceduralDistributions(nowEpoch)
local removed = 0
local pd = ProceduralDistributions and ProceduralDistributions.list
if type(pd) ~= "table" then
return 0
end
for listName, listDef in pairs(pd) do
if type(listDef) == "table" then
removed = removed + removeBlockedEntries(listDef.items, listName, nowEpoch)
if type(listDef.junk) == "table" then
removed = removed + removeBlockedEntries(listDef.junk.items, listName, nowEpoch)
end
end
end
return removed
end
local function patchNestedDistributionTree(rootName, rootTable, nowEpoch)
if type(rootTable) ~= "table" then
return 0
end
local removed = 0
local visited = {}
local function walk(node, path)
if type(node) ~= "table" or visited[node] then
return
end
visited[node] = true
if type(node.items) == "table" then
removed = removed + removeBlockedEntries(node.items, path, nowEpoch)
end
if type(node.junk) == "table" and type(node.junk.items) == "table" then
removed = removed + removeBlockedEntries(node.junk.items, path, nowEpoch)
end
for key, value in pairs(node) do
if key ~= "items" and type(value) == "table" then
local keyName = type(key) == "string" and key or tostring(key)
local nextPath = path .. "." .. keyName
walk(value, nextPath)
end
end
end
walk(rootTable, rootName)
return removed
end
local function ensureProceduralList(listName)
local pd = ProceduralDistributions and ProceduralDistributions.list
if type(pd) ~= "table" then
return nil
end
if type(pd[listName]) ~= "table" then
pd[listName] = {
rolls = 2,
items = {},
junk = { rolls = 1, items = {} },
}
end
if type(pd[listName].items) ~= "table" then
pd[listName].items = {}
end
return pd[listName]
end
local function applyProfilePlacements()
local added = 0
for listName, entries in pairs(profilePlacementsByList) do
local listDef = ensureProceduralList(listName)
if listDef then
for itemType, weight in pairs(entries) do
table.insert(listDef.items, itemType)
table.insert(listDef.items, weight)
added = added + 1
end
end
end
return added
end
local function patchAllDistributions()
local nowEpoch = nil
if os and os.time then
nowEpoch = os.time()
end
local removed = 0
removed = removed + patchProceduralDistributions(nowEpoch)
removed = removed + patchNestedDistributionTree("SuburbsDistributions", SuburbsDistributions, nowEpoch)
removed = removed + patchNestedDistributionTree("VehicleDistributions", VehicleDistributions, nowEpoch)
local added = applyProfilePlacements()
if ItemPickerJava and ItemPickerJava.Parse then
ItemPickerJava.Parse()
end
print(string.format("[OFDistributionBlocker] Removed %d entries and added %d profile entries (managed items: %d).", removed, added, managedSpawnItemCount))
end
Events.OnInitWorld.Add(patchAllDistributions)
Events.OnLoadMapZones.Add(patchAllDistributions)
Events.OnGameStart.Add(patchAllDistributions)