This commit is contained in:
2026-02-12 15:09:07 -05:00
parent dd2d7a3abe
commit c320e8d993
13 changed files with 9168 additions and 30 deletions

View File

@@ -18,6 +18,45 @@ end
local config = safeRequire("OFBlockConfig") local config = safeRequire("OFBlockConfig")
local spawnProfile = safeRequire("OFSpawnProfile") local spawnProfile = safeRequire("OFSpawnProfile")
local sourceCatalog = safeRequire("OFSourceCatalog")
local function buildLookupSet(values)
local lookup = {}
if type(values) ~= "table" then
return lookup
end
for _, value in ipairs(values) do
local s = tostring(value):lower()
lookup[s] = true
end
return lookup
end
local attachmentTypeLookup = buildLookupSet(sourceCatalog.attachments)
local magazineTypeLookup = buildLookupSet(sourceCatalog.magazines)
local function isMagazineType(itemType)
local s = tostring(itemType or ""):lower()
if s == "" then
return false
end
if magazineTypeLookup[s] then
return true
end
if s:sub(1, 10) == "base.clip_" then
return true
end
if s:find("magazine", 1, true) then
return true
end
if s:find("drum", 1, true) then
return true
end
if s:find("clip", 1, true) then
return true
end
return false
end
local function trim(value) local function trim(value)
if type(value) ~= "string" then if type(value) ~= "string" then
@@ -40,6 +79,25 @@ local function normalizeItemType(value)
return "Base." .. s return "Base." .. s
end end
local function getMagazinePartAlias(itemType)
local normalized = normalizeItemType(itemType)
if not normalized then
return nil
end
local lowered = normalized:lower()
if not isMagazineType(lowered) then
return nil
end
if lowered:sub(1, 10) == "base.clip_" then
return nil
end
local short = normalized:match("^Base%.(.+)$")
if not short or short == "" then
return nil
end
return "Base.Clip_" .. short
end
local function normalizePrefix(value) local function normalizePrefix(value)
local s = trim(value) local s = trim(value)
if not s or s == "" then if not s or s == "" then
@@ -271,9 +329,16 @@ local ruleMatchers = compileRules(config.rules, aliasMap)
local function compileSpawnProfile(rawProfile) local function compileSpawnProfile(rawProfile)
local managedItemSet = {} local managedItemSet = {}
local disabledManagedItemSet = {}
local placementsByList = {} local placementsByList = {}
local managedCount = 0 local managedCount = 0
local function clearPlacementsForItem(itemType)
for _, entries in pairs(placementsByList) do
entries[itemType] = nil
end
end
local function addPlacement(listName, itemType, rawWeight) local function addPlacement(listName, itemType, rawWeight)
local cleanList = trim(listName) local cleanList = trim(listName)
local weight = tonumber(rawWeight) local weight = tonumber(rawWeight)
@@ -303,9 +368,18 @@ local function compileSpawnProfile(rawProfile)
end end
if entry.enabled == false then if entry.enabled == false then
disabledManagedItemSet[lowered] = true
local partAlias = getMagazinePartAlias(normalized)
if partAlias then
managedItemSet[partAlias:lower()] = true
disabledManagedItemSet[partAlias:lower()] = true
end
clearPlacementsForItem(normalized)
return return
end end
disabledManagedItemSet[lowered] = nil
if type(entry.placements) == "table" then if type(entry.placements) == "table" then
if entry.placements[1] then if entry.placements[1] then
for _, row in ipairs(entry.placements) do for _, row in ipairs(entry.placements) do
@@ -337,10 +411,10 @@ local function compileSpawnProfile(rawProfile)
end end
end end
return managedItemSet, placementsByList, managedCount return managedItemSet, disabledManagedItemSet, placementsByList, managedCount
end end
local managedSpawnItems, profilePlacementsByList, managedSpawnItemCount = compileSpawnProfile(spawnProfile) local managedSpawnItems, disabledManagedSpawnItems, profilePlacementsByList, managedSpawnItemCount = compileSpawnProfile(spawnProfile)
local function isRuleActive(rule, nowEpoch) local function isRuleActive(rule, nowEpoch)
if not rule.enabled then if not rule.enabled then
@@ -389,6 +463,51 @@ local function shouldBlock(listName, itemType, nowEpoch)
return false return false
end end
local function shouldBlockByRulesOnly(listName, itemType, nowEpoch)
local listLower = (trim(listName) or "unknown"):lower()
local itemLower = normalizeItemType(itemType)
if not itemLower then
return false
end
itemLower = itemLower:lower()
if itemMatches(itemLower, globalMatcher) then
return true
end
for _, entry in ipairs(byListMatchers) do
if entry.pattern ~= "" and listMatchesPattern(listLower, entry.pattern) and itemMatches(itemLower, entry.matcher) then
return true
end
end
for _, rule in ipairs(ruleMatchers) do
if isRuleActive(rule, nowEpoch) and listMatches(listLower, rule.listPatterns) and itemMatches(itemLower, rule.matcher) then
return true
end
end
return false
end
local function isWeaponPartBlockedForUpgrades(itemType, nowEpoch)
local normalized = normalizeItemType(itemType)
if not normalized then
return false
end
local lowered = normalized:lower()
if not attachmentTypeLookup[lowered] and not isMagazineType(lowered) then
return false
end
if disabledManagedSpawnItems[lowered] then
return true
end
return shouldBlockByRulesOnly("GGSWeaponUpgrades", normalized, nowEpoch)
end
local function removeBlockedEntries(items, listName, nowEpoch) local function removeBlockedEntries(items, listName, nowEpoch)
if type(items) ~= "table" then if type(items) ~= "table" then
return 0 return 0
@@ -503,6 +622,52 @@ local function applyProfilePlacements()
return added return added
end end
local function removeBlockedAttachmentEntries(entries, nowEpoch)
if type(entries) ~= "table" then
return 0
end
local removed = 0
for i = #entries, 1, -1 do
local value = entries[i]
if type(value) == "string" and isWeaponPartBlockedForUpgrades(value, nowEpoch) then
table.remove(entries, i)
removed = removed + 1
end
end
return removed
end
local function patchWeaponUpgradeAttachmentSources(nowEpoch)
local removed = 0
if type(GGSWeaponUpgrades) == "table" and type(GGSWeaponUpgrades.Lists) == "table" then
for _, listDef in pairs(GGSWeaponUpgrades.Lists) do
if type(listDef) == "table" then
if type(listDef.items) == "table" then
removed = removed + removeBlockedAttachmentEntries(listDef.items, nowEpoch)
elseif listDef[1] ~= nil then
removed = removed + removeBlockedAttachmentEntries(listDef, nowEpoch)
end
end
end
end
if type(WeaponUpgrades) == "table" then
for _, listDef in pairs(WeaponUpgrades) do
if type(listDef) == "table" then
if type(listDef.items) == "table" then
removed = removed + removeBlockedAttachmentEntries(listDef.items, nowEpoch)
elseif listDef[1] ~= nil then
removed = removed + removeBlockedAttachmentEntries(listDef, nowEpoch)
end
end
end
end
return removed
end
local function patchAllDistributions() local function patchAllDistributions()
local nowEpoch = nil local nowEpoch = nil
if os and os.time then if os and os.time then
@@ -513,13 +678,14 @@ local function patchAllDistributions()
removed = removed + patchProceduralDistributions(nowEpoch) removed = removed + patchProceduralDistributions(nowEpoch)
removed = removed + patchNestedDistributionTree("SuburbsDistributions", SuburbsDistributions, nowEpoch) removed = removed + patchNestedDistributionTree("SuburbsDistributions", SuburbsDistributions, nowEpoch)
removed = removed + patchNestedDistributionTree("VehicleDistributions", VehicleDistributions, nowEpoch) removed = removed + patchNestedDistributionTree("VehicleDistributions", VehicleDistributions, nowEpoch)
local upgradeAttachmentRemoved = patchWeaponUpgradeAttachmentSources(nowEpoch)
local added = applyProfilePlacements() local added = applyProfilePlacements()
if ItemPickerJava and ItemPickerJava.Parse then if ItemPickerJava and ItemPickerJava.Parse then
ItemPickerJava.Parse() ItemPickerJava.Parse()
end end
print(string.format("[OFDistributionBlocker] Removed %d entries and added %d profile entries (managed items: %d).", removed, added, managedSpawnItemCount)) print(string.format("[OFDistributionBlocker] Removed %d distro entries, removed %d upgrade attachments, and added %d profile entries (managed items: %d).", removed, upgradeAttachmentRemoved, added, managedSpawnItemCount))
end end
Events.OnInitWorld.Add(patchAllDistributions) Events.OnInitWorld.Add(patchAllDistributions)

View File

@@ -1,8 +1,8 @@
name=Opinionated Firearms name=Opinionated Firearms
id=opinionated_firearms id=hrsys_opinionated_firearms
author=Riggs0 author=Riggs0
modversion=1.0.0 modversion=1.0.0
versionMin=42.12.13 versionMin=42.12.13
require=GaelGunStore_ALPHA require=\GaelGunStore_ALPHA
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42). description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).

View File

@@ -1,6 +1,6 @@
# Opinionated Firearms # Opinionated Firearms
Project Zomboid B42 patch mod + tooling for managing `GaelGunStore` firearm/attachment spawn distribution. Project Zomboid B42 patch mod + tooling for managing `GaelGunStore` firearm/attachment/magazine spawn distribution.
## Workflow ## Workflow
@@ -27,6 +27,7 @@ node tools/ggs-dist-cli.js extract --ggs-root source/GaelGunStore/42 --out data/
Output contains: Output contains:
- all firearms/attachments from GGS scripts - all firearms/attachments from GGS scripts
- magazines from GGS loot distribution data
- where they spawn (`list`) - where they spawn (`list`)
- base spawn weight (`weight`) - base spawn weight (`weight`)
- sandbox key (`sv`) used by GGS spawn multipliers - sandbox key (`sv`) used by GGS spawn multipliers
@@ -65,6 +66,8 @@ Main patcher: `42/media/lua/server/distribution/OFDistributionBlocker.lua`
- loads block rules (`OFBlockConfig`) and spawn profile (`OFSpawnProfile`) - loads block rules (`OFBlockConfig`) and spawn profile (`OFSpawnProfile`)
- removes blocked/managed entries from distributions - removes blocked/managed entries from distributions
- removes disabled attachment entries from weapon auto-upgrade attachment pools
- removes disabled magazine entries from weapon auto-upgrade attachment pools
- re-adds managed item placements with chosen weights from spawn profile - re-adds managed item placements with chosen weights from spawn profile
- reparses ItemPicker after patching - reparses ItemPicker after patching

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.

View File

@@ -76,6 +76,44 @@ local function mergeByList(baseByList, extraByList)
return merged return merged
end end
local function isMagazineType(itemType)
local s = tostring(itemType or ""):lower()
if s == "" then
return false
end
if s:sub(1, 10) == "base.clip_" then
return true
end
if s:find("magazine", 1, true) then
return true
end
if s:find("drum", 1, true) then
return true
end
if s:find("clip", 1, true) then
return true
end
return false
end
local function collectMagazines(values)
local out = {}
local seen = {}
if type(values) ~= "table" then
return out
end
for _, value in ipairs(values) do
if isMagazineType(value) then
local key = tostring(value):lower()
if not seen[key] then
seen[key] = true
out[#out + 1] = value
end
end
end
return out
end
local defaults = safeRequire("OFBlockRules_Default") local defaults = safeRequire("OFBlockRules_Default")
local user = safeRequire("OFBlockRules_User") local user = safeRequire("OFBlockRules_User")
local sourceCatalog = safeRequire("OFSourceCatalog") local sourceCatalog = safeRequire("OFSourceCatalog")
@@ -83,6 +121,7 @@ local sourceCatalog = safeRequire("OFSourceCatalog")
local aliasCatalog = { local aliasCatalog = {
firearms = sourceCatalog.firearms or {}, firearms = sourceCatalog.firearms or {},
attachments = sourceCatalog.attachments or {}, attachments = sourceCatalog.attachments or {},
magazines = sourceCatalog.magazines or collectMagazines(sourceCatalog.attachments),
ggs_all = sourceCatalog.ggs_all or {}, ggs_all = sourceCatalog.ggs_all or {},
} }

View File

@@ -1,5 +1,34 @@
-- Auto-generated by tools/ggs-dist-cli.js apply -- Auto-generated by tools/ggs-dist-cli.js apply
-- Keep this empty template in git; generated content can overwrite it. -- Generated at: 2026-02-12T19:59:03.395Z
return { return {
items = {}, items = {
["Base.12GClip"] = {
enabled = false,
placements = {
["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1,
},
},
["Base.1P78"] = {
enabled = false,
placements = {
["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1,
},
},
["Base.9mmClip"] = {
enabled = false,
placements = {
["GunStoreMagsAmmo"] = 1,
["PoliceStorageGuns"] = 1,
},
},
["Base.A2000"] = {
enabled = true,
placements = {
["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1,
},
},
},
} }

View File

@@ -1,22 +1,29 @@
-- Auto-generated by tools/ggs-dist-cli.js apply -- Auto-generated by tools/ggs-dist-cli.js apply
-- Generated at: 2026-02-11T22:49:03.184Z -- Generated at: 2026-02-12T19:50:17.985Z
return { return {
items = { items = {
["Base.1P78"] = { ["Base.12GClip"] = {
enabled = true,
placements = {
["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1,
},
},
["Base.1PN93_4"] = {
enabled = false, enabled = false,
placements = { placements = {
["ArmyStorageAmmunition"] = 1, ["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1, ["ArmyStorageGuns"] = 1,
}, },
}, },
["Base.9x39_Silencer"] = { ["Base.1P78"] = {
enabled = false,
placements = {
["ArmyStorageAmmunition"] = 1,
["ArmyStorageGuns"] = 1,
},
},
["Base.9mmClip"] = {
enabled = false,
placements = {
["GunStoreMagsAmmo"] = 1,
["PoliceStorageGuns"] = 1,
},
},
["Base.A2000"] = {
enabled = true, enabled = true,
placements = { placements = {
["ArmyStorageAmmunition"] = 1, ["ArmyStorageAmmunition"] = 1,

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
{ {
"formatVersion": 1, "formatVersion": 1,
"generatedAt": "2026-02-12T19:50:12.875Z",
"entries": [ "entries": [
{ {
"item": "Base.1P78", "item": "Base.A2000",
"enabled": true, "enabled": true,
"placements": [ "placements": [
{ {
@@ -16,7 +17,7 @@
] ]
}, },
{ {
"item": "Base.1PN93_4", "item": "Base.1P78",
"enabled": false, "enabled": false,
"placements": [ "placements": [
{ {
@@ -30,8 +31,8 @@
] ]
}, },
{ {
"item": "Base.9x39_Silencer", "item": "Base.12GClip",
"enabled": true, "enabled": false,
"placements": [ "placements": [
{ {
"list": "ArmyStorageAmmunition", "list": "ArmyStorageAmmunition",
@@ -42,6 +43,20 @@
"weight": 1 "weight": 1
} }
] ]
},
{
"item": "Base.9mmClip",
"enabled": false,
"placements": [
{
"list": "GunStoreMagsAmmo",
"weight": 1
},
{
"list": "PoliceStorageGuns",
"weight": 1
}
]
} }
] ]
} }

View File

@@ -1,8 +1,8 @@
name=Opinionated Firearms name=Opinionated Firearms
id=opinionated_firearms id=hrsys_opinionated_firearms
author=Riggs0 author=Riggs0
modversion=1.0.0 modversion=1.0.0
versionMin=42.12.13 versionMin=42.12.13
require=GaelGunStore_ALPHA require=\GaelGunStore_ALPHA
description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42). description=Opinionated Firearms spawn distribution controller for GaelGunStore (B42).

View File

@@ -11,7 +11,7 @@ Usage:
node tools/ggs-dist-cli.js apply --profile <file> [--out <file>] node tools/ggs-dist-cli.js apply --profile <file> [--out <file>]
Commands: Commands:
extract Build a full firearm/attachment spawn catalog from GaelGunStore. extract Build a full firearm/attachment/magazine spawn catalog from GaelGunStore.
apply Convert a webapp profile JSON into Lua used by the B42 mod patcher. apply Convert a webapp profile JSON into Lua used by the B42 mod patcher.
`.trim()); `.trim());
} }
@@ -117,6 +117,26 @@ function normalizeItemType(item) {
return trimmed.includes(".") ? trimmed : `Base.${trimmed}`; return trimmed.includes(".") ? trimmed : `Base.${trimmed}`;
} }
function isMagazineType(itemType) {
const s = String(itemType || "").toLowerCase();
if (!s) {
return false;
}
if (s.startsWith("base.clip_")) {
return true;
}
if (s.includes("magazine")) {
return true;
}
if (s.includes("drum")) {
return true;
}
if (s.includes("clip")) {
return true;
}
return false;
}
function buildCatalog(ggsRoot) { function buildCatalog(ggsRoot) {
const cwd = process.cwd(); const cwd = process.cwd();
const firearmScriptsDir = path.join(ggsRoot, "media", "scripts", "Firearms"); const firearmScriptsDir = path.join(ggsRoot, "media", "scripts", "Firearms");
@@ -137,12 +157,24 @@ function buildCatalog(ggsRoot) {
const attachments = collectItemTypesFromScripts(attachmentScriptsDir); const attachments = collectItemTypesFromScripts(attachmentScriptsDir);
const firearmSet = new Set(firearms); const firearmSet = new Set(firearms);
const attachmentSet = new Set(attachments); const attachmentSet = new Set(attachments);
const magazineSet = new Set();
const lootEntries = parseLootEntries(lootLuaPath); const lootEntries = parseLootEntries(lootLuaPath);
const perItemLoot = new Map(); const perItemLoot = new Map();
const allLists = new Set(); const allLists = new Set();
const allItems = new Set([...firearms, ...attachments]); const allItems = new Set([...firearms, ...attachments]);
for (const entry of lootEntries) {
const normalized = normalizeItemType(entry.item);
if (!normalized) {
continue;
}
if (isMagazineType(normalized)) {
magazineSet.add(normalized);
allItems.add(normalized);
}
}
for (const entry of lootEntries) { for (const entry of lootEntries) {
const normalized = normalizeItemType(entry.item); const normalized = normalizeItemType(entry.item);
if (!normalized) { if (!normalized) {
@@ -171,6 +203,8 @@ function buildCatalog(ggsRoot) {
let category = "unknown"; let category = "unknown";
if (firearmSet.has(itemType)) { if (firearmSet.has(itemType)) {
category = "firearm"; category = "firearm";
} else if (magazineSet.has(itemType)) {
category = "magazine";
} else if (attachmentSet.has(itemType)) { } else if (attachmentSet.has(itemType)) {
category = "attachment"; category = "attachment";
} }
@@ -202,6 +236,7 @@ function buildCatalog(ggsRoot) {
counts: { counts: {
firearms: firearms.length, firearms: firearms.length,
attachments: attachments.length, attachments: attachments.length,
magazines: magazineSet.size,
totalItems: items.length, totalItems: items.length,
placementRows: lootEntries.length, placementRows: lootEntries.length,
distributionLists: allLists.size, distributionLists: allLists.size,
@@ -296,7 +331,7 @@ function commandExtract(args) {
console.log(`Extracted catalog: ${outPath}`); console.log(`Extracted catalog: ${outPath}`);
console.log( console.log(
`Items=${catalog.counts.totalItems}, Firearms=${catalog.counts.firearms}, Attachments=${catalog.counts.attachments}, Lists=${catalog.counts.distributionLists}` `Items=${catalog.counts.totalItems}, Firearms=${catalog.counts.firearms}, Attachments=${catalog.counts.attachments}, Magazines=${catalog.counts.magazines}, Lists=${catalog.counts.distributionLists}`
); );
} }

View File

@@ -10,7 +10,7 @@
<header class="topbar"> <header class="topbar">
<div> <div>
<h1>Opinionated Firearms Spawn List Builder</h1> <h1>Opinionated Firearms Spawn List Builder</h1>
<p>Edit firearm/attachment spawn enablement, placement lists, and spawn rates.</p> <p>Edit firearm/attachment/magazine spawn enablement, placement lists, and spawn rates.</p>
</div> </div>
<div class="actions"> <div class="actions">
<label class="file-button"> <label class="file-button">
@@ -39,6 +39,7 @@
<option value="all">All categories</option> <option value="all">All categories</option>
<option value="firearm">Firearms</option> <option value="firearm">Firearms</option>
<option value="attachment">Attachments</option> <option value="attachment">Attachments</option>
<option value="magazine">Magazines</option>
<option value="unknown">Unknown</option> <option value="unknown">Unknown</option>
</select> </select>
<select id="spawnFilter"> <select id="spawnFilter">