Skip to content

Commit

Permalink
Correctly model de-duplication bypass (#21)
Browse files Browse the repository at this point in the history
* First pass of logic changes: mListHeads uninit modelling, allows de-dupe bypass for the first acquired item (incl. quantity checks)

* Adjust existing unit tests to match corrected behaviour; add new ones based on mListHeads understanding

* Add tests for explicit duping of non-stackable key items when tab data is missing (reloading / in-game, same result)

* test material stack duping behaviour with tab data info

* make sure listHeadsInit is properly updated and propagated

* E2E tests for newly modelled duping behaviour

* lint: add trailing newline

* Remove OS-generated metadata files from lint check; describe use of linter `-v` flag in README

* Exclude `build` folder from lint

* Add E2E tests for the two cases originally raise in issue #20
  • Loading branch information
pearfalse authored Oct 27, 2023
1 parent 6504513 commit 64ad5d6
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ If you want to introduce new commands, most of the heavy lifting for the parsers
### PR
Do before PR:
- Lint your code
1. `npm run lint-base`: This checks that your files have unix line endings, have no traling whitespaces, and have exactly 1 trailing new line. This might fail if you have auto crlf on git for windows. If you do, **please make sure the remote still has UNIX line ending so the PR automation passes**
1. `npm run lint-base`: This checks that your files have unix line endings, have no traling whitespaces, and have exactly 1 trailing new line. This might fail if you have auto crlf on git for windows. If you do, **please make sure the remote still has UNIX line ending so the PR automation passes**. To debug unexpected errors, run again as `npm run lint-base -- -v` to see which file is failing
- `npm run layer`: This makes sure your imports follow the layer rules (and are sorted correctly)
- `src/data` is the bottom layer. It cannot depend on core or ui components
- `src/core` is the core logic. It can depend on data, but not ui
Expand Down
5 changes: 4 additions & 1 deletion config/base-lint.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
ignore = [
"CNAME",
"build/",
"scripts/base-lint/",
"scripts/typescript-layers/",
".log",
".ttf"
".ttf",
".DS_Store",
"Thumbs.db"
]

windows = []
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/apples_999_twice.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Author: Pearfalse
const TEST = "apples_999_twice";
it(TEST, () => {
expect(TEST).toPassE2ESimulation();
});
export { };
5 changes: 5 additions & 0 deletions src/__tests__/apples_999_twice.in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Init 999 Apple 1 Slate 1 Glider
Save
Break 3 Slots
Reload
Sync GameData
5 changes: 5 additions & 0 deletions src/__tests__/apples_999_twice.out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Init 999 Apple 1 Slate 1 Glider
Save
#
Init 999 Apple 1 Slate 1 Glider 999 Apple
Break 3 Slots
6 changes: 6 additions & 0 deletions src/__tests__/arrows_quantityCheckExceptions.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Author: Pearfalse
const TEST = "arrows_quantityCheckExceptions";
it(TEST, () => {
expect(TEST).toPassE2ESimulation();
});
export { };
24 changes: 24 additions & 0 deletions src/__tests__/arrows_quantityCheckExceptions.in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Init 1 axe 1 bow [life=2200, equip] 5 arrow [equip] 1 slate
Save
Break 2 Slots
Drop bow
D&P 1 axe
Unequip axe
Reload
Save
Sync GameData
Shoot 5 arrows
Save as A1
Get 1 apple
Reload
Drop bow
Drop axe
Save
Eat 1 apple
Reload
Sync GameData
Save
Reload A1
Eat 1 apple
Reload
Sync GameData
8 changes: 8 additions & 0 deletions src/__tests__/arrows_quantityCheckExceptions.out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Init 2200 arrow [equip] 2200 arrow [equip] 1 apple 1 slate
Save

Init 1 axe 1 axe 1 bow [life=2200, equip] 0 arrow [equip] 1 slate
Save as A1

Init 0 arrow [equip] 2200 arrow [equip] 2200 arrow[equip] 1 apple 1 slate
Break 2 slots
6 changes: 6 additions & 0 deletions src/__tests__/gliderOpensGameData.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Author: Pearfalse
const TEST = "gliderOpensGameData";
it(TEST, () => {
expect(TEST).toPassE2ESimulation(true);
});
export { };
12 changes: 12 additions & 0 deletions src/__tests__/gliderOpensGameData.in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# prepare SoR autosave
save as NewGame
get 4 weapon 4 simm 1 glider
break 3 slots
reload NewGame
get 1 slate
eat all simm
get 2 weapon
save
drop all weapon
reload
sync gamedata
5 changes: 5 additions & 0 deletions src/__tests__/gliderOpensGameData.out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
save as NewGame
init 1 glider 1 slate 1 weapon [equip] 1 weapon
save
init 1 weapon [equip] 1 weapon 1 glider 1 slate 1 glider
break 3 slots
6 changes: 6 additions & 0 deletions src/__tests__/slate_VOSDedupe.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Author: Pearfalse
const TEST = "slate_VOSDedupe";
it(TEST, () => {
expect(TEST).toPassE2ESimulation();
});
export { };
10 changes: 10 additions & 0 deletions src/__tests__/slate_VOSDedupe.in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# prepare SoR autosave
save as SoR
init 4 weapon 4 simm 1 slate
break 1 slot
# activate VOS
reload SoR
get 1 weapon
drop weapon
# walk along edge of cliff to hit slate pickup failsafe trigger
get 1 slate
4 changes: 4 additions & 0 deletions src/__tests__/slate_VOSDedupe.out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
save as SoR
init 1 slate
break 1 slot
init gamedata
9 changes: 5 additions & 4 deletions src/core/inventory/Slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ export class Slots {

// Add something to inventory in game
// returns the added slot ref, or undefined if no new slot is added
public add(stack: ItemStack, reloading: boolean, mCount: number | null, flags: GameFlags): Ref<ItemStack> | undefined {
return add(this.core, stack, reloading, mCount, flags);
public add(stack: ItemStack, reloading: boolean, mCount: number | null, flags: GameFlags, listHeadsInit?: Boolean): Ref<ItemStack> | undefined {
return add(this.core, stack, reloading, mCount, flags, listHeadsInit);
}

// this is for all types of item
public equip(item: Item, slot: number, mCount: number) {
public equip(item: Item, slot: number) {
let s = 0;
// unequip same type in first tab
const [firstTabItem, firstTabIndex] = this.core.findFirstTab(item.type, mCount);
// PF: all methods of handling equipping in-game seem to be unaffected by mListHeads
const [firstTabItem, firstTabIndex] = this.core.findFirstTab(item.type, true);
if(firstTabItem){
for(let i = firstTabIndex;i<this.core.length && this.core.get(i).item.tab === item.tab;i++){
if( this.core.get(i).item.type === item.type){
Expand Down
4 changes: 2 additions & 2 deletions src/core/inventory/SlotsCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ export class SlotsCore {
});
}

public findFirstTab(type: ItemType, mCount: number): [Ref<ItemStack> | undefined, number] {
public findFirstTab(type: ItemType, listHeadsInit: Boolean): [Ref<ItemStack> | undefined, number] {
// figure out the tabs first
const tabArray: [ItemTab, number][] = [];
const tabAdded = new Set();
if(mCount !== 0){
if(listHeadsInit){
// scan inventory array for tabs
let lastTab = ItemTab.None;
for(let i =0;i<this.internalSlots.length;i++){
Expand Down
23 changes: 18 additions & 5 deletions src/core/inventory/VisibleInventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export class VisibleInventory implements DisplayableInventory{
// Difference between this.slots.length and what the game thinks the inventory size is
// i.e. number of broken slots
private offset: number;
// Between an inventory wipe (load/new game) and the first item add (from GameData or in the world), tab
// data is empty, which causes de-dupe checks to always be bypassed. Instead of simulating mListHeads
// fully, we just indicate whether to pretend tabs are empty or not.
// https://discord.com/channels/872350971383140422/1000992154140811325/1131656653561925824
private listHeadsInit: Boolean = false;
constructor(slots: Slots){
this.slots = slots;
this.offset = 0;
Expand Down Expand Up @@ -62,12 +67,17 @@ export class VisibleInventory implements DisplayableInventory{
}

public addDirectly(stack: ItemStack, index?: number): Ref<ItemStack>{
return this.slots.addStackDirectly(stack, index);
const r = this.slots.addStackDirectly(stack, index);
// adding an item *always* inits list heads, even if you're bumped up into mCount 0
this.listHeadsInit = true;
return r;
}

// return newly added ref, or lastAdded if no new slots are added
public addWhenReload(stack: ItemStack, lastAdded: Ref<ItemStack> | undefined, flags: GameFlags): Ref<ItemStack> | undefined {
const newlyAdded = this.slots.add(stack, true, this.getMCount(), flags);
const newlyAdded = this.slots.add(stack, true, this.getMCount(), flags, this.listHeadsInit);
// if something was added, tab data is present and de-dupe checks can work
this.listHeadsInit ||= newlyAdded !== undefined;
const mostRecentlyAdded = newlyAdded || lastAdded;
if(mostRecentlyAdded){
// set cook data
Expand All @@ -79,7 +89,8 @@ export class VisibleInventory implements DisplayableInventory{
}

public addInGame(stack: ItemStack, flags: GameFlags) {
this.slots.add(stack, false, this.getMCount(), flags);
this.slots.add(stack, false, this.getMCount(), flags, this.listHeadsInit);
this.listHeadsInit = true;
}

// Standard remove: magically remove item from inventory
Expand All @@ -99,7 +110,7 @@ export class VisibleInventory implements DisplayableInventory{
}

public equip(item: Item, slot: number) {
this.slots.equip(item, slot, this.getMCount());
this.slots.equip(item, slot);
}

public unequip(item: Item, slot: number) {
Expand All @@ -112,6 +123,8 @@ export class VisibleInventory implements DisplayableInventory{
if(count > 0){
this.slots.clearFirst(count);
}
// reloading clears tab data
this.listHeadsInit = false;
}

public updateEquipmentDurability(gameData: GameData) {
Expand Down Expand Up @@ -197,7 +210,7 @@ export class VisibleInventory implements DisplayableInventory{
}

public swap(i: number, j: number) {
this.slots.swap(i,j);
this.slots.swap(i, j);
}

// public countItems(type: ItemType, countAnyWeapon: boolean): number {
Expand Down
112 changes: 108 additions & 4 deletions src/core/inventory/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ describe("core/inventory/add", ()=>{

expect(slots.getView()).toEqualItemStacks([existing1, existing2, existing3]); // not sorted
});
it("should skip arrow 999 check for [bow, weapon, arrow] if mCount = 0", ()=>{
it("should skip arrow 999 check for [bow, weapon, arrow] if no tabs", ()=>{
const mockItem1 = createEquipmentMockItem("BowA", ItemType.Bow);
const mockItem2 = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockItem3 = createArrowMockItem("ArrowA");
Expand All @@ -499,7 +499,7 @@ describe("core/inventory/add", ()=>{
];
const toAdd = createMaterialStack(mockItem3, 999);
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, toAdd, true, 0, TestFlags);
const addedSlot = add(slots, toAdd, true, 0, TestFlags, false);
expect(addedSlot?.get().equals(toAdd)).toBe(true);

expect(slots.getView()).toEqualItemStacks([existing1, existing2, existing3, toAdd]); // not sorted
Expand All @@ -520,7 +520,7 @@ describe("core/inventory/add", ()=>{

expect(slots.getView()).toEqualItemStacks([existing3, toAdd, existing2]); // sorted
});
it("should add unrepeatable if mCount = 0", ()=>{
it("should add unrepeatable if no tab data", ()=>{
const mockItem1 = createKeyMockItem("KeyA");
const mockItem2 = createMaterialMockItem("ItemA");
const existing1 = createMaterialStack(mockItem1, 1);
Expand All @@ -530,11 +530,115 @@ describe("core/inventory/add", ()=>{
existing2,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, existing1, true, 0, TestFlags);
const addedSlot = add(slots, existing1, true, 0, TestFlags, false);
expect(addedSlot?.get().equals(existing1)).toBe(true);

expect(slots.getView()).toEqualItemStacks([existing1, existing2, existing1]); // not sorted
});
it("should skip unrepeatable if tab data present", ()=>{
const mockKey1 = createKeyMockItem("KeyA");
const existing1 = createMaterialStack(mockKey1, 1);
const stacks: ItemStack[] = [
existing1,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, existing1, true, 0, TestFlags, true);
expect(addedSlot === undefined).toBe(true);
});
it("should allow arrows >999 when reloading if existing stack is 0", ()=>{
const mockWeaponItem = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockArrowItem = createArrowMockItem("ArrowA");
const highArrowStack = createMaterialStack(mockArrowItem, 4000);
const stacks: ItemStack[] = [
createEquipmentStack(mockWeaponItem, 10, false),
createMaterialStack(mockArrowItem, 0),
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, highArrowStack, true, null, TestFlags, true);
expect(addedSlot?.get().equals(highArrowStack)).toBe(true);
});
it("should allow duped arrows >999 when reloading save stack is first item", ()=>{
const mockWeaponItem = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockArrowItem = createArrowMockItem("ArrowA");
const highArrowStack = createMaterialStack(mockArrowItem, 4000);
const stacks: ItemStack[] = [
createEquipmentStack(mockWeaponItem, 10, false),
createMaterialStack(mockArrowItem, 5000),
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, highArrowStack, true, null, TestFlags, false);
expect(addedSlot?.get().equals(highArrowStack)).toBe(true);
});
it("should allow key item dupes when tab data is missing", ()=>{
const mockWeaponItem = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockKeyItem = createKeyMockItem("KeyA");
const stackWeapon = createEquipmentStack(mockWeaponItem, 10, false);
const existingKeyItem = mockKeyItem.defaultStack;
const acquiredKeyItem = mockKeyItem.defaultStack;
const stacks: ItemStack[] = [
stackWeapon,
existingKeyItem,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, acquiredKeyItem, true, -1, TestFlags, false);
expect(addedSlot?.get().equals(acquiredKeyItem)).toBe(true);
expect(slots.getView()).toEqualItemStacks([
stackWeapon,
existingKeyItem,
acquiredKeyItem,
]);
});
it("should allow 1000+ materials in two stacks if tab data is missing", ()=>{
const mockMaterial = createMaterialMockItem("MaterialA");
const transferredStack = createMaterialStack(mockMaterial, 999);
const gameDataLeadingStack = createMaterialStack(mockMaterial, 500);
const stacks: ItemStack[] = [
transferredStack,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, gameDataLeadingStack, true, 0, TestFlags, false);
expect(addedSlot?.get().equals(gameDataLeadingStack)).toBe(true);
expect(slots.getView()).toEqualItemStacks([
transferredStack,
gameDataLeadingStack,
]);
});
});
describe("reloading = false", ()=>{
it("should allow key item dupes in-game when tab data is missing", ()=>{
const mockWeaponItem = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockKeyItem = createKeyMockItem("KeyA");
const stackWeapon = createEquipmentStack(mockWeaponItem, 10, false);
const existingKeyItem = mockKeyItem.defaultStack;
const acquiredKeyItem = mockKeyItem.defaultStack;
const stacks: ItemStack[] = [
stackWeapon,
existingKeyItem,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, acquiredKeyItem, false, -1, TestFlags, false);
expect(addedSlot?.get().equals(acquiredKeyItem)).toBe(true);
expect(slots.getView()).toEqualItemStacks([
stackWeapon,
existingKeyItem,
acquiredKeyItem,
]);
});
it("should prevent key item dupes in-game after non-0 mCount sync", ()=>{
const mockWeaponItem = createEquipmentMockItem("WeaponA", ItemType.Weapon);
const mockKeyItem = createKeyMockItem("KeyA");
const stackWeapon = createEquipmentStack(mockWeaponItem, 10, false);
const existingKeyItem = mockKeyItem.defaultStack;
const acquiredKeyItem = mockKeyItem.defaultStack;
const stacks: ItemStack[] = [
stackWeapon,
existingKeyItem,
];
const slots = new SlotsCore(stacks);
const addedSlot = add(slots, acquiredKeyItem, false, 0, TestFlags, true);
expect(addedSlot).toBe(undefined);
expect(slots.getView()).toEqualItemStacks(stacks);
});
})
});
});
Loading

0 comments on commit 64ad5d6

Please sign in to comment.