Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -83,7 +83,13 @@
onClose={onClose}
onRestored={onRestored}
onRestoreSuccess={() => showSuccessToast("Cloud save restored")}
onRestoreError={() => showErrorToast("Failed to restore cloud save")}
onRestoreError={(reason) =>
showErrorToast(
reason === "unformatted"
? "This memory card isn't formatted. Format it in your emulator before restoring."
: "Failed to restore cloud save"
)
}
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 +260,7 @@

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

Check warning on line 263 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 +308,11 @@
);

const removeBackupDownloadProgressListener = (
globalThis.window.electron as any

Check warning on line 311 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 315 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 @@ -94,8 +94,13 @@ function RestoreModal({
onRestoreSuccess={() =>
showSuccessToast("Cloud save restored", SETTINGS_TOAST_OPTIONS)
}
onRestoreError={() =>
showErrorToast("Failed to restore cloud save", SETTINGS_TOAST_OPTIONS)
onRestoreError={(reason) =>
showErrorToast(
reason === "unformatted"
? "This memory card isn't formatted. Format it in your emulator before restoring."
: "Failed to restore cloud save",
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,6 +67,8 @@ 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);

useEffect(() => {
Expand All @@ -78,6 +82,25 @@ export function EmulationCloudRestoreModal({
});
}, [platform, save]);

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

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

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

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

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

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

<HorizontalFocusGroup
regionId={actionsRegionId}
className="emu-save-modal__actions"
Expand All @@ -228,7 +257,7 @@ export function EmulationCloudRestoreModal({
selectedTarget
)}
loading={isBusy}
disabled={!selectedTarget}
disabled={!selectedTarget || 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 @@ -20,6 +20,13 @@

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

.emu-save-modal__restore,
.emu-save-modal__rename {
.emu-save-modal__target {
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,7 @@
"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_more_actions": "More actions",
"cloud_delete": "Delete",
"cloud_delete_title": "Delete this cloud save?",
Expand Down
1 change: 1 addition & 0 deletions src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@
"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_more_actions": "Más acciones",
"cloud_delete": "Eliminar",
"cloud_delete_title": "¿Eliminar esta partida en la nube?",
Expand Down
1 change: 1 addition & 0 deletions src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,7 @@
"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_more_actions": "Mais ações",
"cloud_delete": "Excluir",
"cloud_delete_title": "Excluir este salvamento na nuvem?",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,7 @@
"cloud_restore_no_cards": "Карты памяти не обнаружены. Выберите файл карты для восстановления.",
"cloud_restore_success": "Сохранение восстановлено на вашу карту памяти",
"cloud_restore_failed": "Не удалось восстановить это сохранение",
"cloud_restore_unformatted": "Эта карта памяти ещё не отформатирована. Отформатируйте её в менеджере карт памяти вашего эмулятора, прежде чем восстанавливать на неё сохранение.",
"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
5 changes: 5 additions & 0 deletions src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
DetectedRom,
EmulationCloudSave,
EmulationSavePlatform,
MemcardFormatState,
MemcardRestoreResult,
MemcardRestoreTarget,
} from "@types";
Expand Down Expand Up @@ -544,6 +545,10 @@ declare global {
getMemcardRestoreTargets: (
platform: EmulationSavePlatform
) => Promise<MemcardRestoreTarget[]>;
inspectMemcard: (
platform: EmulationSavePlatform,
cardFilePath: string
) => Promise<MemcardFormatState>;
restoreEmulationSave: (
platform: EmulationSavePlatform,
saveId: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
font-size: 13px;
}

&__hint {
margin: 0;
color: var(--warning, #e3b341);
font-size: 13px;
line-height: 1.4;
}

&__actions {
display: flex;
justify-content: flex-end;
Expand Down
Loading
Loading