Skip to content

Commit df52ac3

Browse files
committed
test: cover zero-count item use guard
1 parent d4c03d3 commit df52ac3

2 files changed

Lines changed: 45 additions & 22 deletions

File tree

internal/player/runtime_test.go

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,30 +1032,53 @@ func TestRuntimeUseItemOnItemRejectsZeroCountSourceOrTargetWithoutMutation(t *te
10321032
}
10331033
}
10341034

1035-
func TestRuntimeUseItemRejectsTemplateMaxAboveItemRefreshByteWithoutMutation(t *testing.T) {
1036-
persisted := loginticket.Character{
1037-
ID: 0x01030102,
1038-
VID: 0x02040102,
1039-
Name: "WideMaxUsePeer",
1040-
Points: [255]int32{1: 700},
1041-
Inventory: []inventory.ItemInstance{{ID: 11, Vnum: 27001, Count: 2, Slot: 5}},
1042-
Quickslots: []loginticket.Quickslot{
1043-
{Position: 2, Type: quickslotproto.TypeItem, Slot: 5},
1035+
func TestRuntimeUseItemRejectsMalformedStackEdgesWithoutMutation(t *testing.T) {
1036+
cases := []struct {
1037+
name string
1038+
inventory []inventory.ItemInstance
1039+
mutate func(*itemcatalog.Template)
1040+
}{
1041+
{
1042+
name: "zero-count live item",
1043+
inventory: []inventory.ItemInstance{{ID: 11, Vnum: 27001, Count: 0, Slot: 5}},
1044+
},
1045+
{
1046+
name: "template max-count exceeds item refresh byte",
1047+
inventory: []inventory.ItemInstance{{ID: 11, Vnum: 27001, Count: 2, Slot: 5}},
1048+
mutate: func(template *itemcatalog.Template) {
1049+
template.MaxCount = 256
1050+
},
10441051
},
10451052
}
1046-
runtime := NewRuntime(persisted, SessionLink{Login: "wide-max-use-peer", CharacterIndex: 1})
1047-
before := runtime.LiveCharacter()
1048-
template := bootstrapConsumableTemplate(27001, 1, 1, 50, "consume:27001:+50")
1049-
template.MaxCount = 256
1053+
for _, tc := range cases {
1054+
t.Run(tc.name, func(t *testing.T) {
1055+
persisted := loginticket.Character{
1056+
ID: 0x01030102,
1057+
VID: 0x02040102,
1058+
Name: "MalformedUsePeer",
1059+
Points: [255]int32{1: 700},
1060+
Inventory: append([]inventory.ItemInstance(nil), tc.inventory...),
1061+
Quickslots: []loginticket.Quickslot{
1062+
{Position: 2, Type: quickslotproto.TypeItem, Slot: 5},
1063+
},
1064+
}
1065+
runtime := NewRuntime(persisted, SessionLink{Login: "malformed-use-peer", CharacterIndex: 1})
1066+
before := runtime.LiveCharacter()
1067+
template := bootstrapConsumableTemplate(27001, 1, 1, 50, "consume:27001:+50")
1068+
if tc.mutate != nil {
1069+
tc.mutate(&template)
1070+
}
10501071

1051-
if result, ok := runtime.UseItem(5, template); ok {
1052-
t.Fatalf("expected over-refresh-byte max_count item use to fail closed, got %+v", result)
1053-
}
1054-
if got := runtime.LiveCharacter(); !reflect.DeepEqual(got, before) {
1055-
t.Fatalf("over-refresh-byte max_count item use mutated live character: got %#v want %#v", got, before)
1056-
}
1057-
if got := runtime.PersistedSnapshot(); !reflect.DeepEqual(got.Inventory, persisted.Inventory) || !reflect.DeepEqual(got.Quickslots, persisted.Quickslots) || got.Points[1] != persisted.Points[1] {
1058-
t.Fatalf("over-refresh-byte max_count item use mutated persisted state: inventory=%#v quickslots=%#v points[1]=%d", got.Inventory, got.Quickslots, got.Points[1])
1072+
if result, ok := runtime.UseItem(5, template); ok {
1073+
t.Fatalf("expected %s item use to fail closed, got %+v", tc.name, result)
1074+
}
1075+
if got := runtime.LiveCharacter(); !reflect.DeepEqual(got, before) {
1076+
t.Fatalf("%s item use mutated live character: got %#v want %#v", tc.name, got, before)
1077+
}
1078+
if got := runtime.PersistedSnapshot(); !reflect.DeepEqual(got.Inventory, persisted.Inventory) || !reflect.DeepEqual(got.Quickslots, persisted.Quickslots) || got.Points[1] != persisted.Points[1] {
1079+
t.Fatalf("%s item use mutated persisted state: inventory=%#v quickslots=%#v points[1]=%d", tc.name, got.Inventory, got.Quickslots, got.Points[1])
1080+
}
1081+
})
10591082
}
10601083
}
10611084

spec/protocol/item-use-bootstrap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ The first consumable path must fail closed when any of these are true:
148148
- the slot's `vnum` does not resolve to a valid non-equippable item template with a valid `use_effect` for direct `ITEM_USE`; `ITEM_USE_TO_ITEM` reuses the template's stack metadata only and does not require or execute `use_effect`
149149
- the resolved template `vnum` does not match the live carried item `vnum`; the player mutation boundary treats mismatched template metadata as fail-closed for both direct `ITEM_USE` and `ITEM_USE_TO_ITEM`
150150
- the resolved template carries an authored `equip_slot`; for `ITEM_USE_TO_ITEM`, this is frozen by player-boundary and packet-path tests as a transfer guard even when source and target stacks otherwise match
151-
- the carried live item snapshot is malformed under the bootstrap item-instance validation rules
151+
- the carried live item snapshot is malformed under the bootstrap item-instance validation rules, including zero-count stacks
152152
- the carried live item is locked
153153
- the requested carried cell has duplicate live item occupancy
154154
- the carried live item stack count already exceeds the resolved template-authored `max_count`; the minimal session/runtime packet path freezes this as no-frame/no-mutation behavior with inventory, quickslots, and point values unchanged

0 commit comments

Comments
 (0)