Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,24 @@
onClose: () => void;
onRestored: () => void;
}>) {
const { t } = useTranslation("settings");
const { showErrorToast, showSuccessToast } = useBigPictureToast();
const unformattedKey =
platform === "ps2"
? "cloud_restore_unformatted_ps2"
: "cloud_restore_unformatted";
return (
<EmulationCloudRestoreModal
save={save}
platform={platform}
onClose={onClose}
onRestored={onRestored}
onRestoreSuccess={() => showSuccessToast("Cloud save restored")}
onRestoreError={() => showErrorToast("Failed to restore cloud save")}
onRestoreSuccess={() => showSuccessToast(t("cloud_restore_success"))}
onRestoreError={(reason) =>
showErrorToast(
t(reason === "unformatted" ? unformattedKey : "cloud_restore_failed")
)
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
regionId={RESTORE_MODAL_REGION_ID}
actionsRegionId={RESTORE_MODAL_ACTIONS_REGION_ID}
pickButtonId={RESTORE_MODAL_PICK_BUTTON_ID}
Expand Down Expand Up @@ -254,7 +263,7 @@

try {
const preview = await (
globalThis.window.electron as any

Check warning on line 266 in src/big-picture/src/components/pages/game/game-settings-modal/cloud-tab.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
).getGameBackupPreview(game.objectId, game.shop);
setBackupPreview(preview);
} catch {
Expand Down Expand Up @@ -302,11 +311,11 @@
);

const removeBackupDownloadProgressListener = (
globalThis.window.electron as any

Check warning on line 314 in src/big-picture/src/components/pages/game/game-settings-modal/cloud-tab.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
).onBackupDownloadProgress(
game.objectId,
game.shop,
(progressEvent: any) => {

Check warning on line 318 in src/big-picture/src/components/pages/game/game-settings-modal/cloud-tab.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (progressEvent.progress !== undefined) {
setBackupDownloadProgress(progressEvent.progress);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,26 @@ function RestoreModal({
onClose,
onRestored,
}: Readonly<RestoreModalProps>) {
const { t } = useTranslation("settings");
const { showErrorToast, showSuccessToast } = useBigPictureToast();
const unformattedKey =
platform === "ps2"
? "cloud_restore_unformatted_ps2"
: "cloud_restore_unformatted";
return (
<EmulationCloudRestoreModal
save={save}
platform={platform}
onClose={onClose}
onRestored={onRestored}
onRestoreSuccess={() =>
showSuccessToast("Cloud save restored", SETTINGS_TOAST_OPTIONS)
showSuccessToast(t("cloud_restore_success"), SETTINGS_TOAST_OPTIONS)
}
onRestoreError={() =>
showErrorToast("Failed to restore cloud save", SETTINGS_TOAST_OPTIONS)
onRestoreError={(reason) =>
showErrorToast(
t(reason === "unformatted" ? unformattedKey : "cloud_restore_failed"),
SETTINGS_TOAST_OPTIONS
)
}
regionId={RESTORE_MODAL_REGION_ID}
actionsRegionId={RESTORE_MODAL_ACTIONS_REGION_ID}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
EmulationCloudSave,
EmulationSavePlatform,
MemcardFormatState,
MemcardRestoreErrorReason,
MemcardRestoreTarget,
} from "@types";
import { useCallback, useEffect, useState } from "react";
Expand Down Expand Up @@ -38,7 +40,7 @@ interface EmulationCloudRestoreModalProps {
onClose: () => void;
onRestored: () => void;
onRestoreSuccess: () => void;
onRestoreError: () => void;
onRestoreError: (reason?: MemcardRestoreErrorReason) => void;
regionId: string;
actionsRegionId: string;
pickButtonId: string;
Expand All @@ -65,19 +67,54 @@ export function EmulationCloudRestoreModal({
const { setFocus } = useNavigation();
const [targets, setTargets] = useState<MemcardRestoreTarget[]>([]);
const [selectedTarget, setSelectedTarget] = useState<string | null>(null);
const [selectedFormat, setSelectedFormat] =
useState<MemcardFormatState | null>(null);
const [isBusy, setIsBusy] = useState(false);

const unformattedKey =
platform === "ps2"
? "cloud_restore_unformatted_ps2"
: "cloud_restore_unformatted";

useEffect(() => {
if (!save) return;

let cancelled = false;
void globalThis.window.electron
.getMemcardRestoreTargets(platform)
.then((foundTargets) => {
if (cancelled) return;
setTargets(foundTargets);
setSelectedTarget(foundTargets[0]?.cardFilePath ?? null);
});

return () => {
cancelled = true;
};
}, [platform, save]);

useEffect(() => {
if (!selectedTarget) {
setSelectedFormat(null);
return;
}

let cancelled = false;
setSelectedFormat(null);
globalThis.window.electron
.inspectMemcard(platform, selectedTarget)
.then((state) => {
if (!cancelled) setSelectedFormat(state);
})
.catch(() => {
if (!cancelled) setSelectedFormat("unreadable");
});

return () => {
cancelled = true;
};
}, [platform, selectedTarget]);

useEffect(() => {
if (!save) return;

Expand Down Expand Up @@ -134,7 +171,7 @@ export function EmulationCloudRestoreModal({
onRestored();
onClose();
} else {
onRestoreError();
onRestoreError(result.reason);
}
} finally {
setIsBusy(false);
Expand Down Expand Up @@ -207,6 +244,10 @@ export function EmulationCloudRestoreModal({
)}
</div>

{selectedFormat === "unformatted" && (
<p className="emu-save-modal__hint">{t(unformattedKey)}</p>
)}

<HorizontalFocusGroup
regionId={actionsRegionId}
className="emu-save-modal__actions"
Expand All @@ -228,7 +269,11 @@ export function EmulationCloudRestoreModal({
selectedTarget
)}
loading={isBusy}
disabled={!selectedTarget}
disabled={
!selectedTarget ||
!selectedFormat ||
selectedFormat === "unformatted"
}
onClick={handleRestore}
>
{t("cloud_restore_confirm")}
Expand Down
7 changes: 7 additions & 0 deletions src/big-picture/src/pages/settings/emulation/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
outline-offset: 2px;
}

.emu-save-modal__hint {
margin: 0;
color: var(--warning, #e3b341);
font-size: 13px;
line-height: 1.4;
}

.emulator-detail,
.emulation-settings__scan-modal,
.emu-save-modal__restore,
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,8 @@
"cloud_restore_no_cards": "No memory cards detected. Pick a card file to restore into.",
"cloud_restore_success": "Save restored to your memory card",
"cloud_restore_failed": "Could not restore this save",
"cloud_restore_unformatted": "This memory card isn't formatted yet. Format it in your emulator's memory card manager before restoring a save into it.",
"cloud_restore_unformatted_ps2": "This memory card isn't formatted. Create a save on it so PCSX2 formats it automatically.",
"cloud_more_actions": "More actions",
"cloud_delete": "Delete",
"cloud_delete_title": "Delete this cloud save?",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,8 @@
"cloud_restore_no_cards": "No se detectaron tarjetas de memoria. Elige un archivo de tarjeta en el que restaurar.",
"cloud_restore_success": "Partida restaurada en tu tarjeta de memoria",
"cloud_restore_failed": "No se pudo restaurar esta partida",
"cloud_restore_unformatted": "Esta tarjeta de memoria aún no está formateada. Formatéala en el administrador de tarjetas de memoria de tu emulador antes de restaurar una partida en ella.",
"cloud_restore_unformatted_ps2": "Esta tarjeta de memoria no está formateada. Crea una partida guardada en ella para que PCSX2 la formatee automáticamente.",
"cloud_more_actions": "Más acciones",
"cloud_delete": "Eliminar",
"cloud_delete_title": "¿Eliminar esta partida en la nube?",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,8 @@
"cloud_restore_no_cards": "Nenhum memory card detectado. Escolha um arquivo de card para restaurar.",
"cloud_restore_success": "Salvamento restaurado no seu memory card",
"cloud_restore_failed": "Não foi possível restaurar este salvamento",
"cloud_restore_unformatted": "Este memory card ainda não está formatado. Formate-o no gerenciador de memory cards do seu emulador antes de restaurar um salvamento nele.",
"cloud_restore_unformatted_ps2": "Este memory card não está formatado. Crie um salvamento nele para que o PCSX2 o formate automaticamente.",
"cloud_more_actions": "Mais ações",
"cloud_delete": "Excluir",
"cloud_delete_title": "Excluir este salvamento na nuvem?",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,8 @@
"cloud_restore_no_cards": "Карты памяти не обнаружены. Выберите файл карты для восстановления.",
"cloud_restore_success": "Сохранение восстановлено на вашу карту памяти",
"cloud_restore_failed": "Не удалось восстановить это сохранение",
"cloud_restore_unformatted": "Эта карта памяти ещё не отформатирована. Отформатируйте её в менеджере карт памяти вашего эмулятора, прежде чем восстанавливать на неё сохранение.",
"cloud_restore_unformatted_ps2": "Эта карта памяти не отформатирована. Создайте на ней сохранение, чтобы PCSX2 отформатировал её автоматически.",
"cloud_more_actions": "Больше действий",
"cloud_delete": "Удалить",
"cloud_delete_title": "Удалить это облачное сохранение?",
Expand Down
1 change: 1 addition & 0 deletions src/main/events/emulators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ import "./ps1-memcard-saves";
import "./export-ps1-save";
import "./upload-emulation-save";
import "./restore-emulation-save";
import "./inspect-memcard";
import "./emulation-saves";
14 changes: 14 additions & 0 deletions src/main/events/emulators/inspect-memcard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { registerEvent } from "../register-event";
import { emulators } from "@main/services";
import type { EmulationSavePlatform, MemcardFormatState } from "@types";

const inspectMemcard = async (
_event: Electron.IpcMainInvokeEvent,
platform: EmulationSavePlatform,
cardFilePath: string
): Promise<MemcardFormatState> =>
platform === "ps2"
? emulators.inspectPs2Card(cardFilePath)
: emulators.inspectPs1Card(cardFilePath);

registerEvent("inspectMemcard", inspectMemcard);
9 changes: 8 additions & 1 deletion src/main/events/emulators/restore-emulation-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ const restoreEmulationSave = async (
platform === "ps2"
? await emulators.importPsuIntoCard(targetCardFilePath, bytes)
: await emulators.importMcsIntoCard(targetCardFilePath, bytes);
return { ok: result.ok, error: result.error };
if (!result.ok) {
logger.error(
"Failed to restore emulation save",
{ platform, saveId, targetCardFilePath, reason: result.reason },
result.error
);
}
return { ok: result.ok, error: result.error, reason: result.reason };
} catch (err) {
logger.error("Failed to restore emulation save", err);
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";

import {
findDataOffset,
inspectPs1Card,
listPs1Saves,
readPs1SaveContents,
} from "./ps1-memory-card";
Expand Down Expand Up @@ -111,6 +112,7 @@ const chainFrom = (directory: Buffer, firstBlock: number): number[] => {
export interface Ps1ImportResult {
ok: boolean;
error?: string;
reason?: "unformatted";
identifier?: string;
}

Expand Down Expand Up @@ -192,6 +194,14 @@ export const importMcsIntoCard = async (
const mcs = parseMcsBuffer(mcsBuffer);
if (!mcs) return { ok: false, error: "Invalid .mcs data" };

if ((await inspectPs1Card(cardFilePath)) === "unformatted") {
return {
ok: false,
reason: "unformatted",
error: "The memory card is not formatted",
};
}

const backupPath = `${cardFilePath}.hydra-bak`;
try {
await fs.copyFile(cardFilePath, backupPath);
Expand All @@ -212,7 +222,11 @@ export const importMcsIntoCard = async (
const dataOffset = findDataOffset(fileBuf, fileBuf.length);
if (dataOffset === null) {
await restore();
return { ok: false, error: "Not a writable PS1 memory card" };
return {
ok: false,
reason: "unformatted",
error: "Not a writable PS1 memory card",
};
}
const applied = applyMcsToCard(fileBuf.subarray(dataOffset), mcs);
if (!applied.ok) {
Expand Down
26 changes: 26 additions & 0 deletions src/main/services/emulators/ps1-memory-card/ps1-memory-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,32 @@ export const findDataOffset = (
return null;
};

export const inspectPs1Card = async (
cardFilePath: string
): Promise<"formatted" | "unformatted" | "unreadable"> => {
let fh: FileHandle | null = null;
try {
fh = await fs.open(cardFilePath, "r");
const stat = await fh.stat();
if (stat.size < PS1_BLOCK_BYTES) return "unformatted";

const probeLen = Math.min(
stat.size,
(HEADER_OFFSET_CANDIDATES.at(-1) ?? 0) + PS1_BLOCK_BYTES
);
const probe = Buffer.alloc(probeLen);
await fh.read(probe, 0, probeLen, 0);

return findDataOffset(probe, stat.size) === null
? "unformatted"
: "formatted";
} catch {
return "unreadable";
} finally {
if (fh) await fh.close().catch(() => undefined);
}
};

const readFilename = (frame: Buffer): string => {
const raw = frame.subarray(
PS1_FILENAME_OFFSET,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FAT_ALLOCATED_BIT,
FAT_VALUE_MASK,
detectEcc,
inspectPs2Card,
listSaves,
parseDirEntry,
readSaveContents,
Expand Down Expand Up @@ -501,6 +502,7 @@ class Ps2CardWriter {
export interface Ps2ImportResult {
ok: boolean;
error?: string;
reason?: "unformatted";
folderName?: string;
}

Expand Down Expand Up @@ -533,6 +535,14 @@ export const importPsuIntoCard = async (
const psu = parsePsuBuffer(psuBuffer);
if (!psu) return { ok: false, error: "Invalid .psu data" };

if ((await inspectPs2Card(cardFilePath)) === "unformatted") {
return {
ok: false,
reason: "unformatted",
error: "The memory card is not formatted",
};
}

const backupPath = `${cardFilePath}.hydra-bak`;
try {
await fs.copyFile(cardFilePath, backupPath);
Expand Down
15 changes: 15 additions & 0 deletions src/main/services/emulators/ps2-memory-card/ps2-memory-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ export const readSuperblock = async (
};
};

export const inspectPs2Card = async (
cardFilePath: string
): Promise<"formatted" | "unformatted" | "unreadable"> => {
let fh: FileHandle | null = null;
try {
fh = await fs.open(cardFilePath, "r");
const sb = await readSuperblock(fh);
return sb ? "formatted" : "unformatted";
} catch {
return "unreadable";
} finally {
if (fh) await fh.close().catch(() => undefined);
}
};

export const detectEcc = (
fileSize: number,
sb: Superblock
Expand Down
6 changes: 6 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type {
EmulationBackupProgress,
EmulationCloudSave,
EmulationSavePlatform,
MemcardFormatState,
MemcardRestoreResult,
MemcardRestoreTarget,
} from "@types";
Expand Down Expand Up @@ -359,6 +360,11 @@ contextBridge.exposeInMainWorld("electron", {
platform: EmulationSavePlatform
): Promise<MemcardRestoreTarget[]> =>
ipcRenderer.invoke("getMemcardRestoreTargets", platform),
inspectMemcard: (
platform: EmulationSavePlatform,
cardFilePath: string
): Promise<MemcardFormatState> =>
ipcRenderer.invoke("inspectMemcard", platform, cardFilePath),
restoreEmulationSave: (
platform: EmulationSavePlatform,
saveId: string,
Expand Down
Loading
Loading