Intial Commit

This commit is contained in:
2026-10-27 17:15:10 -04:00
parent bf6427aac1
commit 1665a8a2f1
7 changed files with 327 additions and 1 deletions
+101
View File
@@ -0,0 +1,101 @@
require "QMIA_State"
require "TimedActions/QMIA_ArrangeAction"
require "LootSorter_Core"
require "LootSorter_Plan"
require "LootSorter_DepositAction"
require "LootSorter_ContextMenu"
local function feedback(playerNum, accepted, enabled)
local player = getSpecificPlayer(playerNum)
if not player then return end
local message = "Could not update Item Arrange mode."
if accepted then
message = enabled and "Item Arrange enabled. Configure sorting to select items."
or "Item Arrange disabled. Deliveries use container storage."
end
LootSorter.notify(player, message)
end
function QMIA.setMark(obj, ci, enabled, playerNum)
local player = getSpecificPlayer(playerNum)
local square = obj and obj:getSquare()
if not player or not QMIA.canAccess(square, player) then return end
local args = { x = square:getX(), y = square:getY(), z = square:getZ(),
oi = obj:getObjectIndex(), ci = ci, enabled = enabled, playerNum = playerNum }
if isClient() then
sendClientCommand(player, QMIA.MOD_ID, "setMark", args)
else
feedback(playerNum, QMIA.applyRequest(player, args), enabled)
end
end
-- Hook Quartermaster's common entry point: both world and inventory menus, every
-- stacked object and every separate container index receive the toggle.
if not QMIA.menuInstalled then
QMIA.menuInstalled = true
local original = LootSorter.addBoxOptions
function LootSorter.addBoxOptions(menu, obj, ci, suffix, playerNum)
original(menu, obj, ci, suffix, playerNum)
local square = obj and obj:getSquare()
if not QMIA.canAccess(square, getSpecificPlayer(playerNum)) then return end
local marked = QMIA.isMarked(obj, ci)
local label = (marked and "[x] Item Arrange" or "[ ] Item Arrange") .. (suffix or "")
local option = menu:addOption(label, nil, function()
QMIA.setMark(obj, ci, not marked, playerNum)
end)
option.onHighlightParams = { obj }
option.onHighlight = LootSorter.onHighlightBox
local tip = ISWorldObjectContextMenu.addToolTip()
tip.description = "Deliver matching items to this container's tile using Item Arrange. "
.. "Uses the same placement as Shift-dropping. No Shift key or mouse aim needed. "
.. "Choose matching categories/items with Configure sorting."
option.toolTip = tip
end
end
-- This changes the destination only. Quartermaster still loads bags, walks, plans
-- trips and protects equipped/favorited/hotbar items using its own implementation.
if not QMIA.depositInstalled then
QMIA.depositInstalled = true
local original = LootSorter.DepositAction.new
function LootSorter.DepositAction:new(character, item, srcContainer, destContainer)
local obj, ci = QMIA.containerTarget(destContainer)
if obj and QMIA.isMarked(obj, ci) then
return QMIArrangeAction:new(character, item, srcContainer, destContainer)
end
return original(self, character, item, srcContainer, destContainer)
end
end
-- A marked container represents display space, so its internal weight capacity
-- must not prevent routing to it. IA.CheckSpace controls placement at execution.
-- Copy records: no global ItemContainer methods or original planner data change.
if not QMIA.plannerInstalled then
QMIA.plannerInstalled = true
local original = LootSorter.planTrips
function LootSorter.planTrips(items, boxes, carriers, budget, accepts)
local adjusted = {}
for i, box in ipairs(boxes) do
local obj, ci = QMIA.containerTarget(box.container)
if obj and QMIA.isMarked(obj, ci) then
local copy = {}
for key, value in pairs(box) do copy[key] = value end
copy.capacity, copy.used = math.huge, 0
adjusted[i] = copy
else
adjusted[i] = box
end
end
return original(items, adjusted, carriers, budget, accepts)
end
end
if not QMIA.resultsInstalled then
QMIA.resultsInstalled = true
Events.OnServerCommand.Add(function(module, command, args)
if module ~= QMIA.MOD_ID or command ~= "markResult" or not args then return end
feedback(tonumber(args.playerNum) or 0, args.accepted, args.enabled)
end)
end
print("[QuartermasterItemArrange] compatibility loaded")
+11
View File
@@ -0,0 +1,11 @@
require "QMIA_State"
Events.OnClientCommand.Add(function(module, command, player, args)
if module ~= QMIA.MOD_ID or command ~= "setMark" then return end
local accepted = QMIA.applyRequest(player, args)
local payload = type(args) == "table" and args or {}
sendServerCommand(player, QMIA.MOD_ID, "markResult", {
accepted = accepted, enabled = payload.enabled == true,
playerNum = payload.playerNum,
})
end)
+60
View File
@@ -0,0 +1,60 @@
-- Per-compartment flags on the original world object. No Quartermaster data is changed.
QMIA = QMIA or {}
QMIA.MOD_ID = "QuartermasterItemArrange"
QMIA.DATA_KEY = "QMIArrange"
function QMIA.isMarked(obj, ci)
if not obj or ci == nil or not obj:hasModData() then return false end
local flags = obj:getModData()[QMIA.DATA_KEY]
return type(flags) == "table" and flags[tostring(ci)] == true
end
function QMIA.containerTarget(container)
if not container then return nil end
local obj = container:getParent()
if not obj or not obj.getContainerCount or not obj.getSquare then return nil end
for ci = 0, obj:getContainerCount() - 1 do
if obj:getContainerByIndex(ci) == container then return obj, ci end
end
end
-- Also called by the authoritative timed action, where Quartermaster's client helpers
-- aren't loaded. Respect safehouse rules on both sides of a multiplayer connection.
function QMIA.canAccess(square, player)
if not square or not player then return false end
if SafeHouse and SafeHouse.isSafehouseAllowLoot then
return SafeHouse.isSafehouseAllowLoot(square, player) ~= false
end
return true
end
function QMIA.writeMark(obj, ci, enabled)
local md = obj:getModData()
local flags = md[QMIA.DATA_KEY] or {}
flags[tostring(ci)] = enabled and true or nil
md[QMIA.DATA_KEY] = flags
obj:transmitModData()
end
-- Validate a client request against the current world. No client-supplied ModData
-- snapshot is accepted, so changing one compartment cannot overwrite another.
function QMIA.applyRequest(player, args)
if type(args) ~= "table" or type(args.enabled) ~= "boolean" then return false end
for _, key in ipairs({ "x", "y", "z", "oi", "ci" }) do
local value = args[key]
if type(value) ~= "number" or value ~= value or value == math.huge
or value == -math.huge or value ~= math.floor(value) then return false end
end
if args.ci < 0 or args.oi < 0 or not player then return false end
local square = getCell():getGridSquare(args.x, args.y, args.z)
if not QMIA.canAccess(square, player) then return false end
if math.abs(player:getX() - args.x) > 20 or math.abs(player:getY() - args.y) > 20
or math.abs(player:getZ() - args.z) > 1 then return false end
local objects = square:getObjects()
if args.oi >= objects:size() then return false end
local obj = objects:get(args.oi)
if not obj or not obj.getContainerCount or args.ci >= obj:getContainerCount()
or not obj:getContainerByIndex(args.ci) then return false end
QMIA.writeMark(obj, args.ci, args.enabled)
return true
end
@@ -0,0 +1,70 @@
require "QMIA_State"
require "IAArrange"
require "TimedActions/IAArrangeAction"
-- Use Item Arrange's actual drop/arrange implementation and duration. Keeping this
-- class in shared makes its complete() available to Build 42 multiplayer servers.
QMIArrangeAction = IAArrangeAction:derive("QMIArrangeAction")
function QMIArrangeAction:new(character, item, srcContainer, destContainer)
local obj = QMIA.containerTarget(destContainer)
local o = IAArrangeAction.new(self, character, item, srcContainer, obj and obj:getSquare())
o.destContainer = destContainer
o.stopOnWalk = true
o.stopOnRun = true
o.stopOnAim = true
return o
end
-- A full surface / missing load must not cancel the remaining Quartermaster haul.
-- Revalidate at start and complete instead; skipped items stay in their carrier.
function QMIArrangeAction:isValid() return true end
function QMIArrangeAction:canArrange()
local item, src, square, player = self.item, self.srcContainer, self.dropSquare, self.character
if not item or not src or not square or not player then return false end
local obj, ci = QMIA.containerTarget(self.destContainer)
if not obj or obj:getObjectIndex() < 0 or obj:getSquare() ~= square
or not QMIA.isMarked(obj, ci) then return false end
-- Only unload carried items. A skipped Quartermaster load is never teleported.
if not src:isInCharacterInventory(player) or not src:contains(item)
or item:getContainer() ~= src or not src:isRemoveItemAllowed(item) then return false end
if item:isEquipped() or item:isFavorite() or item:getIsCraftingConsumed() then return false end
if not QMIA.canAccess(square, player) then return false end
local here = player:getSquare()
if not here or here:getZ() ~= square:getZ()
or math.abs(here:getX() - square:getX()) > 1
or math.abs(here:getY() - square:getY()) > 1
or here:isSomethingTo(square) then return false end
return IA.CheckSpace(square, item) == true
end
function QMIArrangeAction:waitToStart()
if not self:canArrange() then return false end
self.character:faceLocation(self.dropSquare:getX(), self.dropSquare:getY())
local waitingForTransaction = IAArrangeAction.waitToStart(self)
return self.character:shouldBeTurning() or waitingForTransaction
end
function QMIArrangeAction:start()
self.qmiaSkip = not self:canArrange()
if self.qmiaSkip then
self.action:setTime(0)
return
end
self.character:faceLocation(self.dropSquare:getX(), self.dropSquare:getY())
IAArrangeAction.start(self)
end
function QMIArrangeAction:update()
if self.qmiaSkip then return end
self.character:faceLocation(self.dropSquare:getX(), self.dropSquare:getY())
IAArrangeAction.update(self)
end
function QMIArrangeAction:complete()
-- The server rechecks ownership, distance and available space immediately
-- before using Item Arrange's own removal/drop/arrange implementation.
if self.qmiaSkip or not self:canArrange() then return true end
return IAArrangeAction.complete(self)
end
+8
View File
@@ -0,0 +1,8 @@
name=Quartermaster - Item Arrange Compatibility
id=QuartermasterItemArrange
author=Local compatibility add-on
modversion=1.0.0
versionMin=42.20
require=Quartermaster,ItemArrange
loadAfter=Quartermaster,ItemArrange
description=Mark any Quartermaster-sortable container as Item Arrange. Matching deliveries go to its tile using Item Arrange's Shift-drop action, with automatic facing and no keyboard or mouse positioning required.
+76 -1
View File
@@ -1,3 +1,78 @@
# QuartmasterArrange # QuartmasterArrange
Quartermaster X Item Arrange Quartermaster x Item Arrange compatibility for Project Zomboid Build 42.20 and later.
Mod ID: `QuartermasterItemArrange`
Requires [Quartermaster](https://steamcommunity.com/sharedfiles/filedetails/?id=3761216758)
and [Item Arrange](https://steamcommunity.com/sharedfiles/filedetails/?id=3767639280).
## Repository layout
The repository root is the mod folder. It can be linked directly into `Zomboid/mods`:
```text
QuartmasterArrange/
common/
42.20/
mod.info
media/lua/...
```
From PowerShell, with the destination absent:
```powershell
New-Item -ItemType SymbolicLink -Path 'C:\Users\Hudson\Zomboid\mods\QuartermasterItemArrange' -Target 'C:\Users\Hudson\Git\QuartmasterArrange'
```
A physical copy of this mod was installed previously at that destination. Move it
outside the mods folder before creating the link. Windows may require Developer
Mode or an elevated shell for symbolic links.
Alternatively, copy this repository's `common` and `42.20` folders into a single
folder under `Zomboid/mods`. Keep them side by side; no additional nested mod folder
is needed.
## Use
1. Enable **Quartermaster**, **Item Arrange**, and **Quartermaster - Item Arrange Compatibility** for the save, then restart the game.
2. Right-click any container Quartermaster can configure and select **[ ] Item Arrange**. A checked option switches the mode off again.
3. Use **Configure sorting...** to select its categories, specific items, presets and priority.
4. Select **Distribute Loot**. Your character walks to the destination, faces its tile and uses Item Arrange's Shift-drop placement. You do not need to hold Shift or aim the mouse.
The toggle is also available in Quartermaster's inventory context menu. Stacked
objects and separate compartments can be marked independently. Normal destinations
keep their usual container transfers.
## Placement details
- A marked container represents display space on its world tile. Item Arrange controls the final positions on the floor or furniture surfaces.
- The container's internal weight capacity does not limit display deliveries. Available display space is checked when each item is delivered.
- Blocked or full destinations, removed objects and failed loads are skipped without cancelling the remaining haul. Items stay in their source or carrier. Failed display deliveries are not automatically rerouted.
- Item Arrange's item and surface support is unchanged. Unsupported items may become ordinary ground drops.
- Compartments and objects sharing a tile share that tile's arrangement space. The toggle cannot select a particular shelf on the same tile.
- Existing contents remain inside marked containers. Quartermaster still skips items already in their matching destination.
- Displayed items are world items, so they do not count as container contents for Quartermaster's manifest or crafting-from-storage.
- Marks are saved per compartment on the world object. Sorting copy/paste and presets do not copy the independent Item Arrange flag.
## Multiplayer
Install the mod on the server and every client. Add `QuartermasterItemArrange`
after `Quartermaster` and `ItemArrange` in the server's `Mods` list. The required
Workshop IDs are `3761216758` and `3767639280`. This add-on has no published
Workshop ID.
The server validates and saves marks. A shared timed action delegates placement to
Item Arrange. Live multiplayer behavior still needs an in-game test.
## Validation
Built against Quartermaster **0.24.0** and Item Arrange **2.9**. Passed **17 offline
Lua 5.1 integration scenarios** using their actual installed Lua implementations
with mocked game objects, including a full sort queue with mixed destinations.
Every shipping Lua file passed syntax compilation. The game and Java networking
were not exercised.
Start with one supported item delivered to a marked table or shelf, then try a
mixed haul to both a marked display and a normal crate. Repeat on the intended
multiplayer server before relying on it there.
+1
View File
@@ -0,0 +1 @@