528 lines
14 KiB
Lua
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)
|