Skip to content

Commit 0ef487b

Browse files
committed
feat: start inventory parser
1 parent 55dd8f7 commit 0ef487b

6 files changed

Lines changed: 364 additions & 2 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ venv
1313
.venv
1414
__pycache__/
1515
.sentryclirc
16+
17+
# Test directories
18+
saves/
19+
output/

apps/stardew.app/src/lib/file.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
parseCrafting,
88
parseFishing,
99
parseGeneral,
10+
parseInventory,
1011
parseMonsters,
1112
parseMuseum,
1213
parsePerfection,
@@ -123,6 +124,8 @@ export function parseSaveFile(xml: string) {
123124
prefix,
124125
);
125126

127+
const inventory = parseInventory(prefix, saveFile.SaveGame, players);
128+
126129
players.forEach((player) => {
127130
// in here is where we'll call all our parsers and create the player object we'll use
128131
let processedPlayer = {
@@ -164,12 +167,11 @@ export function parseSaveFile(xml: string) {
164167
...parsedAnimals,
165168
horse: player.horseName,
166169
},
170+
inventory,
167171
};
168172
processedPlayers.push(processedPlayer);
169173
});
170174

171-
console.log("processedPlayers", processedPlayers);
172-
173175
// processedPlayers.forEach((p) =>
174176
// console.log(`Player: ${p.general.name} | powers:`, p.powers.collection),
175177
// );

apps/stardew.app/src/lib/parsers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parseCooking } from "@/lib/parsers/cooking";
33
import { parseCrafting } from "@/lib/parsers/crafting";
44
import { parseFishing } from "@/lib/parsers/fishing";
55
import { parseGeneral } from "@/lib/parsers/general";
6+
import { parseInventory } from "@/lib/parsers/inventory";
67
import { parseMonsters } from "@/lib/parsers/monsters";
78
import { parseMuseum } from "@/lib/parsers/museum";
89
import { parsePerfection } from "@/lib/parsers/perfection";
@@ -20,6 +21,7 @@ export {
2021
parseCrafting,
2122
parseFishing,
2223
parseGeneral,
24+
parseInventory,
2325
parseMonsters,
2426
parseMuseum,
2527
parsePerfection,
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
export interface InventoryItem {
2+
name: string;
3+
type: string;
4+
quality: number;
5+
stack: number;
6+
index: number;
7+
category: number;
8+
parentSheetIndex: number;
9+
}
10+
11+
export interface InventoryContainer {
12+
type: string;
13+
location: string;
14+
items: InventoryItem[];
15+
}
16+
17+
export interface InventoryRet {
18+
containers: InventoryContainer[];
19+
totalItems: number;
20+
totalUniqueItems: number;
21+
}
22+
23+
// Constants for container types
24+
const CONTAINER_TYPES = {
25+
PLAYER_INVENTORY: "Player Inventory",
26+
CHEST: "Chest",
27+
FRIDGE: "Fridge",
28+
BUILDING_OUTPUT: "Building Output",
29+
BUILDING_STORAGE: "Building Storage",
30+
UNKNOWN: "Unknown",
31+
} as const;
32+
33+
// Skip paths for player inventories
34+
const SKIP_PATHS = ["SaveGame.player", "SaveGame.farmhands.Farmer"];
35+
36+
// Utility function to parse paths efficiently
37+
function parsePath(path: string) {
38+
const parts = path.split(".");
39+
const hasHeldObject = path.includes("heldObject");
40+
const buildingMatch = path.match(/Building\[(\d+)\]/);
41+
const playerMatch = path.match(/Player\[(\d+)\]/);
42+
43+
return {
44+
parts,
45+
hasHeldObject,
46+
buildingIndex: buildingMatch?.[1],
47+
playerIndex: playerMatch?.[1],
48+
isFridge: path.includes("fridge"),
49+
isOutput: path.includes("output"),
50+
isBuilding: path.includes("Building"),
51+
isPlayer: path.includes("Player"),
52+
};
53+
}
54+
55+
// Optimized function to get object by path
56+
function getObjectByPath(obj: any, pathParts: string[], endIndex: number): any {
57+
let current = obj;
58+
for (let i = 1; i < endIndex; i++) {
59+
const part = pathParts[i];
60+
const bracketIndex = part.indexOf("[");
61+
62+
if (bracketIndex !== -1) {
63+
const arrayName = part.substring(0, bracketIndex);
64+
const index = parseInt(
65+
part.substring(bracketIndex + 1, part.indexOf("]")),
66+
);
67+
current = current[arrayName]?.[index];
68+
} else {
69+
current = current[part];
70+
}
71+
72+
if (!current) return null;
73+
}
74+
return current;
75+
}
76+
77+
function getParentObjectName(
78+
saveGame: any,
79+
pathInfo: ReturnType<typeof parsePath>,
80+
): string | null {
81+
if (!pathInfo.hasHeldObject) return null;
82+
83+
try {
84+
const heldObjectIndex = pathInfo.parts.findIndex((part) =>
85+
part.includes("heldObject"),
86+
);
87+
if (heldObjectIndex === -1) return null;
88+
89+
const parent = getObjectByPath(saveGame, pathInfo.parts, heldObjectIndex);
90+
return parent?.name || parent?.Name || null;
91+
} catch {
92+
return null;
93+
}
94+
}
95+
96+
function getBuildingType(
97+
saveGame: any,
98+
pathInfo: ReturnType<typeof parsePath>,
99+
): string | null {
100+
if (!pathInfo.buildingIndex) return null;
101+
102+
try {
103+
const buildingIndex = pathInfo.parts.findIndex((part) =>
104+
part.includes("Building["),
105+
);
106+
if (buildingIndex === -1) return null;
107+
108+
const building = getObjectByPath(
109+
saveGame,
110+
pathInfo.parts,
111+
buildingIndex + 1,
112+
);
113+
return (
114+
building?.["@_xsi:type"] || building?.indoors?.["@_xsi:type"] || null
115+
);
116+
} catch {
117+
return null;
118+
}
119+
}
120+
121+
function determineContainerType(
122+
obj: any,
123+
pathInfo: ReturnType<typeof parsePath>,
124+
saveGame?: any,
125+
): string {
126+
if (pathInfo.isPlayer) return CONTAINER_TYPES.PLAYER_INVENTORY;
127+
if (pathInfo.isFridge) return CONTAINER_TYPES.FRIDGE;
128+
if (pathInfo.isOutput) return CONTAINER_TYPES.BUILDING_OUTPUT;
129+
if (pathInfo.isBuilding) return CONTAINER_TYPES.BUILDING_STORAGE;
130+
131+
if (obj["@_xsi:type"] === "Chest") {
132+
if (pathInfo.hasHeldObject) {
133+
const parentName = getParentObjectName(saveGame, pathInfo);
134+
return parentName ? `${parentName} Storage` : CONTAINER_TYPES.CHEST;
135+
}
136+
return CONTAINER_TYPES.CHEST;
137+
}
138+
139+
return CONTAINER_TYPES.UNKNOWN;
140+
}
141+
142+
function shouldSkipPath(path: string): boolean {
143+
return SKIP_PATHS.some((skipPath) => path.includes(skipPath));
144+
}
145+
146+
function findItemContainers(
147+
obj: any,
148+
path = "",
149+
saveGame?: any,
150+
): InventoryContainer[] {
151+
if (!obj || typeof obj !== "object") return [];
152+
153+
const containers: InventoryContainer[] = [];
154+
155+
// Check if this object has items
156+
if (obj.items?.Item) {
157+
if (shouldSkipPath(path)) return [];
158+
159+
const items = parseItems(obj.items.Item);
160+
if (items.length === 0) return containers;
161+
162+
const pathInfo = parsePath(path);
163+
const containerType = determineContainerType(obj, pathInfo, saveGame);
164+
const location =
165+
getBuildingType(saveGame, pathInfo) || pathInfo.buildingIndex || "";
166+
167+
containers.push({
168+
type: containerType,
169+
location,
170+
items,
171+
});
172+
}
173+
174+
// Recursively search through all properties
175+
for (const [key, value] of Object.entries(obj)) {
176+
if (!value || typeof value !== "object") continue;
177+
178+
const currentPath = path ? `${path}.${key}` : key;
179+
180+
if (Array.isArray(value)) {
181+
value.forEach((item, index) => {
182+
if (item && typeof item === "object") {
183+
containers.push(
184+
...findItemContainers(item, `${currentPath}[${index}]`, saveGame),
185+
);
186+
}
187+
});
188+
} else {
189+
containers.push(...findItemContainers(value, currentPath, saveGame));
190+
}
191+
}
192+
193+
return containers;
194+
}
195+
196+
function parseItems(itemsData: any): InventoryItem[] {
197+
if (!itemsData) return [];
198+
199+
const itemsArray = Array.isArray(itemsData) ? itemsData : [itemsData];
200+
const items: InventoryItem[] = [];
201+
202+
for (const item of itemsArray) {
203+
if (!item || typeof item !== "object" || item.parentSheetIndex == null) {
204+
continue;
205+
}
206+
207+
items.push({
208+
name: item.name || item.Name || "",
209+
type: item.type || "",
210+
quality: item.quality || 0,
211+
stack: item.stack || item.Stack || 1,
212+
index: item.parentSheetIndex || 0,
213+
category: item.category || -1,
214+
parentSheetIndex: item.parentSheetIndex || 0,
215+
});
216+
}
217+
218+
return items;
219+
}
220+
221+
export function parseInventory(
222+
prefix: string,
223+
SaveGame: any,
224+
player?: any,
225+
): InventoryRet {
226+
try {
227+
// Search through the entire SaveGame object
228+
const allContainers = findItemContainers(SaveGame, "SaveGame", SaveGame);
229+
230+
// Also search through player object if provided
231+
if (player) {
232+
allContainers.push(...findItemContainers(player, "Player", SaveGame));
233+
}
234+
235+
// Calculate totals efficiently
236+
let totalItems = 0;
237+
const uniqueItems = new Set<number>();
238+
239+
for (const container of allContainers) {
240+
totalItems += container.items.length;
241+
for (const item of container.items) {
242+
uniqueItems.add(item.index);
243+
}
244+
}
245+
246+
return {
247+
containers: allContainers,
248+
totalItems,
249+
totalUniqueItems: uniqueItems.size,
250+
};
251+
} catch (err) {
252+
const message = err instanceof Error ? err.message : String(err);
253+
throw new Error(`Error in parseInventory: ${message}`);
254+
}
255+
}

package-test.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "inventory-parser-test",
3+
"version": "1.0.0",
4+
"description": "Test script for the inventory parser",
5+
"main": "test-inventory.js",
6+
"scripts": {
7+
"test": "node test-inventory.js",
8+
"test-ts": "npx ts-node test-inventory.ts"
9+
},
10+
"dependencies": {
11+
"fast-xml-parser": "^4.3.2"
12+
},
13+
"devDependencies": {
14+
"@types/node": "^20.0.0",
15+
"typescript": "^5.0.0",
16+
"ts-node": "^10.9.0"
17+
}
18+
}

0 commit comments

Comments
 (0)