dot-inventory is what is in a player’s bag right now: grid and slot
containers, stacks, weight, equipment slots and nested containers, mutated only
by operations a server can refuse individually.
Requires dot-core. dot-loadout and
dot-ui are optional and neither is named.
Every mutation is an op
inv.apply(DotInvOp.move(&"backpack", uid, &"belt", Vector2i(0, 0)))
Four things follow from that:
- The network shape. A client sends an op; a server answers yes or no. Sending state makes the server diff two documents to work out what the player claims to have done — and “this rifle moved” and “this rifle was destroyed and an identical one appeared” are the same diff.
- Undo. An op names what it did rather than what the result looked like.
- Partial refusal. One move out of ten can be refused without the other nine snapping back.
- Prediction. A client applies locally, sends, and rolls back on a no.
The occupancy map is derived, never stored
A grid holding both a cell map and an item list can have them disagree.
cells_of() and occupied_cells() are computed from the entries on every call.
Two problems that are harder than they look
Moving something one cell to the left. An item moved within its own
container collides with itself, and the obvious workaround — remove, then
place — loses the item when the place half fails. fits(..., ignore_uid) asks
the question that was meant.
Nesting. A bag inside itself is an infinite loop in every traversal —
weight, save, draw, search — so the symptom is a hang rather than a wrong
number. can_nest() walks up from the destination before the move, because
a cycle that already exists cannot be found by anything that has to traverse the
structure to look for it.
Behaviour worth knowing
| The complaint | What the addon does |
|---|---|
| “It made a second stack of five next to my stack of five” | A pickup tops up existing stacks first. |
| “I dragged twenty onto a stack with room for five and nothing happened” | A partial merge moves five and leaves fifteen. |
| “My rifle won’t fit and there’s clearly space” | Rotation is tried at each cell, not after a whole failed pass. |
| “The sort order changes every time I open it” | sort_custom is not stable in Godot, so every comparison breaks ties on the id. |
The panel asks the same question the server will
DotInvPanel is a grid built in code with no art and no Theme, using Godot’s
own drag-and-drop rather than dot-ui’s, so it works in a project with either.
_can_drop_data calls manager.validate(op). A panel with its own idea of what
fits will eventually disagree with the server, and the player then sees a move
accepted on screen and undone a round trip later.
Filtering and sorting are the client’s: the server owns what is in the inventory, and a filter sent to a server costs a round trip per keystroke.
It meets dot-loadout in one file
DotInvLoadoutLink, by duck typing. The division is real:
- dot-loadout — what you own and what you brought in. Permanent, bounded, validated against entitlements before a match.
- dot-inventory — what is in your bag right now. It changes every few seconds, it has a shape, and a server must be able to refuse one move.
Neither direction is automatic: whether a round’s inventory is worth keeping is a design decision, not something an addon should assume.
