Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
06593805a7
|
|||
|
53b0b4317d
|
|||
|
ad8310f143
|
|||
|
5ff5764fa2
|
|||
|
c320e8d993
|
|||
|
dd2d7a3abe
|
|||
|
9c86fe30f6
|
BIN
42/icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,527 +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 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)
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
name=Opinionated Firearms
|
name=Opinionated Firearms
|
||||||
id=opinionated_firearms
|
id=hrsys_opinionated_firearms_testing
|
||||||
author=Riggs0
|
author=Riggs0
|
||||||
modversion=1.0.0
|
modversion=1.0.0
|
||||||
versionMin=42.12.13
|
versionMin=42.12.13
|
||||||
require=GaelGunStore_ALPHA
|
require=\2788256295/ammomaker,\HBVCEFb42
|
||||||
|
description=Opinionated Firearms casing and other changes to Guns of 93, Ammomaker and Hot Brass
|
||||||
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).
|
icon=icon.png
|
||||||
|
poster=preview.png
|
||||||
|
|||||||
BIN
42/preview.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
84
README.md
@@ -1,84 +0,0 @@
|
|||||||
# Opinionated Firearms
|
|
||||||
|
|
||||||
Project Zomboid B42 patch mod + tooling for managing `GaelGunStore` firearm/attachment spawn distribution.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Use CLI `extract` to build a JSON catalog from GGS source (`loot.lua` + scripts).
|
|
||||||
2. Load that catalog JSON in the webapp.
|
|
||||||
3. In the webapp, choose per item:
|
|
||||||
- spawn enabled/disabled
|
|
||||||
- where it should spawn (distribution list)
|
|
||||||
- spawn rate weight
|
|
||||||
4. Export a profile JSON from the webapp.
|
|
||||||
5. Use CLI `apply` to convert the profile JSON into `OFSpawnProfile.lua`.
|
|
||||||
6. Start Project Zomboid with this mod + `GaelGunStore_ALPHA`.
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
Script: `tools/ggs-dist-cli.js`
|
|
||||||
|
|
||||||
### Extract catalog
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node tools/ggs-dist-cli.js extract --ggs-root source/GaelGunStore/42 --out data/ggs-spawn-catalog.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Output contains:
|
|
||||||
|
|
||||||
- all firearms/attachments from GGS scripts
|
|
||||||
- where they spawn (`list`)
|
|
||||||
- base spawn weight (`weight`)
|
|
||||||
- sandbox key (`sv`) used by GGS spawn multipliers
|
|
||||||
|
|
||||||
### Apply webapp profile
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
node tools/ggs-dist-cli.js apply --profile data/of-spawn-profile.json --out common/media/lua/shared/OFSpawnProfile.lua
|
|
||||||
```
|
|
||||||
|
|
||||||
This writes the Lua profile the mod reads at runtime.
|
|
||||||
|
|
||||||
## Webapp
|
|
||||||
|
|
||||||
Path: `webapp/index.html`
|
|
||||||
|
|
||||||
Serve locally (recommended):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python -m http.server 8080
|
|
||||||
# open http://localhost:8080/webapp/
|
|
||||||
```
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
- import extracted catalog JSON
|
|
||||||
- import existing profile JSON
|
|
||||||
- filter/search full item list
|
|
||||||
- toggle per-item spawn enabled
|
|
||||||
- edit per-list placements and weights
|
|
||||||
- export profile JSON for CLI `apply`
|
|
||||||
|
|
||||||
## Runtime mod behavior
|
|
||||||
|
|
||||||
Main patcher: `42/media/lua/server/distribution/OFDistributionBlocker.lua`
|
|
||||||
|
|
||||||
- loads block rules (`OFBlockConfig`) and spawn profile (`OFSpawnProfile`)
|
|
||||||
- removes blocked/managed entries from distributions
|
|
||||||
- re-adds managed item placements with chosen weights from spawn profile
|
|
||||||
- reparses ItemPicker after patching
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `mod.info`
|
|
||||||
- `42/mod.info`
|
|
||||||
- `42/media/lua/server/distribution/OFDistributionBlocker.lua`
|
|
||||||
- `common/media/lua/shared/OFSpawnProfile.lua`
|
|
||||||
- `common/media/lua/shared/OFBlockConfig.lua`
|
|
||||||
- `common/media/lua/shared/OFBlockRules_Default.lua`
|
|
||||||
- `common/media/lua/shared/OFBlockRules_User.lua`
|
|
||||||
- `common/media/lua/shared/OFSourceCatalog.lua`
|
|
||||||
- `tools/ggs-dist-cli.js`
|
|
||||||
- `webapp/index.html`
|
|
||||||
- `webapp/styles.css`
|
|
||||||
- `webapp/app.js`
|
|
||||||
BIN
art/icon.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
art/icon.psd
Normal file
BIN
art/logo.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
art/logo.psd
Normal file
BIN
art/preview.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
art/preview.psd
Normal file
BIN
art/workshop-preview.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,368 @@
|
|||||||
|
return {
|
||||||
|
|
||||||
|
WepFire = {
|
||||||
|
"Base.83Cheetah",
|
||||||
|
"Base.AKM",
|
||||||
|
"Base.AKSport",
|
||||||
|
"Base.Anaconda",
|
||||||
|
"Base.AR15",
|
||||||
|
"Base.AR180",
|
||||||
|
"Base.AssaultRifle",
|
||||||
|
"Base.AssaultRifle2",
|
||||||
|
"Base.Auto5",
|
||||||
|
"Base.Auto5_Alt",
|
||||||
|
"Base.BenelliM3",
|
||||||
|
"Base.BenelliM3_Alt",
|
||||||
|
"Base.Beretta682",
|
||||||
|
"Base.Beretta682_Alt",
|
||||||
|
"Base.Beretta92FS",
|
||||||
|
"Base.BHP",
|
||||||
|
"Base.Bobcat",
|
||||||
|
"Base.Brown3006BAR",
|
||||||
|
"Base.Brown308BAR",
|
||||||
|
"Base.BrownCitori",
|
||||||
|
"Base.BrownCitori_Alt",
|
||||||
|
"Base.Buckmark",
|
||||||
|
"Base.CalicoPistol",
|
||||||
|
"Base.CalicoRifle",
|
||||||
|
"Base.CAR15",
|
||||||
|
"Base.Colt9mm",
|
||||||
|
"Base.ColtArmy",
|
||||||
|
"Base.ColtDet",
|
||||||
|
"Base.Command1911",
|
||||||
|
"Base.CZ75B",
|
||||||
|
"Base.DE357",
|
||||||
|
"Base.DeltaElite",
|
||||||
|
"Base.Dissy",
|
||||||
|
"Base.DoubleBarrelShotgun",
|
||||||
|
"Base.DoubleBarrelShotgun_Alt",
|
||||||
|
"Base.DoubleBarrelShotgunSawnoff",
|
||||||
|
"Base.DoubleBarrelShotgunSawnoff_Alt",
|
||||||
|
"Base.FAL",
|
||||||
|
"Base.Glock17",
|
||||||
|
"Base.Glock17L",
|
||||||
|
"Base.Glock19",
|
||||||
|
"Base.Glock20",
|
||||||
|
"Base.Glock21",
|
||||||
|
"Base.Glock22",
|
||||||
|
"Base.Glock23",
|
||||||
|
"Base.Glock24",
|
||||||
|
"Base.Gov1911",
|
||||||
|
"Base.GP100",
|
||||||
|
"Base.GP100_Alt",
|
||||||
|
"Base.GP101",
|
||||||
|
"Base.GP101_Alt",
|
||||||
|
"Base.Hammerless",
|
||||||
|
"Base.HBAR",
|
||||||
|
"Base.HK91",
|
||||||
|
"Base.HuntingRifle",
|
||||||
|
"Base.Ithaca37",
|
||||||
|
"Base.Ithaca37_Alt",
|
||||||
|
"Base.Ithaca37Riot",
|
||||||
|
"Base.Ithaca37Riot_Alt",
|
||||||
|
"Base.J22",
|
||||||
|
"Base.Javelina",
|
||||||
|
"Base.L395K",
|
||||||
|
"Base.L395K_Alt",
|
||||||
|
"Base.Longslide",
|
||||||
|
"Base.M1903",
|
||||||
|
"Base.M1917",
|
||||||
|
"Base.M1A",
|
||||||
|
"Base.M1ABush",
|
||||||
|
"Base.M1Carbine",
|
||||||
|
"Base.M1Garand",
|
||||||
|
"Base.M24",
|
||||||
|
"Base.M249",
|
||||||
|
"Base.M3GreaseGun",
|
||||||
|
"Base.M590",
|
||||||
|
"Base.M590_Alt",
|
||||||
|
"Base.M60",
|
||||||
|
"Base.M635",
|
||||||
|
"Base.M723",
|
||||||
|
"Base.M727",
|
||||||
|
"Base.M733",
|
||||||
|
"Base.M870",
|
||||||
|
"Base.M870_Alt",
|
||||||
|
"Base.MAC10",
|
||||||
|
"Base.Marlin3363030",
|
||||||
|
"Base.Marlin33644",
|
||||||
|
"Base.Marlin39A",
|
||||||
|
"Base.Marlin45",
|
||||||
|
"Base.Marlin60",
|
||||||
|
"Base.Marlin9",
|
||||||
|
"Base.Mauser98K",
|
||||||
|
"Base.Mini14",
|
||||||
|
"Base.Mini30",
|
||||||
|
"Base.Moss500",
|
||||||
|
"Base.Moss500_Alt",
|
||||||
|
"Base.Moss590",
|
||||||
|
"Base.Moss590_Alt",
|
||||||
|
"Base.MP5",
|
||||||
|
"Base.Officer1911",
|
||||||
|
"Base.P226",
|
||||||
|
"Base.P228",
|
||||||
|
"Base.P380",
|
||||||
|
"Base.P7M13",
|
||||||
|
"Base.P7M8",
|
||||||
|
"Base.Pistol",
|
||||||
|
"Base.Pistol2",
|
||||||
|
"Base.Pistol3",
|
||||||
|
"Base.PPK",
|
||||||
|
"Base.Python",
|
||||||
|
"Base.Python_Alt",
|
||||||
|
"Base.R223Rem788",
|
||||||
|
"Base.R223RugM77",
|
||||||
|
"Base.R3006Rem700",
|
||||||
|
"Base.R3006RugM77",
|
||||||
|
"Base.R3030Rem788",
|
||||||
|
"Base.R308Rem700",
|
||||||
|
"Base.R308RugM77",
|
||||||
|
"Base.R44Rem788",
|
||||||
|
"Base.Raven25",
|
||||||
|
"Base.Rem1100",
|
||||||
|
"Base.Rem1100_Alt",
|
||||||
|
"Base.Rem1187P",
|
||||||
|
"Base.Rem1187P_Alt",
|
||||||
|
"Base.Rem223760",
|
||||||
|
"Base.Rem3006760",
|
||||||
|
"Base.Rem308760",
|
||||||
|
"Base.Rem870P",
|
||||||
|
"Base.Rem870P_Alt",
|
||||||
|
"Base.Revolver",
|
||||||
|
"Base.Revolver_Long",
|
||||||
|
"Base.Revolver_Short",
|
||||||
|
"Base.Rug1022",
|
||||||
|
"Base.Rugmark",
|
||||||
|
"Base.RugP89",
|
||||||
|
"Base.RugP90",
|
||||||
|
"Base.SavageFox",
|
||||||
|
"Base.SavageFox_Alt",
|
||||||
|
"Base.SecuritySix",
|
||||||
|
"Base.SecuritySix_Alt",
|
||||||
|
"Base.Shotgun",
|
||||||
|
"Base.Shotgun_Alt",
|
||||||
|
"Base.ShotgunSawnoff",
|
||||||
|
"Base.ShotgunSawnoff_Alt",
|
||||||
|
"Base.SKS",
|
||||||
|
"Base.SOAuto5",
|
||||||
|
"Base.SOAuto5_Alt",
|
||||||
|
"Base.SOBeretta682",
|
||||||
|
"Base.SOBeretta682_Alt",
|
||||||
|
"Base.SOBrownCitori",
|
||||||
|
"Base.SOBrownCitori_Alt",
|
||||||
|
"Base.SOIthaca37",
|
||||||
|
"Base.SOIthaca37_Alt",
|
||||||
|
"Base.SOL395K",
|
||||||
|
"Base.SOL395K_Alt",
|
||||||
|
"Base.SOM24",
|
||||||
|
"Base.SOMoss500",
|
||||||
|
"Base.SOMoss500_Alt",
|
||||||
|
"Base.SOR223Rem700",
|
||||||
|
"Base.SOR223Rem788",
|
||||||
|
"Base.SOR3006Rem700",
|
||||||
|
"Base.SOR3030Rem788",
|
||||||
|
"Base.SOR308Rem700",
|
||||||
|
"Base.SOR308Rem788",
|
||||||
|
"Base.SOR44Rem788",
|
||||||
|
"Base.SORem1100",
|
||||||
|
"Base.SORem1100_Alt",
|
||||||
|
"Base.SOSavageFox",
|
||||||
|
"Base.SOSavageFox_Alt",
|
||||||
|
"Base.SOW223WinM70",
|
||||||
|
"Base.SOW3006WinM70",
|
||||||
|
"Base.SOW308WinM70",
|
||||||
|
"Base.SOWin1200",
|
||||||
|
"Base.SOWin1200_Alt",
|
||||||
|
"Base.SOWin1400",
|
||||||
|
"Base.SOWin1400_Alt",
|
||||||
|
"Base.SOWin1912",
|
||||||
|
"Base.SOWin1912_Alt",
|
||||||
|
"Base.SOWin37",
|
||||||
|
"Base.SOWin37_Alt",
|
||||||
|
"Base.SPAS12",
|
||||||
|
"Base.SPAS12_Alt",
|
||||||
|
"Base.SW10",
|
||||||
|
"Base.SW17",
|
||||||
|
"Base.SW4006",
|
||||||
|
"Base.SW4506",
|
||||||
|
"Base.SW586",
|
||||||
|
"Base.SW586_Alt",
|
||||||
|
"Base.SW5906",
|
||||||
|
"Base.SW65",
|
||||||
|
"Base.SW65_Alt",
|
||||||
|
"Base.T56",
|
||||||
|
"Base.TEC9",
|
||||||
|
"Base.USP40",
|
||||||
|
"Base.USP9",
|
||||||
|
"Base.Uzi",
|
||||||
|
"Base.Valmet",
|
||||||
|
"Base.VarmintRifle",
|
||||||
|
"Base.W223WinM70",
|
||||||
|
"Base.W3006WinM70",
|
||||||
|
"Base.W308WinM70",
|
||||||
|
"Base.Win1200",
|
||||||
|
"Base.Win1200_Alt",
|
||||||
|
"Base.Win1200Def",
|
||||||
|
"Base.Win1200Def_Alt",
|
||||||
|
"Base.Win1400",
|
||||||
|
"Base.Win1400_Alt",
|
||||||
|
"Base.Win1912",
|
||||||
|
"Base.Win1912_Alt",
|
||||||
|
"Base.Win30067400",
|
||||||
|
"Base.Win3087400",
|
||||||
|
"Base.Win37",
|
||||||
|
"Base.Win37_Alt",
|
||||||
|
"Base.Win61",
|
||||||
|
"Base.Win69",
|
||||||
|
"Base.Win943030",
|
||||||
|
"Base.Win94357",
|
||||||
|
"Base.Win94357_Alt",
|
||||||
|
"Base.Win9445",
|
||||||
|
},
|
||||||
|
|
||||||
|
WepAmmoMag = {
|
||||||
|
"Base.100CalicoMag",
|
||||||
|
"Base.1022Mag",
|
||||||
|
"Base.10M14Mag",
|
||||||
|
"Base.15BHPMag",
|
||||||
|
"Base.17BHPMag",
|
||||||
|
"Base.1908Mag",
|
||||||
|
"Base.1911MagExtend",
|
||||||
|
"Base.1911MagExtendSS",
|
||||||
|
"Base.1911MagSS",
|
||||||
|
"Base.20M16AR180Mag",
|
||||||
|
"Base.20M16Mag",
|
||||||
|
"Base.20M9Mag",
|
||||||
|
"Base.20MP5Mag",
|
||||||
|
"Base.223Rem760Mag",
|
||||||
|
"Base.223RemMag",
|
||||||
|
"Base.25Rug1022Mag",
|
||||||
|
"Base.25UziColtSMGMag",
|
||||||
|
"Base.25UziMag",
|
||||||
|
"Base.3006BARMag",
|
||||||
|
"Base.3006clip",
|
||||||
|
"Base.3006Rem760Mag",
|
||||||
|
"Base.3006WinMag",
|
||||||
|
"Base.3030RemMag",
|
||||||
|
"Base.308BARMag",
|
||||||
|
"Base.308Clip",
|
||||||
|
"Base.308Rem760Mag",
|
||||||
|
"Base.308WinMag",
|
||||||
|
"Base.30M14Mag",
|
||||||
|
"Base.30M16AR180Mag",
|
||||||
|
"Base.30M1CarMag",
|
||||||
|
"Base.30Mini14Mag",
|
||||||
|
"Base.4006Mag",
|
||||||
|
"Base.40AKMag",
|
||||||
|
"Base.40AR180Mag",
|
||||||
|
"Base.40M16AR180Mag",
|
||||||
|
"Base.44Clip",
|
||||||
|
"Base.44RemMag",
|
||||||
|
"Base.4506Mag",
|
||||||
|
"Base.45Clip",
|
||||||
|
"Base.45Moonclip",
|
||||||
|
"Base.556Clip",
|
||||||
|
"Base.5906Mag",
|
||||||
|
"Base.83Mag",
|
||||||
|
"Base.9mmClip",
|
||||||
|
"Base.AK5Mag",
|
||||||
|
"Base.AKBakeMag",
|
||||||
|
"Base.AKDrumMag",
|
||||||
|
"Base.AKMag",
|
||||||
|
"Base.AR180Mag",
|
||||||
|
"Base.BetaCMag",
|
||||||
|
"Base.BHPMag",
|
||||||
|
"Base.BHPMagSS",
|
||||||
|
"Base.BobcatMag",
|
||||||
|
"Base.BuckMag",
|
||||||
|
"Base.CalicoMag",
|
||||||
|
"Base.ColtSMGMag",
|
||||||
|
"Base.CZ75BMag",
|
||||||
|
"Base.DE357Mag",
|
||||||
|
"Base.DeltaEliteMag",
|
||||||
|
"Base.DeltaEliteMagBlue",
|
||||||
|
"Base.DeltaEliteMagExtend",
|
||||||
|
"Base.DeltaEliteMagExtendBlue",
|
||||||
|
"Base.EblocClip",
|
||||||
|
"Base.FALMag",
|
||||||
|
"Base.G17ExtMag",
|
||||||
|
"Base.G17Mag",
|
||||||
|
"Base.G17StickMag",
|
||||||
|
"Base.G19ExtMag",
|
||||||
|
"Base.G19Mag",
|
||||||
|
"Base.G20Mag",
|
||||||
|
"Base.G21Mag",
|
||||||
|
"Base.G22Mag",
|
||||||
|
"Base.G23Mag",
|
||||||
|
"Base.HK91Mag",
|
||||||
|
"Base.HKS10A357",
|
||||||
|
"Base.HKS10A38",
|
||||||
|
"Base.HKS22K",
|
||||||
|
"Base.HKS29M44",
|
||||||
|
"Base.HKS36A357",
|
||||||
|
"Base.HKS36A38",
|
||||||
|
"Base.HKS586A357",
|
||||||
|
"Base.HKS586A38",
|
||||||
|
"Base.HKSDSA38",
|
||||||
|
"Base.HKSMK3A357",
|
||||||
|
"Base.HKSMK3A38",
|
||||||
|
"Base.HKSPYA357",
|
||||||
|
"Base.HKSPYA38",
|
||||||
|
"Base.J22Mag",
|
||||||
|
"Base.L395KMag",
|
||||||
|
"Base.L395KMagSlugs",
|
||||||
|
"Base.M14Clip",
|
||||||
|
"Base.M1CarMag",
|
||||||
|
"Base.M249Box",
|
||||||
|
"Base.M3GreaseMag",
|
||||||
|
"Base.M60Box",
|
||||||
|
"Base.MAC10Mag",
|
||||||
|
"Base.MACGreaseMag",
|
||||||
|
"Base.MarkMag",
|
||||||
|
"Base.Mauserclip",
|
||||||
|
"Base.Mini14Mag",
|
||||||
|
"Base.Mini30Mag",
|
||||||
|
"Base.MP5Mag",
|
||||||
|
"Base.OfficerMag",
|
||||||
|
"Base.P226Mag",
|
||||||
|
"Base.P228Mag",
|
||||||
|
"Base.P380Mag",
|
||||||
|
"Base.P7M13Mag",
|
||||||
|
"Base.P7M8Mag",
|
||||||
|
"Base.P89Mag",
|
||||||
|
"Base.P90Mag",
|
||||||
|
"Base.PPKMag",
|
||||||
|
"Base.R25Mag",
|
||||||
|
"Base.Size3CompII357",
|
||||||
|
"Base.Size3CompII38",
|
||||||
|
"Base.Size3CompIII357",
|
||||||
|
"Base.Size3CompIII38",
|
||||||
|
"Base.Size4CompII357",
|
||||||
|
"Base.Size4CompII38",
|
||||||
|
"Base.Size4CompIII357",
|
||||||
|
"Base.Size4CompIII38",
|
||||||
|
"Base.Size5CompII357",
|
||||||
|
"Base.Size5CompII38",
|
||||||
|
"Base.Size5CompIII357",
|
||||||
|
"Base.Size5CompIII38",
|
||||||
|
"Base.Size6CompII357",
|
||||||
|
"Base.Size6CompII38",
|
||||||
|
"Base.Size6CompIII357",
|
||||||
|
"Base.Size6CompIII38",
|
||||||
|
"Base.SKSclip",
|
||||||
|
"Base.SpeedStrip357",
|
||||||
|
"Base.SpeedStrip38",
|
||||||
|
"Base.SpeedStrip44",
|
||||||
|
"Base.TEC9Mag",
|
||||||
|
"Base.TherARMag",
|
||||||
|
"Base.TherMini14Mag",
|
||||||
|
"Base.USP40Mag",
|
||||||
|
"Base.USP9Mag",
|
||||||
|
"Base.UziColtSMGMag",
|
||||||
|
"Base.UziMag",
|
||||||
|
"Base.ValmetMag",
|
||||||
|
"Base.Win69AMag",
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
local OFBCGuns93ModPackInjector = {
|
||||||
|
installed = false,
|
||||||
|
tickHookAdded = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local APPLY_MODS_MODULE = "BetterContainers/Categorize/Categories/_applyMods"
|
||||||
|
local GUNS93_PACK_PATH = "mods/guns93_items"
|
||||||
|
|
||||||
|
local BC_MOD_IDS = {
|
||||||
|
"EURY_CONTAINERS",
|
||||||
|
"REORDER_CONTAINERS",
|
||||||
|
"\\EURY_CONTAINERS",
|
||||||
|
"\\REORDER_CONTAINERS",
|
||||||
|
}
|
||||||
|
|
||||||
|
local GUNS93_MOD_IDS = {
|
||||||
|
"guns93",
|
||||||
|
"\\guns93",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isSortingToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.AddSorting ~= nil then
|
||||||
|
return vars.AddSorting == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getActivatedModsList()
|
||||||
|
if type(getActivatedMods) ~= "function" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local mods = getActivatedMods()
|
||||||
|
if not mods or type(mods.contains) ~= "function" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return mods
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isBetterContainersActive()
|
||||||
|
local mods = getActivatedModsList()
|
||||||
|
if not mods then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #BC_MOD_IDS do
|
||||||
|
if mods:contains(BC_MOD_IDS[i]) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isGuns93Active()
|
||||||
|
local mods = getActivatedModsList()
|
||||||
|
if not mods then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #GUNS93_MOD_IDS do
|
||||||
|
if mods:contains(GUNS93_MOD_IDS[i]) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function installApplyModsWrapper()
|
||||||
|
if OFBCGuns93ModPackInjector.installed then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isSortingToggleEnabled() then
|
||||||
|
OFBCGuns93ModPackInjector.installed = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local bcState = isBetterContainersActive()
|
||||||
|
if bcState == false then
|
||||||
|
OFBCGuns93ModPackInjector.installed = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if bcState == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if _G.OF_GUNS93_BC_APPLYMODS_WRAPPED then
|
||||||
|
OFBCGuns93ModPackInjector.installed = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, applyMods = pcall(require, APPLY_MODS_MODULE)
|
||||||
|
if not ok or type(applyMods) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(package) ~= "table" or type(package.loaded) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function wrappedApplyMods(loadCategoryFile, dlog)
|
||||||
|
applyMods(loadCategoryFile, dlog)
|
||||||
|
|
||||||
|
if not isGuns93Active() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(loadCategoryFile) == "function" then
|
||||||
|
loadCategoryFile(GUNS93_PACK_PATH)
|
||||||
|
if dlog then
|
||||||
|
dlog("Loaded category pack guns93_items")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.loaded[APPLY_MODS_MODULE] = wrappedApplyMods
|
||||||
|
_G.OF_GUNS93_BC_APPLYMODS_WRAPPED = true
|
||||||
|
OFBCGuns93ModPackInjector.installed = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryInstallOnTick()
|
||||||
|
if installApplyModsWrapper() then
|
||||||
|
OFBCGuns93ModPackInjector.tickHookAdded = false
|
||||||
|
Events.OnTick.Remove(tryInstallOnTick)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureTickHook()
|
||||||
|
if OFBCGuns93ModPackInjector.tickHookAdded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
OFBCGuns93ModPackInjector.tickHookAdded = true
|
||||||
|
Events.OnTick.Add(tryInstallOnTick)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not installApplyModsWrapper() then
|
||||||
|
ensureTickHook()
|
||||||
|
end
|
||||||
|
|
||||||
|
if Events.OnMainMenuEnter and type(Events.OnMainMenuEnter.Add) == "function" then
|
||||||
|
Events.OnMainMenuEnter.Add(installApplyModsWrapper)
|
||||||
|
end
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
local OFGGSCasingDisablePatch = {
|
||||||
|
patched = false,
|
||||||
|
hasDebugStackInfo = type(debug) == "table" and type(debug.getinfo) == "function",
|
||||||
|
}
|
||||||
|
|
||||||
|
local BLOCKED_GGS_CASING_TYPES = {
|
||||||
|
["Base.pistol_casing"] = true,
|
||||||
|
["Base.revolver_casing"] = true,
|
||||||
|
["Base.rifle_casing"] = true,
|
||||||
|
["Base.shells_casing"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isGgsShellEmitterCall()
|
||||||
|
if not OFGGSCasingDisablePatch.hasDebugStackInfo then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for stackLevel = 3, 10 do
|
||||||
|
local info = debug.getinfo(stackLevel, "S")
|
||||||
|
if not info then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
local source = info.source
|
||||||
|
if type(source) == "string" and string.find(source, "GGS_ShellCasingEmitter.lua", 1, true) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isPatchToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.HandleHotBrassCasingSpawnUseAmmoMaker ~= nil then
|
||||||
|
return vars.HandleHotBrassCasingSpawnUseAmmoMaker == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Backward compatibility for existing worlds.
|
||||||
|
if vars and vars.HBVCEFAmmoMakerPatch ~= nil then
|
||||||
|
return vars.HBVCEFAmmoMakerPatch == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyPatch()
|
||||||
|
if OFGGSCasingDisablePatch.patched then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isPatchToggleEnabled() then
|
||||||
|
OFGGSCasingDisablePatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(__classmetatables) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if not zombie or not zombie.iso or not zombie.iso.IsoGridSquare or not zombie.iso.IsoGridSquare.class then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local squareMetatable = __classmetatables[zombie.iso.IsoGridSquare.class]
|
||||||
|
if not squareMetatable or type(squareMetatable.__index) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local originalAddWorldInventoryItem = squareMetatable.__index.AddWorldInventoryItem
|
||||||
|
if type(originalAddWorldInventoryItem) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
squareMetatable.__index.AddWorldInventoryItem = function(square, itemType, ...)
|
||||||
|
if BLOCKED_GGS_CASING_TYPES[itemType] and
|
||||||
|
(not OFGGSCasingDisablePatch.hasDebugStackInfo or isGgsShellEmitterCall())
|
||||||
|
then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return originalAddWorldInventoryItem(square, itemType, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
OFGGSCasingDisablePatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryPatchOnTick()
|
||||||
|
if applyPatch() then
|
||||||
|
Events.OnTick.Remove(tryPatchOnTick)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not applyPatch() then
|
||||||
|
Events.OnTick.Add(tryPatchOnTick)
|
||||||
|
end
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
local OFHotBrassPatch = {
|
||||||
|
patched = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local AMMO_TYPE_ALIAS_BY_ITEM = {
|
||||||
|
["Base.9x39Bullets"] = "9x39",
|
||||||
|
["Base.Bullets22LR"] = "22LR",
|
||||||
|
["Base.Bullets50Magnum"] = "50AE",
|
||||||
|
["Base.762x54rBullets"] = "762x54R",
|
||||||
|
["Base.792x57Bullets"] = "792x57Maus",
|
||||||
|
-- Common spelling variants seen in third-party weapon scripts.
|
||||||
|
["Base.308Bulets"] = "308Win",
|
||||||
|
["Base.762x54rBulets"] = "762x54R",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isPatchToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.HandleHotBrassCasingSpawnUseAmmoMaker ~= nil then
|
||||||
|
return vars.HandleHotBrassCasingSpawnUseAmmoMaker == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Backward compatibility for existing worlds.
|
||||||
|
if vars and vars.HBVCEFAmmoMakerPatch ~= nil then
|
||||||
|
return vars.HBVCEFAmmoMakerPatch == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isSessionEligible()
|
||||||
|
return isPatchToggleEnabled()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAmmoMakerFiredFromAmmoDataKey(ammoDataKey)
|
||||||
|
if type(ammoDataKey) ~= "string" or ammoDataKey == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(ammoMakerAmmoData) ~= "table" or type(ammoMakerAmmoParts) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local ammoData = ammoMakerAmmoData[ammoDataKey]
|
||||||
|
if type(ammoData) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local casingType = ammoData.casingType
|
||||||
|
if type(casingType) ~= "string" or casingType == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local partData = ammoMakerAmmoParts[casingType]
|
||||||
|
if type(partData) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local firedType = partData.partFired or partData.partOld
|
||||||
|
if type(firedType) ~= "string" or firedType == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return firedType
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAmmoMakerFiredFromItemKey(ammoType)
|
||||||
|
if type(ammoType) ~= "string" or ammoType == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(ammoMakerAmmoTypes) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local typeData = ammoMakerAmmoTypes[ammoType]
|
||||||
|
if type(typeData) ~= "table" or type(typeData.ammoTypes) ~= "table" or #typeData.ammoTypes == 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local activeIndex = 1
|
||||||
|
if type(typeData.modIds) == "table" and type(ammoMakerCompatibleMods) == "table" then
|
||||||
|
for i = 1, #typeData.modIds do
|
||||||
|
local modId = typeData.modIds[i]
|
||||||
|
if ammoMakerCompatibleMods[modId] == true then
|
||||||
|
activeIndex = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if activeIndex < 1 or activeIndex > #typeData.ammoTypes then
|
||||||
|
activeIndex = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local activeAmmoKey = typeData.ammoTypes[activeIndex]
|
||||||
|
local activeFiredType = getAmmoMakerFiredFromAmmoDataKey(activeAmmoKey)
|
||||||
|
if activeFiredType then
|
||||||
|
return activeFiredType
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, #typeData.ammoTypes do
|
||||||
|
local fallbackFiredType = getAmmoMakerFiredFromAmmoDataKey(typeData.ammoTypes[i])
|
||||||
|
if fallbackFiredType then
|
||||||
|
return fallbackFiredType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAmmoMakerFiredCasing(ammoType)
|
||||||
|
if type(ammoType) ~= "string" or ammoType == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local directFiredType = getAmmoMakerFiredFromItemKey(ammoType)
|
||||||
|
if directFiredType then
|
||||||
|
return directFiredType
|
||||||
|
end
|
||||||
|
|
||||||
|
local aliasAmmoDataKey = AMMO_TYPE_ALIAS_BY_ITEM[ammoType]
|
||||||
|
if aliasAmmoDataKey then
|
||||||
|
local aliasFiredType = getAmmoMakerFiredFromAmmoDataKey(aliasAmmoDataKey)
|
||||||
|
if aliasFiredType then
|
||||||
|
return aliasFiredType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local lowerAmmoType = string.lower(ammoType)
|
||||||
|
for aliasItemType, ammoDataKey in pairs(AMMO_TYPE_ALIAS_BY_ITEM) do
|
||||||
|
if string.lower(aliasItemType) == lowerAmmoType then
|
||||||
|
local aliasFiredType = getAmmoMakerFiredFromAmmoDataKey(ammoDataKey)
|
||||||
|
if aliasFiredType then
|
||||||
|
return aliasFiredType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(ammoMakerAmmoTypes) == "table" then
|
||||||
|
for itemType, typeData in pairs(ammoMakerAmmoTypes) do
|
||||||
|
if type(itemType) == "string" and string.lower(itemType) == lowerAmmoType then
|
||||||
|
if type(typeData) == "table" and type(typeData.ammoTypes) == "table" then
|
||||||
|
for i = 1, #typeData.ammoTypes do
|
||||||
|
local fallbackFiredType = getAmmoMakerFiredFromAmmoDataKey(typeData.ammoTypes[i])
|
||||||
|
if fallbackFiredType then
|
||||||
|
return fallbackFiredType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyPatch()
|
||||||
|
if OFHotBrassPatch.patched then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isSessionEligible() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(SpentCasingPhysics) ~= "table" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(SpentCasingPhysics.getItemToEject) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(SpentCasingPhysics.doSpawnCasing) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local originalGetItemToEject = SpentCasingPhysics.getItemToEject
|
||||||
|
local originalDoSpawnCasing = SpentCasingPhysics.doSpawnCasing
|
||||||
|
|
||||||
|
SpentCasingPhysics.getItemToEject = function(ammoType)
|
||||||
|
local mappedType = getAmmoMakerFiredCasing(ammoType)
|
||||||
|
if mappedType then
|
||||||
|
return mappedType
|
||||||
|
end
|
||||||
|
|
||||||
|
return originalGetItemToEject(ammoType)
|
||||||
|
end
|
||||||
|
|
||||||
|
SpentCasingPhysics.doSpawnCasing = function(player, weapon, params, racking, optionalItem)
|
||||||
|
if not optionalItem and weapon and weapon.getAmmoType then
|
||||||
|
local ammoTypeObj = weapon:getAmmoType()
|
||||||
|
local ammoType = ammoTypeObj and ammoTypeObj.getItemKey and ammoTypeObj:getItemKey() or ammoTypeObj
|
||||||
|
if ammoType then
|
||||||
|
local mappedType = getAmmoMakerFiredCasing(tostring(ammoType))
|
||||||
|
if mappedType then
|
||||||
|
optionalItem = mappedType
|
||||||
|
if racking then
|
||||||
|
-- Force rack ejects to stay as empties for this patch.
|
||||||
|
racking = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return originalDoSpawnCasing(player, weapon, params, racking, optionalItem)
|
||||||
|
end
|
||||||
|
|
||||||
|
OFHotBrassPatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryPatchOnTick()
|
||||||
|
if OFHotBrassPatch.patched then
|
||||||
|
Events.OnTick.Remove(tryPatchOnTick)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
applyPatch()
|
||||||
|
end
|
||||||
|
|
||||||
|
Events.OnTick.Add(tryPatchOnTick)
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
local function safeRequire(moduleName)
|
|
||||||
local ok, result = pcall(require, moduleName)
|
|
||||||
if ok and type(result) == "table" then
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function appendArray(target, source)
|
|
||||||
if type(source) ~= "table" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
for _, value in ipairs(source) do
|
|
||||||
target[#target + 1] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mergeSpec(baseSpec, extraSpec)
|
|
||||||
local merged = {
|
|
||||||
items = {},
|
|
||||||
prefixes = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if type(baseSpec) == "table" then
|
|
||||||
appendArray(merged.items, baseSpec.items)
|
|
||||||
appendArray(merged.prefixes, baseSpec.prefixes)
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(extraSpec) == "table" then
|
|
||||||
appendArray(merged.items, extraSpec.items)
|
|
||||||
appendArray(merged.prefixes, extraSpec.prefixes)
|
|
||||||
end
|
|
||||||
|
|
||||||
return merged
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mergeAliasTables(baseAliases, extraAliases)
|
|
||||||
local merged = {}
|
|
||||||
if type(baseAliases) == "table" then
|
|
||||||
for key, list in pairs(baseAliases) do
|
|
||||||
merged[key] = merged[key] or {}
|
|
||||||
appendArray(merged[key], list)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if type(extraAliases) == "table" then
|
|
||||||
for key, list in pairs(extraAliases) do
|
|
||||||
merged[key] = merged[key] or {}
|
|
||||||
appendArray(merged[key], list)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return merged
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mergeRuleTables(baseRules, extraRules)
|
|
||||||
local merged = {}
|
|
||||||
appendArray(merged, baseRules)
|
|
||||||
appendArray(merged, extraRules)
|
|
||||||
return merged
|
|
||||||
end
|
|
||||||
|
|
||||||
local function mergeByList(baseByList, extraByList)
|
|
||||||
local merged = {}
|
|
||||||
|
|
||||||
if type(baseByList) == "table" then
|
|
||||||
for listName, spec in pairs(baseByList) do
|
|
||||||
merged[listName] = mergeSpec(nil, spec)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(extraByList) == "table" then
|
|
||||||
for listName, spec in pairs(extraByList) do
|
|
||||||
merged[listName] = mergeSpec(merged[listName], spec)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return merged
|
|
||||||
end
|
|
||||||
|
|
||||||
local defaults = safeRequire("OFBlockRules_Default")
|
|
||||||
local user = safeRequire("OFBlockRules_User")
|
|
||||||
local sourceCatalog = safeRequire("OFSourceCatalog")
|
|
||||||
|
|
||||||
local aliasCatalog = {
|
|
||||||
firearms = sourceCatalog.firearms or {},
|
|
||||||
attachments = sourceCatalog.attachments or {},
|
|
||||||
ggs_all = sourceCatalog.ggs_all or {},
|
|
||||||
}
|
|
||||||
|
|
||||||
local merged = {
|
|
||||||
global = mergeSpec(defaults.global, user.global),
|
|
||||||
byList = mergeByList(defaults.byList, user.byList),
|
|
||||||
rules = mergeRuleTables(defaults.rules, user.rules),
|
|
||||||
aliases = mergeAliasTables(defaults.aliases, user.aliases),
|
|
||||||
}
|
|
||||||
|
|
||||||
merged.aliases = mergeAliasTables(aliasCatalog, merged.aliases)
|
|
||||||
|
|
||||||
return merged
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- Default rule set for Opinionated Firearms distribution blocking.
|
|
||||||
-- Keep this empty for a clean-slate baseline.
|
|
||||||
return {
|
|
||||||
global = {
|
|
||||||
items = {},
|
|
||||||
prefixes = {},
|
|
||||||
},
|
|
||||||
byList = {
|
|
||||||
-- Example:
|
|
||||||
-- GunStorePistols = { items = { "Base.AA12" } },
|
|
||||||
},
|
|
||||||
rules = {
|
|
||||||
-- Example rule:
|
|
||||||
-- {
|
|
||||||
-- id = "weekend-attachment-ban",
|
|
||||||
-- enabled = false,
|
|
||||||
-- where = { lists = { "GunStorePistols", "PoliceStorageGuns" } },
|
|
||||||
-- when = { startsAt = 1762473600, endsAt = 1762732800 }, -- unix epoch UTC
|
|
||||||
-- block = {
|
|
||||||
-- items = { "@attachments" },
|
|
||||||
-- prefixes = { "Base.Clip_" },
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
},
|
|
||||||
aliases = {
|
|
||||||
-- Additional custom aliases can be added here.
|
|
||||||
-- Example:
|
|
||||||
-- police_vote_list = { "Base.AA12", "Base.AK47" },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
-- User-editable rules.
|
|
||||||
-- The web app can overwrite this file with vote results later.
|
|
||||||
return {
|
|
||||||
global = {
|
|
||||||
items = {
|
|
||||||
-- "Base.AA12",
|
|
||||||
-- "@attachments",
|
|
||||||
},
|
|
||||||
prefixes = {
|
|
||||||
-- "Base.Clip_",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
byList = {
|
|
||||||
-- PoliceStorageGuns = {
|
|
||||||
-- items = { "Base.AK47" },
|
|
||||||
-- },
|
|
||||||
},
|
|
||||||
rules = {
|
|
||||||
-- {
|
|
||||||
-- id = "example-scheduled-block",
|
|
||||||
-- enabled = false,
|
|
||||||
-- where = { lists = { "GunStorePistols", "ArmyStorageGuns" } },
|
|
||||||
-- when = { startsAt = 1762473600, endsAt = 1762732800 }, -- unix epoch UTC
|
|
||||||
-- block = {
|
|
||||||
-- items = { "Base.AK74u", "@firearms" },
|
|
||||||
-- prefixes = { "Base.Clip_" },
|
|
||||||
-- },
|
|
||||||
-- },
|
|
||||||
},
|
|
||||||
aliases = {
|
|
||||||
-- event_list = { "Base.AK74", "Base.AK47" },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Auto-generated by tools/ggs-dist-cli.js apply
|
|
||||||
-- Keep this empty template in git; generated content can overwrite it.
|
|
||||||
return {
|
|
||||||
items = {},
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
local BLOCKED_AMMOMAKER_COMMANDS = {
|
||||||
|
returnCasingsOnAttack = true,
|
||||||
|
incrementSpendtRoundCount = true,
|
||||||
|
returnCasingsOnReload = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isPatchToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.HandleHotBrassCasingSpawnUseAmmoMaker ~= nil then
|
||||||
|
return vars.HandleHotBrassCasingSpawnUseAmmoMaker == true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Backward compatibility for existing worlds.
|
||||||
|
if vars and vars.HBVCEFAmmoMakerPatch ~= nil then
|
||||||
|
return vars.HBVCEFAmmoMakerPatch == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shouldBlockAmmoMakerCasingCommands()
|
||||||
|
return isPatchToggleEnabled()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getModuleAndCommand(...)
|
||||||
|
local argc = select("#", ...)
|
||||||
|
if argc < 2 then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local firstArg = select(1, ...)
|
||||||
|
if type(firstArg) == "string" then
|
||||||
|
return firstArg, select(2, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
if argc >= 3 then
|
||||||
|
return select(2, ...), select(3, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function installCommandGate()
|
||||||
|
if _G.OpinionatedFirearms_AmmoMakerCommandGateInstalled then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(sendClientCommand) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
_G.OpinionatedFirearms_AmmoMakerCommandGateInstalled = true
|
||||||
|
_G.OpinionatedFirearms_OriginalSendClientCommand = sendClientCommand
|
||||||
|
|
||||||
|
sendClientCommand = function(...)
|
||||||
|
local module, command = getModuleAndCommand(...)
|
||||||
|
if module == "ammomaker" and BLOCKED_AMMOMAKER_COMMANDS[command] and shouldBlockAmmoMakerCasingCommands() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return _G.OpinionatedFirearms_OriginalSendClientCommand(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureGateOnTick()
|
||||||
|
if installCommandGate() then
|
||||||
|
Events.OnTick.Remove(ensureGateOnTick)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not installCommandGate() then
|
||||||
|
Events.OnTick.Add(ensureGateOnTick)
|
||||||
|
end
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
local OFAmmoMakerDisplayCategoryPatch = {
|
||||||
|
patched = false,
|
||||||
|
tickHookAdded = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local SPENT_CASINGS_CATEGORY = "OFSpentCasings"
|
||||||
|
local AMMO_PART_CATEGORY = "OFAmmoPart"
|
||||||
|
|
||||||
|
local function isSortingToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.AddSorting ~= nil then
|
||||||
|
return vars.AddSorting == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function addAmmoMakerType(typeSet, fullType)
|
||||||
|
if type(fullType) ~= "string" or fullType == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not string.find(fullType, "^ammomaker%.") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
typeSet[fullType] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function setDisplayCategory(scriptManager, fullType, category)
|
||||||
|
local scriptItem = scriptManager:FindItem(fullType)
|
||||||
|
if not scriptItem then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(scriptItem.DoParam) == "function" then
|
||||||
|
scriptItem:DoParam("DisplayCategory = " .. category)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(scriptItem.setDisplayCategory) == "function" then
|
||||||
|
scriptItem:setDisplayCategory(category)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collectAmmoMakerTypes()
|
||||||
|
if type(ammoMakerAmmoParts) ~= "table" then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local spentCasingTypes = {}
|
||||||
|
local ammoPartTypes = {}
|
||||||
|
|
||||||
|
for _, partData in pairs(ammoMakerAmmoParts) do
|
||||||
|
if type(partData) == "table" then
|
||||||
|
local partClass = partData.partClass
|
||||||
|
if partClass == "Casing" or partClass == "Hull" then
|
||||||
|
addAmmoMakerType(spentCasingTypes, partData.partOld)
|
||||||
|
addAmmoMakerType(spentCasingTypes, partData.partFired)
|
||||||
|
addAmmoMakerType(ammoPartTypes, partData.boxType)
|
||||||
|
addAmmoMakerType(ammoPartTypes, partData.bagType)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return spentCasingTypes, ammoPartTypes
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyCategory(scriptManager, itemTypes, category)
|
||||||
|
local patchedAny = false
|
||||||
|
|
||||||
|
for fullType in pairs(itemTypes) do
|
||||||
|
if setDisplayCategory(scriptManager, fullType, category) then
|
||||||
|
patchedAny = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return patchedAny
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyPatch()
|
||||||
|
if OFAmmoMakerDisplayCategoryPatch.patched then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isSortingToggleEnabled() then
|
||||||
|
OFAmmoMakerDisplayCategoryPatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local scriptManager = ScriptManager and ScriptManager.instance
|
||||||
|
if not scriptManager or type(scriptManager.FindItem) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local spentCasingTypes, ammoPartTypes = collectAmmoMakerTypes()
|
||||||
|
if not spentCasingTypes or not ammoPartTypes then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local patchedSpentCasings = applyCategory(scriptManager, spentCasingTypes, SPENT_CASINGS_CATEGORY)
|
||||||
|
local patchedAmmoParts = applyCategory(scriptManager, ammoPartTypes, AMMO_PART_CATEGORY)
|
||||||
|
if not patchedSpentCasings and not patchedAmmoParts then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
OFAmmoMakerDisplayCategoryPatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryPatchOnTick()
|
||||||
|
if applyPatch() then
|
||||||
|
OFAmmoMakerDisplayCategoryPatch.tickHookAdded = false
|
||||||
|
Events.OnTick.Remove(tryPatchOnTick)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureTickHook()
|
||||||
|
if OFAmmoMakerDisplayCategoryPatch.tickHookAdded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
OFAmmoMakerDisplayCategoryPatch.tickHookAdded = true
|
||||||
|
Events.OnTick.Add(tryPatchOnTick)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not applyPatch() then
|
||||||
|
ensureTickHook()
|
||||||
|
end
|
||||||
|
|
||||||
|
if Events.OnMainMenuEnter and type(Events.OnMainMenuEnter.Add) == "function" then
|
||||||
|
Events.OnMainMenuEnter.Add(function()
|
||||||
|
OFAmmoMakerDisplayCategoryPatch.patched = false
|
||||||
|
if not applyPatch() then
|
||||||
|
ensureTickHook()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
@@ -0,0 +1,920 @@
|
|||||||
|
local OFGuns93DisplayCategoryPatch = {
|
||||||
|
patched = false,
|
||||||
|
tickHookAdded = false,
|
||||||
|
repatchQueued = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local BC_FIREARM_CATEGORY = "WepFire"
|
||||||
|
local BC_MAGAZINE_CATEGORY = "WepAmmoMag"
|
||||||
|
|
||||||
|
local GUNS93_SENTINEL_FIREARM = "Base.83Cheetah"
|
||||||
|
local GUNS93_SENTINEL_MAGAZINE = "Base.83Mag"
|
||||||
|
local GAEL_GUNSTORE_SENTINEL_FIREARM = "Base.A91"
|
||||||
|
local GAEL_GUNSTORE_SENTINEL_MAGAZINE = "Base.545x39Clip30"
|
||||||
|
|
||||||
|
local function resolveWeaponCategories()
|
||||||
|
return BC_FIREARM_CATEGORY, BC_MAGAZINE_CATEGORY
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isSortingToggleEnabled()
|
||||||
|
local vars = SandboxVars and SandboxVars.OpinionatedFirearms
|
||||||
|
if vars and vars.AddSorting ~= nil then
|
||||||
|
return vars.AddSorting == true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local GUNS93_FIREARMS = {
|
||||||
|
["Base.83Cheetah"] = true,
|
||||||
|
["Base.AKM"] = true,
|
||||||
|
["Base.AKSport"] = true,
|
||||||
|
["Base.Anaconda"] = true,
|
||||||
|
["Base.AR15"] = true,
|
||||||
|
["Base.AR180"] = true,
|
||||||
|
["Base.AssaultRifle"] = true,
|
||||||
|
["Base.AssaultRifle2"] = true,
|
||||||
|
["Base.Auto5"] = true,
|
||||||
|
["Base.Auto5_Alt"] = true,
|
||||||
|
["Base.BenelliM3"] = true,
|
||||||
|
["Base.BenelliM3_Alt"] = true,
|
||||||
|
["Base.Beretta682"] = true,
|
||||||
|
["Base.Beretta682_Alt"] = true,
|
||||||
|
["Base.Beretta92FS"] = true,
|
||||||
|
["Base.BHP"] = true,
|
||||||
|
["Base.Bobcat"] = true,
|
||||||
|
["Base.Brown3006BAR"] = true,
|
||||||
|
["Base.Brown308BAR"] = true,
|
||||||
|
["Base.BrownCitori"] = true,
|
||||||
|
["Base.BrownCitori_Alt"] = true,
|
||||||
|
["Base.Buckmark"] = true,
|
||||||
|
["Base.CalicoPistol"] = true,
|
||||||
|
["Base.CalicoRifle"] = true,
|
||||||
|
["Base.CAR15"] = true,
|
||||||
|
["Base.Colt9mm"] = true,
|
||||||
|
["Base.ColtArmy"] = true,
|
||||||
|
["Base.ColtDet"] = true,
|
||||||
|
["Base.Command1911"] = true,
|
||||||
|
["Base.CZ75B"] = true,
|
||||||
|
["Base.DE357"] = true,
|
||||||
|
["Base.DeltaElite"] = true,
|
||||||
|
["Base.Dissy"] = true,
|
||||||
|
["Base.DoubleBarrelShotgun"] = true,
|
||||||
|
["Base.DoubleBarrelShotgun_Alt"] = true,
|
||||||
|
["Base.DoubleBarrelShotgunSawnoff"] = true,
|
||||||
|
["Base.DoubleBarrelShotgunSawnoff_Alt"] = true,
|
||||||
|
["Base.FAL"] = true,
|
||||||
|
["Base.Glock17"] = true,
|
||||||
|
["Base.Glock17L"] = true,
|
||||||
|
["Base.Glock19"] = true,
|
||||||
|
["Base.Glock20"] = true,
|
||||||
|
["Base.Glock21"] = true,
|
||||||
|
["Base.Glock22"] = true,
|
||||||
|
["Base.Glock23"] = true,
|
||||||
|
["Base.Glock24"] = true,
|
||||||
|
["Base.Gov1911"] = true,
|
||||||
|
["Base.GP100"] = true,
|
||||||
|
["Base.GP100_Alt"] = true,
|
||||||
|
["Base.GP101"] = true,
|
||||||
|
["Base.GP101_Alt"] = true,
|
||||||
|
["Base.Hammerless"] = true,
|
||||||
|
["Base.HBAR"] = true,
|
||||||
|
["Base.HK91"] = true,
|
||||||
|
["Base.HuntingRifle"] = true,
|
||||||
|
["Base.Ithaca37"] = true,
|
||||||
|
["Base.Ithaca37_Alt"] = true,
|
||||||
|
["Base.Ithaca37Riot"] = true,
|
||||||
|
["Base.Ithaca37Riot_Alt"] = true,
|
||||||
|
["Base.J22"] = true,
|
||||||
|
["Base.Javelina"] = true,
|
||||||
|
["Base.L395K"] = true,
|
||||||
|
["Base.L395K_Alt"] = true,
|
||||||
|
["Base.Longslide"] = true,
|
||||||
|
["Base.M1903"] = true,
|
||||||
|
["Base.M1917"] = true,
|
||||||
|
["Base.M1A"] = true,
|
||||||
|
["Base.M1ABush"] = true,
|
||||||
|
["Base.M1Carbine"] = true,
|
||||||
|
["Base.M1Garand"] = true,
|
||||||
|
["Base.M24"] = true,
|
||||||
|
["Base.M249"] = true,
|
||||||
|
["Base.M3GreaseGun"] = true,
|
||||||
|
["Base.M590"] = true,
|
||||||
|
["Base.M590_Alt"] = true,
|
||||||
|
["Base.M60"] = true,
|
||||||
|
["Base.M635"] = true,
|
||||||
|
["Base.M723"] = true,
|
||||||
|
["Base.M727"] = true,
|
||||||
|
["Base.M733"] = true,
|
||||||
|
["Base.M870"] = true,
|
||||||
|
["Base.M870_Alt"] = true,
|
||||||
|
["Base.MAC10"] = true,
|
||||||
|
["Base.Marlin3363030"] = true,
|
||||||
|
["Base.Marlin33644"] = true,
|
||||||
|
["Base.Marlin39A"] = true,
|
||||||
|
["Base.Marlin45"] = true,
|
||||||
|
["Base.Marlin60"] = true,
|
||||||
|
["Base.Marlin9"] = true,
|
||||||
|
["Base.Mauser98K"] = true,
|
||||||
|
["Base.Mini14"] = true,
|
||||||
|
["Base.Mini30"] = true,
|
||||||
|
["Base.Moss500"] = true,
|
||||||
|
["Base.Moss500_Alt"] = true,
|
||||||
|
["Base.Moss590"] = true,
|
||||||
|
["Base.Moss590_Alt"] = true,
|
||||||
|
["Base.MP5"] = true,
|
||||||
|
["Base.Officer1911"] = true,
|
||||||
|
["Base.P226"] = true,
|
||||||
|
["Base.P228"] = true,
|
||||||
|
["Base.P380"] = true,
|
||||||
|
["Base.P7M13"] = true,
|
||||||
|
["Base.P7M8"] = true,
|
||||||
|
["Base.Pistol"] = true,
|
||||||
|
["Base.Pistol2"] = true,
|
||||||
|
["Base.Pistol3"] = true,
|
||||||
|
["Base.PPK"] = true,
|
||||||
|
["Base.Python"] = true,
|
||||||
|
["Base.Python_Alt"] = true,
|
||||||
|
["Base.R223Rem788"] = true,
|
||||||
|
["Base.R223RugM77"] = true,
|
||||||
|
["Base.R3006Rem700"] = true,
|
||||||
|
["Base.R3006RugM77"] = true,
|
||||||
|
["Base.R3030Rem788"] = true,
|
||||||
|
["Base.R308Rem700"] = true,
|
||||||
|
["Base.R308RugM77"] = true,
|
||||||
|
["Base.R44Rem788"] = true,
|
||||||
|
["Base.Raven25"] = true,
|
||||||
|
["Base.Rem1100"] = true,
|
||||||
|
["Base.Rem1100_Alt"] = true,
|
||||||
|
["Base.Rem1187P"] = true,
|
||||||
|
["Base.Rem1187P_Alt"] = true,
|
||||||
|
["Base.Rem223760"] = true,
|
||||||
|
["Base.Rem3006760"] = true,
|
||||||
|
["Base.Rem308760"] = true,
|
||||||
|
["Base.Rem870P"] = true,
|
||||||
|
["Base.Rem870P_Alt"] = true,
|
||||||
|
["Base.Revolver"] = true,
|
||||||
|
["Base.Revolver_Long"] = true,
|
||||||
|
["Base.Revolver_Short"] = true,
|
||||||
|
["Base.Rug1022"] = true,
|
||||||
|
["Base.Rugmark"] = true,
|
||||||
|
["Base.RugP89"] = true,
|
||||||
|
["Base.RugP90"] = true,
|
||||||
|
["Base.SavageFox"] = true,
|
||||||
|
["Base.SavageFox_Alt"] = true,
|
||||||
|
["Base.SecuritySix"] = true,
|
||||||
|
["Base.SecuritySix_Alt"] = true,
|
||||||
|
["Base.Shotgun"] = true,
|
||||||
|
["Base.Shotgun_Alt"] = true,
|
||||||
|
["Base.ShotgunSawnoff"] = true,
|
||||||
|
["Base.ShotgunSawnoff_Alt"] = true,
|
||||||
|
["Base.SKS"] = true,
|
||||||
|
["Base.SOAuto5"] = true,
|
||||||
|
["Base.SOAuto5_Alt"] = true,
|
||||||
|
["Base.SOBeretta682"] = true,
|
||||||
|
["Base.SOBeretta682_Alt"] = true,
|
||||||
|
["Base.SOBrownCitori"] = true,
|
||||||
|
["Base.SOBrownCitori_Alt"] = true,
|
||||||
|
["Base.SOIthaca37"] = true,
|
||||||
|
["Base.SOIthaca37_Alt"] = true,
|
||||||
|
["Base.SOL395K"] = true,
|
||||||
|
["Base.SOL395K_Alt"] = true,
|
||||||
|
["Base.SOM24"] = true,
|
||||||
|
["Base.SOMoss500"] = true,
|
||||||
|
["Base.SOMoss500_Alt"] = true,
|
||||||
|
["Base.SOR223Rem700"] = true,
|
||||||
|
["Base.SOR223Rem788"] = true,
|
||||||
|
["Base.SOR3006Rem700"] = true,
|
||||||
|
["Base.SOR3030Rem788"] = true,
|
||||||
|
["Base.SOR308Rem700"] = true,
|
||||||
|
["Base.SOR308Rem788"] = true,
|
||||||
|
["Base.SOR44Rem788"] = true,
|
||||||
|
["Base.SORem1100"] = true,
|
||||||
|
["Base.SORem1100_Alt"] = true,
|
||||||
|
["Base.SOSavageFox"] = true,
|
||||||
|
["Base.SOSavageFox_Alt"] = true,
|
||||||
|
["Base.SOW223WinM70"] = true,
|
||||||
|
["Base.SOW3006WinM70"] = true,
|
||||||
|
["Base.SOW308WinM70"] = true,
|
||||||
|
["Base.SOWin1200"] = true,
|
||||||
|
["Base.SOWin1200_Alt"] = true,
|
||||||
|
["Base.SOWin1400"] = true,
|
||||||
|
["Base.SOWin1400_Alt"] = true,
|
||||||
|
["Base.SOWin1912"] = true,
|
||||||
|
["Base.SOWin1912_Alt"] = true,
|
||||||
|
["Base.SOWin37"] = true,
|
||||||
|
["Base.SOWin37_Alt"] = true,
|
||||||
|
["Base.SPAS12"] = true,
|
||||||
|
["Base.SPAS12_Alt"] = true,
|
||||||
|
["Base.SW10"] = true,
|
||||||
|
["Base.SW17"] = true,
|
||||||
|
["Base.SW4006"] = true,
|
||||||
|
["Base.SW4506"] = true,
|
||||||
|
["Base.SW586"] = true,
|
||||||
|
["Base.SW586_Alt"] = true,
|
||||||
|
["Base.SW5906"] = true,
|
||||||
|
["Base.SW65"] = true,
|
||||||
|
["Base.SW65_Alt"] = true,
|
||||||
|
["Base.T56"] = true,
|
||||||
|
["Base.TEC9"] = true,
|
||||||
|
["Base.USP40"] = true,
|
||||||
|
["Base.USP9"] = true,
|
||||||
|
["Base.Uzi"] = true,
|
||||||
|
["Base.Valmet"] = true,
|
||||||
|
["Base.VarmintRifle"] = true,
|
||||||
|
["Base.W223WinM70"] = true,
|
||||||
|
["Base.W3006WinM70"] = true,
|
||||||
|
["Base.W308WinM70"] = true,
|
||||||
|
["Base.Win1200"] = true,
|
||||||
|
["Base.Win1200_Alt"] = true,
|
||||||
|
["Base.Win1200Def"] = true,
|
||||||
|
["Base.Win1200Def_Alt"] = true,
|
||||||
|
["Base.Win1400"] = true,
|
||||||
|
["Base.Win1400_Alt"] = true,
|
||||||
|
["Base.Win1912"] = true,
|
||||||
|
["Base.Win1912_Alt"] = true,
|
||||||
|
["Base.Win30067400"] = true,
|
||||||
|
["Base.Win3087400"] = true,
|
||||||
|
["Base.Win37"] = true,
|
||||||
|
["Base.Win37_Alt"] = true,
|
||||||
|
["Base.Win61"] = true,
|
||||||
|
["Base.Win69"] = true,
|
||||||
|
["Base.Win943030"] = true,
|
||||||
|
["Base.Win94357"] = true,
|
||||||
|
["Base.Win94357_Alt"] = true,
|
||||||
|
["Base.Win9445"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local GUNS93_MAGAZINES = {
|
||||||
|
["Base.100CalicoMag"] = true,
|
||||||
|
["Base.1022Mag"] = true,
|
||||||
|
["Base.10M14Mag"] = true,
|
||||||
|
["Base.15BHPMag"] = true,
|
||||||
|
["Base.17BHPMag"] = true,
|
||||||
|
["Base.1908Mag"] = true,
|
||||||
|
["Base.1911MagExtend"] = true,
|
||||||
|
["Base.1911MagExtendSS"] = true,
|
||||||
|
["Base.1911MagSS"] = true,
|
||||||
|
["Base.20M16AR180Mag"] = true,
|
||||||
|
["Base.20M16Mag"] = true,
|
||||||
|
["Base.20M9Mag"] = true,
|
||||||
|
["Base.20MP5Mag"] = true,
|
||||||
|
["Base.223Rem760Mag"] = true,
|
||||||
|
["Base.223RemMag"] = true,
|
||||||
|
["Base.25Rug1022Mag"] = true,
|
||||||
|
["Base.25UziColtSMGMag"] = true,
|
||||||
|
["Base.25UziMag"] = true,
|
||||||
|
["Base.3006BARMag"] = true,
|
||||||
|
["Base.3006clip"] = true,
|
||||||
|
["Base.3006Rem760Mag"] = true,
|
||||||
|
["Base.3006WinMag"] = true,
|
||||||
|
["Base.3030RemMag"] = true,
|
||||||
|
["Base.308BARMag"] = true,
|
||||||
|
["Base.308Clip"] = true,
|
||||||
|
["Base.308Rem760Mag"] = true,
|
||||||
|
["Base.308WinMag"] = true,
|
||||||
|
["Base.30M14Mag"] = true,
|
||||||
|
["Base.30M16AR180Mag"] = true,
|
||||||
|
["Base.30M1CarMag"] = true,
|
||||||
|
["Base.30Mini14Mag"] = true,
|
||||||
|
["Base.4006Mag"] = true,
|
||||||
|
["Base.40AKMag"] = true,
|
||||||
|
["Base.40AR180Mag"] = true,
|
||||||
|
["Base.40M16AR180Mag"] = true,
|
||||||
|
["Base.44Clip"] = true,
|
||||||
|
["Base.44RemMag"] = true,
|
||||||
|
["Base.4506Mag"] = true,
|
||||||
|
["Base.45Clip"] = true,
|
||||||
|
["Base.45Moonclip"] = true,
|
||||||
|
["Base.556Clip"] = true,
|
||||||
|
["Base.5906Mag"] = true,
|
||||||
|
["Base.83Mag"] = true,
|
||||||
|
["Base.9mmClip"] = true,
|
||||||
|
["Base.AK5Mag"] = true,
|
||||||
|
["Base.AKBakeMag"] = true,
|
||||||
|
["Base.AKDrumMag"] = true,
|
||||||
|
["Base.AKMag"] = true,
|
||||||
|
["Base.AR180Mag"] = true,
|
||||||
|
["Base.BetaCMag"] = true,
|
||||||
|
["Base.BHPMag"] = true,
|
||||||
|
["Base.BHPMagSS"] = true,
|
||||||
|
["Base.BobcatMag"] = true,
|
||||||
|
["Base.BuckMag"] = true,
|
||||||
|
["Base.CalicoMag"] = true,
|
||||||
|
["Base.ColtSMGMag"] = true,
|
||||||
|
["Base.CZ75BMag"] = true,
|
||||||
|
["Base.DE357Mag"] = true,
|
||||||
|
["Base.DeltaEliteMag"] = true,
|
||||||
|
["Base.DeltaEliteMagBlue"] = true,
|
||||||
|
["Base.DeltaEliteMagExtend"] = true,
|
||||||
|
["Base.DeltaEliteMagExtendBlue"] = true,
|
||||||
|
["Base.EblocClip"] = true,
|
||||||
|
["Base.FALMag"] = true,
|
||||||
|
["Base.G17ExtMag"] = true,
|
||||||
|
["Base.G17Mag"] = true,
|
||||||
|
["Base.G17StickMag"] = true,
|
||||||
|
["Base.G19ExtMag"] = true,
|
||||||
|
["Base.G19Mag"] = true,
|
||||||
|
["Base.G20Mag"] = true,
|
||||||
|
["Base.G21Mag"] = true,
|
||||||
|
["Base.G22Mag"] = true,
|
||||||
|
["Base.G23Mag"] = true,
|
||||||
|
["Base.HK91Mag"] = true,
|
||||||
|
["Base.HKS10A357"] = true,
|
||||||
|
["Base.HKS10A38"] = true,
|
||||||
|
["Base.HKS22K"] = true,
|
||||||
|
["Base.HKS29M44"] = true,
|
||||||
|
["Base.HKS36A357"] = true,
|
||||||
|
["Base.HKS36A38"] = true,
|
||||||
|
["Base.HKS586A357"] = true,
|
||||||
|
["Base.HKS586A38"] = true,
|
||||||
|
["Base.HKSDSA38"] = true,
|
||||||
|
["Base.HKSMK3A357"] = true,
|
||||||
|
["Base.HKSMK3A38"] = true,
|
||||||
|
["Base.HKSPYA357"] = true,
|
||||||
|
["Base.HKSPYA38"] = true,
|
||||||
|
["Base.J22Mag"] = true,
|
||||||
|
["Base.L395KMag"] = true,
|
||||||
|
["Base.L395KMagSlugs"] = true,
|
||||||
|
["Base.M14Clip"] = true,
|
||||||
|
["Base.M1CarMag"] = true,
|
||||||
|
["Base.M249Box"] = true,
|
||||||
|
["Base.M3GreaseMag"] = true,
|
||||||
|
["Base.M60Box"] = true,
|
||||||
|
["Base.MAC10Mag"] = true,
|
||||||
|
["Base.MACGreaseMag"] = true,
|
||||||
|
["Base.MarkMag"] = true,
|
||||||
|
["Base.Mauserclip"] = true,
|
||||||
|
["Base.Mini14Mag"] = true,
|
||||||
|
["Base.Mini30Mag"] = true,
|
||||||
|
["Base.MP5Mag"] = true,
|
||||||
|
["Base.OfficerMag"] = true,
|
||||||
|
["Base.P226Mag"] = true,
|
||||||
|
["Base.P228Mag"] = true,
|
||||||
|
["Base.P380Mag"] = true,
|
||||||
|
["Base.P7M13Mag"] = true,
|
||||||
|
["Base.P7M8Mag"] = true,
|
||||||
|
["Base.P89Mag"] = true,
|
||||||
|
["Base.P90Mag"] = true,
|
||||||
|
["Base.PPKMag"] = true,
|
||||||
|
["Base.R25Mag"] = true,
|
||||||
|
["Base.Size3CompII357"] = true,
|
||||||
|
["Base.Size3CompII38"] = true,
|
||||||
|
["Base.Size3CompIII357"] = true,
|
||||||
|
["Base.Size3CompIII38"] = true,
|
||||||
|
["Base.Size4CompII357"] = true,
|
||||||
|
["Base.Size4CompII38"] = true,
|
||||||
|
["Base.Size4CompIII357"] = true,
|
||||||
|
["Base.Size4CompIII38"] = true,
|
||||||
|
["Base.Size5CompII357"] = true,
|
||||||
|
["Base.Size5CompII38"] = true,
|
||||||
|
["Base.Size5CompIII357"] = true,
|
||||||
|
["Base.Size5CompIII38"] = true,
|
||||||
|
["Base.Size6CompII357"] = true,
|
||||||
|
["Base.Size6CompII38"] = true,
|
||||||
|
["Base.Size6CompIII357"] = true,
|
||||||
|
["Base.Size6CompIII38"] = true,
|
||||||
|
["Base.SKSclip"] = true,
|
||||||
|
["Base.SpeedStrip357"] = true,
|
||||||
|
["Base.SpeedStrip38"] = true,
|
||||||
|
["Base.SpeedStrip44"] = true,
|
||||||
|
["Base.TEC9Mag"] = true,
|
||||||
|
["Base.TherARMag"] = true,
|
||||||
|
["Base.TherMini14Mag"] = true,
|
||||||
|
["Base.USP40Mag"] = true,
|
||||||
|
["Base.USP9Mag"] = true,
|
||||||
|
["Base.UziColtSMGMag"] = true,
|
||||||
|
["Base.UziMag"] = true,
|
||||||
|
["Base.ValmetMag"] = true,
|
||||||
|
["Base.Win69AMag"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local GAEL_GUNSTORE_FIREARMS = {
|
||||||
|
["Base.A2000"] = true,
|
||||||
|
["Base.A91"] = true,
|
||||||
|
["Base.AA12"] = true,
|
||||||
|
["Base.ACE21"] = true,
|
||||||
|
["Base.ACE23"] = true,
|
||||||
|
["Base.ACE52_CQB"] = true,
|
||||||
|
["Base.ACE53"] = true,
|
||||||
|
["Base.ACR"] = true,
|
||||||
|
["Base.ADS"] = true,
|
||||||
|
["Base.AEK"] = true,
|
||||||
|
["Base.AEK919"] = true,
|
||||||
|
["Base.AK_minidrako"] = true,
|
||||||
|
["Base.AK101"] = true,
|
||||||
|
["Base.AK103"] = true,
|
||||||
|
["Base.AK12"] = true,
|
||||||
|
["Base.AK19"] = true,
|
||||||
|
["Base.AK47"] = true,
|
||||||
|
["Base.AK5C"] = true,
|
||||||
|
["Base.AK74"] = true,
|
||||||
|
["Base.AK74u"] = true,
|
||||||
|
["Base.AK74u_long"] = true,
|
||||||
|
["Base.AK9"] = true,
|
||||||
|
["Base.AKM"] = true,
|
||||||
|
["Base.AKU12"] = true,
|
||||||
|
["Base.AMD65"] = true,
|
||||||
|
["Base.AN94"] = true,
|
||||||
|
["Base.Anaconda"] = true,
|
||||||
|
["Base.APC9K"] = true,
|
||||||
|
["Base.AR10"] = true,
|
||||||
|
["Base.AR15"] = true,
|
||||||
|
["Base.AR160"] = true,
|
||||||
|
["Base.AR6951"] = true,
|
||||||
|
["Base.ASH_12"] = true,
|
||||||
|
["Base.AssaultRifle"] = true,
|
||||||
|
["Base.AssaultRifle2"] = true,
|
||||||
|
["Base.AUG_9mm"] = true,
|
||||||
|
["Base.AUG_A1"] = true,
|
||||||
|
["Base.AUG_A2"] = true,
|
||||||
|
["Base.Automag357"] = true,
|
||||||
|
["Base.Automag44"] = true,
|
||||||
|
["Base.Automag50AE"] = true,
|
||||||
|
["Base.AWS"] = true,
|
||||||
|
["Base.BAR"] = true,
|
||||||
|
["Base.Becker_Shotgun"] = true,
|
||||||
|
["Base.Becker_Shotgun_Short"] = true,
|
||||||
|
["Base.BenelliM4"] = true,
|
||||||
|
["Base.Beretta_A400"] = true,
|
||||||
|
["Base.Beretta_A400_Short"] = true,
|
||||||
|
["Base.Beretta_PX4"] = true,
|
||||||
|
["Base.Browning_Auto"] = true,
|
||||||
|
["Base.Browning_Auto_Short"] = true,
|
||||||
|
["Base.BrowningHP"] = true,
|
||||||
|
["Base.Carcano"] = true,
|
||||||
|
["Base.CBJ"] = true,
|
||||||
|
["Base.CETME"] = true,
|
||||||
|
["Base.CircuitJudgeRifle"] = true,
|
||||||
|
["Base.Colt9mm"] = true,
|
||||||
|
["Base.ColtNavy1851"] = true,
|
||||||
|
["Base.ColtNavyExorcist"] = true,
|
||||||
|
["Base.ColtPeacemaker1873"] = true,
|
||||||
|
["Base.Coonan357"] = true,
|
||||||
|
["Base.CS5"] = true,
|
||||||
|
["Base.CZ75"] = true,
|
||||||
|
["Base.CZ805"] = true,
|
||||||
|
["Base.CZScorpion"] = true,
|
||||||
|
["Base.DB_Condor"] = true,
|
||||||
|
["Base.DB_Condor_sawn"] = true,
|
||||||
|
["Base.DDM4"] = true,
|
||||||
|
["Base.Deagle357_gold"] = true,
|
||||||
|
["Base.Deagle50AE"] = true,
|
||||||
|
["Base.DeagleCar14"] = true,
|
||||||
|
["Base.DeLisle"] = true,
|
||||||
|
["Base.DoubleBarrelShotgun"] = true,
|
||||||
|
["Base.DoubleBarrelShotgunSawnoff"] = true,
|
||||||
|
["Base.DVB15"] = true,
|
||||||
|
["Base.ENARM_Pentagun"] = true,
|
||||||
|
["Base.Enfield"] = true,
|
||||||
|
["Base.FAL"] = true,
|
||||||
|
["Base.FAL_CQB"] = true,
|
||||||
|
["Base.FAMAS"] = true,
|
||||||
|
["Base.FiveSeven"] = true,
|
||||||
|
["Base.FN2000"] = true,
|
||||||
|
["Base.FN502_22LR"] = true,
|
||||||
|
["Base.FNX45"] = true,
|
||||||
|
["Base.G17"] = true,
|
||||||
|
["Base.G18"] = true,
|
||||||
|
["Base.G2"] = true,
|
||||||
|
["Base.G27"] = true,
|
||||||
|
["Base.G36C"] = true,
|
||||||
|
["Base.G3A3"] = true,
|
||||||
|
["Base.G43"] = true,
|
||||||
|
["Base.Galil"] = true,
|
||||||
|
["Base.Glock_tactical"] = true,
|
||||||
|
["Base.Glock23"] = true,
|
||||||
|
["Base.Glock43"] = true,
|
||||||
|
["Base.GOL"] = true,
|
||||||
|
["Base.Grizzly50AE"] = true,
|
||||||
|
["Base.Groza"] = true,
|
||||||
|
["Base.GSH18"] = true,
|
||||||
|
["Base.HK_121"] = true,
|
||||||
|
["Base.HK416"] = true,
|
||||||
|
["Base.HKG28"] = true,
|
||||||
|
["Base.HKMK23"] = true,
|
||||||
|
["Base.HoneyBadger"] = true,
|
||||||
|
["Base.HuntingRifle"] = true,
|
||||||
|
["Base.IA2"] = true,
|
||||||
|
["Base.IA2_308"] = true,
|
||||||
|
["Base.Jackhammer"] = true,
|
||||||
|
["Base.Jericho941"] = true,
|
||||||
|
["Base.JNG90"] = true,
|
||||||
|
["Base.K2"] = true,
|
||||||
|
["Base.K7"] = true,
|
||||||
|
["Base.KAC_PDW"] = true,
|
||||||
|
["Base.Kark98"] = true,
|
||||||
|
["Base.Kimber1911"] = true,
|
||||||
|
["Base.Kriss9mm"] = true,
|
||||||
|
["Base.KrissVector45"] = true,
|
||||||
|
["Base.KS23"] = true,
|
||||||
|
["Base.KSG"] = true,
|
||||||
|
["Base.L115A"] = true,
|
||||||
|
["Base.L85"] = true,
|
||||||
|
["Base.L86"] = true,
|
||||||
|
["Base.L96"] = true,
|
||||||
|
["Base.LanchesterMK1"] = true,
|
||||||
|
["Base.Lewis"] = true,
|
||||||
|
["Base.LR300"] = true,
|
||||||
|
["Base.LSAT"] = true,
|
||||||
|
["Base.LVOA"] = true,
|
||||||
|
["Base.M1"] = true,
|
||||||
|
["Base.M110"] = true,
|
||||||
|
["Base.M16A2"] = true,
|
||||||
|
["Base.M1887"] = true,
|
||||||
|
["Base.M1887_Short"] = true,
|
||||||
|
["Base.M1A1"] = true,
|
||||||
|
["Base.M200"] = true,
|
||||||
|
["Base.M21"] = true,
|
||||||
|
["Base.M24"] = true,
|
||||||
|
["Base.M240B"] = true,
|
||||||
|
["Base.M249"] = true,
|
||||||
|
["Base.M39"] = true,
|
||||||
|
["Base.M4"] = true,
|
||||||
|
["Base.M40"] = true,
|
||||||
|
["Base.M60E4"] = true,
|
||||||
|
["Base.M620"] = true,
|
||||||
|
["Base.M82A3"] = true,
|
||||||
|
["Base.M9_Samurai"] = true,
|
||||||
|
["Base.M93R"] = true,
|
||||||
|
["Base.M98B"] = true,
|
||||||
|
["Base.MAB38A"] = true,
|
||||||
|
["Base.MAC10"] = true,
|
||||||
|
["Base.MAS36"] = true,
|
||||||
|
["Base.MAT49"] = true,
|
||||||
|
["Base.MatebaGrifone"] = true,
|
||||||
|
["Base.MG131"] = true,
|
||||||
|
["Base.MG4"] = true,
|
||||||
|
["Base.MG42"] = true,
|
||||||
|
["Base.MG710"] = true,
|
||||||
|
["Base.Micro_UZI"] = true,
|
||||||
|
["Base.Mini_14"] = true,
|
||||||
|
["Base.Minimi"] = true,
|
||||||
|
["Base.MK18"] = true,
|
||||||
|
["Base.Mosin"] = true,
|
||||||
|
["Base.Mossber500"] = true,
|
||||||
|
["Base.Mossber590"] = true,
|
||||||
|
["Base.MP_R8"] = true,
|
||||||
|
["Base.MP18"] = true,
|
||||||
|
["Base.MP1911"] = true,
|
||||||
|
["Base.MP40"] = true,
|
||||||
|
["Base.MP5"] = true,
|
||||||
|
["Base.MP5K"] = true,
|
||||||
|
["Base.MP5SD"] = true,
|
||||||
|
["Base.MP7"] = true,
|
||||||
|
["Base.MP9"] = true,
|
||||||
|
["Base.MPX"] = true,
|
||||||
|
["Base.MSST"] = true,
|
||||||
|
["Base.MTAR"] = true,
|
||||||
|
["Base.MTS_255"] = true,
|
||||||
|
["Base.MTS_255_Short"] = true,
|
||||||
|
["Base.MX4"] = true,
|
||||||
|
["Base.Nagant_M1895"] = true,
|
||||||
|
["Base.Negev"] = true,
|
||||||
|
["Base.OTS_33"] = true,
|
||||||
|
["Base.P220"] = true,
|
||||||
|
["Base.P220_Elite"] = true,
|
||||||
|
["Base.P228"] = true,
|
||||||
|
["Base.P38"] = true,
|
||||||
|
["Base.P90"] = true,
|
||||||
|
["Base.P99"] = true,
|
||||||
|
["Base.P99_Kilin"] = true,
|
||||||
|
["Base.PB6P9"] = true,
|
||||||
|
["Base.Pistol"] = true,
|
||||||
|
["Base.pistol_shotgun"] = true,
|
||||||
|
["Base.Pistol2"] = true,
|
||||||
|
["Base.Pistol3"] = true,
|
||||||
|
["Base.PKP"] = true,
|
||||||
|
["Base.PP_Bizon"] = true,
|
||||||
|
["Base.PP2000"] = true,
|
||||||
|
["Base.PP93"] = true,
|
||||||
|
["Base.PPSH41"] = true,
|
||||||
|
["Base.Python357"] = true,
|
||||||
|
["Base.QBA"] = true,
|
||||||
|
["Base.QBB95"] = true,
|
||||||
|
["Base.QBS09"] = true,
|
||||||
|
["Base.QBS09_Short"] = true,
|
||||||
|
["Base.QBZ951"] = true,
|
||||||
|
["Base.R5"] = true,
|
||||||
|
["Base.Remington1100"] = true,
|
||||||
|
["Base.Remington1100_Short"] = true,
|
||||||
|
["Base.Remington121"] = true,
|
||||||
|
["Base.Remington870"] = true,
|
||||||
|
["Base.Remington870_Short"] = true,
|
||||||
|
["Base.Revolver"] = true,
|
||||||
|
["Base.Revolver_long"] = true,
|
||||||
|
["Base.Revolver_short"] = true,
|
||||||
|
["Base.Revolver38"] = true,
|
||||||
|
["Base.Revolver666"] = true,
|
||||||
|
["Base.Rhino20DS"] = true,
|
||||||
|
["Base.RMB93"] = true,
|
||||||
|
["Base.RPD"] = true,
|
||||||
|
["Base.RPK"] = true,
|
||||||
|
["Base.RPK12"] = true,
|
||||||
|
["Base.RPK16"] = true,
|
||||||
|
["Base.RSH12"] = true,
|
||||||
|
["Base.Ruger10_22"] = true,
|
||||||
|
["Base.Ruger357"] = true,
|
||||||
|
["Base.RugerLC"] = true,
|
||||||
|
["Base.SA58"] = true,
|
||||||
|
["Base.Saiga12"] = true,
|
||||||
|
["Base.Saiga9mm"] = true,
|
||||||
|
["Base.Samurai_aw"] = true,
|
||||||
|
["Base.Samurai_kendo"] = true,
|
||||||
|
["Base.SAR21"] = true,
|
||||||
|
["Base.ScarH"] = true,
|
||||||
|
["Base.ScarL"] = true,
|
||||||
|
["Base.Schofield1875"] = true,
|
||||||
|
["Base.Scout_elite"] = true,
|
||||||
|
["Base.ScrapRevolver"] = true,
|
||||||
|
["Base.Shorty"] = true,
|
||||||
|
["Base.Shotgun"] = true,
|
||||||
|
["Base.ShotgunSawnoff"] = true,
|
||||||
|
["Base.SIG_553"] = true,
|
||||||
|
["Base.SIG516"] = true,
|
||||||
|
["Base.Silenced_Sten"] = true,
|
||||||
|
["Base.Sjorgen"] = true,
|
||||||
|
["Base.Sjorgen_Short"] = true,
|
||||||
|
["Base.SKS"] = true,
|
||||||
|
["Base.SKS_carbine"] = true,
|
||||||
|
["Base.SKS_carbine_short"] = true,
|
||||||
|
["Base.Snub22LR"] = true,
|
||||||
|
["Base.SPAS12"] = true,
|
||||||
|
["Base.SPAS15"] = true,
|
||||||
|
["Base.Springfield_sniper"] = true,
|
||||||
|
["Base.SR1M"] = true,
|
||||||
|
["Base.SR338"] = true,
|
||||||
|
["Base.SR3M"] = true,
|
||||||
|
["Base.SR47"] = true,
|
||||||
|
["Base.SS2V5"] = true,
|
||||||
|
["Base.Sten_MK5"] = true,
|
||||||
|
["Base.Striker"] = true,
|
||||||
|
["Base.SV98"] = true,
|
||||||
|
["Base.SVD"] = true,
|
||||||
|
["Base.SVD_short"] = true,
|
||||||
|
["Base.SVD12"] = true,
|
||||||
|
["Base.SVDK"] = true,
|
||||||
|
["Base.SVDK_short"] = true,
|
||||||
|
["Base.SVT_40"] = true,
|
||||||
|
["Base.SVU"] = true,
|
||||||
|
["Base.SW1905"] = true,
|
||||||
|
["Base.SW1917"] = true,
|
||||||
|
["Base.SW500"] = true,
|
||||||
|
["Base.SW629"] = true,
|
||||||
|
["Base.SWM1854"] = true,
|
||||||
|
["Base.SWM1894"] = true,
|
||||||
|
["Base.SWM3"] = true,
|
||||||
|
["Base.SWM327"] = true,
|
||||||
|
["Base.SWM629_Deluxe"] = true,
|
||||||
|
["Base.SWMP_12"] = true,
|
||||||
|
["Base.Taurus_raging_bull"] = true,
|
||||||
|
["Base.Taurus_raging_bull460"] = true,
|
||||||
|
["Base.Taurus_RT85"] = true,
|
||||||
|
["Base.Taurus606"] = true,
|
||||||
|
["Base.TEC9"] = true,
|
||||||
|
["Base.Thompson"] = true,
|
||||||
|
["Base.TMP"] = true,
|
||||||
|
["Base.Type81"] = true,
|
||||||
|
["Base.Type88"] = true,
|
||||||
|
["Base.UMP45"] = true,
|
||||||
|
["Base.UMP45_long"] = true,
|
||||||
|
["Base.USAS12"] = true,
|
||||||
|
["Base.USP45"] = true,
|
||||||
|
["Base.UZI"] = true,
|
||||||
|
["Base.V_M87"] = true,
|
||||||
|
["Base.ValmetM82"] = true,
|
||||||
|
["Base.VarmintRifle"] = true,
|
||||||
|
["Base.VEPR"] = true,
|
||||||
|
["Base.Veresk"] = true,
|
||||||
|
["Base.VictorySW22"] = true,
|
||||||
|
["Base.VP70"] = true,
|
||||||
|
["Base.VR80"] = true,
|
||||||
|
["Base.VSK"] = true,
|
||||||
|
["Base.VSS"] = true,
|
||||||
|
["Base.VSS_Tactical"] = true,
|
||||||
|
["Base.VSSK"] = true,
|
||||||
|
["Base.VZ58"] = true,
|
||||||
|
["Base.VZ61"] = true,
|
||||||
|
["Base.WA2000"] = true,
|
||||||
|
["Base.Webley_MK_snub"] = true,
|
||||||
|
["Base.Webley_Revolver"] = true,
|
||||||
|
["Base.Wieger940"] = true,
|
||||||
|
["Base.Wildey"] = true,
|
||||||
|
["Base.Winchester1886"] = true,
|
||||||
|
["Base.Winchester1895"] = true,
|
||||||
|
["Base.Winchester1897"] = true,
|
||||||
|
["Base.X86"] = true,
|
||||||
|
["Base.XD"] = true,
|
||||||
|
["Base.XM8"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local GAEL_GUNSTORE_MAGAZINES = {
|
||||||
|
["Base.12GClip"] = true,
|
||||||
|
["Base.12GClip14"] = true,
|
||||||
|
["Base.12GDrum24"] = true,
|
||||||
|
["Base.22LRClip"] = true,
|
||||||
|
["Base.22LRClip50"] = true,
|
||||||
|
["Base.22LRDrum100"] = true,
|
||||||
|
["Base.308Box150"] = true,
|
||||||
|
["Base.308Clip"] = true,
|
||||||
|
["Base.308Clip40"] = true,
|
||||||
|
["Base.308Drum100"] = true,
|
||||||
|
["Base.308Drum60"] = true,
|
||||||
|
["Base.357Clip"] = true,
|
||||||
|
["Base.357Drum45"] = true,
|
||||||
|
["Base.44Clip"] = true,
|
||||||
|
["Base.44Clip20"] = true,
|
||||||
|
["Base.44Drum50"] = true,
|
||||||
|
["Base.45Clip"] = true,
|
||||||
|
["Base.45Clip25"] = true,
|
||||||
|
["Base.45Drum100"] = true,
|
||||||
|
["Base.45Drum50"] = true,
|
||||||
|
["Base.50Clip"] = true,
|
||||||
|
["Base.50Clip18"] = true,
|
||||||
|
["Base.50MagnumClip"] = true,
|
||||||
|
["Base.50MagnumClip18"] = true,
|
||||||
|
["Base.50MagnumDrum40"] = true,
|
||||||
|
["Base.545x39Clip30"] = true,
|
||||||
|
["Base.545x39Clip60"] = true,
|
||||||
|
["Base.545x39Drum100"] = true,
|
||||||
|
["Base.556Box150"] = true,
|
||||||
|
["Base.556Clip"] = true,
|
||||||
|
["Base.556Drum_100rnd"] = true,
|
||||||
|
["Base.556Drum_60rnd"] = true,
|
||||||
|
["Base.762x39Clip"] = true,
|
||||||
|
["Base.762x39Clip45"] = true,
|
||||||
|
["Base.762x39Drum100"] = true,
|
||||||
|
["Base.762x39Drum73"] = true,
|
||||||
|
["Base.762x54rBox150"] = true,
|
||||||
|
["Base.762x54rClip"] = true,
|
||||||
|
["Base.762x54rClip40"] = true,
|
||||||
|
["Base.792x57Box75"] = true,
|
||||||
|
["Base.792x57Box97"] = true,
|
||||||
|
["Base.792x57Clip"] = true,
|
||||||
|
["Base.792x57Clip40"] = true,
|
||||||
|
["Base.9mmClip"] = true,
|
||||||
|
["Base.9mmClip30"] = true,
|
||||||
|
["Base.9mmDrum100"] = true,
|
||||||
|
["Base.9mmDrum50"] = true,
|
||||||
|
["Base.9mmDrum75"] = true,
|
||||||
|
["Base.9x39Clip"] = true,
|
||||||
|
["Base.9x39Clip40"] = true,
|
||||||
|
["Base.9x39Drum60"] = true,
|
||||||
|
["Base.BizonClip64"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
local function setDisplayCategory(scriptManager, fullType, category)
|
||||||
|
local scriptItem = scriptManager:FindItem(fullType)
|
||||||
|
if not scriptItem then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(scriptItem.DoParam) == "function" then
|
||||||
|
scriptItem:DoParam("DisplayCategory = " .. category)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(scriptItem.setDisplayCategory) == "function" then
|
||||||
|
scriptItem:setDisplayCategory(category)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function areSupportedItemsReady(scriptManager)
|
||||||
|
if not scriptManager then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local guns93Firearm = scriptManager:FindItem(GUNS93_SENTINEL_FIREARM)
|
||||||
|
local guns93Magazine = scriptManager:FindItem(GUNS93_SENTINEL_MAGAZINE)
|
||||||
|
local guns93Ready = guns93Firearm ~= nil and guns93Magazine ~= nil
|
||||||
|
|
||||||
|
local gaelFirearm = scriptManager:FindItem(GAEL_GUNSTORE_SENTINEL_FIREARM)
|
||||||
|
local gaelMagazine = scriptManager:FindItem(GAEL_GUNSTORE_SENTINEL_MAGAZINE)
|
||||||
|
local gaelReady = gaelFirearm ~= nil and gaelMagazine ~= nil
|
||||||
|
|
||||||
|
return guns93Ready or gaelReady
|
||||||
|
end
|
||||||
|
|
||||||
|
local function applyPatch()
|
||||||
|
if OFGuns93DisplayCategoryPatch.patched then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isSortingToggleEnabled() then
|
||||||
|
OFGuns93DisplayCategoryPatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local scriptManager = ScriptManager and ScriptManager.instance
|
||||||
|
if not scriptManager or type(scriptManager.FindItem) ~= "function" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local firearmCategory, magazineCategory = resolveWeaponCategories()
|
||||||
|
|
||||||
|
if not areSupportedItemsReady(scriptManager) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local patchedCount = 0
|
||||||
|
for fullType in pairs(GUNS93_FIREARMS) do
|
||||||
|
if setDisplayCategory(scriptManager, fullType, firearmCategory) then
|
||||||
|
patchedCount = patchedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for fullType in pairs(GUNS93_MAGAZINES) do
|
||||||
|
if setDisplayCategory(scriptManager, fullType, magazineCategory) then
|
||||||
|
patchedCount = patchedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for fullType in pairs(GAEL_GUNSTORE_FIREARMS) do
|
||||||
|
if setDisplayCategory(scriptManager, fullType, firearmCategory) then
|
||||||
|
patchedCount = patchedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for fullType in pairs(GAEL_GUNSTORE_MAGAZINES) do
|
||||||
|
if setDisplayCategory(scriptManager, fullType, magazineCategory) then
|
||||||
|
patchedCount = patchedCount + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if patchedCount == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
OFGuns93DisplayCategoryPatch.patched = true
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryPatchOnTick()
|
||||||
|
if applyPatch() then
|
||||||
|
OFGuns93DisplayCategoryPatch.tickHookAdded = false
|
||||||
|
Events.OnTick.Remove(tryPatchOnTick)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensureTickHook()
|
||||||
|
if OFGuns93DisplayCategoryPatch.tickHookAdded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
OFGuns93DisplayCategoryPatch.tickHookAdded = true
|
||||||
|
Events.OnTick.Add(tryPatchOnTick)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function queueRepatchNow()
|
||||||
|
OFGuns93DisplayCategoryPatch.patched = false
|
||||||
|
if not applyPatch() then
|
||||||
|
ensureTickHook()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runQueuedRepatch()
|
||||||
|
OFGuns93DisplayCategoryPatch.repatchQueued = false
|
||||||
|
Events.OnTick.Remove(runQueuedRepatch)
|
||||||
|
queueRepatchNow()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function queueRepatch()
|
||||||
|
if OFGuns93DisplayCategoryPatch.repatchQueued then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
OFGuns93DisplayCategoryPatch.repatchQueued = true
|
||||||
|
Events.OnTick.Add(runQueuedRepatch)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tryInstallBetterContainersRepatchHook()
|
||||||
|
local ok, helpers = pcall(require, "BetterContainers/Helpers")
|
||||||
|
if not ok or type(helpers) ~= "table" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local eventName = helpers.OPTIONS_APPLIED
|
||||||
|
if type(eventName) ~= "string" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local event = Events[eventName]
|
||||||
|
if event and type(event.Add) == "function" then
|
||||||
|
event.Add(queueRepatch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not applyPatch() then
|
||||||
|
ensureTickHook()
|
||||||
|
end
|
||||||
|
|
||||||
|
if Events.OnMainMenuEnter and type(Events.OnMainMenuEnter.Add) == "function" then
|
||||||
|
Events.OnMainMenuEnter.Add(queueRepatch)
|
||||||
|
end
|
||||||
|
|
||||||
|
tryInstallBetterContainersRepatchHook()
|
||||||
|
|
||||||
7
common/media/lua/shared/Translate/EN/IG_UI_EN.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
IGUI_EN = {
|
||||||
|
IGUI_ItemCat_WepFire = "Weapon - Firearm",
|
||||||
|
IGUI_ItemCat_WepAmmoMag = "Weapon - Magazine",
|
||||||
|
IGUI_ItemCat_Casings = "Spent Casings",
|
||||||
|
IGUI_ItemCat_OFSpentCasings = "Spent Casings",
|
||||||
|
IGUI_ItemCat_OFAmmoPart = "Ammo Part",
|
||||||
|
}
|
||||||
7
common/media/lua/shared/Translate/EN/Sandbox_EN.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Sandbox_EN = {
|
||||||
|
Sandbox_OpinionatedFirearms = "Opinionated Firearms",
|
||||||
|
Sandbox_HandleHotBrassCasingSpawnUseAmmoMaker = "Handle Hot Brass Casing Spawn - Use Ammomaker",
|
||||||
|
Sandbox_HandleHotBrassCasingSpawnUseAmmoMaker_tooltip = "When enabled, Hot Brass ejects Ammo Maker empties for supported ammo (including Guns93 and Gael Gun Store) instead of default casings.",
|
||||||
|
Sandbox_AddSorting = "Add Sorting",
|
||||||
|
Sandbox_AddSorting_tooltip = "When enabled, apply Opinionated Firearms sorting/display categories for supported weapons, magazines, and ammo parts.",
|
||||||
|
}
|
||||||
14
common/media/sandbox-options.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
VERSION = 1,
|
||||||
|
option OpinionatedFirearms.HandleHotBrassCasingSpawnUseAmmoMaker {
|
||||||
|
type = boolean,
|
||||||
|
default = true,
|
||||||
|
page = OpinionatedFirearms,
|
||||||
|
translation = HandleHotBrassCasingSpawnUseAmmoMaker,
|
||||||
|
}
|
||||||
|
|
||||||
|
option OpinionatedFirearms.AddSorting {
|
||||||
|
type = boolean,
|
||||||
|
default = true,
|
||||||
|
page = OpinionatedFirearms,
|
||||||
|
translation = AddSorting,
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
-- Auto-generated by tools/ggs-dist-cli.js apply
|
|
||||||
-- Generated at: 2026-02-11T22:49:03.184Z
|
|
||||||
return {
|
|
||||||
items = {
|
|
||||||
["Base.1P78"] = {
|
|
||||||
enabled = true,
|
|
||||||
placements = {
|
|
||||||
["ArmyStorageAmmunition"] = 1,
|
|
||||||
["ArmyStorageGuns"] = 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
["Base.1PN93_4"] = {
|
|
||||||
enabled = false,
|
|
||||||
placements = {
|
|
||||||
["ArmyStorageAmmunition"] = 1,
|
|
||||||
["ArmyStorageGuns"] = 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
["Base.9x39_Silencer"] = {
|
|
||||||
enabled = true,
|
|
||||||
placements = {
|
|
||||||
["ArmyStorageAmmunition"] = 1,
|
|
||||||
["ArmyStorageGuns"] = 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
102537
data/ggs-spawn-catalog.json
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"item": "Base.1P78",
|
|
||||||
"enabled": true,
|
|
||||||
"placements": [
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageAmmunition",
|
|
||||||
"weight": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageGuns",
|
|
||||||
"weight": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"item": "Base.1PN93_4",
|
|
||||||
"enabled": false,
|
|
||||||
"placements": [
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageAmmunition",
|
|
||||||
"weight": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageGuns",
|
|
||||||
"weight": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"item": "Base.9x39_Silencer",
|
|
||||||
"enabled": true,
|
|
||||||
"placements": [
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageAmmunition",
|
|
||||||
"weight": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"list": "ArmyStorageGuns",
|
|
||||||
"weight": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
9
mod.info
@@ -1,8 +1,9 @@
|
|||||||
name=Opinionated Firearms
|
name=Opinionated Firearms
|
||||||
id=opinionated_firearms
|
id=hrsys_opinionated_firearms
|
||||||
author=Riggs0
|
author=Riggs0
|
||||||
modversion=1.0.0
|
modversion=1.0.0
|
||||||
versionMin=42.12.13
|
versionMin=42.12.13
|
||||||
require=GaelGunStore_ALPHA
|
require=\2788256295/ammomaker,\HBVCEFb42
|
||||||
|
description=Opinionated Firearms casing and other changes to Guns of 93, Ammomaker and Hot Brass
|
||||||
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).
|
icon=icon.png
|
||||||
|
poster=preview.png
|
||||||
|
|||||||
BIN
preview.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
@@ -1,352 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
|
|
||||||
function printUsage() {
|
|
||||||
console.log(`
|
|
||||||
Usage:
|
|
||||||
node tools/ggs-dist-cli.js extract [--ggs-root <path>] [--out <file>]
|
|
||||||
node tools/ggs-dist-cli.js apply --profile <file> [--out <file>]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
extract Build a full firearm/attachment spawn catalog from GaelGunStore.
|
|
||||||
apply Convert a webapp profile JSON into Lua used by the B42 mod patcher.
|
|
||||||
`.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const args = { _: [] };
|
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
|
||||||
const token = argv[i];
|
|
||||||
if (token.startsWith("--")) {
|
|
||||||
const key = token.slice(2);
|
|
||||||
const next = argv[i + 1];
|
|
||||||
if (!next || next.startsWith("--")) {
|
|
||||||
args[key] = true;
|
|
||||||
} else {
|
|
||||||
args[key] = next;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args._.push(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureDir(filePath) {
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function readText(filePath) {
|
|
||||||
return fs.readFileSync(filePath, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeText(filePath, content) {
|
|
||||||
ensureDir(filePath);
|
|
||||||
fs.writeFileSync(filePath, content, "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkFiles(rootDir, predicate, out) {
|
|
||||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(rootDir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walkFiles(fullPath, predicate, out);
|
|
||||||
} else if (predicate(fullPath)) {
|
|
||||||
out.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectItemTypesFromScripts(rootDir) {
|
|
||||||
const files = [];
|
|
||||||
walkFiles(rootDir, (fullPath) => fullPath.toLowerCase().endsWith(".txt"), files);
|
|
||||||
const itemSet = new Set();
|
|
||||||
const itemPattern = /^\s*item\s+([A-Za-z0-9_]+)/gm;
|
|
||||||
for (const filePath of files) {
|
|
||||||
const text = readText(filePath);
|
|
||||||
let match = itemPattern.exec(text);
|
|
||||||
while (match) {
|
|
||||||
itemSet.add(`Base.${match[1]}`);
|
|
||||||
match = itemPattern.exec(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(itemSet).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseLootEntries(lootLuaPath) {
|
|
||||||
const lootLua = readText(lootLuaPath);
|
|
||||||
const pattern =
|
|
||||||
/\{\s*list\s*=\s*"([^"]+)"\s*,\s*item\s*=\s*"([^"]+)"\s*,\s*weight\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*,\s*sv\s*=\s*"([^"]+)"\s*\}/g;
|
|
||||||
const entries = [];
|
|
||||||
let match = pattern.exec(lootLua);
|
|
||||||
while (match) {
|
|
||||||
entries.push({
|
|
||||||
list: match[1],
|
|
||||||
item: match[2],
|
|
||||||
weight: Number.parseFloat(match[3]),
|
|
||||||
sv: match[4],
|
|
||||||
});
|
|
||||||
match = pattern.exec(lootLua);
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function aggregatePlacements(entries) {
|
|
||||||
const placementMap = new Map();
|
|
||||||
for (const entry of entries) {
|
|
||||||
const previous = placementMap.get(entry.list) || 0;
|
|
||||||
placementMap.set(entry.list, previous + entry.weight);
|
|
||||||
}
|
|
||||||
return Array.from(placementMap.entries())
|
|
||||||
.map(([list, weight]) => ({ list, weight: Number(weight.toFixed(6)) }))
|
|
||||||
.sort((a, b) => a.list.localeCompare(b.list));
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeItemType(item) {
|
|
||||||
if (typeof item !== "string") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = item.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return trimmed.includes(".") ? trimmed : `Base.${trimmed}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildCatalog(ggsRoot) {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const firearmScriptsDir = path.join(ggsRoot, "media", "scripts", "Firearms");
|
|
||||||
const attachmentScriptsDir = path.join(ggsRoot, "media", "scripts", "GunPartItem");
|
|
||||||
const lootLuaPath = path.join(ggsRoot, "media", "lua", "server", "item", "loot.lua");
|
|
||||||
|
|
||||||
if (!fs.existsSync(firearmScriptsDir)) {
|
|
||||||
throw new Error(`Missing firearms scripts dir: ${firearmScriptsDir}`);
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(attachmentScriptsDir)) {
|
|
||||||
throw new Error(`Missing attachments scripts dir: ${attachmentScriptsDir}`);
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(lootLuaPath)) {
|
|
||||||
throw new Error(`Missing loot file: ${lootLuaPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const firearms = collectItemTypesFromScripts(firearmScriptsDir);
|
|
||||||
const attachments = collectItemTypesFromScripts(attachmentScriptsDir);
|
|
||||||
const firearmSet = new Set(firearms);
|
|
||||||
const attachmentSet = new Set(attachments);
|
|
||||||
|
|
||||||
const lootEntries = parseLootEntries(lootLuaPath);
|
|
||||||
const perItemLoot = new Map();
|
|
||||||
const allLists = new Set();
|
|
||||||
const allItems = new Set([...firearms, ...attachments]);
|
|
||||||
|
|
||||||
for (const entry of lootEntries) {
|
|
||||||
const normalized = normalizeItemType(entry.item);
|
|
||||||
if (!normalized) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!allItems.has(normalized)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
allLists.add(entry.list);
|
|
||||||
|
|
||||||
const existing = perItemLoot.get(normalized) || [];
|
|
||||||
existing.push({
|
|
||||||
list: entry.list,
|
|
||||||
weight: entry.weight,
|
|
||||||
sv: entry.sv,
|
|
||||||
});
|
|
||||||
perItemLoot.set(normalized, existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = Array.from(allItems)
|
|
||||||
.sort()
|
|
||||||
.map((itemType) => {
|
|
||||||
const placements = perItemLoot.get(itemType) || [];
|
|
||||||
const aggregatedPlacements = aggregatePlacements(placements);
|
|
||||||
const svKeys = Array.from(new Set(placements.map((p) => p.sv))).sort();
|
|
||||||
let category = "unknown";
|
|
||||||
if (firearmSet.has(itemType)) {
|
|
||||||
category = "firearm";
|
|
||||||
} else if (attachmentSet.has(itemType)) {
|
|
||||||
category = "attachment";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
item: itemType,
|
|
||||||
shortId: itemType.replace(/^Base\./, ""),
|
|
||||||
category,
|
|
||||||
defaultEnabled: false,
|
|
||||||
spawnControlKeys: svKeys,
|
|
||||||
placements,
|
|
||||||
aggregatedPlacements,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
formatVersion: 1,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
source: {
|
|
||||||
ggsRoot: path.relative(cwd, path.resolve(ggsRoot)) || ".",
|
|
||||||
lootLuaPath: path.relative(cwd, path.resolve(lootLuaPath)),
|
|
||||||
firearmScriptsDir: path.relative(cwd, path.resolve(firearmScriptsDir)),
|
|
||||||
attachmentScriptsDir: path.relative(cwd, path.resolve(attachmentScriptsDir)),
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
spawnRateFormula: "effectiveWeight = baseWeight * lootAmountMultiplier * SandboxVars[sv]",
|
|
||||||
lootAmountMultiplierLookup: [0, 0.25, 0.5, 1, 2, 4],
|
|
||||||
},
|
|
||||||
counts: {
|
|
||||||
firearms: firearms.length,
|
|
||||||
attachments: attachments.length,
|
|
||||||
totalItems: items.length,
|
|
||||||
placementRows: lootEntries.length,
|
|
||||||
distributionLists: allLists.size,
|
|
||||||
},
|
|
||||||
lists: Array.from(allLists).sort(),
|
|
||||||
items,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLuaString(value) {
|
|
||||||
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toLuaProfile(profileJson) {
|
|
||||||
if (!profileJson || !Array.isArray(profileJson.entries)) {
|
|
||||||
throw new Error("Profile JSON must contain an entries array.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsMap = new Map();
|
|
||||||
for (const rawEntry of profileJson.entries) {
|
|
||||||
const itemType = normalizeItemType(rawEntry && rawEntry.item);
|
|
||||||
if (!itemType) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enabled = rawEntry.enabled !== false;
|
|
||||||
const placements = {};
|
|
||||||
|
|
||||||
if (Array.isArray(rawEntry.placements)) {
|
|
||||||
for (const placement of rawEntry.placements) {
|
|
||||||
const listName =
|
|
||||||
placement && typeof placement.list === "string" ? placement.list.trim() : "";
|
|
||||||
const weight = Number.parseFloat(placement && placement.weight);
|
|
||||||
if (!listName || !Number.isFinite(weight) || weight <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
placements[listName] = Number(weight.toFixed(6));
|
|
||||||
}
|
|
||||||
} else if (rawEntry && typeof rawEntry.placements === "object" && rawEntry.placements) {
|
|
||||||
for (const [listName, rawWeight] of Object.entries(rawEntry.placements)) {
|
|
||||||
const cleanList = listName.trim();
|
|
||||||
const weight = Number.parseFloat(rawWeight);
|
|
||||||
if (!cleanList || !Number.isFinite(weight) || weight <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
placements[cleanList] = Number(weight.toFixed(6));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsMap.set(itemType, {
|
|
||||||
enabled,
|
|
||||||
placements,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemTypes = Array.from(itemsMap.keys()).sort();
|
|
||||||
const lines = [];
|
|
||||||
lines.push("-- Auto-generated by tools/ggs-dist-cli.js apply");
|
|
||||||
lines.push(`-- Generated at: ${new Date().toISOString()}`);
|
|
||||||
lines.push("return {");
|
|
||||||
lines.push(" items = {");
|
|
||||||
|
|
||||||
for (const itemType of itemTypes) {
|
|
||||||
const config = itemsMap.get(itemType);
|
|
||||||
lines.push(` [${formatLuaString(itemType)}] = {`);
|
|
||||||
lines.push(` enabled = ${config.enabled ? "true" : "false"},`);
|
|
||||||
lines.push(" placements = {");
|
|
||||||
|
|
||||||
const listNames = Object.keys(config.placements).sort();
|
|
||||||
for (const listName of listNames) {
|
|
||||||
const weight = config.placements[listName];
|
|
||||||
lines.push(` [${formatLuaString(listName)}] = ${weight},`);
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(" },");
|
|
||||||
lines.push(" },");
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(" },");
|
|
||||||
lines.push("}");
|
|
||||||
lines.push("");
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
function commandExtract(args) {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const ggsRoot = path.resolve(cwd, args["ggs-root"] || path.join("source", "GaelGunStore", "42"));
|
|
||||||
const outPath = path.resolve(cwd, args.out || path.join("data", "ggs-spawn-catalog.json"));
|
|
||||||
|
|
||||||
const catalog = buildCatalog(ggsRoot);
|
|
||||||
writeText(outPath, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
||||||
|
|
||||||
console.log(`Extracted catalog: ${outPath}`);
|
|
||||||
console.log(
|
|
||||||
`Items=${catalog.counts.totalItems}, Firearms=${catalog.counts.firearms}, Attachments=${catalog.counts.attachments}, Lists=${catalog.counts.distributionLists}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function commandApply(args) {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const profilePath = args.profile ? path.resolve(cwd, args.profile) : null;
|
|
||||||
const outPath = path.resolve(
|
|
||||||
cwd,
|
|
||||||
args.out || path.join("common", "media", "lua", "shared", "OFSpawnProfile.lua")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!profilePath) {
|
|
||||||
throw new Error("Missing required --profile <file> argument.");
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(profilePath)) {
|
|
||||||
throw new Error(`Profile file not found: ${profilePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileJson = JSON.parse(readText(profilePath));
|
|
||||||
const luaProfile = toLuaProfile(profileJson);
|
|
||||||
writeText(outPath, luaProfile);
|
|
||||||
|
|
||||||
console.log(`Applied profile to Lua: ${outPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const args = parseArgs(process.argv.slice(2));
|
|
||||||
const command = args._[0];
|
|
||||||
|
|
||||||
if (!command || command === "help" || args.help) {
|
|
||||||
printUsage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === "extract") {
|
|
||||||
commandExtract(args);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === "apply") {
|
|
||||||
commandApply(args);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unknown command: ${command}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
main();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[ggs-dist-cli] ${error.message}`);
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
557
webapp/app.js
@@ -1,557 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
catalog: null,
|
|
||||||
profileByItem: {},
|
|
||||||
selectedItem: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dom = {
|
|
||||||
statusText: document.getElementById("statusText"),
|
|
||||||
catalogFile: document.getElementById("catalogFile"),
|
|
||||||
profileFile: document.getElementById("profileFile"),
|
|
||||||
exportProfile: document.getElementById("exportProfile"),
|
|
||||||
searchInput: document.getElementById("searchInput"),
|
|
||||||
categoryFilter: document.getElementById("categoryFilter"),
|
|
||||||
spawnFilter: document.getElementById("spawnFilter"),
|
|
||||||
itemTableBody: document.getElementById("itemTableBody"),
|
|
||||||
selectedDetails: document.getElementById("selectedDetails"),
|
|
||||||
resetSelected: document.getElementById("resetSelected"),
|
|
||||||
};
|
|
||||||
|
|
||||||
function setStatus(text) {
|
|
||||||
dom.statusText.textContent = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readFileText(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => resolve(String(reader.result || ""));
|
|
||||||
reader.onerror = () => reject(new Error(`Failed to read ${file.name}`));
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeNumber(value, fallback = 0) {
|
|
||||||
const n = Number.parseFloat(value);
|
|
||||||
return Number.isFinite(n) ? n : fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeItemType(item) {
|
|
||||||
if (typeof item !== "string") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const trimmed = item.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return trimmed.includes(".") ? trimmed : `Base.${trimmed}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeCatalog(raw) {
|
|
||||||
if (!raw || !Array.isArray(raw.items)) {
|
|
||||||
throw new Error("Invalid catalog format. Missing items array.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const lists = new Set(Array.isArray(raw.lists) ? raw.lists : []);
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
for (const rawItem of raw.items) {
|
|
||||||
const itemType = normalizeItemType(rawItem.item);
|
|
||||||
if (!itemType) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aggregated = Array.isArray(rawItem.aggregatedPlacements)
|
|
||||||
? rawItem.aggregatedPlacements
|
|
||||||
: [];
|
|
||||||
const placements = {};
|
|
||||||
for (const placement of aggregated) {
|
|
||||||
const listName = placement && typeof placement.list === "string" ? placement.list.trim() : "";
|
|
||||||
const weight = safeNumber(placement && placement.weight, 0);
|
|
||||||
if (!listName || weight <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
placements[listName] = Number(weight.toFixed(6));
|
|
||||||
lists.add(listName);
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
item: itemType,
|
|
||||||
shortId: typeof rawItem.shortId === "string" ? rawItem.shortId : itemType.replace(/^Base\./, ""),
|
|
||||||
category: typeof rawItem.category === "string" ? rawItem.category : "unknown",
|
|
||||||
defaultEnabled: rawItem.defaultEnabled !== false,
|
|
||||||
spawnControlKeys: Array.isArray(rawItem.spawnControlKeys) ? rawItem.spawnControlKeys : [],
|
|
||||||
defaultPlacements: placements,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items.sort((a, b) => a.item.localeCompare(b.item));
|
|
||||||
return {
|
|
||||||
generatedAt: raw.generatedAt || null,
|
|
||||||
source: raw.source || {},
|
|
||||||
items,
|
|
||||||
lists: Array.from(lists).sort(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeProfileFromCatalog() {
|
|
||||||
const profileByItem = {};
|
|
||||||
for (const item of state.catalog.items) {
|
|
||||||
const placements = {};
|
|
||||||
for (const [listName, weight] of Object.entries(item.defaultPlacements)) {
|
|
||||||
placements[listName] = weight;
|
|
||||||
}
|
|
||||||
profileByItem[item.item] = {
|
|
||||||
item: item.item,
|
|
||||||
category: item.category,
|
|
||||||
enabled: item.defaultEnabled,
|
|
||||||
placements,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
state.profileByItem = profileByItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProfileEntry(itemType) {
|
|
||||||
return state.profileByItem[itemType] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProfileEntry(itemType, updater) {
|
|
||||||
const current = getProfileEntry(itemType);
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updater(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFilteredItems() {
|
|
||||||
if (!state.catalog) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const search = dom.searchInput.value.trim().toLowerCase();
|
|
||||||
const category = dom.categoryFilter.value;
|
|
||||||
const spawnState = dom.spawnFilter.value;
|
|
||||||
|
|
||||||
return state.catalog.items.filter((item) => {
|
|
||||||
const entry = getProfileEntry(item.item);
|
|
||||||
if (!entry) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (category !== "all" && item.category !== category) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (spawnState === "enabled" && !entry.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (spawnState === "disabled" && entry.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!search) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
item.item.toLowerCase().includes(search) ||
|
|
||||||
item.shortId.toLowerCase().includes(search)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItemTable() {
|
|
||||||
const filteredItems = getFilteredItems();
|
|
||||||
dom.itemTableBody.innerHTML = "";
|
|
||||||
|
|
||||||
for (const item of filteredItems) {
|
|
||||||
const entry = getProfileEntry(item.item);
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
if (state.selectedItem === item.item) {
|
|
||||||
row.classList.add("selected");
|
|
||||||
}
|
|
||||||
|
|
||||||
const spawnTd = document.createElement("td");
|
|
||||||
const spawnCheck = document.createElement("input");
|
|
||||||
spawnCheck.type = "checkbox";
|
|
||||||
spawnCheck.checked = entry.enabled;
|
|
||||||
spawnCheck.addEventListener("click", (event) => event.stopPropagation());
|
|
||||||
spawnCheck.addEventListener("change", () => {
|
|
||||||
updateProfileEntry(item.item, (target) => {
|
|
||||||
target.enabled = spawnCheck.checked;
|
|
||||||
});
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
spawnTd.appendChild(spawnCheck);
|
|
||||||
|
|
||||||
const itemTd = document.createElement("td");
|
|
||||||
itemTd.textContent = item.shortId;
|
|
||||||
|
|
||||||
const categoryTd = document.createElement("td");
|
|
||||||
categoryTd.textContent = item.category;
|
|
||||||
|
|
||||||
const listsTd = document.createElement("td");
|
|
||||||
listsTd.textContent = String(Object.keys(entry.placements).length);
|
|
||||||
|
|
||||||
row.appendChild(spawnTd);
|
|
||||||
row.appendChild(itemTd);
|
|
||||||
row.appendChild(categoryTd);
|
|
||||||
row.appendChild(listsTd);
|
|
||||||
|
|
||||||
row.addEventListener("click", () => {
|
|
||||||
state.selectedItem = item.item;
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
|
|
||||||
dom.itemTableBody.appendChild(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPlacementsTable(item, entry, container) {
|
|
||||||
const table = document.createElement("table");
|
|
||||||
table.className = "placements-table";
|
|
||||||
const thead = document.createElement("thead");
|
|
||||||
thead.innerHTML = "<tr><th>List</th><th>Weight</th><th></th></tr>";
|
|
||||||
table.appendChild(thead);
|
|
||||||
|
|
||||||
const tbody = document.createElement("tbody");
|
|
||||||
const placementNames = Object.keys(entry.placements).sort();
|
|
||||||
|
|
||||||
for (const listName of placementNames) {
|
|
||||||
const weight = entry.placements[listName];
|
|
||||||
const tr = document.createElement("tr");
|
|
||||||
|
|
||||||
const listTd = document.createElement("td");
|
|
||||||
listTd.textContent = listName;
|
|
||||||
|
|
||||||
const weightTd = document.createElement("td");
|
|
||||||
const weightInput = document.createElement("input");
|
|
||||||
weightInput.type = "number";
|
|
||||||
weightInput.min = "0";
|
|
||||||
weightInput.step = "0.001";
|
|
||||||
weightInput.value = String(weight);
|
|
||||||
weightInput.addEventListener("change", () => {
|
|
||||||
const next = safeNumber(weightInput.value, 0);
|
|
||||||
if (next <= 0) {
|
|
||||||
delete entry.placements[listName];
|
|
||||||
} else {
|
|
||||||
entry.placements[listName] = Number(next.toFixed(6));
|
|
||||||
}
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
weightTd.appendChild(weightInput);
|
|
||||||
|
|
||||||
const actionTd = document.createElement("td");
|
|
||||||
const removeBtn = document.createElement("button");
|
|
||||||
removeBtn.type = "button";
|
|
||||||
removeBtn.className = "small-btn remove";
|
|
||||||
removeBtn.textContent = "Remove";
|
|
||||||
removeBtn.addEventListener("click", () => {
|
|
||||||
delete entry.placements[listName];
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
actionTd.appendChild(removeBtn);
|
|
||||||
|
|
||||||
tr.appendChild(listTd);
|
|
||||||
tr.appendChild(weightTd);
|
|
||||||
tr.appendChild(actionTd);
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
|
|
||||||
table.appendChild(tbody);
|
|
||||||
container.appendChild(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelectedDetails() {
|
|
||||||
const selected = state.selectedItem;
|
|
||||||
dom.selectedDetails.innerHTML = "";
|
|
||||||
|
|
||||||
if (!state.catalog || !selected) {
|
|
||||||
dom.selectedDetails.textContent = "Select an item to edit placements and spawn rate.";
|
|
||||||
dom.selectedDetails.className = "details-empty";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = state.catalog.items.find((it) => it.item === selected);
|
|
||||||
const entry = getProfileEntry(selected);
|
|
||||||
if (!item || !entry) {
|
|
||||||
dom.selectedDetails.textContent = "Selected item not found.";
|
|
||||||
dom.selectedDetails.className = "details-empty";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dom.selectedDetails.className = "details-body";
|
|
||||||
|
|
||||||
const itemHeader = document.createElement("div");
|
|
||||||
itemHeader.className = "item-header";
|
|
||||||
itemHeader.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<h3 class="item-title">${item.item}</h3>
|
|
||||||
<span class="badge">${item.category}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
dom.selectedDetails.appendChild(itemHeader);
|
|
||||||
|
|
||||||
const enabledRow = document.createElement("div");
|
|
||||||
enabledRow.className = "inline-row";
|
|
||||||
const enabledInput = document.createElement("input");
|
|
||||||
enabledInput.type = "checkbox";
|
|
||||||
enabledInput.checked = entry.enabled;
|
|
||||||
enabledInput.addEventListener("change", () => {
|
|
||||||
entry.enabled = enabledInput.checked;
|
|
||||||
renderItemTable();
|
|
||||||
});
|
|
||||||
|
|
||||||
const enabledLabel = document.createElement("label");
|
|
||||||
enabledLabel.textContent = "Spawn enabled";
|
|
||||||
enabledLabel.prepend(enabledInput);
|
|
||||||
enabledLabel.style.display = "inline-flex";
|
|
||||||
enabledLabel.style.alignItems = "center";
|
|
||||||
enabledLabel.style.gap = "0.35rem";
|
|
||||||
enabledRow.appendChild(enabledLabel);
|
|
||||||
dom.selectedDetails.appendChild(enabledRow);
|
|
||||||
|
|
||||||
const placementsLabel = document.createElement("p");
|
|
||||||
placementsLabel.textContent = "Placements (distribution list + spawn rate weight):";
|
|
||||||
placementsLabel.style.margin = "0 0 0.4rem";
|
|
||||||
dom.selectedDetails.appendChild(placementsLabel);
|
|
||||||
|
|
||||||
renderPlacementsTable(item, entry, dom.selectedDetails);
|
|
||||||
|
|
||||||
const addRow = document.createElement("div");
|
|
||||||
addRow.className = "inline-row";
|
|
||||||
|
|
||||||
const listSelect = document.createElement("select");
|
|
||||||
const usedLists = new Set(Object.keys(entry.placements));
|
|
||||||
const availableLists = state.catalog.lists.filter((listName) => !usedLists.has(listName));
|
|
||||||
for (const listName of availableLists) {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = listName;
|
|
||||||
option.textContent = listName;
|
|
||||||
listSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
const customInput = document.createElement("input");
|
|
||||||
customInput.type = "text";
|
|
||||||
customInput.placeholder = "or custom list name";
|
|
||||||
|
|
||||||
const weightInput = document.createElement("input");
|
|
||||||
weightInput.type = "number";
|
|
||||||
weightInput.min = "0";
|
|
||||||
weightInput.step = "0.001";
|
|
||||||
weightInput.value = "1";
|
|
||||||
|
|
||||||
const addButton = document.createElement("button");
|
|
||||||
addButton.type = "button";
|
|
||||||
addButton.className = "small-btn";
|
|
||||||
addButton.textContent = "Add Placement";
|
|
||||||
addButton.addEventListener("click", () => {
|
|
||||||
const custom = customInput.value.trim();
|
|
||||||
const selectedList = custom || listSelect.value;
|
|
||||||
const weight = safeNumber(weightInput.value, 0);
|
|
||||||
if (!selectedList || weight <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entry.placements[selectedList] = Number(weight.toFixed(6));
|
|
||||||
if (!state.catalog.lists.includes(selectedList)) {
|
|
||||||
state.catalog.lists.push(selectedList);
|
|
||||||
state.catalog.lists.sort();
|
|
||||||
}
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
|
|
||||||
addRow.appendChild(listSelect);
|
|
||||||
addRow.appendChild(customInput);
|
|
||||||
addRow.appendChild(weightInput);
|
|
||||||
addRow.appendChild(addButton);
|
|
||||||
dom.selectedDetails.appendChild(addRow);
|
|
||||||
|
|
||||||
const resetRow = document.createElement("div");
|
|
||||||
resetRow.className = "inline-row";
|
|
||||||
|
|
||||||
const restoreButton = document.createElement("button");
|
|
||||||
restoreButton.type = "button";
|
|
||||||
restoreButton.className = "small-btn";
|
|
||||||
restoreButton.textContent = "Restore Catalog Placements";
|
|
||||||
restoreButton.addEventListener("click", () => {
|
|
||||||
entry.enabled = item.defaultEnabled;
|
|
||||||
entry.placements = { ...item.defaultPlacements };
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearButton = document.createElement("button");
|
|
||||||
clearButton.type = "button";
|
|
||||||
clearButton.className = "small-btn remove";
|
|
||||||
clearButton.textContent = "Clear Placements";
|
|
||||||
clearButton.addEventListener("click", () => {
|
|
||||||
entry.placements = {};
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
});
|
|
||||||
|
|
||||||
resetRow.appendChild(restoreButton);
|
|
||||||
resetRow.appendChild(clearButton);
|
|
||||||
dom.selectedDetails.appendChild(resetRow);
|
|
||||||
|
|
||||||
const meta = document.createElement("p");
|
|
||||||
meta.className = "meta";
|
|
||||||
meta.innerHTML = `
|
|
||||||
Sandbox weight controls: ${item.spawnControlKeys.length ? item.spawnControlKeys.join(", ") : "none"}<br>
|
|
||||||
Catalog list count: ${Object.keys(item.defaultPlacements).length}
|
|
||||||
`;
|
|
||||||
dom.selectedDetails.appendChild(meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildExportProfile() {
|
|
||||||
const entries = Object.keys(state.profileByItem)
|
|
||||||
.sort()
|
|
||||||
.map((itemType) => {
|
|
||||||
const entry = state.profileByItem[itemType];
|
|
||||||
const placements = Object.keys(entry.placements)
|
|
||||||
.sort()
|
|
||||||
.map((listName) => ({
|
|
||||||
list: listName,
|
|
||||||
weight: Number(entry.placements[listName]),
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
item: itemType,
|
|
||||||
category: entry.category,
|
|
||||||
enabled: entry.enabled,
|
|
||||||
placements,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
formatVersion: 1,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
sourceCatalog: {
|
|
||||||
generatedAt: state.catalog ? state.catalog.generatedAt : null,
|
|
||||||
source: state.catalog ? state.catalog.source : {},
|
|
||||||
},
|
|
||||||
entries,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadTextFile(fileName, content) {
|
|
||||||
const blob = new Blob([content], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const anchor = document.createElement("a");
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = fileName;
|
|
||||||
anchor.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCatalogFileSelected() {
|
|
||||||
const file = dom.catalogFile.files[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await readFileText(file);
|
|
||||||
const rawCatalog = JSON.parse(text);
|
|
||||||
state.catalog = normalizeCatalog(rawCatalog);
|
|
||||||
initializeProfileFromCatalog();
|
|
||||||
state.selectedItem = state.catalog.items.length ? state.catalog.items[0].item : null;
|
|
||||||
setStatus(
|
|
||||||
`Catalog loaded (${state.catalog.items.length} items, ${state.catalog.lists.length} lists).`
|
|
||||||
);
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
} catch (error) {
|
|
||||||
setStatus(`Catalog load failed: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
dom.catalogFile.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onProfileFileSelected() {
|
|
||||||
if (!state.catalog) {
|
|
||||||
setStatus("Load a catalog first.");
|
|
||||||
dom.profileFile.value = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = dom.profileFile.files[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await readFileText(file);
|
|
||||||
const raw = JSON.parse(text);
|
|
||||||
if (!Array.isArray(raw.entries)) {
|
|
||||||
throw new Error("Profile must contain an entries array.");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const row of raw.entries) {
|
|
||||||
const itemType = normalizeItemType(row.item);
|
|
||||||
if (!itemType || !state.profileByItem[itemType]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const entry = state.profileByItem[itemType];
|
|
||||||
entry.enabled = row.enabled !== false;
|
|
||||||
entry.placements = {};
|
|
||||||
if (Array.isArray(row.placements)) {
|
|
||||||
for (const placement of row.placements) {
|
|
||||||
const listName =
|
|
||||||
placement && typeof placement.list === "string" ? placement.list.trim() : "";
|
|
||||||
const weight = safeNumber(placement && placement.weight, 0);
|
|
||||||
if (!listName || weight <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
entry.placements[listName] = Number(weight.toFixed(6));
|
|
||||||
if (!state.catalog.lists.includes(listName)) {
|
|
||||||
state.catalog.lists.push(listName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.catalog.lists.sort();
|
|
||||||
setStatus("Profile loaded and applied to current catalog.");
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
} catch (error) {
|
|
||||||
setStatus(`Profile load failed: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
dom.profileFile.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExportProfile() {
|
|
||||||
if (!state.catalog) {
|
|
||||||
setStatus("Load a catalog before exporting.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = buildExportProfile();
|
|
||||||
downloadTextFile("of-spawn-profile.json", `${JSON.stringify(payload, null, 2)}\n`);
|
|
||||||
setStatus("Profile exported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
function onResetSelected() {
|
|
||||||
if (!state.catalog || !state.selectedItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const item = state.catalog.items.find((it) => it.item === state.selectedItem);
|
|
||||||
const entry = getProfileEntry(state.selectedItem);
|
|
||||||
if (!item || !entry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entry.enabled = item.defaultEnabled;
|
|
||||||
entry.placements = { ...item.defaultPlacements };
|
|
||||||
renderItemTable();
|
|
||||||
renderSelectedDetails();
|
|
||||||
setStatus(`Reset ${item.shortId} to catalog defaults.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
dom.catalogFile.addEventListener("change", onCatalogFileSelected);
|
|
||||||
dom.profileFile.addEventListener("change", onProfileFileSelected);
|
|
||||||
dom.exportProfile.addEventListener("click", onExportProfile);
|
|
||||||
dom.searchInput.addEventListener("input", renderItemTable);
|
|
||||||
dom.categoryFilter.addEventListener("change", renderItemTable);
|
|
||||||
dom.spawnFilter.addEventListener("change", renderItemTable);
|
|
||||||
dom.resetSelected.addEventListener("click", onResetSelected);
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Opinionated Firearms Spawn List Builder</title>
|
|
||||||
<link rel="stylesheet" href="./styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="topbar">
|
|
||||||
<div>
|
|
||||||
<h1>Opinionated Firearms Spawn List Builder</h1>
|
|
||||||
<p>Edit firearm/attachment spawn enablement, placement lists, and spawn rates.</p>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<label class="file-button">
|
|
||||||
Load Catalog JSON
|
|
||||||
<input id="catalogFile" type="file" accept=".json,application/json">
|
|
||||||
</label>
|
|
||||||
<label class="file-button secondary">
|
|
||||||
Load Profile JSON
|
|
||||||
<input id="profileFile" type="file" accept=".json,application/json">
|
|
||||||
</label>
|
|
||||||
<button id="exportProfile" type="button">Export Profile JSON</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="status">
|
|
||||||
<span id="statusText">Load an extracted catalog JSON to begin.</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<main class="layout">
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-head">
|
|
||||||
<h2>Items</h2>
|
|
||||||
<div class="filters">
|
|
||||||
<input id="searchInput" type="search" placeholder="Search item ID...">
|
|
||||||
<select id="categoryFilter">
|
|
||||||
<option value="all">All categories</option>
|
|
||||||
<option value="firearm">Firearms</option>
|
|
||||||
<option value="attachment">Attachments</option>
|
|
||||||
<option value="unknown">Unknown</option>
|
|
||||||
</select>
|
|
||||||
<select id="spawnFilter">
|
|
||||||
<option value="all">All spawn states</option>
|
|
||||||
<option value="enabled">Spawn enabled</option>
|
|
||||||
<option value="disabled">Spawn disabled</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Spawn</th>
|
|
||||||
<th>Item</th>
|
|
||||||
<th>Category</th>
|
|
||||||
<th>Spawn Loacation</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="itemTableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel details-panel">
|
|
||||||
<div class="panel-head">
|
|
||||||
<h2>Selected Item</h2>
|
|
||||||
<button id="resetSelected" type="button">Reset to Catalog</button>
|
|
||||||
</div>
|
|
||||||
<div id="selectedDetails" class="details-empty">Select an item to edit placements and spawn rate.</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script src="./app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg: #f4efe6;
|
|
||||||
--panel: #fffaf1;
|
|
||||||
--panel-2: #f8f0e2;
|
|
||||||
--line: #d8ccb8;
|
|
||||||
--text: #2f2418;
|
|
||||||
--muted: #7d6852;
|
|
||||||
--accent: #9c3f1f;
|
|
||||||
--accent-2: #4f6f52;
|
|
||||||
--focus: #c77800;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
|
|
||||||
color: var(--text);
|
|
||||||
background:
|
|
||||||
radial-gradient(1200px 700px at 95% 0%, #e2c8a0 0%, transparent 60%),
|
|
||||||
linear-gradient(145deg, #f8f2e7 0%, var(--bg) 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1.1rem 1.3rem 1rem;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
background: rgba(255, 250, 241, 0.8);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar p {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-button,
|
|
||||||
button {
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
background: var(--accent);
|
|
||||||
padding: 0.48rem 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-button.secondary {
|
|
||||||
border-color: var(--accent-2);
|
|
||||||
background: var(--accent-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-button input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover,
|
|
||||||
.file-button:hover {
|
|
||||||
filter: brightness(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus-visible,
|
|
||||||
.file-button:focus-within {
|
|
||||||
outline: 2px solid var(--focus);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
padding: 0.5rem 1.3rem;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--muted);
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(320px, 1.1fr) minmax(320px, 0.9fr);
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 1rem 1.3rem 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 0.8rem;
|
|
||||||
min-height: 66vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.75rem 0.8rem;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
background: var(--panel-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-head h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.45rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="search"],
|
|
||||||
select,
|
|
||||||
input[type="number"],
|
|
||||||
input[type="text"] {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: #fff;
|
|
||||||
color: var(--text);
|
|
||||||
border-radius: 0.45rem;
|
|
||||||
padding: 0.35rem 0.45rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus-visible,
|
|
||||||
select:focus-visible {
|
|
||||||
outline: 2px solid var(--focus);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrap {
|
|
||||||
overflow: auto;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.45rem 0.55rem;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
font-size: 0.83rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
background: #fff2de;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr.selected {
|
|
||||||
background: #f9dfbc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-panel {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-body {
|
|
||||||
padding: 0.8rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-empty {
|
|
||||||
padding: 1.1rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.15rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placements-table th,
|
|
||||||
.placements-table td {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 0.34rem 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-btn {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: #fff;
|
|
||||||
color: var(--text);
|
|
||||||
border-radius: 0.35rem;
|
|
||||||
padding: 0.2rem 0.45rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-btn.remove {
|
|
||||||
border-color: #b74a38;
|
|
||||||
color: #8f2617;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
margin: 0.7rem 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
.layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
min-height: 45vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||