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)