Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for compressed saves (#130) #131

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"mysql2": "^3.10.1",
"next": "^14.2.3",
"next-themes": "^0.2.1",
"pako": "^2.1.0",
"postcss": "8.4.24",
"posthog-js": "^1.110.0",
"react": "18.2.0",
Expand All @@ -68,6 +69,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@types/pako": "^2.0.3",
"drizzle-kit": "^0.22.7",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11"
Expand Down
22 changes: 19 additions & 3 deletions src/components/dialogs/upload-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useContext } from "react";
import Dropzone from "react-dropzone";
import { toast } from "sonner";
import { Button } from "../ui/button";
import pako from "pako";

interface Props {
open: boolean;
Expand Down Expand Up @@ -41,13 +42,27 @@ export const UploadDialog = ({ open, setOpen }: Props) => {
uploadPromise = new Promise((resolve, reject) => {
reader.onload = async function (event) {
try {
const players = parseSaveFile(event.target?.result as string);
const arrayBuffer = event.target?.result as ArrayBuffer;
const content = new Uint8Array(arrayBuffer);

let decompressedContent;
if (content[0] === 120) { // This is also how vanilla checks if a save is compressed. Around SaveGame.cs Line 671
// File is zlib compressed
const decompressed = pako.inflate(content);
decompressedContent = new TextDecoder().decode(decompressed);
} else {
// File is not compressed
decompressedContent = new TextDecoder().decode(content);
}

const players = parseSaveFile(decompressedContent);
await uploadPlayers(players);
resolve("Your save file was successfully uploaded!");
} catch (err) {
console.error("Error processing file:", err);
reject(err instanceof Error ? err.message : "Unknown error.");
}
};
}
});

// Start the loading toast
Expand All @@ -61,7 +76,8 @@ export const UploadDialog = ({ open, setOpen }: Props) => {
uploadPromise = null;
};

reader.readAsText(file);
// reader.readAsText(file);
reader.readAsArrayBuffer(file);
};

return (
Expand Down
17 changes: 6 additions & 11 deletions src/lib/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,34 +60,29 @@ export function parseSaveFile(xml: string) {
// objects are unprocessed and will be used to parse each player's data
players = getAllFarmhands(saveFile.SaveGame);

// find the prefix to use for attributes (xsi for pc, p3 for mobile)
const prefix =
typeof saveFile.SaveGame["@_xmlns:xsi"] === "undefined" ? "p3" : "xsi";

// console.log(prefix === "xsi" ? "PC" : "Mobile");

const parsedBundles = parseBundles(
saveFile.SaveGame.bundleData,
saveFile.SaveGame.locations.GameLocation.find(
(obj: any) => obj[`@_${prefix}:type`] === "CommunityCenter",
(obj: any) => Object.entries(obj).some(([k, v]) => k.endsWith(':type') && v == 'CommunityCenter'),

),
version,
);

const parsedMuseum = parseMuseum(
saveFile.SaveGame.locations.GameLocation.find(
(obj: any) => obj[`@_${prefix}:type`] === "LibraryMuseum",
(obj: any) => Object.entries(obj).some(([k, v]) => k.endsWith(':type') && v == 'LibraryMuseum'),
),
version,
);

const parsedWalnuts = parseWalnuts(saveFile.SaveGame);

// obelisks and golden clock
const parsedPerfection = parsePerfection(prefix, saveFile.SaveGame);
const parsedPerfection = parsePerfection(saveFile.SaveGame);

// Map of uniqueMultiplayerID to array of children names
const children = findChildren(prefix, saveFile.SaveGame);
const children = findChildren(saveFile.SaveGame);

let processedPlayers: any[] = [];

Expand Down
3 changes: 1 addition & 2 deletions src/lib/parsers/perfection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export interface PerfectionRet {
}

export const parsePerfection = (
prefix: string,
SaveGame: any,
): PerfectionRet => {
let numObelisks = 0;
Expand All @@ -27,7 +26,7 @@ export const parsePerfection = (

// For now, we'll only look in the GameLocation named "Farm", if this is wrong, we'll fix it later
for (const location of SaveGame.locations.GameLocation) {
if (!(location[`@_${prefix}:type`] === "Farm")) continue;
if (!(Object.entries(location).some(([k, v]) => k.endsWith(':type') && v == 'Farm'))) continue;

if (!location.buildings)
return { numObelisks, goldenClock, perfectionWaivers };
Expand Down
7 changes: 3 additions & 4 deletions src/lib/parsers/social.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import villagers from "@/data/villagers.json";

export function findChildren(
prefix: string,
SaveGame: any
): Map<string, number> {
// for now we only care about the number of children, may have to adjust
Expand All @@ -18,8 +17,8 @@ export function findChildren(
if (Array.isArray(location.characters.NPC)) {
// multiple characters in location
for (const NPC of location.characters.NPC) {
if (!NPC[`@_${prefix}:type`]) continue;
if (NPC[`@_${prefix}:type`] === "Child") {
if (!Object.keys(NPC).some(e => e.endsWith(':type'))) continue;
if (Object.entries(NPC).some(([k, v]) => k.endsWith(':type') && v == 'Child')) {
// found a child, increment count, or add to map if not present
children.set(
NPC.idOfParent,
Expand All @@ -30,7 +29,7 @@ export function findChildren(
} else {
// only one character in location, check if it's a child
let NPC = location.characters.NPC;
if (NPC[`@_${prefix}:type`] === "Child") {
if (Object.entries(NPC).some(([k, v]) => k.endsWith(':type') && v == 'Child')) {
// found a child, increment count, or add to map if not present
children.set(NPC.idOfParent, (children.get(NPC.idOfParent) ?? 0) + 1);
}
Expand Down
Loading