Compare commits

7 Commits
pages ... main

Author SHA1 Message Date
06593805a7 add GGS Support 2026-02-16 00:33:14 -05:00
53b0b4317d Update to make catagories 2026-02-15 13:11:32 -05:00
ad8310f143 Finished 2026-02-14 00:13:03 -05:00
5ff5764fa2 Clear for new thing 2026-02-13 20:38:51 -05:00
c320e8d993 update 2026-02-12 15:09:07 -05:00
dd2d7a3abe ser 2026-02-11 23:50:06 -05:00
9c86fe30f6 foor 2026-02-11 23:44:58 -05:00
37 changed files with 2009 additions and 106481 deletions

BIN
42/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -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)

View File

@@ -1,8 +1,9 @@
name=Opinionated Firearms
id=opinionated_firearms
id=hrsys_opinionated_firearms_testing
author=Riggs0
modversion=1.0.0
versionMin=42.12.13
require=GaelGunStore_ALPHA
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).
require=\2788256295/ammomaker,\HBVCEFb42
description=Opinionated Firearms casing and other changes to Guns of 93, Ammomaker and Hot Brass
icon=icon.png
poster=preview.png

BIN
42/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
art/icon.psd Normal file

Binary file not shown.

BIN
art/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
art/logo.psd Normal file

Binary file not shown.

BIN
art/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
art/preview.psd Normal file

Binary file not shown.

BIN
art/workshop-preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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",
},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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" },
},
}

View File

@@ -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" },
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 = {},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View 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",
}

View 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.",
}

View 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,
}

View File

@@ -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,
},
},
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
]
}
]
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,8 +1,9 @@
name=Opinionated Firearms
id=opinionated_firearms
id=hrsys_opinionated_firearms
author=Riggs0
modversion=1.0.0
versionMin=42.12.13
require=GaelGunStore_ALPHA
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).
require=\2788256295/ammomaker,\HBVCEFb42
description=Opinionated Firearms casing and other changes to Guns of 93, Ammomaker and Hot Brass
icon=icon.png
poster=preview.png

BIN
preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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;
}
}