Skip to content

Commit a00508c

Browse files
committed
fix: reject restricted item drops
1 parent c303658 commit a00508c

5 files changed

Lines changed: 43 additions & 5 deletions

File tree

docs/qa/manual-client-checklist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ Expected result:
293293
Expected result:
294294
- valid pickup removes the ground actor, refreshes the carried inventory slot or compatible stack according to the authored stack metadata, and shows the normal pickup notice
295295
- a non-zero gold/elk field follows the gold-drop path regardless of the packed item position: gold decreases, the carried inventory remains unchanged, and a gold ground marker appears
296-
- loaded drop template metadata whose carried stack already exceeds the authored `max_count` fails closed: no ground actor, no carried-slot deletion/update, and no quickslot mutation is visible
296+
- loaded drop template metadata whose carried stack already exceeds the authored `max_count`, is transfer-guarded with `anti_drop` / `anti_give` / `anti_sell`, or is rejected by selected-character job/sex/min-level restrictions fails closed: no ground actor, no carried-slot deletion/update, and no quickslot mutation is visible
297297
- if a corrupt/disposable fixture reaches the shared-world ground-handle seam with stale equipment-slot metadata on an otherwise unequipped ground snapshot, registration fails closed and no temporary ground actor becomes available
298298
- if a corrupt/disposable fixture has duplicate live items in the same carried cell, `ITEM_DROP` / `ITEM_DROP2` fails closed with no ground actor, no carried-slot deletion/update, no quickslot mutation, and no persisted-state mutation
299299
- missing, malformed, mismatched, or ground-count-over-template-`max_count` authored pickup template metadata fails closed: no item pickup notice, no inventory mutation, and the ground handle remains available for a later valid retry

internal/player/runtime.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ func (r *Runtime) DropInventoryItem(slot inventory.SlotIndex, count uint16) (inv
371371
}
372372

373373
func (r *Runtime) DropInventoryItemWithTemplate(slot inventory.SlotIndex, count uint16, template itemcatalog.Template) (inventory.MoveResult, bool) {
374-
if !itemcatalog.ValidTemplate(template) {
374+
if !itemcatalog.ValidTemplate(template) || !r.CanUseTemplate(template) {
375375
return inventory.MoveResult{}, false
376376
}
377377
return r.dropInventoryItem(slot, count, template)

internal/player/runtime_inventory_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,44 @@ func TestDropInventoryItemWithTemplateRejectsDropTransferGuardsWithoutMutation(t
104104
}
105105
}
106106

107+
func TestDropInventoryItemWithTemplateRejectsSelectedCharacterRestrictionsWithoutMutation(t *testing.T) {
108+
cases := []struct {
109+
name string
110+
character loginticket.Character
111+
template itemcatalog.Template
112+
}{
113+
{
114+
name: "anti warrior",
115+
character: loginticket.Character{Job: 0, RaceNum: 0, Level: 10},
116+
template: itemcatalog.Template{Vnum: 27001, Name: "Warrior Restricted Potion", Stackable: true, MaxCount: 200, AntiWarrior: true},
117+
},
118+
{
119+
name: "anti female",
120+
character: loginticket.Character{Job: 1, RaceNum: 1, Level: 10},
121+
template: itemcatalog.Template{Vnum: 27001, Name: "Female Restricted Potion", Stackable: true, MaxCount: 200, AntiFemale: true},
122+
},
123+
{
124+
name: "min level",
125+
character: loginticket.Character{Job: 0, RaceNum: 0, Level: 5},
126+
template: itemcatalog.Template{Vnum: 27001, Name: "Veteran Potion", Stackable: true, MaxCount: 200, MinLevel: 10},
127+
},
128+
}
129+
for _, tc := range cases {
130+
t.Run(tc.name, func(t *testing.T) {
131+
tc.character.Inventory = []inventory.ItemInstance{{ID: 1, Vnum: 27001, Count: 5, Slot: 5}}
132+
runtime := NewRuntime(tc.character, SessionLink{})
133+
before := runtime.LiveInventory()
134+
135+
if _, ok := runtime.DropInventoryItemWithTemplate(5, 1, tc.template); ok {
136+
t.Fatalf("expected %s item drop to be rejected", tc.name)
137+
}
138+
if got := runtime.LiveInventory(); !reflect.DeepEqual(got, before) {
139+
t.Fatalf("%s drop mutated inventory: got %#v want %#v", tc.name, got, before)
140+
}
141+
})
142+
}
143+
}
144+
107145
func TestDropInventoryItemWithTemplateRejectsMismatchedTemplateWithoutMutation(t *testing.T) {
108146
runtime := NewRuntime(loginticket.Character{
109147
Inventory: []inventory.ItemInstance{{ID: 1, Vnum: 27001, Count: 5, Slot: 5}},

spec/protocol/item-drop-pickup-bootstrap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ For the first live runtime slice, accepted drops are self-facing and persistence
120120
2. `ITEM_DROP2` uses the requested non-zero count when it fits the current stack; a zero or oversized count is normalized to the whole stack before inventory mutation, matching the observed legacy `DropItem` count normalization. The minimal runtime now freezes both zero-count and oversized-count normalization through the packet path: either request shape removes the whole carried stack, clears only item quickslots pointing at that removed slot, preserves unrelated skill/command quickslots with the same byte slot value, and persists that snapshot before exposing the ground item. Partial counted drops keep the original item in the source carried slot and therefore preserve item quickslots, as well as unrelated skill/command quickslots, pointing at that slot.
121121
3. If an authored item-template snapshot is loaded, the carried item's `vnum` must resolve to a valid template before the drop mutates inventory; missing metadata fails closed with no frames and no live inventory, quickslot, ground-handle, or persistence mutation. If a loaded template is present, it must validate under the item-template bootstrap contract, its `vnum` must match the carried item's live `vnum`, and the carried stack count must not exceed the template-authored `max_count`; malformed, mismatched, or over-template-max stack metadata fails closed before live inventory, quickslots, ground handles, or persistence are mutated. The deterministic bootstrap fallback snapshot used when no item-template file exists remains valid for local smoke/runtime startup; this missing-template guard applies only after an authored item-template snapshot has been loaded.
122122
4. If the selected carried item is locked, the requested carried cell has duplicate live item occupancy, or the carried item would become malformed while applying the drop mutation, the drop fails closed before live inventory, quickslots, ground handles, or persistence are mutated.
123-
5. If the carried item's loaded template is marked `anti_drop`, `anti_give`, or `anti_sell`, the drop is rejected before live inventory, quickslots, ground handles, or persistence are mutated, and the selected session receives one self-only `CHAT_TYPE_INFO` system message (`"You cannot drop this item."`). This keeps player-requested drops of bound/non-transferable bootstrap items fail-closed while forced system-drop, localization text catalogs, and death-drop policy stay out of scope.
123+
5. If the carried item's loaded template is marked `anti_drop`, `anti_give`, or `anti_sell`, or carries an authored selected-character restriction (`anti_warrior`, `anti_assassin`, `anti_sura`, `anti_shaman`, `anti_male`, `anti_female`, or `min_level` above the selected character's current level), the drop is rejected before live inventory, quickslots, ground handles, or persistence are mutated. Transfer-guard rejections return one self-only `CHAT_TYPE_INFO` system message (`"You cannot drop this item."`); selected-character restriction rejections fail closed with no frames. This keeps player-requested drops of bound/non-transferable or class/sex/level-restricted bootstrap items fail-closed while forced system-drop, localization text catalogs, and death-drop policy stay out of scope.
124124
6. The selected player's live inventory is removed or decremented, then the selected character snapshot is persisted through the existing account-store path.
125125
7. Whole-stack drops clear item quickslots pointing at the removed slot.
126126
8. The server returns the carried-slot mutation frame first (`GC::ITEM_DEL` or `GC::ITEM_UPDATE`), then any quickslot deletes, then one self-only `GC::ITEM_GROUND_ADD` at the selected character's current coordinates followed by `GC::ITEM_OWNERSHIP` naming the dropping character.

spec/protocol/packet-matrix.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ Planned rows may temporarily use `Header = TBD` when the project freezes the fam
100100
| `ITEM_USE` | client -> server | `0x0501` | game | documented | first owned client-originated item-use request; payload is packed `TItemPos(window:uint8, cell:uint16)`; current live runtime accepts only carried inventory positions (`window = INVENTORY`, `cell < 90`), fails closed for locked, malformed, or selected-character job/sex-restricted carried-slot items, and routes accepted requests through the same bootstrap template-backed consumable path already exercised by `/use_item <slot>` |
101101
| `ITEM_USE_TO_ITEM` | client -> server | `0x0506` | game | documented | first owned drag-to-item item-use path; payload is source packed `TItemPos` followed by target packed `TItemPos`; current live runtime accepts only carried inventory stack-on-stack consolidation for compatible same-`vnum` stackable templates whose authored `max_count` fits the owned one-byte item refresh count range (`<= 255`), emits the same self-only inventory refresh family as carried `ITEM_MOVE`, persists the merged inventory, deletes only source item quickslots when a full merge removes that source stack, and fails closed without normal `use_effect` fallback for incompatible or unsupported targets; empty source/target slots, same-cell source/target requests, non-stackable templates, authored `anti_stack`/`anti_drop`/`anti_give`/`anti_sell` templates, selected-character job/sex anti-flag templates, locked source/target stacks, and over-template-max stacks are verified as no-frame/no-mutation guards |
102102
| `ITEM_MOVE` | client -> server | `0x0504` | game | documented | first owned client-originated carried-slot move/split/merge request; payload is source packed `TItemPos`, destination packed `TItemPos`, and `count uint8`; current live runtime accepts only carried inventory positions (`window = INVENTORY`, `cell < 90`), treats `count = 0` as a full-stack move that succeeds into an empty destination, swaps with an incompatible occupied destination, or merges into a compatible occupied destination up to the source template `max_count`, accepts empty-destination partial splits and template-bounded compatible occupied-destination merges for non-zero counts, treats a non-zero count that covers the whole source stack as the same full-stack carried move/swap path for incompatible occupied destinations, fails closed for partial incompatible occupied-destination counted moves, oversized non-zero counts, template-`max_count` overflow, storage-overflowing destination stack counts, and locked source/destination slots until richer stack and item-lock semantics are owned, reuses the selected-character inventory persistence boundary, keeps identity-changing moves/splits/swaps on self-only `ITEM_DEL` / `ITEM_SET`, uses self-only `ITEM_UPDATE` count refreshes for compatible occupied-destination partial merges, emits `ITEM_DEL(source)` then `ITEM_UPDATE(destination)` for exact counted or zero-count full-stack compatible merges, and deletes source item quickslots when that full-stack merge removes the source stack |
103-
| `ITEM_DROP` | client -> server | `0x0502` | game | documented | drop request with packed `TItemPos` plus `elk uint32`; the shipped runtime now accepts carried-slot drops as self-only inventory removal plus `ITEM_GROUND_ADD` + `ITEM_OWNERSHIP` |
104-
| `ITEM_DROP2` | client -> server | `0x0503` | game | documented | counted drop request with packed `TItemPos` plus `gold uint32` and `count uint8`; the shipped runtime now accepts non-zero carried-slot counts that fit the source stack as self-only inventory decrement/removal plus `ITEM_GROUND_ADD` + `ITEM_OWNERSHIP` |
103+
| `ITEM_DROP` | client -> server | `0x0502` | game | documented | drop request with packed `TItemPos` plus `elk uint32`; the shipped runtime now accepts carried-slot drops as self-only inventory removal plus `ITEM_GROUND_ADD` + `ITEM_OWNERSHIP`, while loaded template transfer guards and selected-character restrictions fail closed before mutation |
104+
| `ITEM_DROP2` | client -> server | `0x0503` | game | documented | counted drop request with packed `TItemPos` plus `gold uint32` and `count uint8`; the shipped runtime now accepts non-zero carried-slot counts that fit the source stack as self-only inventory decrement/removal plus `ITEM_GROUND_ADD` + `ITEM_OWNERSHIP`, while loaded template transfer guards and selected-character restrictions fail closed before mutation |
105105
| `ITEM_PICKUP` | client -> server | `0x0505` | game | documented | pickup request with `vid uint32`; `internal/game` dispatch is frozen, and the shipped runtime now accepts visible-world pickup of temporary ground handles created by accepted carried-slot drops; stackable owner pickup first tries a full merge into the lowest compatible carried stack before falling back to empty-slot placement |
106106
| `ITEM_OWNERSHIP` | server -> client | `0x0517` | game | documented | first owned ownership-label companion for bootstrap ground items; payload is `vid uint32` plus fixed `owner_name[25]`; current runtime emits it immediately after every `ITEM_GROUND_ADD` for accepted drops, visible-peer drop fanout, radius-AOI ground re-entry rebuilds, and transfer destination ground re-entry rebuilds; timers, permission changes, and party ownership policy remain deferred |
107107
| `ITEM_GET` | server -> client | `0x0518` | game | documented | first owned pickup-notice companion; payload is `vnum uint32`, `count uint8`, `arg uint8`, and fixed `from_name[25]`; current runtime emits normal/self `arg = 0` after accepted owner pickup, including stack-merge pickups, and the first party-shaped owner-delivery notices for visible live peers, while real party membership remains deferred |

0 commit comments

Comments
 (0)