diff --git a/42.20/media/lua/client/QMIA_Integration.lua b/42.20/media/lua/client/QMIA_Integration.lua new file mode 100644 index 0000000..bfc8118 --- /dev/null +++ b/42.20/media/lua/client/QMIA_Integration.lua @@ -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") diff --git a/42.20/media/lua/server/QMIA_Server.lua b/42.20/media/lua/server/QMIA_Server.lua new file mode 100644 index 0000000..d52cbac --- /dev/null +++ b/42.20/media/lua/server/QMIA_Server.lua @@ -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) diff --git a/42.20/media/lua/shared/QMIA_State.lua b/42.20/media/lua/shared/QMIA_State.lua new file mode 100644 index 0000000..1c5179a --- /dev/null +++ b/42.20/media/lua/shared/QMIA_State.lua @@ -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 diff --git a/42.20/media/lua/shared/TimedActions/QMIA_ArrangeAction.lua b/42.20/media/lua/shared/TimedActions/QMIA_ArrangeAction.lua new file mode 100644 index 0000000..6f98db2 --- /dev/null +++ b/42.20/media/lua/shared/TimedActions/QMIA_ArrangeAction.lua @@ -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 diff --git a/42.20/mod.info b/42.20/mod.info new file mode 100644 index 0000000..fc44c19 --- /dev/null +++ b/42.20/mod.info @@ -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. diff --git a/README.md b/README.md index 7a4a916..c242e51 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,78 @@ # QuartmasterArrange -Quartermaster X Item Arrange \ No newline at end of file +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. diff --git a/common/.keep b/common/.keep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/common/.keep @@ -0,0 +1 @@ +