Intial Commit
This commit is contained in:
@@ -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")
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user