Clear for new thing
This commit is contained in:
@@ -1,693 +0,0 @@
|
||||
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 sourceCatalog = safeRequire("OFSourceCatalog")
|
||||
|
||||
local function buildLookupSet(values)
|
||||
local lookup = {}
|
||||
if type(values) ~= "table" then
|
||||
return lookup
|
||||
end
|
||||
for _, value in ipairs(values) do
|
||||
local s = tostring(value):lower()
|
||||
lookup[s] = true
|
||||
end
|
||||
return lookup
|
||||
end
|
||||
|
||||
local attachmentTypeLookup = buildLookupSet(sourceCatalog.attachments)
|
||||
local magazineTypeLookup = buildLookupSet(sourceCatalog.magazines)
|
||||
|
||||
local function isMagazineType(itemType)
|
||||
local s = tostring(itemType or ""):lower()
|
||||
if s == "" then
|
||||
return false
|
||||
end
|
||||
if magazineTypeLookup[s] then
|
||||
return true
|
||||
end
|
||||
if s:sub(1, 10) == "base.clip_" then
|
||||
return true
|
||||
end
|
||||
if s:find("magazine", 1, true) then
|
||||
return true
|
||||
end
|
||||
if s:find("drum", 1, true) then
|
||||
return true
|
||||
end
|
||||
if s:find("clip", 1, true) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
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 getMagazinePartAlias(itemType)
|
||||
local normalized = normalizeItemType(itemType)
|
||||
if not normalized then
|
||||
return nil
|
||||
end
|
||||
local lowered = normalized:lower()
|
||||
if not isMagazineType(lowered) then
|
||||
return nil
|
||||
end
|
||||
if lowered:sub(1, 10) == "base.clip_" then
|
||||
return nil
|
||||
end
|
||||
local short = normalized:match("^Base%.(.+)$")
|
||||
if not short or short == "" then
|
||||
return nil
|
||||
end
|
||||
return "Base.Clip_" .. short
|
||||
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 disabledManagedItemSet = {}
|
||||
local placementsByList = {}
|
||||
local managedCount = 0
|
||||
|
||||
local function clearPlacementsForItem(itemType)
|
||||
for _, entries in pairs(placementsByList) do
|
||||
entries[itemType] = nil
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
disabledManagedItemSet[lowered] = true
|
||||
local partAlias = getMagazinePartAlias(normalized)
|
||||
if partAlias then
|
||||
managedItemSet[partAlias:lower()] = true
|
||||
disabledManagedItemSet[partAlias:lower()] = true
|
||||
end
|
||||
clearPlacementsForItem(normalized)
|
||||
return
|
||||
end
|
||||
|
||||
disabledManagedItemSet[lowered] = nil
|
||||
|
||||
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, disabledManagedItemSet, placementsByList, managedCount
|
||||
end
|
||||
|
||||
local managedSpawnItems, disabledManagedSpawnItems, 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 shouldBlockByRulesOnly(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 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 isWeaponPartBlockedForUpgrades(itemType, nowEpoch)
|
||||
local normalized = normalizeItemType(itemType)
|
||||
if not normalized then
|
||||
return false
|
||||
end
|
||||
|
||||
local lowered = normalized:lower()
|
||||
if not attachmentTypeLookup[lowered] and not isMagazineType(lowered) then
|
||||
return false
|
||||
end
|
||||
|
||||
if disabledManagedSpawnItems[lowered] then
|
||||
return true
|
||||
end
|
||||
|
||||
return shouldBlockByRulesOnly("GGSWeaponUpgrades", normalized, nowEpoch)
|
||||
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 removeBlockedAttachmentEntries(entries, nowEpoch)
|
||||
if type(entries) ~= "table" then
|
||||
return 0
|
||||
end
|
||||
|
||||
local removed = 0
|
||||
for i = #entries, 1, -1 do
|
||||
local value = entries[i]
|
||||
if type(value) == "string" and isWeaponPartBlockedForUpgrades(value, nowEpoch) then
|
||||
table.remove(entries, i)
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
return removed
|
||||
end
|
||||
|
||||
local function patchWeaponUpgradeAttachmentSources(nowEpoch)
|
||||
local removed = 0
|
||||
|
||||
if type(GGSWeaponUpgrades) == "table" and type(GGSWeaponUpgrades.Lists) == "table" then
|
||||
for _, listDef in pairs(GGSWeaponUpgrades.Lists) do
|
||||
if type(listDef) == "table" then
|
||||
if type(listDef.items) == "table" then
|
||||
removed = removed + removeBlockedAttachmentEntries(listDef.items, nowEpoch)
|
||||
elseif listDef[1] ~= nil then
|
||||
removed = removed + removeBlockedAttachmentEntries(listDef, nowEpoch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if type(WeaponUpgrades) == "table" then
|
||||
for _, listDef in pairs(WeaponUpgrades) do
|
||||
if type(listDef) == "table" then
|
||||
if type(listDef.items) == "table" then
|
||||
removed = removed + removeBlockedAttachmentEntries(listDef.items, nowEpoch)
|
||||
elseif listDef[1] ~= nil then
|
||||
removed = removed + removeBlockedAttachmentEntries(listDef, nowEpoch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return removed
|
||||
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 upgradeAttachmentRemoved = patchWeaponUpgradeAttachmentSources(nowEpoch)
|
||||
local added = applyProfilePlacements()
|
||||
|
||||
if ItemPickerJava and ItemPickerJava.Parse then
|
||||
ItemPickerJava.Parse()
|
||||
end
|
||||
|
||||
print(string.format("[OFDistributionBlocker] Removed %d distro entries, removed %d upgrade attachments, and added %d profile entries (managed items: %d).", removed, upgradeAttachmentRemoved, added, managedSpawnItemCount))
|
||||
end
|
||||
|
||||
Events.OnInitWorld.Add(patchAllDistributions)
|
||||
Events.OnLoadMapZones.Add(patchAllDistributions)
|
||||
Events.OnGameStart.Add(patchAllDistributions)
|
||||
Reference in New Issue
Block a user