console page: Cheats for the running game

The console page gets a Cheats section (rail: Home, Files, Cheats, Settings; Home's
Cheats shortcut now shows). It lists the running game's cheats as switches: X or the
square button switches the focused one, rows are inert while a switch runs, and the
helper's state is read again after every switch. No game, no cheats, the game closing
while the page is open, and helper errors each say what to do next, with Refresh.

The helper adapts the cheat methods exactly as the PC engine calls them:
/api/cheats/status (CHEATS_STATUS), /api/cheats/list {title_id} (CHEATS_GET, every
entry re-escaped, a cut reply keeps its whole entries) and /api/cheats/toggle
{title_id,index,on} (CHEATS_TOGGLE, its free-text refusals mapped to tokens).
Downloads and deletes stay in the PC app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
phantomptrandClaude Opus 5.5 committed 2026-10-17 09:39:58 -07:00
1 parent 297a76efd2
commit e4d069a3f0
32 files changed
+1999 -3

No files matched your search

+264
View File
@@ -0,0 +1,264 @@
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../api", async (importOriginal) => {
const real = await importOriginal<typeof import("../api")>();
return { ...real, call: vi.fn() };
});
import { ApiError, call } from "../api";
import { SECTIONS } from "../sections";
import { t } from "../strings";
import { CheatsView, focusedCheatIndex, type CheatsViewProps } from "./Cheats";
import {
cheatsPhase,
createCheatsController,
EMPTY_STATE,
readList,
readStatus,
toggleErrorText,
type CheatMod,
type CheatsState,
} from "./model";
const mockCall = vi.mocked(call);
const GAME = "PPSA01650";
const mod = (over: Partial<CheatMod> = {}): CheatMod => ({
index: 0,
name: "Infinite HP",
desc: "",
button: false,
on: false,
...over,
});
/** A fake helper: status says `game` runs; list answers `mods`; toggle flips in place. */
function helper(opts: { game: string | null; mods?: CheatMod[]; toggleError?: ApiError; engine?: boolean }) {
const h = { ...opts, mods: (opts.mods ?? []).map((m) => ({ ...m })), calls: [] as [string, unknown][] };
mockCall.mockImplementation(async (route: string, body?: object) => {
h.calls.push([route, body]);
if (route === "/api/cheats/status") return { engine: h.engine ?? false, game: h.game ? { title_id: h.game } : null } as never;
if (route === "/api/cheats/list") return { mods: h.mods, truncated: false } as never;
if (route === "/api/cheats/toggle") {
if (h.toggleError) throw h.toggleError;
const b = body as { index: number; on: boolean };
const m = h.mods.find((x) => x.index === b.index)!;
if (!m.button) m.on = b.on;
return { ok: true, on: b.on } as never;
}
throw new Error(`unexpected ${route}`);
});
return h;
}
const view = (state: Partial<CheatsState>, over: Partial<CheatsViewProps> = {}) =>
renderToStaticMarkup(
<CheatsView state={{ ...EMPTY_STATE, ...state }} onToggle={() => {}} onRefresh={() => {}} {...over} />,
);
beforeEach(() => {
mockCall.mockReset();
});
describe("cheats section", () => {
it("is_registered_between_files_and_settings", () => {
expect(SECTIONS.map((s) => s.id)).toEqual(["home", "files", "cheats", "settings"]);
expect(SECTIONS.find((s) => s.id === "cheats")?.labelKey).toBe("cw_cheats_title");
});
it("no_game_says_what_to_do", async () => {
helper({ game: null });
const states: CheatsState[] = [];
const c = createCheatsController((s) => states.push(s));
await c.loadStatus();
expect(cheatsPhase(c.get())).toEqual({ kind: "no_game" });
const html = view(c.get());
expect(html).toContain(t("cw_cheats_no_game"));
expect(html).toContain(t("cw_cheats_no_game_next"));
expect(t("cw_cheats_no_game_next")).toBe("Start a game, then open Cheats.");
expect(html).not.toContain("data-cheat-index");
// No list read without a game.
expect(mockCall.mock.calls.map((x) => x[0])).toEqual(["/api/cheats/status"]);
});
it("no_cheats_points_to_the_pc_app", async () => {
helper({ game: GAME, mods: [] });
const c = createCheatsController(() => {});
await c.loadStatus();
expect(cheatsPhase(c.get())).toEqual({ kind: "none", title: GAME });
const html = view(c.get());
expect(html).toContain(t("cw_cheats_none_next"));
expect(t("cw_cheats_none_next")).toBe("Add cheats for this game from the PS5Upload app on your PC.");
expect(html).toContain(GAME);
});
it("lists_the_running_games_cheats_as_switches", async () => {
helper({ game: GAME, mods: [mod({ index: 0, on: true }), mod({ index: 3, name: "Max money", desc: "999999" }), mod({ index: 4, name: "Heal", button: true })] });
const c = createCheatsController(() => {});
await c.loadStatus();
expect(mockCall).toHaveBeenCalledWith("/api/cheats/list", { title_id: GAME });
const html = view(c.get());
expect(html).toContain('role="switch" aria-checked="true"');
expect(html).toContain('data-cheat-index="3"');
expect(html).toContain("999999");
// A one-shot cheat is an Apply button, not a switch.
expect(html).toMatch(/<button type="button" class="[^"]*cw-cheat"[^>]*data-cheat-index="4"/);
expect(html).toContain(t("cw_cheats_apply"));
// The engine is off: the header says a switch-on starts it.
expect(html).toContain(t("cw_cheats_engine_off"));
});
it("toggle_calls_the_route_and_reflects_the_result", async () => {
const h = helper({ game: GAME, mods: [mod({ index: 0 }), mod({ index: 1, name: "B" })] });
const c = createCheatsController(() => {});
await c.loadStatus();
const p = c.toggle(c.get().list!.mods[1]);
// In flight: every row is inert, the pressed one says so.
expect(c.get().busy).toBe(1);
const busyHtml = view(c.get());
expect(busyHtml.match(/aria-disabled="true"/g)).toHaveLength(2);
expect(busyHtml).toContain(t("cw_cheats_switching"));
// A second press meanwhile is ignored.
await c.toggle(c.get().list!.mods[0]);
await p;
expect(h.calls.filter((x) => x[0] === "/api/cheats/toggle")).toEqual([
["/api/cheats/toggle", { title_id: GAME, index: 1, on: true }],
]);
// Re-read after the toggle: the helper's state is what shows.
expect(h.calls[h.calls.length - 1][0]).toBe("/api/cheats/list");
expect(c.get().busy).toBeNull();
expect(c.get().list!.mods.find((m) => m.index === 1)?.on).toBe(true);
expect(view(c.get())).not.toContain("aria-disabled");
// And off again.
await c.toggle(c.get().list!.mods[1]);
expect(h.calls.filter((x) => x[0] === "/api/cheats/toggle")[1][1]).toEqual({ title_id: GAME, index: 1, on: false });
expect(c.get().list!.mods[1].on).toBe(false);
});
it("a_one_shot_cheat_is_always_applied_on", async () => {
const h = helper({ game: GAME, mods: [mod({ index: 2, button: true })] });
const c = createCheatsController(() => {});
await c.loadStatus();
await c.toggle(c.get().list!.mods[0]);
await c.toggle(c.get().list!.mods[0]);
expect(h.calls.filter((x) => x[0] === "/api/cheats/toggle").map((x) => (x[1] as { on: boolean }).on)).toEqual([true, true]);
});
it("game_exited_while_open_drops_the_switches", async () => {
const h = helper({ game: GAME, mods: [mod()] });
const c = createCheatsController(() => {});
await c.loadStatus();
expect(cheatsPhase(c.get()).kind).toBe("list");
h.game = null;
await c.loadStatus();
expect(cheatsPhase(c.get())).toEqual({ kind: "exited", title: GAME });
expect(c.get().list).toBeNull();
const html = view(c.get());
expect(html).toContain(t("cw_cheats_exited"));
expect(html).not.toContain("data-cheat-index");
expect(html).toContain('data-action="cheats-refresh"');
});
it("toggle_after_the_game_closed_shows_closed_not_an_error", async () => {
const h = helper({ game: GAME, mods: [mod()] });
const c = createCheatsController(() => {});
await c.loadStatus();
h.game = null;
h.toggleError = new ApiError(409, "game_not_running");
await c.toggle(c.get().list!.mods[0]);
expect(c.get().message).toBeNull();
expect(cheatsPhase(c.get()).kind).toBe("exited");
});
it("another_game_never_shows_the_old_games_cheats", async () => {
const h = helper({ game: GAME, mods: [mod({ name: "Old" })] });
const c = createCheatsController(() => {});
await c.loadStatus();
// The list read for the new game is slow: nothing of the old game shows meanwhile.
h.game = "CUSA07842";
let release!: () => void;
mockCall.mockImplementationOnce(async () => ({ engine: false, game: { title_id: "CUSA07842" } }) as never);
mockCall.mockImplementationOnce(
() => new Promise((res) => (release = () => res({ mods: [mod({ name: "New" })], truncated: false } as never))),
);
const p = c.loadStatus();
await Promise.resolve();
expect(cheatsPhase(c.get()).kind).toBe("loading");
expect(view(c.get())).not.toContain("Old");
release();
await p;
expect(view(c.get())).toContain("New");
});
it("helper_errors_are_short_and_retryable", async () => {
mockCall.mockRejectedValue(new ApiError(500, "oom"));
const c = createCheatsController(() => {});
await c.loadStatus();
const ph = cheatsPhase(c.get());
expect(ph.kind).toBe("error");
const html = view(c.get());
expect(html).toContain("oom");
expect(html).toContain('role="alert"');
expect(html).toContain('data-action="cheats-refresh"');
// Offline reads as offline; a 401 is left to the reload banner.
expect(toggleErrorText(new ApiError(0, "offline"))).toBe(t("cw_home_err_offline"));
expect(toggleErrorText(new ApiError(401, "token"))).toBeNull();
expect(toggleErrorText(new ApiError(409, "module_missing"))).toBe(t("cw_cheats_err_module"));
expect(toggleErrorText(new ApiError(400, "bad_index"))).toBe(t("cw_cheats_err_changed"));
expect(toggleErrorText(new ApiError(500, "toggle_failed"))).toContain("toggle_failed");
});
it("toggle_failure_keeps_the_list_and_says_why", async () => {
const h = helper({ game: GAME, mods: [mod()] });
const c = createCheatsController(() => {});
await c.loadStatus();
h.toggleError = new ApiError(409, "module_missing");
await c.toggle(c.get().list!.mods[0]);
expect(c.get().message).toBe(t("cw_cheats_err_module"));
expect(cheatsPhase(c.get()).kind).toBe("list");
expect(c.get().list!.mods[0].on).toBe(false);
expect(view(c.get())).toContain(t("cw_cheats_err_module"));
});
it("refresh_rereads_status_and_list", async () => {
const h = helper({ game: GAME, mods: [mod()] });
const c = createCheatsController(() => {});
await c.loadStatus();
h.calls.length = 0;
h.mods[0].on = true; // the PC switched it meanwhile
await c.refresh();
expect(h.calls.map((x) => x[0])).toEqual(["/api/cheats/status", "/api/cheats/list"]);
expect(c.get().list!.mods[0].on).toBe(true);
});
it("disposed_controller_stops_reporting", async () => {
helper({ game: GAME, mods: [mod()] });
const seen = vi.fn();
const c = createCheatsController(seen);
c.dispose();
await c.loadStatus();
expect(seen).not.toHaveBeenCalled();
});
it("replies_are_checked", () => {
expect(readStatus(null)).toEqual({ engine: false, game: null });
expect(readStatus({ engine: true, game: { title_id: GAME } })).toEqual({ engine: true, game: { title_id: GAME } });
expect(readStatus({ engine: "yes", game: { title_id: 5 } })).toEqual({ engine: false, game: null });
const l = readList(GAME, { mods: [{ index: 1, name: "A", on: true }, { index: "2", name: "B" }, { index: 3 }, null], truncated: true });
expect(l).toEqual({ title: GAME, mods: [{ index: 1, name: "A", desc: "", button: false, on: true }], truncated: true });
expect(readList(GAME, "nope")).toEqual({ title: GAME, mods: [], truncated: false });
});
it("square_targets_the_focused_cheat_only", () => {
const row = { closest: () => ({ getAttribute: () => "7" }) } as unknown as Element;
const root = { contains: (el: Element) => el === row } as unknown as Element;
expect(focusedCheatIndex(root, row)).toBe(7);
const outside = { closest: () => ({ getAttribute: () => "7" }) } as unknown as Element;
expect(focusedCheatIndex(root, outside)).toBeNull();
const notACheat = { closest: () => null } as unknown as Element;
const root2 = { contains: () => true } as unknown as Element;
expect(focusedCheatIndex(root2, notACheat)).toBeNull();
expect(focusedCheatIndex(null, row)).toBeNull();
});
});
+221
View File
@@ -0,0 +1,221 @@
import { useEffect, useRef, useState } from "react";
import { Gamepad2, RefreshCw } from "lucide-react";
import { useAppCover } from "../home/Home";
import { onNavAction } from "../nav";
import { t } from "../strings";
import { pollWhileVisible } from "../ui/visiblePoll";
import {
cheatsPhase,
createCheatsController,
EMPTY_STATE,
type CheatMod,
type CheatsController,
type CheatsState,
} from "./model";
/** The running game is looked for this often while Cheats is open and the page is visible. */
export const CHEATS_POLL_MS = 5_000;
export interface CheatsViewProps {
state: CheatsState;
/** The running game's cover (an object URL), or null (the generic icon shows). */
cover?: string | null;
onCoverError?: () => void;
onToggle: (mod: CheatMod) => void;
onRefresh: () => void;
}
/**
* One cheat: a switch (✕ clicks it, □ switches the focused one), or Apply for a one-shot cheat.
* While any switch runs the rows are aria-disabled, not disabled: a disabled button drops the
* focus ring to nowhere, and the controller (model.toggle) ignores presses meanwhile anyway.
*/
function CheatRow({ mod, busy, disabled, onToggle }: { mod: CheatMod; busy: boolean; disabled: boolean; onToggle: (m: CheatMod) => void }) {
let tag: string;
if (busy) tag = t("cw_cheats_switching");
else if (mod.button) tag = t("cw_cheats_apply");
else tag = t(mod.on ? "cw_cheats_on" : "cw_cheats_off");
return (
<li className="cw-cheat-item">
<button
type="button"
role={mod.button ? undefined : "switch"}
aria-checked={mod.button ? undefined : mod.on}
aria-busy={busy || undefined}
aria-disabled={disabled || undefined}
className={`cw-focusable cw-button cw-cheat${mod.on && !mod.button ? " is-on" : ""}`}
data-cheat-index={mod.index}
onClick={() => onToggle(mod)}
>
<span className="cw-cheat-text">
<span className="cw-cheat-name">{mod.name}</span>
{mod.desc ? <span className="cw-dim cw-cheat-desc">{mod.desc}</span> : null}
</span>
<span className={`cw-tag${mod.on && !mod.button ? " is-on" : ""}`}>{tag}</span>
</button>
</li>
);
}
function RefreshButton({ onRefresh }: { onRefresh: () => void }) {
return (
<button type="button" className="cw-focusable cw-button" data-action="cheats-refresh" onClick={onRefresh}>
<RefreshCw aria-hidden />
<span>{t("cw_cheats_refresh")}</span>
</button>
);
}
/** An empty state: what is true, what to do next, and Refresh. */
function Empty({ title, next, onRefresh, tone }: { title: string; next?: string; onRefresh: () => void; tone?: "error" }) {
return (
<div className={`cw-card cw-cheats-empty${tone === "error" ? " is-error" : ""}`} role={tone === "error" ? "alert" : "status"}>
<p className="cw-cheats-empty-title">{title}</p>
{next ? <p className="cw-dim">{next}</p> : null}
<RefreshButton onRefresh={onRefresh} />
</div>
);
}
/** Cheats without state: everything comes in through props (tests render it directly). */
export function CheatsView(p: CheatsViewProps) {
const phase = cheatsPhase(p.state);
const s = p.state;
const title = phase.kind === "list" || phase.kind === "none" ? phase.title : null;
let body;
switch (phase.kind) {
case "loading":
body = <p className="cw-dim">{t("cw_cheats_loading")}</p>;
break;
case "error":
body = <Empty title={phase.text} onRefresh={p.onRefresh} tone="error" />;
break;
case "no_game":
body = <Empty title={t("cw_cheats_no_game")} next={t("cw_cheats_no_game_next")} onRefresh={p.onRefresh} />;
break;
case "exited":
body = <Empty title={t("cw_cheats_exited")} next={t("cw_cheats_exited_next")} onRefresh={p.onRefresh} />;
break;
case "none":
body = <Empty title={t("cw_cheats_none")} next={t("cw_cheats_none_next")} onRefresh={p.onRefresh} />;
break;
case "list":
body = (
<>
<ul className="cw-cheats">
{phase.list.mods.map((m) => (
<CheatRow key={m.index} mod={m} busy={s.busy === m.index} disabled={s.busy !== null} onToggle={p.onToggle} />
))}
</ul>
{phase.list.truncated ? <p className="cw-dim cw-cheats-note">{t("cw_cheats_truncated")}</p> : null}
</>
);
break;
}
return (
<div className="cw-cheats-section">
<h1 className="cw-h1">{t("cw_cheats_title")}</h1>
{title ? (
<section className="cw-card cw-game" aria-label={t("cw_home_now_running")}>
{p.cover ? (
<img className="cw-game-cover" src={p.cover} alt="" onError={p.onCoverError} />
) : (
<Gamepad2 className="cw-game-icon" aria-hidden />
)}
<div className="cw-game-text">
<div className="cw-dim">{t("cw_home_now_running")}</div>
<div className="cw-game-title">{title}</div>
<div className="cw-dim cw-cheats-how">
{s.status && !s.status.engine ? t("cw_cheats_engine_off") : t("cw_cheats_how")}
</div>
</div>
<RefreshButton onRefresh={p.onRefresh} />
</section>
) : null}
{s.message ? (
<div className="cw-msg is-error" role="alert">
{s.message}
</div>
) : null}
{body}
</div>
);
}
/** □ on a focused cheat inside `root`: that cheat's index, or null. */
export function focusedCheatIndex(root: Element | null, active: Element | null): number | null {
if (!root || !active || !root.contains(active)) return null;
const el = active.closest("[data-cheat-index]");
if (!el) return null;
const n = Number(el.getAttribute("data-cheat-index"));
return Number.isInteger(n) ? n : null;
}
/** The Cheats section: the running game's cheats, each switched on or off from the controller. */
export function Cheats() {
const [state, setState] = useState<CheatsState>(EMPTY_STATE);
const ctl = useRef<CheatsController | null>(null);
const root = useRef<HTMLDivElement>(null);
// Opening the section reads at once; then the status every few seconds while the page shows.
// Coming back to the page (it was hidden) reads the cheats again too: the PC may have changed them.
useEffect(() => {
const c = createCheatsController(setState);
ctl.current = c;
const stop = pollWhileVisible(() => void c.loadStatus(), CHEATS_POLL_MS);
const onVisible = () => {
if (!document.hidden) void c.loadList();
};
document.addEventListener("visibilitychange", onVisible);
return () => {
stop();
document.removeEventListener("visibilitychange", onVisible);
c.dispose();
ctl.current = null;
};
}, []);
// Create re-reads (App's fallback still re-reads the console status); □ switches the focused cheat.
useEffect(
() =>
onNavAction(
(a) => {
const c = ctl.current;
if (!c) return false;
if (a === "refresh") {
void c.refresh();
return false;
}
if (a === "select") {
const index = focusedCheatIndex(root.current, document.activeElement);
if (index == null) return false;
const mod = c.get().list?.mods.find((m) => m.index === index);
if (mod) void c.toggle(mod);
return true;
}
return false;
},
() => root.current,
),
[],
);
const titleId = state.status?.game?.title_id ?? null;
const loadedCover = useAppCover(titleId);
const [coverFailed, setCoverFailed] = useState<string | null>(null);
const cover = loadedCover && coverFailed !== loadedCover ? loadedCover : null;
return (
<div ref={root}>
<CheatsView
state={state}
cover={cover}
onCoverError={() => setCoverFailed(loadedCover)}
onToggle={(m) => void ctl.current?.toggle(m)}
onRefresh={() => void ctl.current?.refresh()}
/>
</div>
);
}
+228
View File
@@ -0,0 +1,228 @@
/**
* The Cheats section's state, kept out of React so tests drive it directly.
*
* The helper's routes (payload/src/webui_api.c) adapt the cheat methods the PC
* engine calls (engine/crates/ps5upload-core/src/cheats.rs):
* /api/cheats/status → {engine, game: {title_id} | null} (CHEATS_STATUS)
* /api/cheats/list {title_id} → {mods: [{index,name,desc,button,on}], truncated} (CHEATS_GET)
* /api/cheats/toggle {title_id, index, on} → {ok, on} (CHEATS_TOGGLE)
* A toggle needs that game running (the helper patches the live process); a
* switch-on also turns the helper's cheat engine on.
*/
import { ApiError, call, isOffline } from "../api";
import { t } from "../strings";
export interface CheatsStatus {
/** The helper's cheat engine (re-applies switched-on cheats when the game restarts). */
engine: boolean;
game: { title_id: string } | null;
}
export interface CheatMod {
/** The helper's flat index across the game's cheat files (what a toggle names). */
index: number;
name: string;
desc: string;
/** A one-shot cheat: applied each time, it has no on/off state. */
button: boolean;
on: boolean;
}
export interface CheatList {
title: string;
mods: CheatMod[];
/** The helper's reply was cut: not every cheat is listed. */
truncated: boolean;
}
export interface CheatsState {
status: CheatsStatus | null;
/** The last status read failed (null once one succeeds). */
statusError: string | null;
/** The last game seen running while the section was open: tells "closed" from "never ran". */
seen: string | null;
/** The running game's cheats; null while not read (or no game runs). */
list: CheatList | null;
listError: string | null;
/** The index of the cheat being switched, or null. */
busy: number | null;
/** The last toggle's failure, one translated line. */
message: string | null;
}
export type CheatsPhase =
| { kind: "loading" }
| { kind: "error"; text: string }
| { kind: "no_game" }
| { kind: "exited"; title: string }
| { kind: "none"; title: string }
| { kind: "list"; title: string; list: CheatList };
export const EMPTY_STATE: CheatsState = {
status: null,
statusError: null,
seen: null,
list: null,
listError: null,
busy: null,
message: null,
};
/** What the section shows for `s`. Never a stale list: only the running game's own cheats. */
export function cheatsPhase(s: CheatsState): CheatsPhase {
if (!s.status) return s.statusError ? { kind: "error", text: s.statusError } : { kind: "loading" };
const game = s.status.game;
if (!game) return s.seen ? { kind: "exited", title: s.seen } : { kind: "no_game" };
const title = game.title_id;
if (s.list && s.list.title === title) {
return s.list.mods.length ? { kind: "list", title, list: s.list } : { kind: "none", title };
}
if (s.listError) return { kind: "error", text: s.listError };
return { kind: "loading" };
}
/** A failed read as one line; null for a 401 (the reload banner says it). */
export function loadErrorText(err: unknown): string | null {
if (err instanceof ApiError) {
if (err.status === 401) return null;
if (isOffline(err)) return t("cw_home_err_offline");
return t("cw_cheats_err_load", { code: err.code });
}
return t("cw_cheats_err_load", { code: err instanceof Error ? err.message : String(err) });
}
/** A failed toggle as one line; null when the page says it some other way (game gone, 401). */
export function toggleErrorText(err: unknown): string | null {
if (err instanceof ApiError) {
if (err.status === 401 || err.code === "game_not_running") return null;
if (isOffline(err)) return t("cw_home_err_offline");
if (err.code === "module_missing") return t("cw_cheats_err_module");
if (err.code === "bad_index" || err.code === "no_cheats") return t("cw_cheats_err_changed");
return t("cw_cheats_err_toggle", { code: err.code });
}
return t("cw_cheats_err_toggle", { code: err instanceof Error ? err.message : String(err) });
}
const str = (v: unknown): string => (typeof v === "string" ? v : "");
/** The status reply, checked (a helper that answered nonsense reads as "no game"). */
export function readStatus(r: unknown): CheatsStatus {
const o = (r ?? {}) as { engine?: unknown; game?: unknown };
const g = o.game as { title_id?: unknown } | null | undefined;
const title = g && typeof g.title_id === "string" && g.title_id ? g.title_id : null;
return { engine: o.engine === true, game: title ? { title_id: title } : null };
}
/** The list reply, checked: entries without an index or a name are dropped. */
export function readList(title: string, r: unknown): CheatList {
const o = (r ?? {}) as { mods?: unknown; truncated?: unknown };
const mods: CheatMod[] = [];
if (Array.isArray(o.mods)) {
for (const m of o.mods as Record<string, unknown>[]) {
if (!m || typeof m.index !== "number" || !Number.isInteger(m.index) || !str(m.name)) continue;
mods.push({ index: m.index, name: str(m.name), desc: str(m.desc), button: m.button === true, on: m.on === true });
}
}
return { title, mods, truncated: o.truncated === true };
}
export interface CheatsController {
/** Read the status; a new game's cheats are read with it. */
loadStatus(): Promise<void>;
/** Read the running game's cheats again. */
loadList(): Promise<void>;
/** Switch `mod` (a one-shot cheat is applied); ignored while another switch runs. */
toggle(mod: CheatMod): Promise<void>;
/** Create / coming back: the status and the cheats again. */
refresh(): Promise<void>;
/** Stop reporting (the section closed). */
dispose(): void;
get(): CheatsState;
}
export function createCheatsController(onChange: (s: CheatsState) => void): CheatsController {
let s: CheatsState = EMPTY_STATE;
let live = true;
/** Every list read gets a number; only the newest one's answer is kept. */
let listSeq = 0;
const set = (patch: Partial<CheatsState>) => {
if (!live) return;
s = { ...s, ...patch };
onChange(s);
};
const title = () => s.status?.game?.title_id ?? null;
const loadList = async () => {
const tid = title();
if (!tid) return;
const seq = ++listSeq;
try {
const r = await call<unknown>("/api/cheats/list", { title_id: tid });
if (seq !== listSeq || title() !== tid) return;
set({ list: readList(tid, r), listError: null });
} catch (e) {
if (seq !== listSeq || title() !== tid) return;
// A list already shown stays (a failed refresh); the first read's failure is the page's error.
set({ listError: s.list?.title === tid ? null : (loadErrorText(e) ?? s.listError) });
}
};
const loadStatus = async () => {
let next: CheatsStatus;
try {
next = readStatus(await call<unknown>("/api/cheats/status"));
} catch (e) {
set({ statusError: loadErrorText(e) ?? s.statusError });
return;
}
const before = title();
const now = next.game?.title_id ?? null;
if (now === before) {
set({ status: next, statusError: null });
return;
}
// Another game, or none: the old switches go at once (never toggle a game that is not running).
listSeq++;
set({ status: next, statusError: null, list: null, listError: null, message: null, seen: now ?? s.seen });
if (now) await loadList();
};
const toggle = async (mod: CheatMod) => {
const tid = title();
if (s.busy !== null || !tid || s.list?.title !== tid) return;
const on = mod.button ? true : !mod.on;
set({ busy: mod.index, message: null });
let gone = false;
try {
const r = await call<{ on?: unknown }>("/api/cheats/toggle", { title_id: tid, index: mod.index, on });
// Show the switch's new state at once; the re-read below confirms it.
if (!mod.button && s.list?.title === tid) {
const done = r && typeof r.on === "boolean" ? r.on : on;
set({ list: { ...s.list, mods: s.list.mods.map((m) => (m.index === mod.index ? { ...m, on: done } : m)) } });
}
} catch (e) {
gone = e instanceof ApiError && e.code === "game_not_running";
set({ message: toggleErrorText(e) });
}
set({ busy: null });
if (gone) await loadStatus();
else await loadList();
};
return {
loadStatus,
loadList,
toggle,
refresh: async () => {
const before = title();
await loadStatus();
// loadStatus read the list already when the game changed.
if (before && title() === before) await loadList();
},
dispose: () => {
live = false;
},
get: () => s,
};
}
+98
View File
@@ -790,3 +790,101 @@ button {
min-height: 0;
padding: 0.2rem 0.9rem;
}
/* ---- Cheats: three columns of equal rows, so a game's usual dozen-plus cheats fit 1540x700 ---- */
/* The rail names the section (as on Home): the heading stays for screen readers only. */
.cw-cheats-section .cw-h1 {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.cw-cheats-section .cw-game {
padding: 0.4rem 0.8rem;
margin-bottom: 0.6rem;
}
.cw-cheats-section .cw-game-cover {
width: 3.2rem;
height: 3.2rem;
}
.cw-cheats-how {
font-size: 0.85rem;
}
.cw-cheats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-gap: 0.5rem 0.8rem;
gap: 0.5rem 0.8rem;
list-style: none;
margin: 0;
padding: 0;
}
.cw-cheat-item {
display: flex;
min-width: 0;
}
.cw-cheat {
flex: 1;
min-width: 0;
justify-content: space-between;
min-height: 3.3rem;
padding: 0.35rem 0.9rem;
text-align: left;
border-radius: var(--radius-card);
background: var(--color-surface);
box-shadow: inset 0 0 0 1px var(--color-border);
}
/* On is the filled tag, never a ring: a ring is what focus looks like. */
.cw-cheat .cw-tag.is-on {
background: var(--accent-fill);
color: var(--color-accent-contrast);
box-shadow: none;
}
.cw-cheat[aria-disabled="true"] {
cursor: progress;
}
.cw-cheat[aria-disabled="true"]:not([aria-busy="true"]) {
opacity: 0.6;
}
.cw-cheat-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.cw-cheat-name,
.cw-cheat-desc {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cw-cheat-name {
font-weight: 600;
}
.cw-cheat-desc {
font-size: 0.8rem;
}
.cw-cheat .cw-tag {
flex: none;
min-width: 3.2rem;
text-align: center;
}
.cw-cheats-note {
margin: 0.6rem 0 0;
}
.cw-cheats-empty {
max-width: 44rem;
padding: 1rem 1.2rem;
}
.cw-cheats-empty p {
margin: 0 0 0.6rem;
}
.cw-cheats-empty-title {
font-size: 1.1rem;
font-weight: 600;
}
.cw-cheats-empty.is-error .cw-cheats-empty-title {
color: var(--color-bad);
}
+1 -1
View File
@@ -361,7 +361,7 @@ function useVolumes(tick: number): { volumes: Volume[] | null; error: boolean }
const TITLE_ID = /^[A-Z]{4}\d{5}$/;
/** The running game's cover: its icon0.png read through the helper, as an object URL; null when unreadable. */
function useAppCover(titleId: string | null): string | null {
export function useAppCover(titleId: string | null): string | null {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
setUrl(null);
+9 -2
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { Folder, House, Settings, type LucideIcon } from "lucide-react";
import { Folder, House, Settings, Sparkles, type LucideIcon } from "lucide-react";
import { Cheats } from "./cheats/Cheats";
import { Files } from "./files/Files";
import { Home } from "./home/Home";
import { Settings as SettingsSection } from "./settings/Settings";
@@ -34,7 +35,7 @@ export interface Section {
acceptsParams?: boolean;
}
// Registry: Home, Files, (Cheats: added by its own task), Settings. Files opens at ctx.params.path
// Registry: Home, Files, Cheats (the running game's), Settings. Files opens at ctx.params.path
// (acceptsParams), so Home's drive cards open it at a drive.
export const SECTIONS: Section[] = [
{
@@ -50,6 +51,12 @@ export const SECTIONS: Section[] = [
render: (ctx) => <Files ctx={ctx} />,
acceptsParams: true,
},
{
id: "cheats",
icon: Sparkles,
labelKey: "cw_cheats_title",
render: () => <Cheats />,
},
{
id: "settings",
icon: Settings,
+420
View File
@@ -1,6 +1,26 @@
{
"ar": {
"cw_actions": "إجراءات",
"cw_cheats_apply": "تطبيق",
"cw_cheats_engine_off": "محرك الشفرات متوقف. يبدأ عند تشغيل أي شفرة.",
"cw_cheats_err_changed": "تغيّرت ملفات الشفرات. أُعيدت قراءة القائمة.",
"cw_cheats_err_load": "تعذّرت قراءة الشفرات ({code}). اضغط تحديث للمحاولة مرة أخرى.",
"cw_cheats_err_module": "تحتاج هذه الشفرة إلى جزء من اللعبة لم يُحمَّل بعد. حاول لاحقًا أثناء اللعب.",
"cw_cheats_err_toggle": "تعذّر تبديل الشفرة ({code}).",
"cw_cheats_exited": "تم إغلاق اللعبة.",
"cw_cheats_exited_next": "شغّلها مرة أخرى وستعود شفراتها إلى هنا.",
"cw_cheats_how": "يقوم ✕ أو □ بتشغيل الشفرة المحددة أو إيقافها.",
"cw_cheats_loading": "جارٍ تحميل الشفرات…",
"cw_cheats_no_game": "لا توجد لعبة قيد التشغيل.",
"cw_cheats_no_game_next": "شغّل لعبة، ثم افتح الشفرات.",
"cw_cheats_none": "لا توجد شفرات لهذه اللعبة.",
"cw_cheats_none_next": "أضف شفرات لهذه اللعبة من تطبيق PS5Upload على حاسوبك.",
"cw_cheats_off": "إيقاف",
"cw_cheats_on": "تشغيل",
"cw_cheats_refresh": "تحديث",
"cw_cheats_switching": "جارٍ التبديل…",
"cw_cheats_title": "الشفرات",
"cw_cheats_truncated": "لا تتسع هذه الصفحة لكل الشفرات. يعرضها تطبيق PS5Upload على حاسوبك كلها.",
"cw_dialog_cancel": "إلغاء",
"cw_files_bad_name": "استخدم اسمًا بدون «/».",
"cw_files_batch_copy": "جارٍ نسخ {at} من {total}…",
@@ -149,6 +169,26 @@
},
"bn": {
"cw_actions": "কাজ",
"cw_cheats_apply": "প্রয়োগ করুন",
"cw_cheats_engine_off": "চিট ইঞ্জিন বন্ধ আছে। কোনো চিট চালু করলে এটি শুরু হয়।",
"cw_cheats_err_changed": "চিট ফাইল বদলে গেছে। তালিকা আবার পড়া হয়েছে।",
"cw_cheats_err_load": "চিট পড়া যায়নি ({code})। আবার চেষ্টা করতে রিফ্রেশ চাপুন।",
"cw_cheats_err_module": "এই চিটের জন্য গেমের এমন একটি অংশ দরকার যা এখনো লোড হয়নি। গেমে পরে আবার চেষ্টা করুন।",
"cw_cheats_err_toggle": "চিট বদলানো যায়নি ({code})।",
"cw_cheats_exited": "গেমটি বন্ধ হয়ে গেছে।",
"cw_cheats_exited_next": "আবার চালু করুন, এর চিটগুলো আবার এখানে দেখা যাবে।",
"cw_cheats_how": "✕ বা □ নির্বাচিত চিট চালু বা বন্ধ করে।",
"cw_cheats_loading": "চিট লোড হচ্ছে…",
"cw_cheats_no_game": "কোনো গেম চলছে না।",
"cw_cheats_no_game_next": "একটি গেম চালু করুন, তারপর চিট খুলুন।",
"cw_cheats_none": "এই গেমের জন্য কোনো চিট নেই।",
"cw_cheats_none_next": "আপনার PC-র PS5Upload অ্যাপ থেকে এই গেমের চিট যোগ করুন।",
"cw_cheats_off": "বন্ধ",
"cw_cheats_on": "চালু",
"cw_cheats_refresh": "রিফ্রেশ",
"cw_cheats_switching": "বদলানো হচ্ছে…",
"cw_cheats_title": "চিট",
"cw_cheats_truncated": "সব চিট এখানে ধরে না। আপনার PC-র PS5Upload অ্যাপ সবগুলো দেখায়।",
"cw_dialog_cancel": "বাতিল",
"cw_files_bad_name": "“/” ছাড়া একটি নাম দিন।",
"cw_files_batch_copy": "{total}টির মধ্যে {at} কপি হচ্ছে…",
@@ -297,6 +337,26 @@
},
"de": {
"cw_actions": "Aktionen",
"cw_cheats_apply": "Anwenden",
"cw_cheats_engine_off": "Die Cheat-Engine ist aus. Sie startet, sobald du einen Cheat einschaltest.",
"cw_cheats_err_changed": "Die Cheat-Dateien haben sich geändert. Die Liste wurde neu gelesen.",
"cw_cheats_err_load": "Cheats konnten nicht gelesen werden ({code}). Drücke Aktualisieren, um es erneut zu versuchen.",
"cw_cheats_err_module": "Dieser Cheat braucht einen Teil des Spiels, der noch nicht geladen ist. Versuche es später im Spiel erneut.",
"cw_cheats_err_toggle": "Der Cheat konnte nicht geschaltet werden ({code}).",
"cw_cheats_exited": "Das Spiel wurde beendet.",
"cw_cheats_exited_next": "Starte es erneut, dann erscheinen seine Cheats wieder hier.",
"cw_cheats_how": "✕ oder □ schaltet den markierten Cheat ein oder aus.",
"cw_cheats_loading": "Cheats werden geladen…",
"cw_cheats_no_game": "Es läuft kein Spiel.",
"cw_cheats_no_game_next": "Starte ein Spiel und öffne dann Cheats.",
"cw_cheats_none": "Keine Cheats für dieses Spiel.",
"cw_cheats_none_next": "Füge Cheats für dieses Spiel in der PS5Upload-App auf deinem PC hinzu.",
"cw_cheats_off": "Aus",
"cw_cheats_on": "An",
"cw_cheats_refresh": "Aktualisieren",
"cw_cheats_switching": "Wird geschaltet…",
"cw_cheats_title": "Cheats",
"cw_cheats_truncated": "Nicht alle Cheats passen hierher. Die PS5Upload-App auf deinem PC zeigt alle.",
"cw_dialog_cancel": "Abbrechen",
"cw_files_bad_name": "Verwende einen Namen ohne „/“.",
"cw_files_batch_copy": "Kopiere {at} von {total}…",
@@ -445,6 +505,26 @@
},
"en": {
"cw_actions": "Actions",
"cw_cheats_apply": "Apply",
"cw_cheats_engine_off": "The cheat engine is off. Switching a cheat on starts it.",
"cw_cheats_err_changed": "The cheat files changed. The list was read again.",
"cw_cheats_err_load": "Could not read the cheats ({code}). Press Refresh to try again.",
"cw_cheats_err_module": "This cheat needs a part of the game that has not loaded yet. Try again later in the game.",
"cw_cheats_err_toggle": "Could not switch the cheat ({code}).",
"cw_cheats_exited": "The game was closed.",
"cw_cheats_exited_next": "Start it again and its cheats come back here.",
"cw_cheats_how": "✕ or □ switches the focused cheat on or off.",
"cw_cheats_loading": "Loading cheats…",
"cw_cheats_no_game": "No game is running.",
"cw_cheats_no_game_next": "Start a game, then open Cheats.",
"cw_cheats_none": "No cheats for this game.",
"cw_cheats_none_next": "Add cheats for this game from the PS5Upload app on your PC.",
"cw_cheats_off": "Off",
"cw_cheats_on": "On",
"cw_cheats_refresh": "Refresh",
"cw_cheats_switching": "Switching…",
"cw_cheats_title": "Cheats",
"cw_cheats_truncated": "Not every cheat fits here. The PS5Upload app on your PC shows them all.",
"cw_dialog_cancel": "Cancel",
"cw_files_bad_name": "Use a name without “/”.",
"cw_files_batch_copy": "Copying {at} of {total}…",
@@ -593,6 +673,26 @@
},
"es": {
"cw_actions": "Acciones",
"cw_cheats_apply": "Aplicar",
"cw_cheats_engine_off": "El motor de trucos está apagado. Se inicia al activar un truco.",
"cw_cheats_err_changed": "Los archivos de trucos han cambiado. Se ha vuelto a leer la lista.",
"cw_cheats_err_load": "No se pudieron leer los trucos ({code}). Pulsa Actualizar para intentarlo de nuevo.",
"cw_cheats_err_module": "Este truco necesita una parte del juego que aún no se ha cargado. Inténtalo más adelante en el juego.",
"cw_cheats_err_toggle": "No se pudo cambiar el truco ({code}).",
"cw_cheats_exited": "El juego se ha cerrado.",
"cw_cheats_exited_next": "Vuelve a iniciarlo y sus trucos aparecerán aquí de nuevo.",
"cw_cheats_how": "✕ o □ activa o desactiva el truco seleccionado.",
"cw_cheats_loading": "Cargando trucos…",
"cw_cheats_no_game": "No hay ningún juego en marcha.",
"cw_cheats_no_game_next": "Inicia un juego y luego abre Trucos.",
"cw_cheats_none": "No hay trucos para este juego.",
"cw_cheats_none_next": "Añade trucos para este juego desde la app PS5Upload en tu PC.",
"cw_cheats_off": "Desactivado",
"cw_cheats_on": "Activado",
"cw_cheats_refresh": "Actualizar",
"cw_cheats_switching": "Cambiando…",
"cw_cheats_title": "Trucos",
"cw_cheats_truncated": "No caben todos los trucos aquí. La app PS5Upload en tu PC los muestra todos.",
"cw_dialog_cancel": "Cancelar",
"cw_files_bad_name": "Usa un nombre sin «/».",
"cw_files_batch_copy": "Copiando {at} de {total}…",
@@ -741,6 +841,26 @@
},
"fa": {
"cw_actions": "کارها",
"cw_cheats_apply": "اعمال",
"cw_cheats_engine_off": "موتور چیت خاموش است. با روشن کردن یک چیت راه‌اندازی می‌شود.",
"cw_cheats_err_changed": "فایل‌های چیت تغییر کردند. فهرست دوباره خوانده شد.",
"cw_cheats_err_load": "خواندن چیت‌ها ممکن نشد ({code}). برای تلاش دوباره تازه‌سازی را بزنید.",
"cw_cheats_err_module": "این چیت به بخشی از بازی نیاز دارد که هنوز بارگیری نشده است. بعداً در بازی دوباره امتحان کنید.",
"cw_cheats_err_toggle": "تغییر وضعیت چیت ممکن نشد ({code}).",
"cw_cheats_exited": "بازی بسته شد.",
"cw_cheats_exited_next": "دوباره اجرایش کنید تا چیت‌هایش دوباره اینجا نمایش داده شوند.",
"cw_cheats_how": "✕ یا □ چیت انتخاب‌شده را روشن یا خاموش می‌کند.",
"cw_cheats_loading": "در حال بارگیری چیت‌ها…",
"cw_cheats_no_game": "هیچ بازی‌ای در حال اجرا نیست.",
"cw_cheats_no_game_next": "یک بازی را اجرا کنید، سپس چیت‌ها را باز کنید.",
"cw_cheats_none": "برای این بازی چیتی وجود ندارد.",
"cw_cheats_none_next": "چیت‌های این بازی را از برنامه PS5Upload روی رایانه‌تان اضافه کنید.",
"cw_cheats_off": "خاموش",
"cw_cheats_on": "روشن",
"cw_cheats_refresh": "تازه‌سازی",
"cw_cheats_switching": "در حال تغییر…",
"cw_cheats_title": "چیت‌ها",
"cw_cheats_truncated": "همه چیت‌ها اینجا جا نمی‌شوند. برنامه PS5Upload روی رایانه‌تان همه را نشان می‌دهد.",
"cw_dialog_cancel": "لغو",
"cw_files_bad_name": "نامی بدون «/» انتخاب کنید.",
"cw_files_batch_copy": "در حال کپی {at} از {total}…",
@@ -889,6 +1009,26 @@
},
"fr": {
"cw_actions": "Actions",
"cw_cheats_apply": "Appliquer",
"cw_cheats_engine_off": "Le moteur de cheats est arrêté. Il démarre dès que vous activez un cheat.",
"cw_cheats_err_changed": "Les fichiers de cheats ont changé. La liste a été relue.",
"cw_cheats_err_load": "Impossible de lire les cheats ({code}). Appuyez sur Actualiser pour réessayer.",
"cw_cheats_err_module": "Ce cheat a besoin d'une partie du jeu qui n'est pas encore chargée. Réessayez plus tard dans le jeu.",
"cw_cheats_err_toggle": "Impossible de changer le cheat ({code}).",
"cw_cheats_exited": "Le jeu a été fermé.",
"cw_cheats_exited_next": "Relancez-le et ses cheats réapparaîtront ici.",
"cw_cheats_how": "✕ ou □ active ou désactive le cheat sélectionné.",
"cw_cheats_loading": "Chargement des cheats…",
"cw_cheats_no_game": "Aucun jeu n'est lancé.",
"cw_cheats_no_game_next": "Lancez un jeu, puis ouvrez Cheats.",
"cw_cheats_none": "Aucun cheat pour ce jeu.",
"cw_cheats_none_next": "Ajoutez des cheats pour ce jeu depuis l'app PS5Upload sur votre PC.",
"cw_cheats_off": "Désactivé",
"cw_cheats_on": "Activé",
"cw_cheats_refresh": "Actualiser",
"cw_cheats_switching": "Changement…",
"cw_cheats_title": "Cheats",
"cw_cheats_truncated": "Tous les cheats ne tiennent pas ici. L'app PS5Upload sur votre PC les affiche tous.",
"cw_dialog_cancel": "Annuler",
"cw_files_bad_name": "Utilisez un nom sans « / ».",
"cw_files_batch_copy": "Copie {at} sur {total}…",
@@ -1037,6 +1177,26 @@
},
"hi": {
"cw_actions": "कार्य",
"cw_cheats_apply": "लागू करें",
"cw_cheats_engine_off": "चीट इंजन बंद है। कोई चीट चालू करने पर यह शुरू हो जाता है।",
"cw_cheats_err_changed": "चीट फ़ाइलें बदल गईं। सूची फिर से पढ़ी गई।",
"cw_cheats_err_load": "चीट नहीं पढ़े जा सके ({code})। फिर से कोशिश करने के लिए रीफ़्रेश दबाएँ।",
"cw_cheats_err_module": "इस चीट को गेम का ऐसा हिस्सा चाहिए जो अभी लोड नहीं हुआ है। गेम में आगे चलकर फिर से कोशिश करें।",
"cw_cheats_err_toggle": "चीट नहीं बदला जा सका ({code})।",
"cw_cheats_exited": "गेम बंद हो गया।",
"cw_cheats_exited_next": "इसे फिर से शुरू करें, इसके चीट यहाँ वापस आ जाएँगे।",
"cw_cheats_how": "✕ या □ चुने गए चीट को चालू या बंद करता है।",
"cw_cheats_loading": "चीट लोड हो रहे हैं…",
"cw_cheats_no_game": "कोई गेम नहीं चल रहा है।",
"cw_cheats_no_game_next": "कोई गेम शुरू करें, फिर चीट खोलें।",
"cw_cheats_none": "इस गेम के लिए कोई चीट नहीं है।",
"cw_cheats_none_next": "इस गेम के चीट अपने PC पर PS5Upload ऐप से जोड़ें।",
"cw_cheats_off": "बंद",
"cw_cheats_on": "चालू",
"cw_cheats_refresh": "रीफ़्रेश करें",
"cw_cheats_switching": "बदला जा रहा है…",
"cw_cheats_title": "चीट",
"cw_cheats_truncated": "सभी चीट यहाँ नहीं समाते। आपके PC पर PS5Upload ऐप सभी दिखाता है।",
"cw_dialog_cancel": "रद्द करें",
"cw_files_bad_name": "ऐसा नाम रखें जिसमें “/” न हो।",
"cw_files_batch_copy": "{total} में से {at} कॉपी हो रहा है…",
@@ -1185,6 +1345,26 @@
},
"hu": {
"cw_actions": "Műveletek",
"cw_cheats_apply": "Alkalmaz",
"cw_cheats_engine_off": "A csalásmotor ki van kapcsolva. Elindul, amikor bekapcsolsz egy csalást.",
"cw_cheats_err_changed": "A csalásfájlok megváltoztak. A lista újra beolvasva.",
"cw_cheats_err_load": "Nem sikerült beolvasni a csalásokat ({code}). Nyomd meg a Frissítést az újrapróbáláshoz.",
"cw_cheats_err_module": "Ehhez a csaláshoz a játék egy még be nem töltött része kell. Próbáld újra később a játékban.",
"cw_cheats_err_toggle": "Nem sikerült átváltani a csalást ({code}).",
"cw_cheats_exited": "A játék bezárult.",
"cw_cheats_exited_next": "Indítsd el újra, és a csalásai ismét itt lesznek.",
"cw_cheats_how": "Az ✕ vagy a □ be- vagy kikapcsolja a kijelölt csalást.",
"cw_cheats_loading": "Csalások betöltése…",
"cw_cheats_no_game": "Nem fut játék.",
"cw_cheats_no_game_next": "Indíts el egy játékot, majd nyisd meg a Csalásokat.",
"cw_cheats_none": "Ehhez a játékhoz nincsenek csalások.",
"cw_cheats_none_next": "Adj hozzá csalásokat ehhez a játékhoz a PC-n futó PS5Upload alkalmazásból.",
"cw_cheats_off": "Ki",
"cw_cheats_on": "Be",
"cw_cheats_refresh": "Frissítés",
"cw_cheats_switching": "Váltás…",
"cw_cheats_title": "Csalások",
"cw_cheats_truncated": "Nem fér ide minden csalás. A PC-n futó PS5Upload alkalmazás mindet mutatja.",
"cw_dialog_cancel": "Mégse",
"cw_files_bad_name": "A név nem tartalmazhat „/” jelet.",
"cw_files_batch_copy": "Másolás: {at}/{total}…",
@@ -1333,6 +1513,26 @@
},
"id": {
"cw_actions": "Tindakan",
"cw_cheats_apply": "Terapkan",
"cw_cheats_engine_off": "Mesin cheat mati. Mesin menyala saat Anda menyalakan cheat.",
"cw_cheats_err_changed": "File cheat berubah. Daftar telah dibaca ulang.",
"cw_cheats_err_load": "Tidak dapat membaca cheat ({code}). Tekan Muat ulang untuk mencoba lagi.",
"cw_cheats_err_module": "Cheat ini memerlukan bagian game yang belum dimuat. Coba lagi nanti di dalam game.",
"cw_cheats_err_toggle": "Tidak dapat mengalihkan cheat ({code}).",
"cw_cheats_exited": "Game telah ditutup.",
"cw_cheats_exited_next": "Jalankan lagi dan cheat-nya akan muncul kembali di sini.",
"cw_cheats_how": "✕ atau □ menyalakan atau mematikan cheat yang dipilih.",
"cw_cheats_loading": "Memuat cheat…",
"cw_cheats_no_game": "Tidak ada game yang berjalan.",
"cw_cheats_no_game_next": "Jalankan game, lalu buka Cheat.",
"cw_cheats_none": "Tidak ada cheat untuk game ini.",
"cw_cheats_none_next": "Tambahkan cheat untuk game ini dari aplikasi PS5Upload di PC Anda.",
"cw_cheats_off": "Mati",
"cw_cheats_on": "Nyala",
"cw_cheats_refresh": "Muat ulang",
"cw_cheats_switching": "Mengalihkan…",
"cw_cheats_title": "Cheat",
"cw_cheats_truncated": "Tidak semua cheat muat di sini. Aplikasi PS5Upload di PC Anda menampilkan semuanya.",
"cw_dialog_cancel": "Batal",
"cw_files_bad_name": "Gunakan nama tanpa “/”.",
"cw_files_batch_copy": "Menyalin {at} dari {total}…",
@@ -1481,6 +1681,26 @@
},
"it": {
"cw_actions": "Azioni",
"cw_cheats_apply": "Applica",
"cw_cheats_engine_off": "Il motore dei cheat è spento. Si avvia quando attivi un cheat.",
"cw_cheats_err_changed": "I file dei cheat sono cambiati. L'elenco è stato riletto.",
"cw_cheats_err_load": "Impossibile leggere i cheat ({code}). Premi Aggiorna per riprovare.",
"cw_cheats_err_module": "Questo cheat richiede una parte del gioco non ancora caricata. Riprova più avanti nel gioco.",
"cw_cheats_err_toggle": "Impossibile cambiare il cheat ({code}).",
"cw_cheats_exited": "Il gioco è stato chiuso.",
"cw_cheats_exited_next": "Riavvialo e i suoi cheat torneranno qui.",
"cw_cheats_how": "✕ o □ attiva o disattiva il cheat selezionato.",
"cw_cheats_loading": "Caricamento dei cheat…",
"cw_cheats_no_game": "Nessun gioco in esecuzione.",
"cw_cheats_no_game_next": "Avvia un gioco, poi apri Cheat.",
"cw_cheats_none": "Nessun cheat per questo gioco.",
"cw_cheats_none_next": "Aggiungi i cheat per questo gioco dall'app PS5Upload sul tuo PC.",
"cw_cheats_off": "Disattivo",
"cw_cheats_on": "Attivo",
"cw_cheats_refresh": "Aggiorna",
"cw_cheats_switching": "Cambio in corso…",
"cw_cheats_title": "Cheat",
"cw_cheats_truncated": "Non tutti i cheat entrano qui. L'app PS5Upload sul tuo PC li mostra tutti.",
"cw_dialog_cancel": "Annulla",
"cw_files_bad_name": "Usa un nome senza “/”.",
"cw_files_batch_copy": "Copia {at} di {total}…",
@@ -1629,6 +1849,26 @@
},
"ja": {
"cw_actions": "操作",
"cw_cheats_apply": "適用",
"cw_cheats_engine_off": "チートエンジンはオフです。チートをオンにすると起動します。",
"cw_cheats_err_changed": "チートファイルが変更されました。一覧を読み込み直しました。",
"cw_cheats_err_load": "チートを読み込めませんでした ({code})。更新を押してもう一度お試しください。",
"cw_cheats_err_module": "このチートには、まだ読み込まれていないゲームの部分が必要です。ゲームを進めてから再度お試しください。",
"cw_cheats_err_toggle": "チートを切り替えられませんでした ({code})。",
"cw_cheats_exited": "ゲームが終了しました。",
"cw_cheats_exited_next": "もう一度起動すると、チートがここに戻ります。",
"cw_cheats_how": "✕ か □ で選択中のチートをオン/オフします。",
"cw_cheats_loading": "チートを読み込み中…",
"cw_cheats_no_game": "ゲームが起動していません。",
"cw_cheats_no_game_next": "ゲームを起動してから、チートを開いてください。",
"cw_cheats_none": "このゲームのチートはありません。",
"cw_cheats_none_next": "このゲームのチートは PC の PS5Upload アプリから追加してください。",
"cw_cheats_off": "オフ",
"cw_cheats_on": "オン",
"cw_cheats_refresh": "更新",
"cw_cheats_switching": "切り替え中…",
"cw_cheats_title": "チート",
"cw_cheats_truncated": "すべてのチートはここに表示しきれません。PC の PS5Upload アプリですべて確認できます。",
"cw_dialog_cancel": "キャンセル",
"cw_files_bad_name": "「/」を含まない名前にしてください。",
"cw_files_batch_copy": "コピー中 {at}/{total}…",
@@ -1777,6 +2017,26 @@
},
"ko": {
"cw_actions": "작업",
"cw_cheats_apply": "적용",
"cw_cheats_engine_off": "치트 엔진이 꺼져 있습니다. 치트를 켜면 시작됩니다.",
"cw_cheats_err_changed": "치트 파일이 바뀌었습니다. 목록을 다시 읽었습니다.",
"cw_cheats_err_load": "치트를 읽지 못했습니다 ({code}). 새로 고침을 눌러 다시 시도하세요.",
"cw_cheats_err_module": "이 치트에는 아직 로드되지 않은 게임 부분이 필요합니다. 게임을 더 진행한 뒤 다시 시도하세요.",
"cw_cheats_err_toggle": "치트를 전환하지 못했습니다 ({code}).",
"cw_cheats_exited": "게임이 종료되었습니다.",
"cw_cheats_exited_next": "다시 시작하면 치트가 여기에 다시 나타납니다.",
"cw_cheats_how": "✕ 또는 □ 버튼으로 선택한 치트를 켜거나 끕니다.",
"cw_cheats_loading": "치트를 불러오는 중…",
"cw_cheats_no_game": "실행 중인 게임이 없습니다.",
"cw_cheats_no_game_next": "게임을 시작한 다음 치트를 여세요.",
"cw_cheats_none": "이 게임의 치트가 없습니다.",
"cw_cheats_none_next": "PC의 PS5Upload 앱에서 이 게임의 치트를 추가하세요.",
"cw_cheats_off": "꺼짐",
"cw_cheats_on": "켜짐",
"cw_cheats_refresh": "새로 고침",
"cw_cheats_switching": "전환 중…",
"cw_cheats_title": "치트",
"cw_cheats_truncated": "모든 치트를 여기에 표시할 수 없습니다. PC의 PS5Upload 앱에서 모두 볼 수 있습니다.",
"cw_dialog_cancel": "취소",
"cw_files_bad_name": "“/”가 없는 이름을 사용하세요.",
"cw_files_batch_copy": "복사 중 {at}/{total}…",
@@ -1925,6 +2185,26 @@
},
"pl": {
"cw_actions": "Działania",
"cw_cheats_apply": "Zastosuj",
"cw_cheats_engine_off": "Silnik kodów jest wyłączony. Uruchomi się, gdy włączysz kod.",
"cw_cheats_err_changed": "Pliki kodów się zmieniły. Lista została wczytana ponownie.",
"cw_cheats_err_load": "Nie udało się odczytać kodów ({code}). Naciśnij Odśwież, aby spróbować ponownie.",
"cw_cheats_err_module": "Ten kod wymaga części gry, która nie jest jeszcze wczytana. Spróbuj później w grze.",
"cw_cheats_err_toggle": "Nie udało się przełączyć kodu ({code}).",
"cw_cheats_exited": "Gra została zamknięta.",
"cw_cheats_exited_next": "Uruchom ją ponownie, a jej kody znów się tu pojawią.",
"cw_cheats_how": "✕ lub □ włącza albo wyłącza zaznaczony kod.",
"cw_cheats_loading": "Wczytywanie kodów…",
"cw_cheats_no_game": "Żadna gra nie jest uruchomiona.",
"cw_cheats_no_game_next": "Uruchom grę, a potem otwórz Kody.",
"cw_cheats_none": "Brak kodów dla tej gry.",
"cw_cheats_none_next": "Dodaj kody dla tej gry w aplikacji PS5Upload na komputerze.",
"cw_cheats_off": "Wył.",
"cw_cheats_on": "Wł.",
"cw_cheats_refresh": "Odśwież",
"cw_cheats_switching": "Przełączanie…",
"cw_cheats_title": "Kody",
"cw_cheats_truncated": "Nie wszystkie kody się tu mieszczą. Aplikacja PS5Upload na komputerze pokazuje wszystkie.",
"cw_dialog_cancel": "Anuluj",
"cw_files_bad_name": "Użyj nazwy bez „/”.",
"cw_files_batch_copy": "Kopiowanie {at} z {total}…",
@@ -2073,6 +2353,26 @@
},
"pt-BR": {
"cw_actions": "Ações",
"cw_cheats_apply": "Aplicar",
"cw_cheats_engine_off": "O motor de cheats está desligado. Ele inicia quando você liga um cheat.",
"cw_cheats_err_changed": "Os arquivos de cheats mudaram. A lista foi lida de novo.",
"cw_cheats_err_load": "Não foi possível ler os cheats ({code}). Pressione Atualizar para tentar de novo.",
"cw_cheats_err_module": "Este cheat precisa de uma parte do jogo que ainda não foi carregada. Tente de novo mais tarde no jogo.",
"cw_cheats_err_toggle": "Não foi possível alternar o cheat ({code}).",
"cw_cheats_exited": "O jogo foi fechado.",
"cw_cheats_exited_next": "Inicie-o de novo e os cheats dele voltam a aparecer aqui.",
"cw_cheats_how": "✕ ou □ liga ou desliga o cheat selecionado.",
"cw_cheats_loading": "Carregando cheats…",
"cw_cheats_no_game": "Nenhum jogo em execução.",
"cw_cheats_no_game_next": "Inicie um jogo e depois abra Cheats.",
"cw_cheats_none": "Nenhum cheat para este jogo.",
"cw_cheats_none_next": "Adicione cheats para este jogo pelo app PS5Upload no seu PC.",
"cw_cheats_off": "Desligado",
"cw_cheats_on": "Ligado",
"cw_cheats_refresh": "Atualizar",
"cw_cheats_switching": "Alternando…",
"cw_cheats_title": "Cheats",
"cw_cheats_truncated": "Nem todos os cheats cabem aqui. O app PS5Upload no seu PC mostra todos.",
"cw_dialog_cancel": "Cancelar",
"cw_files_bad_name": "Use um nome sem “/”.",
"cw_files_batch_copy": "Copiando {at} de {total}…",
@@ -2221,6 +2521,26 @@
},
"ru": {
"cw_actions": "Действия",
"cw_cheats_apply": "Применить",
"cw_cheats_engine_off": "Движок читов выключен. Он запустится, когда вы включите чит.",
"cw_cheats_err_changed": "Файлы читов изменились. Список прочитан заново.",
"cw_cheats_err_load": "Не удалось прочитать читы ({code}). Нажмите «Обновить», чтобы повторить.",
"cw_cheats_err_module": "Этому читу нужна часть игры, которая ещё не загружена. Попробуйте позже в игре.",
"cw_cheats_err_toggle": "Не удалось переключить чит ({code}).",
"cw_cheats_exited": "Игра закрыта.",
"cw_cheats_exited_next": "Запустите её снова, и её читы снова появятся здесь.",
"cw_cheats_how": "✕ или □ включает или выключает выбранный чит.",
"cw_cheats_loading": "Загрузка читов…",
"cw_cheats_no_game": "Игра не запущена.",
"cw_cheats_no_game_next": "Запустите игру, затем откройте «Читы».",
"cw_cheats_none": "Для этой игры нет читов.",
"cw_cheats_none_next": "Добавьте читы для этой игры в приложении PS5Upload на ПК.",
"cw_cheats_off": "Выкл.",
"cw_cheats_on": "Вкл.",
"cw_cheats_refresh": "Обновить",
"cw_cheats_switching": "Переключение…",
"cw_cheats_title": "Читы",
"cw_cheats_truncated": "Здесь помещаются не все читы. Приложение PS5Upload на ПК показывает все.",
"cw_dialog_cancel": "Отмена",
"cw_files_bad_name": "Имя не должно содержать «/».",
"cw_files_batch_copy": "Копирование {at} из {total}…",
@@ -2369,6 +2689,26 @@
},
"th": {
"cw_actions": "การทำงาน",
"cw_cheats_apply": "ใช้",
"cw_cheats_engine_off": "เอนจินสูตรโกงปิดอยู่ จะเริ่มทำงานเมื่อคุณเปิดสูตรโกง",
"cw_cheats_err_changed": "ไฟล์สูตรโกงเปลี่ยนไป อ่านรายการใหม่แล้ว",
"cw_cheats_err_load": "อ่านสูตรโกงไม่ได้ ({code}) กดรีเฟรชเพื่อลองอีกครั้ง",
"cw_cheats_err_module": "สูตรโกงนี้ต้องใช้ส่วนของเกมที่ยังไม่ได้โหลด ลองอีกครั้งภายหลังในเกม",
"cw_cheats_err_toggle": "สลับสูตรโกงไม่ได้ ({code})",
"cw_cheats_exited": "เกมถูกปิดแล้ว",
"cw_cheats_exited_next": "เริ่มเกมอีกครั้ง แล้วสูตรโกงจะกลับมาที่นี่",
"cw_cheats_how": "✕ หรือ □ เปิดหรือปิดสูตรโกงที่เลือกอยู่",
"cw_cheats_loading": "กำลังโหลดสูตรโกง…",
"cw_cheats_no_game": "ไม่มีเกมที่กำลังทำงาน",
"cw_cheats_no_game_next": "เริ่มเกมก่อน แล้วจึงเปิดเมนูโกง",
"cw_cheats_none": "ไม่มีสูตรโกงสำหรับเกมนี้",
"cw_cheats_none_next": "เพิ่มสูตรโกงสำหรับเกมนี้จากแอป PS5Upload บนพีซีของคุณ",
"cw_cheats_off": "ปิด",
"cw_cheats_on": "เปิด",
"cw_cheats_refresh": "รีเฟรช",
"cw_cheats_switching": "กำลังสลับ…",
"cw_cheats_title": "โกง",
"cw_cheats_truncated": "แสดงสูตรโกงได้ไม่ครบที่นี่ แอป PS5Upload บนพีซีของคุณแสดงได้ทั้งหมด",
"cw_dialog_cancel": "ยกเลิก",
"cw_files_bad_name": "ใช้ชื่อที่ไม่มี “/”",
"cw_files_batch_copy": "กำลังคัดลอก {at} จาก {total}…",
@@ -2517,6 +2857,26 @@
},
"tr": {
"cw_actions": "İşlemler",
"cw_cheats_apply": "Uygula",
"cw_cheats_engine_off": "Hile motoru kapalı. Bir hileyi açtığınızda başlar.",
"cw_cheats_err_changed": "Hile dosyaları değişti. Liste yeniden okundu.",
"cw_cheats_err_load": "Hileler okunamadı ({code}). Yeniden denemek için Yenile'ye basın.",
"cw_cheats_err_module": "Bu hile, oyunun henüz yüklenmemiş bir bölümüne ihtiyaç duyuyor. Oyunda daha sonra tekrar deneyin.",
"cw_cheats_err_toggle": "Hile değiştirilemedi ({code}).",
"cw_cheats_exited": "Oyun kapatıldı.",
"cw_cheats_exited_next": "Yeniden başlatın, hileleri tekrar burada görünür.",
"cw_cheats_how": "✕ veya □ seçili hileyi açar ya da kapatır.",
"cw_cheats_loading": "Hileler yükleniyor…",
"cw_cheats_no_game": "Çalışan oyun yok.",
"cw_cheats_no_game_next": "Bir oyun başlatın, sonra Hileler'i açın.",
"cw_cheats_none": "Bu oyun için hile yok.",
"cw_cheats_none_next": "Bu oyun için hileleri bilgisayarınızdaki PS5Upload uygulamasından ekleyin.",
"cw_cheats_off": "Kapalı",
"cw_cheats_on": "Açık",
"cw_cheats_refresh": "Yenile",
"cw_cheats_switching": "Değiştiriliyor…",
"cw_cheats_title": "Hileler",
"cw_cheats_truncated": "Tüm hileler buraya sığmıyor. Bilgisayarınızdaki PS5Upload uygulaması hepsini gösterir.",
"cw_dialog_cancel": "İptal",
"cw_files_bad_name": "“/” içermeyen bir ad kullanın.",
"cw_files_batch_copy": "Kopyalanıyor: {at}/{total}…",
@@ -2665,6 +3025,26 @@
},
"vi": {
"cw_actions": "Thao tác",
"cw_cheats_apply": "Áp dụng",
"cw_cheats_engine_off": "Bộ máy cheat đang tắt. Nó sẽ khởi động khi bạn bật một cheat.",
"cw_cheats_err_changed": "Tệp cheat đã thay đổi. Danh sách đã được đọc lại.",
"cw_cheats_err_load": "Không đọc được cheat ({code}). Nhấn Làm mới để thử lại.",
"cw_cheats_err_module": "Cheat này cần một phần của trò chơi chưa được tải. Hãy thử lại sau trong trò chơi.",
"cw_cheats_err_toggle": "Không chuyển được cheat ({code}).",
"cw_cheats_exited": "Trò chơi đã đóng.",
"cw_cheats_exited_next": "Mở lại trò chơi, cheat của nó sẽ hiện lại ở đây.",
"cw_cheats_how": "✕ hoặc □ bật hoặc tắt cheat đang chọn.",
"cw_cheats_loading": "Đang tải cheat…",
"cw_cheats_no_game": "Không có trò chơi nào đang chạy.",
"cw_cheats_no_game_next": "Hãy mở một trò chơi, rồi mở Cheat.",
"cw_cheats_none": "Không có cheat cho trò chơi này.",
"cw_cheats_none_next": "Thêm cheat cho trò chơi này từ ứng dụng PS5Upload trên PC của bạn.",
"cw_cheats_off": "Tắt",
"cw_cheats_on": "Bật",
"cw_cheats_refresh": "Làm mới",
"cw_cheats_switching": "Đang chuyển…",
"cw_cheats_title": "Cheat",
"cw_cheats_truncated": "Không thể hiển thị hết cheat ở đây. Ứng dụng PS5Upload trên PC hiển thị tất cả.",
"cw_dialog_cancel": "Hủy",
"cw_files_bad_name": "Dùng tên không có “/”.",
"cw_files_batch_copy": "Đang sao chép {at}/{total}…",
@@ -2813,6 +3193,26 @@
},
"zh-CN": {
"cw_actions": "操作",
"cw_cheats_apply": "应用",
"cw_cheats_engine_off": "金手指引擎已关闭。开启任一金手指时会自动启动。",
"cw_cheats_err_changed": "金手指文件已更改,已重新读取列表。",
"cw_cheats_err_load": "无法读取金手指({code})。请按刷新重试。",
"cw_cheats_err_module": "此金手指需要游戏中尚未加载的部分。请在游戏中稍后再试。",
"cw_cheats_err_toggle": "无法切换金手指({code})。",
"cw_cheats_exited": "游戏已关闭。",
"cw_cheats_exited_next": "再次启动游戏,它的金手指会重新显示在这里。",
"cw_cheats_how": "按 ✕ 或 □ 开启或关闭选中的金手指。",
"cw_cheats_loading": "正在加载金手指…",
"cw_cheats_no_game": "没有正在运行的游戏。",
"cw_cheats_no_game_next": "请先启动游戏,再打开金手指。",
"cw_cheats_none": "此游戏没有金手指。",
"cw_cheats_none_next": "请在电脑上的 PS5Upload 应用中为此游戏添加金手指。",
"cw_cheats_off": "关",
"cw_cheats_on": "开",
"cw_cheats_refresh": "刷新",
"cw_cheats_switching": "正在切换…",
"cw_cheats_title": "金手指",
"cw_cheats_truncated": "此处无法显示全部金手指。电脑上的 PS5Upload 应用可显示全部。",
"cw_dialog_cancel": "取消",
"cw_files_bad_name": "名称中不能包含“/”。",
"cw_files_batch_copy": "正在复制 {at}/{total}…",
@@ -2961,6 +3361,26 @@
},
"zh-TW": {
"cw_actions": "操作",
"cw_cheats_apply": "套用",
"cw_cheats_engine_off": "金手指引擎已關閉。開啟任一金手指時會自動啟動。",
"cw_cheats_err_changed": "金手指檔案已變更,已重新讀取清單。",
"cw_cheats_err_load": "無法讀取金手指({code})。請按重新整理再試一次。",
"cw_cheats_err_module": "此金手指需要遊戲中尚未載入的部分。請在遊戲中稍後再試。",
"cw_cheats_err_toggle": "無法切換金手指({code})。",
"cw_cheats_exited": "遊戲已關閉。",
"cw_cheats_exited_next": "再次啟動遊戲,它的金手指會重新顯示在這裡。",
"cw_cheats_how": "按 ✕ 或 □ 開啟或關閉選取的金手指。",
"cw_cheats_loading": "正在載入金手指…",
"cw_cheats_no_game": "沒有正在執行的遊戲。",
"cw_cheats_no_game_next": "請先啟動遊戲,再開啟金手指。",
"cw_cheats_none": "此遊戲沒有金手指。",
"cw_cheats_none_next": "請在電腦上的 PS5Upload 應用程式中為此遊戲新增金手指。",
"cw_cheats_off": "關",
"cw_cheats_on": "開",
"cw_cheats_refresh": "重新整理",
"cw_cheats_switching": "正在切換…",
"cw_cheats_title": "金手指",
"cw_cheats_truncated": "此處無法顯示全部金手指。電腦上的 PS5Upload 應用程式可顯示全部。",
"cw_dialog_cancel": "取消",
"cw_files_bad_name": "名稱不能包含「/」。",
"cw_files_batch_copy": "正在複製 {at}/{total}…",
+20
View File
@@ -4176,6 +4176,26 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB:
"cw_theme_oled": "OLED",
"cw_theme_peach": "خوخي",
"cw_settings_contrast": "تباين عالٍ",
"cw_cheats_title": "الشفرات",
"cw_cheats_how": "يقوم ✕ أو □ بتشغيل الشفرة المحددة أو إيقافها.",
"cw_cheats_loading": "جارٍ تحميل الشفرات…",
"cw_cheats_no_game": "لا توجد لعبة قيد التشغيل.",
"cw_cheats_no_game_next": "شغّل لعبة، ثم افتح الشفرات.",
"cw_cheats_none": "لا توجد شفرات لهذه اللعبة.",
"cw_cheats_none_next": "أضف شفرات لهذه اللعبة من تطبيق PS5Upload على حاسوبك.",
"cw_cheats_exited": "تم إغلاق اللعبة.",
"cw_cheats_exited_next": "شغّلها مرة أخرى وستعود شفراتها إلى هنا.",
"cw_cheats_refresh": "تحديث",
"cw_cheats_on": "تشغيل",
"cw_cheats_off": "إيقاف",
"cw_cheats_apply": "تطبيق",
"cw_cheats_switching": "جارٍ التبديل…",
"cw_cheats_engine_off": "محرك الشفرات متوقف. يبدأ عند تشغيل أي شفرة.",
"cw_cheats_truncated": "لا تتسع هذه الصفحة لكل الشفرات. يعرضها تطبيق PS5Upload على حاسوبك كلها.",
"cw_cheats_err_load": "تعذّرت قراءة الشفرات ({code}). اضغط تحديث للمحاولة مرة أخرى.",
"cw_cheats_err_module": "تحتاج هذه الشفرة إلى جزء من اللعبة لم يُحمَّل بعد. حاول لاحقًا أثناء اللعب.",
"cw_cheats_err_changed": "تغيّرت ملفات الشفرات. أُعيدت قراءة القائمة.",
"cw_cheats_err_toggle": "تعذّر تبديل الشفرة ({code}).",
};
export default ar;
+20
View File
@@ -4176,6 +4176,26 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা
"cw_theme_oled": "OLED",
"cw_theme_peach": "পিচ",
"cw_settings_contrast": "উচ্চ কনট্রাস্ট",
"cw_cheats_title": "চিট",
"cw_cheats_how": "✕ বা □ নির্বাচিত চিট চালু বা বন্ধ করে।",
"cw_cheats_loading": "চিট লোড হচ্ছে…",
"cw_cheats_no_game": "কোনো গেম চলছে না।",
"cw_cheats_no_game_next": "একটি গেম চালু করুন, তারপর চিট খুলুন।",
"cw_cheats_none": "এই গেমের জন্য কোনো চিট নেই।",
"cw_cheats_none_next": "আপনার PC-র PS5Upload অ্যাপ থেকে এই গেমের চিট যোগ করুন।",
"cw_cheats_exited": "গেমটি বন্ধ হয়ে গেছে।",
"cw_cheats_exited_next": "আবার চালু করুন, এর চিটগুলো আবার এখানে দেখা যাবে।",
"cw_cheats_refresh": "রিফ্রেশ",
"cw_cheats_on": "চালু",
"cw_cheats_off": "বন্ধ",
"cw_cheats_apply": "প্রয়োগ করুন",
"cw_cheats_switching": "বদলানো হচ্ছে…",
"cw_cheats_engine_off": "চিট ইঞ্জিন বন্ধ আছে। কোনো চিট চালু করলে এটি শুরু হয়।",
"cw_cheats_truncated": "সব চিট এখানে ধরে না। আপনার PC-র PS5Upload অ্যাপ সবগুলো দেখায়।",
"cw_cheats_err_load": "চিট পড়া যায়নি ({code})। আবার চেষ্টা করতে রিফ্রেশ চাপুন।",
"cw_cheats_err_module": "এই চিটের জন্য গেমের এমন একটি অংশ দরকার যা এখনো লোড হয়নি। গেমে পরে আবার চেষ্টা করুন।",
"cw_cheats_err_changed": "চিট ফাইল বদলে গেছে। তালিকা আবার পড়া হয়েছে।",
"cw_cheats_err_toggle": "চিট বদলানো যায়নি ({code})।",
};
export default bn;
+20
View File
@@ -4184,6 +4184,26 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Pfirsich",
"cw_settings_contrast": "Hoher Kontrast",
"cw_cheats_title": "Cheats",
"cw_cheats_how": "✕ oder □ schaltet den markierten Cheat ein oder aus.",
"cw_cheats_loading": "Cheats werden geladen…",
"cw_cheats_no_game": "Es läuft kein Spiel.",
"cw_cheats_no_game_next": "Starte ein Spiel und öffne dann Cheats.",
"cw_cheats_none": "Keine Cheats für dieses Spiel.",
"cw_cheats_none_next": "Füge Cheats für dieses Spiel in der PS5Upload-App auf deinem PC hinzu.",
"cw_cheats_exited": "Das Spiel wurde beendet.",
"cw_cheats_exited_next": "Starte es erneut, dann erscheinen seine Cheats wieder hier.",
"cw_cheats_refresh": "Aktualisieren",
"cw_cheats_on": "An",
"cw_cheats_off": "Aus",
"cw_cheats_apply": "Anwenden",
"cw_cheats_switching": "Wird geschaltet…",
"cw_cheats_engine_off": "Die Cheat-Engine ist aus. Sie startet, sobald du einen Cheat einschaltest.",
"cw_cheats_truncated": "Nicht alle Cheats passen hierher. Die PS5Upload-App auf deinem PC zeigt alle.",
"cw_cheats_err_load": "Cheats konnten nicht gelesen werden ({code}). Drücke Aktualisieren, um es erneut zu versuchen.",
"cw_cheats_err_module": "Dieser Cheat braucht einen Teil des Spiels, der noch nicht geladen ist. Versuche es später im Spiel erneut.",
"cw_cheats_err_changed": "Die Cheat-Dateien haben sich geändert. Die Liste wurde neu gelesen.",
"cw_cheats_err_toggle": "Der Cheat konnte nicht geschaltet werden ({code}).",
};
export default de;
+20
View File
@@ -4499,6 +4499,26 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Peach",
"cw_settings_contrast": "High contrast",
"cw_cheats_title": "Cheats",
"cw_cheats_how": "✕ or □ switches the focused cheat on or off.",
"cw_cheats_loading": "Loading cheats…",
"cw_cheats_no_game": "No game is running.",
"cw_cheats_no_game_next": "Start a game, then open Cheats.",
"cw_cheats_none": "No cheats for this game.",
"cw_cheats_none_next": "Add cheats for this game from the PS5Upload app on your PC.",
"cw_cheats_exited": "The game was closed.",
"cw_cheats_exited_next": "Start it again and its cheats come back here.",
"cw_cheats_refresh": "Refresh",
"cw_cheats_on": "On",
"cw_cheats_off": "Off",
"cw_cheats_apply": "Apply",
"cw_cheats_switching": "Switching…",
"cw_cheats_engine_off": "The cheat engine is off. Switching a cheat on starts it.",
"cw_cheats_truncated": "Not every cheat fits here. The PS5Upload app on your PC shows them all.",
"cw_cheats_err_load": "Could not read the cheats ({code}). Press Refresh to try again.",
"cw_cheats_err_module": "This cheat needs a part of the game that has not loaded yet. Try again later in the game.",
"cw_cheats_err_changed": "The cheat files changed. The list was read again.",
"cw_cheats_err_toggle": "Could not switch the cheat ({code}).",
};
export default en;
+20
View File
@@ -4126,6 +4126,26 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Melocotón",
"cw_settings_contrast": "Alto contraste",
"cw_cheats_title": "Trucos",
"cw_cheats_how": "✕ o □ activa o desactiva el truco seleccionado.",
"cw_cheats_loading": "Cargando trucos…",
"cw_cheats_no_game": "No hay ningún juego en marcha.",
"cw_cheats_no_game_next": "Inicia un juego y luego abre Trucos.",
"cw_cheats_none": "No hay trucos para este juego.",
"cw_cheats_none_next": "Añade trucos para este juego desde la app PS5Upload en tu PC.",
"cw_cheats_exited": "El juego se ha cerrado.",
"cw_cheats_exited_next": "Vuelve a iniciarlo y sus trucos aparecerán aquí de nuevo.",
"cw_cheats_refresh": "Actualizar",
"cw_cheats_on": "Activado",
"cw_cheats_off": "Desactivado",
"cw_cheats_apply": "Aplicar",
"cw_cheats_switching": "Cambiando…",
"cw_cheats_engine_off": "El motor de trucos está apagado. Se inicia al activar un truco.",
"cw_cheats_truncated": "No caben todos los trucos aquí. La app PS5Upload en tu PC los muestra todos.",
"cw_cheats_err_load": "No se pudieron leer los trucos ({code}). Pulsa Actualizar para intentarlo de nuevo.",
"cw_cheats_err_module": "Este truco necesita una parte del juego que aún no se ha cargado. Inténtalo más adelante en el juego.",
"cw_cheats_err_changed": "Los archivos de trucos han cambiado. Se ha vuelto a leer la lista.",
"cw_cheats_err_toggle": "No se pudo cambiar el truco ({code}).",
};
export default es;
+20
View File
@@ -4477,6 +4477,26 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "هلویی",
"cw_settings_contrast": "کنتراست بالا",
"cw_cheats_title": "چیت‌ها",
"cw_cheats_how": "✕ یا □ چیت انتخاب‌شده را روشن یا خاموش می‌کند.",
"cw_cheats_loading": "در حال بارگیری چیت‌ها…",
"cw_cheats_no_game": "هیچ بازی‌ای در حال اجرا نیست.",
"cw_cheats_no_game_next": "یک بازی را اجرا کنید، سپس چیت‌ها را باز کنید.",
"cw_cheats_none": "برای این بازی چیتی وجود ندارد.",
"cw_cheats_none_next": "چیت‌های این بازی را از برنامه PS5Upload روی رایانه‌تان اضافه کنید.",
"cw_cheats_exited": "بازی بسته شد.",
"cw_cheats_exited_next": "دوباره اجرایش کنید تا چیت‌هایش دوباره اینجا نمایش داده شوند.",
"cw_cheats_refresh": "تازه‌سازی",
"cw_cheats_on": "روشن",
"cw_cheats_off": "خاموش",
"cw_cheats_apply": "اعمال",
"cw_cheats_switching": "در حال تغییر…",
"cw_cheats_engine_off": "موتور چیت خاموش است. با روشن کردن یک چیت راه‌اندازی می‌شود.",
"cw_cheats_truncated": "همه چیت‌ها اینجا جا نمی‌شوند. برنامه PS5Upload روی رایانه‌تان همه را نشان می‌دهد.",
"cw_cheats_err_load": "خواندن چیت‌ها ممکن نشد ({code}). برای تلاش دوباره تازه‌سازی را بزنید.",
"cw_cheats_err_module": "این چیت به بخشی از بازی نیاز دارد که هنوز بارگیری نشده است. بعداً در بازی دوباره امتحان کنید.",
"cw_cheats_err_changed": "فایل‌های چیت تغییر کردند. فهرست دوباره خوانده شد.",
"cw_cheats_err_toggle": "تغییر وضعیت چیت ممکن نشد ({code}).",
};
export default fa;
+20
View File
@@ -4194,6 +4194,26 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Pêche",
"cw_settings_contrast": "Contraste élevé",
"cw_cheats_title": "Cheats",
"cw_cheats_how": "✕ ou □ active ou désactive le cheat sélectionné.",
"cw_cheats_loading": "Chargement des cheats…",
"cw_cheats_no_game": "Aucun jeu n'est lancé.",
"cw_cheats_no_game_next": "Lancez un jeu, puis ouvrez Cheats.",
"cw_cheats_none": "Aucun cheat pour ce jeu.",
"cw_cheats_none_next": "Ajoutez des cheats pour ce jeu depuis l'app PS5Upload sur votre PC.",
"cw_cheats_exited": "Le jeu a été fermé.",
"cw_cheats_exited_next": "Relancez-le et ses cheats réapparaîtront ici.",
"cw_cheats_refresh": "Actualiser",
"cw_cheats_on": "Activé",
"cw_cheats_off": "Désactivé",
"cw_cheats_apply": "Appliquer",
"cw_cheats_switching": "Changement…",
"cw_cheats_engine_off": "Le moteur de cheats est arrêté. Il démarre dès que vous activez un cheat.",
"cw_cheats_truncated": "Tous les cheats ne tiennent pas ici. L'app PS5Upload sur votre PC les affiche tous.",
"cw_cheats_err_load": "Impossible de lire les cheats ({code}). Appuyez sur Actualiser pour réessayer.",
"cw_cheats_err_module": "Ce cheat a besoin d'une partie du jeu qui n'est pas encore chargée. Réessayez plus tard dans le jeu.",
"cw_cheats_err_changed": "Les fichiers de cheats ont changé. La liste a été relue.",
"cw_cheats_err_toggle": "Impossible de changer le cheat ({code}).",
};
export default fr;
+20
View File
@@ -4192,6 +4192,26 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ
"cw_theme_oled": "OLED",
"cw_theme_peach": "पीच",
"cw_settings_contrast": "उच्च कंट्रास्ट",
"cw_cheats_title": "चीट",
"cw_cheats_how": "✕ या □ चुने गए चीट को चालू या बंद करता है।",
"cw_cheats_loading": "चीट लोड हो रहे हैं…",
"cw_cheats_no_game": "कोई गेम नहीं चल रहा है।",
"cw_cheats_no_game_next": "कोई गेम शुरू करें, फिर चीट खोलें।",
"cw_cheats_none": "इस गेम के लिए कोई चीट नहीं है।",
"cw_cheats_none_next": "इस गेम के चीट अपने PC पर PS5Upload ऐप से जोड़ें।",
"cw_cheats_exited": "गेम बंद हो गया।",
"cw_cheats_exited_next": "इसे फिर से शुरू करें, इसके चीट यहाँ वापस आ जाएँगे।",
"cw_cheats_refresh": "रीफ़्रेश करें",
"cw_cheats_on": "चालू",
"cw_cheats_off": "बंद",
"cw_cheats_apply": "लागू करें",
"cw_cheats_switching": "बदला जा रहा है…",
"cw_cheats_engine_off": "चीट इंजन बंद है। कोई चीट चालू करने पर यह शुरू हो जाता है।",
"cw_cheats_truncated": "सभी चीट यहाँ नहीं समाते। आपके PC पर PS5Upload ऐप सभी दिखाता है।",
"cw_cheats_err_load": "चीट नहीं पढ़े जा सके ({code})। फिर से कोशिश करने के लिए रीफ़्रेश दबाएँ।",
"cw_cheats_err_module": "इस चीट को गेम का ऐसा हिस्सा चाहिए जो अभी लोड नहीं हुआ है। गेम में आगे चलकर फिर से कोशिश करें।",
"cw_cheats_err_changed": "चीट फ़ाइलें बदल गईं। सूची फिर से पढ़ी गई।",
"cw_cheats_err_toggle": "चीट नहीं बदला जा सका ({code})।",
};
export default hi;
+20
View File
@@ -4491,6 +4491,26 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Barack",
"cw_settings_contrast": "Magas kontraszt",
"cw_cheats_title": "Csalások",
"cw_cheats_how": "Az ✕ vagy a □ be- vagy kikapcsolja a kijelölt csalást.",
"cw_cheats_loading": "Csalások betöltése…",
"cw_cheats_no_game": "Nem fut játék.",
"cw_cheats_no_game_next": "Indíts el egy játékot, majd nyisd meg a Csalásokat.",
"cw_cheats_none": "Ehhez a játékhoz nincsenek csalások.",
"cw_cheats_none_next": "Adj hozzá csalásokat ehhez a játékhoz a PC-n futó PS5Upload alkalmazásból.",
"cw_cheats_exited": "A játék bezárult.",
"cw_cheats_exited_next": "Indítsd el újra, és a csalásai ismét itt lesznek.",
"cw_cheats_refresh": "Frissítés",
"cw_cheats_on": "Be",
"cw_cheats_off": "Ki",
"cw_cheats_apply": "Alkalmaz",
"cw_cheats_switching": "Váltás…",
"cw_cheats_engine_off": "A csalásmotor ki van kapcsolva. Elindul, amikor bekapcsolsz egy csalást.",
"cw_cheats_truncated": "Nem fér ide minden csalás. A PC-n futó PS5Upload alkalmazás mindet mutatja.",
"cw_cheats_err_load": "Nem sikerült beolvasni a csalásokat ({code}). Nyomd meg a Frissítést az újrapróbáláshoz.",
"cw_cheats_err_module": "Ehhez a csaláshoz a játék egy még be nem töltött része kell. Próbáld újra később a játékban.",
"cw_cheats_err_changed": "A csalásfájlok megváltoztak. A lista újra beolvasva.",
"cw_cheats_err_toggle": "Nem sikerült átváltani a csalást ({code}).",
};
export default hu;
+20
View File
@@ -4185,6 +4185,26 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Peach",
"cw_settings_contrast": "Kontras tinggi",
"cw_cheats_title": "Cheat",
"cw_cheats_how": "✕ atau □ menyalakan atau mematikan cheat yang dipilih.",
"cw_cheats_loading": "Memuat cheat…",
"cw_cheats_no_game": "Tidak ada game yang berjalan.",
"cw_cheats_no_game_next": "Jalankan game, lalu buka Cheat.",
"cw_cheats_none": "Tidak ada cheat untuk game ini.",
"cw_cheats_none_next": "Tambahkan cheat untuk game ini dari aplikasi PS5Upload di PC Anda.",
"cw_cheats_exited": "Game telah ditutup.",
"cw_cheats_exited_next": "Jalankan lagi dan cheat-nya akan muncul kembali di sini.",
"cw_cheats_refresh": "Muat ulang",
"cw_cheats_on": "Nyala",
"cw_cheats_off": "Mati",
"cw_cheats_apply": "Terapkan",
"cw_cheats_switching": "Mengalihkan…",
"cw_cheats_engine_off": "Mesin cheat mati. Mesin menyala saat Anda menyalakan cheat.",
"cw_cheats_truncated": "Tidak semua cheat muat di sini. Aplikasi PS5Upload di PC Anda menampilkan semuanya.",
"cw_cheats_err_load": "Tidak dapat membaca cheat ({code}). Tekan Muat ulang untuk mencoba lagi.",
"cw_cheats_err_module": "Cheat ini memerlukan bagian game yang belum dimuat. Coba lagi nanti di dalam game.",
"cw_cheats_err_changed": "File cheat berubah. Daftar telah dibaca ulang.",
"cw_cheats_err_toggle": "Tidak dapat mengalihkan cheat ({code}).",
};
export default id;
+20
View File
@@ -4126,6 +4126,26 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Pesca",
"cw_settings_contrast": "Contrasto elevato",
"cw_cheats_title": "Cheat",
"cw_cheats_how": "✕ o □ attiva o disattiva il cheat selezionato.",
"cw_cheats_loading": "Caricamento dei cheat…",
"cw_cheats_no_game": "Nessun gioco in esecuzione.",
"cw_cheats_no_game_next": "Avvia un gioco, poi apri Cheat.",
"cw_cheats_none": "Nessun cheat per questo gioco.",
"cw_cheats_none_next": "Aggiungi i cheat per questo gioco dall'app PS5Upload sul tuo PC.",
"cw_cheats_exited": "Il gioco è stato chiuso.",
"cw_cheats_exited_next": "Riavvialo e i suoi cheat torneranno qui.",
"cw_cheats_refresh": "Aggiorna",
"cw_cheats_on": "Attivo",
"cw_cheats_off": "Disattivo",
"cw_cheats_apply": "Applica",
"cw_cheats_switching": "Cambio in corso…",
"cw_cheats_engine_off": "Il motore dei cheat è spento. Si avvia quando attivi un cheat.",
"cw_cheats_truncated": "Non tutti i cheat entrano qui. L'app PS5Upload sul tuo PC li mostra tutti.",
"cw_cheats_err_load": "Impossibile leggere i cheat ({code}). Premi Aggiorna per riprovare.",
"cw_cheats_err_module": "Questo cheat richiede una parte del gioco non ancora caricata. Riprova più avanti nel gioco.",
"cw_cheats_err_changed": "I file dei cheat sono cambiati. L'elenco è stato riletto.",
"cw_cheats_err_toggle": "Impossibile cambiare il cheat ({code}).",
};
export default it;
+20
View File
@@ -4197,6 +4197,26 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail
"cw_theme_oled": "OLED",
"cw_theme_peach": "ピーチ",
"cw_settings_contrast": "ハイコントラスト",
"cw_cheats_title": "チート",
"cw_cheats_how": "✕ か □ で選択中のチートをオン/オフします。",
"cw_cheats_loading": "チートを読み込み中…",
"cw_cheats_no_game": "ゲームが起動していません。",
"cw_cheats_no_game_next": "ゲームを起動してから、チートを開いてください。",
"cw_cheats_none": "このゲームのチートはありません。",
"cw_cheats_none_next": "このゲームのチートは PC の PS5Upload アプリから追加してください。",
"cw_cheats_exited": "ゲームが終了しました。",
"cw_cheats_exited_next": "もう一度起動すると、チートがここに戻ります。",
"cw_cheats_refresh": "更新",
"cw_cheats_on": "オン",
"cw_cheats_off": "オフ",
"cw_cheats_apply": "適用",
"cw_cheats_switching": "切り替え中…",
"cw_cheats_engine_off": "チートエンジンはオフです。チートをオンにすると起動します。",
"cw_cheats_truncated": "すべてのチートはここに表示しきれません。PC の PS5Upload アプリですべて確認できます。",
"cw_cheats_err_load": "チートを読み込めませんでした ({code})。更新を押してもう一度お試しください。",
"cw_cheats_err_module": "このチートには、まだ読み込まれていないゲームの部分が必要です。ゲームを進めてから再度お試しください。",
"cw_cheats_err_changed": "チートファイルが変更されました。一覧を読み込み直しました。",
"cw_cheats_err_toggle": "チートを切り替えられませんでした ({code})。",
};
export default ja;
+20
View File
@@ -4195,6 +4195,26 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det
"cw_theme_oled": "OLED",
"cw_theme_peach": "피치",
"cw_settings_contrast": "고대비",
"cw_cheats_title": "치트",
"cw_cheats_how": "✕ 또는 □ 버튼으로 선택한 치트를 켜거나 끕니다.",
"cw_cheats_loading": "치트를 불러오는 중…",
"cw_cheats_no_game": "실행 중인 게임이 없습니다.",
"cw_cheats_no_game_next": "게임을 시작한 다음 치트를 여세요.",
"cw_cheats_none": "이 게임의 치트가 없습니다.",
"cw_cheats_none_next": "PC의 PS5Upload 앱에서 이 게임의 치트를 추가하세요.",
"cw_cheats_exited": "게임이 종료되었습니다.",
"cw_cheats_exited_next": "다시 시작하면 치트가 여기에 다시 나타납니다.",
"cw_cheats_refresh": "새로 고침",
"cw_cheats_on": "켜짐",
"cw_cheats_off": "꺼짐",
"cw_cheats_apply": "적용",
"cw_cheats_switching": "전환 중…",
"cw_cheats_engine_off": "치트 엔진이 꺼져 있습니다. 치트를 켜면 시작됩니다.",
"cw_cheats_truncated": "모든 치트를 여기에 표시할 수 없습니다. PC의 PS5Upload 앱에서 모두 볼 수 있습니다.",
"cw_cheats_err_load": "치트를 읽지 못했습니다 ({code}). 새로 고침을 눌러 다시 시도하세요.",
"cw_cheats_err_module": "이 치트에는 아직 로드되지 않은 게임 부분이 필요합니다. 게임을 더 진행한 뒤 다시 시도하세요.",
"cw_cheats_err_changed": "치트 파일이 바뀌었습니다. 목록을 다시 읽었습니다.",
"cw_cheats_err_toggle": "치트를 전환하지 못했습니다 ({code}).",
};
export default ko;
+20
View File
@@ -4431,6 +4431,26 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Brzoskwinia",
"cw_settings_contrast": "Wysoki kontrast",
"cw_cheats_title": "Kody",
"cw_cheats_how": "✕ lub □ włącza albo wyłącza zaznaczony kod.",
"cw_cheats_loading": "Wczytywanie kodów…",
"cw_cheats_no_game": "Żadna gra nie jest uruchomiona.",
"cw_cheats_no_game_next": "Uruchom grę, a potem otwórz Kody.",
"cw_cheats_none": "Brak kodów dla tej gry.",
"cw_cheats_none_next": "Dodaj kody dla tej gry w aplikacji PS5Upload na komputerze.",
"cw_cheats_exited": "Gra została zamknięta.",
"cw_cheats_exited_next": "Uruchom ją ponownie, a jej kody znów się tu pojawią.",
"cw_cheats_refresh": "Odśwież",
"cw_cheats_on": "Wł.",
"cw_cheats_off": "Wył.",
"cw_cheats_apply": "Zastosuj",
"cw_cheats_switching": "Przełączanie…",
"cw_cheats_engine_off": "Silnik kodów jest wyłączony. Uruchomi się, gdy włączysz kod.",
"cw_cheats_truncated": "Nie wszystkie kody się tu mieszczą. Aplikacja PS5Upload na komputerze pokazuje wszystkie.",
"cw_cheats_err_load": "Nie udało się odczytać kodów ({code}). Naciśnij Odśwież, aby spróbować ponownie.",
"cw_cheats_err_module": "Ten kod wymaga części gry, która nie jest jeszcze wczytana. Spróbuj później w grze.",
"cw_cheats_err_changed": "Pliki kodów się zmieniły. Lista została wczytana ponownie.",
"cw_cheats_err_toggle": "Nie udało się przełączyć kodu ({code}).",
};
export default pl;
+20
View File
@@ -4193,6 +4193,26 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Pêssego",
"cw_settings_contrast": "Alto contraste",
"cw_cheats_title": "Cheats",
"cw_cheats_how": "✕ ou □ liga ou desliga o cheat selecionado.",
"cw_cheats_loading": "Carregando cheats…",
"cw_cheats_no_game": "Nenhum jogo em execução.",
"cw_cheats_no_game_next": "Inicie um jogo e depois abra Cheats.",
"cw_cheats_none": "Nenhum cheat para este jogo.",
"cw_cheats_none_next": "Adicione cheats para este jogo pelo app PS5Upload no seu PC.",
"cw_cheats_exited": "O jogo foi fechado.",
"cw_cheats_exited_next": "Inicie-o de novo e os cheats dele voltam a aparecer aqui.",
"cw_cheats_refresh": "Atualizar",
"cw_cheats_on": "Ligado",
"cw_cheats_off": "Desligado",
"cw_cheats_apply": "Aplicar",
"cw_cheats_switching": "Alternando…",
"cw_cheats_engine_off": "O motor de cheats está desligado. Ele inicia quando você liga um cheat.",
"cw_cheats_truncated": "Nem todos os cheats cabem aqui. O app PS5Upload no seu PC mostra todos.",
"cw_cheats_err_load": "Não foi possível ler os cheats ({code}). Pressione Atualizar para tentar de novo.",
"cw_cheats_err_module": "Este cheat precisa de uma parte do jogo que ainda não foi carregada. Tente de novo mais tarde no jogo.",
"cw_cheats_err_changed": "Os arquivos de cheats mudaram. A lista foi lida de novo.",
"cw_cheats_err_toggle": "Não foi possível alternar o cheat ({code}).",
};
export default pt_BR;
+20
View File
@@ -4126,6 +4126,26 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла:
"cw_theme_oled": "OLED",
"cw_theme_peach": "Персик",
"cw_settings_contrast": "Высокая контрастность",
"cw_cheats_title": "Читы",
"cw_cheats_how": "✕ или □ включает или выключает выбранный чит.",
"cw_cheats_loading": "Загрузка читов…",
"cw_cheats_no_game": "Игра не запущена.",
"cw_cheats_no_game_next": "Запустите игру, затем откройте «Читы».",
"cw_cheats_none": "Для этой игры нет читов.",
"cw_cheats_none_next": "Добавьте читы для этой игры в приложении PS5Upload на ПК.",
"cw_cheats_exited": "Игра закрыта.",
"cw_cheats_exited_next": "Запустите её снова, и её читы снова появятся здесь.",
"cw_cheats_refresh": "Обновить",
"cw_cheats_on": "Вкл.",
"cw_cheats_off": "Выкл.",
"cw_cheats_apply": "Применить",
"cw_cheats_switching": "Переключение…",
"cw_cheats_engine_off": "Движок читов выключен. Он запустится, когда вы включите чит.",
"cw_cheats_truncated": "Здесь помещаются не все читы. Приложение PS5Upload на ПК показывает все.",
"cw_cheats_err_load": "Не удалось прочитать читы ({code}). Нажмите «Обновить», чтобы повторить.",
"cw_cheats_err_module": "Этому читу нужна часть игры, которая ещё не загружена. Попробуйте позже в игре.",
"cw_cheats_err_changed": "Файлы читов изменились. Список прочитан заново.",
"cw_cheats_err_toggle": "Не удалось переключить чит ({code}).",
};
export default ru;
+20
View File
@@ -4185,6 +4185,26 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ
"cw_theme_oled": "OLED",
"cw_theme_peach": "พีช",
"cw_settings_contrast": "คอนทราสต์สูง",
"cw_cheats_title": "โกง",
"cw_cheats_how": "✕ หรือ □ เปิดหรือปิดสูตรโกงที่เลือกอยู่",
"cw_cheats_loading": "กำลังโหลดสูตรโกง…",
"cw_cheats_no_game": "ไม่มีเกมที่กำลังทำงาน",
"cw_cheats_no_game_next": "เริ่มเกมก่อน แล้วจึงเปิดเมนูโกง",
"cw_cheats_none": "ไม่มีสูตรโกงสำหรับเกมนี้",
"cw_cheats_none_next": "เพิ่มสูตรโกงสำหรับเกมนี้จากแอป PS5Upload บนพีซีของคุณ",
"cw_cheats_exited": "เกมถูกปิดแล้ว",
"cw_cheats_exited_next": "เริ่มเกมอีกครั้ง แล้วสูตรโกงจะกลับมาที่นี่",
"cw_cheats_refresh": "รีเฟรช",
"cw_cheats_on": "เปิด",
"cw_cheats_off": "ปิด",
"cw_cheats_apply": "ใช้",
"cw_cheats_switching": "กำลังสลับ…",
"cw_cheats_engine_off": "เอนจินสูตรโกงปิดอยู่ จะเริ่มทำงานเมื่อคุณเปิดสูตรโกง",
"cw_cheats_truncated": "แสดงสูตรโกงได้ไม่ครบที่นี่ แอป PS5Upload บนพีซีของคุณแสดงได้ทั้งหมด",
"cw_cheats_err_load": "อ่านสูตรโกงไม่ได้ ({code}) กดรีเฟรชเพื่อลองอีกครั้ง",
"cw_cheats_err_module": "สูตรโกงนี้ต้องใช้ส่วนของเกมที่ยังไม่ได้โหลด ลองอีกครั้งภายหลังในเกม",
"cw_cheats_err_changed": "ไฟล์สูตรโกงเปลี่ยนไป อ่านรายการใหม่แล้ว",
"cw_cheats_err_toggle": "สลับสูตรโกงไม่ได้ ({code})",
};
export default th;
+20
View File
@@ -4183,6 +4183,26 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Şeftali",
"cw_settings_contrast": "Yüksek kontrast",
"cw_cheats_title": "Hileler",
"cw_cheats_how": "✕ veya □ seçili hileyi açar ya da kapatır.",
"cw_cheats_loading": "Hileler yükleniyor…",
"cw_cheats_no_game": "Çalışan oyun yok.",
"cw_cheats_no_game_next": "Bir oyun başlatın, sonra Hileler'i açın.",
"cw_cheats_none": "Bu oyun için hile yok.",
"cw_cheats_none_next": "Bu oyun için hileleri bilgisayarınızdaki PS5Upload uygulamasından ekleyin.",
"cw_cheats_exited": "Oyun kapatıldı.",
"cw_cheats_exited_next": "Yeniden başlatın, hileleri tekrar burada görünür.",
"cw_cheats_refresh": "Yenile",
"cw_cheats_on": "Açık",
"cw_cheats_off": "Kapalı",
"cw_cheats_apply": "Uygula",
"cw_cheats_switching": "Değiştiriliyor…",
"cw_cheats_engine_off": "Hile motoru kapalı. Bir hileyi açtığınızda başlar.",
"cw_cheats_truncated": "Tüm hileler buraya sığmıyor. Bilgisayarınızdaki PS5Upload uygulaması hepsini gösterir.",
"cw_cheats_err_load": "Hileler okunamadı ({code}). Yeniden denemek için Yenile'ye basın.",
"cw_cheats_err_module": "Bu hile, oyunun henüz yüklenmemiş bir bölümüne ihtiyaç duyuyor. Oyunda daha sonra tekrar deneyin.",
"cw_cheats_err_changed": "Hile dosyaları değişti. Liste yeniden okundu.",
"cw_cheats_err_toggle": "Hile değiştirilemedi ({code}).",
};
export default tr;
+20
View File
@@ -4178,6 +4178,26 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "Đào",
"cw_settings_contrast": "Tương phản cao",
"cw_cheats_title": "Cheat",
"cw_cheats_how": "✕ hoặc □ bật hoặc tắt cheat đang chọn.",
"cw_cheats_loading": "Đang tải cheat…",
"cw_cheats_no_game": "Không có trò chơi nào đang chạy.",
"cw_cheats_no_game_next": "Hãy mở một trò chơi, rồi mở Cheat.",
"cw_cheats_none": "Không có cheat cho trò chơi này.",
"cw_cheats_none_next": "Thêm cheat cho trò chơi này từ ứng dụng PS5Upload trên PC của bạn.",
"cw_cheats_exited": "Trò chơi đã đóng.",
"cw_cheats_exited_next": "Mở lại trò chơi, cheat của nó sẽ hiện lại ở đây.",
"cw_cheats_refresh": "Làm mới",
"cw_cheats_on": "Bật",
"cw_cheats_off": "Tắt",
"cw_cheats_apply": "Áp dụng",
"cw_cheats_switching": "Đang chuyển…",
"cw_cheats_engine_off": "Bộ máy cheat đang tắt. Nó sẽ khởi động khi bạn bật một cheat.",
"cw_cheats_truncated": "Không thể hiển thị hết cheat ở đây. Ứng dụng PS5Upload trên PC hiển thị tất cả.",
"cw_cheats_err_load": "Không đọc được cheat ({code}). Nhấn Làm mới để thử lại.",
"cw_cheats_err_module": "Cheat này cần một phần của trò chơi chưa được tải. Hãy thử lại sau trong trò chơi.",
"cw_cheats_err_changed": "Tệp cheat đã thay đổi. Danh sách đã được đọc lại.",
"cw_cheats_err_toggle": "Không chuyển được cheat ({code}).",
};
export default vi;
+20
View File
@@ -4132,6 +4132,26 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "蜜桃",
"cw_settings_contrast": "高对比度",
"cw_cheats_title": "金手指",
"cw_cheats_how": "按 ✕ 或 □ 开启或关闭选中的金手指。",
"cw_cheats_loading": "正在加载金手指…",
"cw_cheats_no_game": "没有正在运行的游戏。",
"cw_cheats_no_game_next": "请先启动游戏,再打开金手指。",
"cw_cheats_none": "此游戏没有金手指。",
"cw_cheats_none_next": "请在电脑上的 PS5Upload 应用中为此游戏添加金手指。",
"cw_cheats_exited": "游戏已关闭。",
"cw_cheats_exited_next": "再次启动游戏,它的金手指会重新显示在这里。",
"cw_cheats_refresh": "刷新",
"cw_cheats_on": "开",
"cw_cheats_off": "关",
"cw_cheats_apply": "应用",
"cw_cheats_switching": "正在切换…",
"cw_cheats_engine_off": "金手指引擎已关闭。开启任一金手指时会自动启动。",
"cw_cheats_truncated": "此处无法显示全部金手指。电脑上的 PS5Upload 应用可显示全部。",
"cw_cheats_err_load": "无法读取金手指({code})。请按刷新重试。",
"cw_cheats_err_module": "此金手指需要游戏中尚未加载的部分。请在游戏中稍后再试。",
"cw_cheats_err_changed": "金手指文件已更改,已重新读取列表。",
"cw_cheats_err_toggle": "无法切换金手指({code})。",
};
export default zh_CN;
+20
View File
@@ -4126,6 +4126,26 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}",
"cw_theme_oled": "OLED",
"cw_theme_peach": "蜜桃",
"cw_settings_contrast": "高對比",
"cw_cheats_title": "金手指",
"cw_cheats_how": "按 ✕ 或 □ 開啟或關閉選取的金手指。",
"cw_cheats_loading": "正在載入金手指…",
"cw_cheats_no_game": "沒有正在執行的遊戲。",
"cw_cheats_no_game_next": "請先啟動遊戲,再開啟金手指。",
"cw_cheats_none": "此遊戲沒有金手指。",
"cw_cheats_none_next": "請在電腦上的 PS5Upload 應用程式中為此遊戲新增金手指。",
"cw_cheats_exited": "遊戲已關閉。",
"cw_cheats_exited_next": "再次啟動遊戲,它的金手指會重新顯示在這裡。",
"cw_cheats_refresh": "重新整理",
"cw_cheats_on": "開",
"cw_cheats_off": "關",
"cw_cheats_apply": "套用",
"cw_cheats_switching": "正在切換…",
"cw_cheats_engine_off": "金手指引擎已關閉。開啟任一金手指時會自動啟動。",
"cw_cheats_truncated": "此處無法顯示全部金手指。電腦上的 PS5Upload 應用程式可顯示全部。",
"cw_cheats_err_load": "無法讀取金手指({code})。請按重新整理再試一次。",
"cw_cheats_err_module": "此金手指需要遊戲中尚未載入的部分。請在遊戲中稍後再試。",
"cw_cheats_err_changed": "金手指檔案已變更,已重新讀取清單。",
"cw_cheats_err_toggle": "無法切換金手指({code})。",
};
export default zh_TW;
+26
View File
@@ -84,4 +84,30 @@ int webui_is_game_title_id(const char *tid);
* *app_id set, 0 when no game runs, -1 when the reply does not parse. */
int webui_focus_running(const char *json, size_t n, char *title_id, size_t cap, uint32_t *app_id);
/* ---- cheats (CHEATS_STATUS / CHEATS_GET / CHEATS_TOGGLE, as the engine calls them) ---- */
/* 1 when `tid` can name a game to the cheat methods: 4 to 15 ASCII letters and digits. */
int webui_cheats_title_ok(const char *tid);
/* A CHEATS_STATUS reply ({"enabled":bool,"patches_last":N,"patches_total":N,"game_running":bool,
* "game_title_id":"..","game_pid":N}) as {"engine":bool,"game":{"title_id":".."}|null}. `game` is
* null unless a game runs under a usable title id (webui_cheats_title_ok) that is not the page's own
* tile. 0, or -1 (no "enabled", or does not fit; out is then ""). */
int webui_cheats_status_to_json(const char *json, size_t n, char *out, size_t cap);
/* A CHEATS_GET reply ({"title_id":..,"mods":[{"index":N,"name":..,"desc":..,"type":"checkbox"|"button",
* "on":bool},...]}, or {"mods":[],"error":"no cheat files found"}) as
* {"mods":[{"index":N,"name":"..","desc":"..","button":bool,"on":bool},...],"truncated":bool}
* Every string is re-escaped (valid JSON whatever the cheat file held). A mod without an index or a
* name is left out. The helper's reply can end mid-entry (its 64 KiB buffer): the entries before the
* cut are kept and "truncated" is set; so too when `out` is full. Returns the number of mods written,
* or -1: no mods array, or the reply names an error other than "no cheat files found" (copied to
* `err`, cut to err_cap - 1), or not even the empty list fits. */
int webui_cheats_mods_to_json(const char *json, size_t n, char *out, size_t cap, char *err, size_t err_cap);
/* A failed CHEATS_TOGGLE's cause ({"ok":false,"err":"<text>"}) as a token the page acts on:
* "game_not_running" (no game, or a different game than the one asked for), "no_cheats",
* "bad_index", "module_missing", else "toggle_failed". */
const char *webui_cheats_toggle_error(const char *cause, size_t n);
#endif
+104
View File
@@ -751,6 +751,107 @@ int webui_api_tile_off(void) {
return rc;
}
/* ---- cheats: the running game's cheats, through the methods the engine calls (cheats.rs) ---- */
#define API_CHEATS_JSON (96u * 1024u) /* CHEATS_GET's 64 KiB reply, re-escaped */
/* A required title id the cheat methods can take (webui_cheats_title_ok); else 400 bad_title_id. */
static int need_title(int fd, const char *b, size_t n, char *out, size_t cap) {
if (webui_json_str(b, n, "title_id", out, cap) == 1 && webui_cheats_title_ok(out)) return 0;
send_bad(fd, "bad_title_id");
return -1;
}
/* CHEATS_STATUS (empty request): {"engine":bool,"game":{"title_id":..}|null}. Cheap (the helper
* caches the running game): the page polls it. */
static void r_cheats_status(const char *b, size_t n, uint8_t *out, int fd) {
const uint8_t *t;
size_t ol, tl;
char json[96];
int st;
(void)b;
(void)n;
st = call_text(AVA1_METHOD_CHEATS_STATUS, NULL, 0, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, out, ol);
return;
}
if (webui_cheats_status_to_json((const char *)t, tl, json, sizeof json) != 0) send_error(fd, 500, "bad_cheats_reply", 16);
else send_json(fd, 200, json, strlen(json));
}
/* {title_id}: CHEATS_GET {"title_id":..}, as {"mods":[{index,name,desc,button,on}],"truncated":bool}. */
static void r_cheats_list(const char *b, size_t n, uint8_t *out, int fd) {
char title[32], req[64], err[API_CAUSE_MAX];
const uint8_t *t;
size_t ol, tl;
char *json;
int st, k;
if (need_title(fd, b, n, title, sizeof title) != 0) return;
k = snprintf(req, sizeof req, "{\"title_id\":\"%s\"}", title); /* letters and digits only */
st = call_text(AVA1_METHOD_CHEATS_GET, req, (size_t)k, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
send_status_error(fd, st, out, ol);
return;
}
json = malloc(API_CHEATS_JSON);
if (!json) {
send_error(fd, 500, "out_of_memory", 13);
return;
}
if (webui_cheats_mods_to_json((const char *)t, tl, json, API_CHEATS_JSON, err, sizeof err) < 0) {
if (err[0]) send_error(fd, 500, err, strlen(err));
else send_error(fd, 500, "bad_cheats_reply", 16);
} else {
send_json(fd, 200, json, strlen(json));
}
free(json);
}
static int cheats_error_http(const char *token) {
if (strcmp(token, "game_not_running") == 0 || strcmp(token, "module_missing") == 0) return 409;
if (strcmp(token, "no_cheats") == 0) return 404;
if (strcmp(token, "bad_index") == 0) return 400;
return 500;
}
/* {title_id, index, on}: CHEATS_TOGGLE as the engine sends it. The helper refuses unless that game
* is running (it patches the live process) and turns its cheat engine on with any switch-on. A
* refusal is {"error":"game_not_running"|"no_cheats"|"bad_index"|"module_missing"|"toggle_failed"}. */
static void r_cheats_toggle(const char *b, size_t n, uint8_t *out, int fd) {
char title[32], req[96];
const uint8_t *t;
size_t ol, tl;
uint64_t index;
int on, st, k;
if (need_title(fd, b, n, title, sizeof title) != 0) return;
if (webui_json_u64(b, n, "index", &index) != 1 || index > 0x7fffffffu) {
send_bad(fd, "bad_index");
return;
}
if (webui_json_bool(b, n, "on", &on) != 1) {
send_bad(fd, "bad_on");
return;
}
k = snprintf(req, sizeof req, "{\"title_id\":\"%s\",\"index\":%lu,\"on\":%s}", title, (unsigned long)index,
on ? "true" : "false");
st = call_text(AVA1_METHOD_CHEATS_TOGGLE, req, (size_t)k, out, &ol, &t, &tl);
if (st != AVA1_STATUS_OK) {
/* the handler's whole {"ok":false,"err":..} body is the cause (mgmt_call_text_keep) */
if (ol && out[0] == '{') {
const char *tok = webui_cheats_toggle_error((const char *)out, ol);
send_error(fd, cheats_error_http(tok), tok, strlen(tok));
} else {
send_status_error(fd, st, out, ol);
}
return;
}
{
const char *ok = on ? "{\"ok\":true,\"on\":true}" : "{\"ok\":true,\"on\":false}";
send_json(fd, 200, ok, strlen(ok));
}
}
/* ---- input log ---- */
/* One append at a time (the file is a ring rewritten in place), and the time of the last one: a
@@ -823,6 +924,9 @@ static const struct {
{ "/api/settings", r_settings },
{ "/api/settings/set", r_settings_set },
{ "/api/input-log", r_input_log },
{ "/api/cheats/status", r_cheats_status },
{ "/api/cheats/list", r_cheats_list },
{ "/api/cheats/toggle", r_cheats_toggle },
};
/* The page's own headers: only same-origin fetches, inline script/style (the page is one file),
+117
View File
@@ -609,3 +609,120 @@ int webui_focus_running(const char *json, size_t n, char *title_id, size_t cap,
}
return 0;
}
/* ---- cheats ---- */
int webui_cheats_title_ok(const char *tid) {
size_t i, n = tid ? strlen(tid) : 0;
if (n < 4 || n > 15) return 0;
for (i = 0; i < n; i++) {
char c = tid[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))) return 0;
}
return 1;
}
int webui_cheats_status_to_json(const char *json, size_t n, char *out, size_t cap) {
char tid[32];
int enabled = 0, running = 0, game, k;
if (cap) out[0] = '\0';
if (webui_json_bool(json, n, "enabled", &enabled) != 1) return -1;
if (webui_json_bool(json, n, "game_running", &running) != 1) running = 0;
game = running && webui_json_str(json, n, "game_title_id", tid, sizeof tid) == 1 && webui_cheats_title_ok(tid) &&
strcmp(tid, WEBUI_TILE_TITLE_ID) != 0;
if (game)
k = snprintf(out, cap, "{\"engine\":%s,\"game\":{\"title_id\":\"%s\"}}", enabled ? "true" : "false", tid);
else
k = snprintf(out, cap, "{\"engine\":%s,\"game\":null}", enabled ? "true" : "false");
if (k < 0 || (size_t)k >= cap) {
if (cap) out[0] = '\0';
return -1;
}
return 0;
}
/* Room kept for the list's tail: `],"truncated":false}` and the NUL. */
#define CHEATS_TAIL_ROOM 24u
#define CHEATS_NO_FILES "no cheat files found" /* cheats.c cheats_list_mods: an empty list */
int webui_cheats_mods_to_json(const char *json, size_t n, char *out, size_t cap, char *err, size_t err_cap) {
const char *e = json + n, *p, *q;
char msg[160], name[512], desc[512], type[32];
size_t pos;
int written = 0, truncated = 0;
if (cap) out[0] = '\0';
if (err_cap) err[0] = '\0';
if (webui_json_str(json, n, "error", msg, sizeof msg) == 1 && strcmp(msg, CHEATS_NO_FILES) != 0) {
if (err_cap) {
size_t l = strlen(msg) < err_cap - 1 ? strlen(msg) : err_cap - 1;
memcpy(err, msg, l);
err[l] = '\0';
}
return -1;
}
p = top_value(json, n, "mods");
if (!p || *p != '[') return -1;
if (cap < sizeof "{\"mods\":[" - 1 + CHEATS_TAIL_ROOM) return -1;
memcpy(out, "{\"mods\":[", 9);
pos = 9;
q = p + 1;
for (;;) {
const char *el, *end;
size_t l;
uint64_t idx;
int on = 0;
q = ws(q, e);
if (q >= e) {
truncated = 1; /* cut before the closing ']' */
break;
}
if (*q == ']') break;
el = q;
end = skip_val(q, e);
if (!end) {
truncated = 1; /* cut mid-entry */
break;
}
l = (size_t)(end - el);
if (*el == '{' && webui_json_u64(el, l, "index", &idx) == 1 && idx <= 0x7fffffffu &&
webui_json_str(el, l, "name", name, sizeof name) == 1 && name[0]) {
jw_t w;
char head[48];
int hl;
if (webui_json_str(el, l, "desc", desc, sizeof desc) != 1) desc[0] = '\0';
if (webui_json_str(el, l, "type", type, sizeof type) != 1) type[0] = '\0';
if (webui_json_bool(el, l, "on", &on) != 1) on = 0;
memset(&w, 0, sizeof w);
w.out = out + pos;
w.cap = cap - pos - CHEATS_TAIL_ROOM + 1; /* pos + entry stays CHEATS_TAIL_ROOM short of cap */
hl = snprintf(head, sizeof head, "%s{\"index\":%llu,\"name\":\"", written ? "," : "", (unsigned long long)idx);
put(&w, head, (size_t)hl);
esc(&w, name, strlen(name));
put(&w, "\",\"desc\":\"", 10);
esc(&w, desc, strlen(desc));
put(&w, strcmp(type, "button") == 0 ? "\",\"button\":true" : "\",\"button\":false", strcmp(type, "button") == 0 ? 15 : 16);
put(&w, on ? ",\"on\":true}" : ",\"on\":false}", on ? 11 : 12);
if (w.full) {
truncated = 1;
break;
}
pos += w.wrote;
written++;
}
q = ws(end, e);
if (q < e && *q == ',') q++;
}
snprintf(out + pos, cap - pos, "],\"truncated\":%s}", truncated ? "true" : "false");
return written;
}
const char *webui_cheats_toggle_error(const char *cause, size_t n) {
char msg[300];
if (!cause || !n || webui_json_str(cause, n, "err", msg, sizeof msg) != 1) return "toggle_failed";
if (strstr(msg, "no game is currently running") || strstr(msg, "does not match cheat target"))
return "game_not_running";
if (strstr(msg, "no cheat file")) return "no_cheats";
if (strstr(msg, "index out of range")) return "bad_index";
if (strstr(msg, "is not loaded in the target process")) return "module_missing";
return "toggle_failed";
}
+91
View File
@@ -536,6 +536,94 @@ static void array_span(void) {
CHECK(webui_json_array("{\"events\":[\"a\"", 13, "events", &v, &len) == -1); /* cut short */
}
/* ---- cheats ---- */
static void cheats_status(void) {
char out[256];
CHECK(webui_cheats_title_ok("PPSA01650") && webui_cheats_title_ok("CUSA07842") && webui_cheats_title_ok("FAKE1"));
CHECK(!webui_cheats_title_ok("") && !webui_cheats_title_ok("ABC") && !webui_cheats_title_ok("PPSA0165\"") &&
!webui_cheats_title_ok("PPSA/0165") && !webui_cheats_title_ok("ABCDEFGHIJKLMNOP") && !webui_cheats_title_ok(NULL));
const char *j = "{\"enabled\":true,\"patches_last\":2,\"patches_total\":5,\"game_running\":true,"
"\"game_title_id\":\"PPSA01650\",\"game_pid\":77}";
CHECK(webui_cheats_status_to_json(j, strlen(j), out, sizeof out) == 0);
CHECK(strcmp(out, "{\"engine\":true,\"game\":{\"title_id\":\"PPSA01650\"}}") == 0);
j = "{\"enabled\":false,\"patches_last\":0,\"patches_total\":0,\"game_running\":false,\"game_title_id\":\"\","
"\"game_pid\":0}";
CHECK(webui_cheats_status_to_json(j, strlen(j), out, sizeof out) == 0);
CHECK(strcmp(out, "{\"engine\":false,\"game\":null}") == 0);
/* running but no usable title id, or the page's own tile: no game */
j = "{\"enabled\":false,\"game_running\":true,\"game_title_id\":\"\"}";
CHECK(webui_cheats_status_to_json(j, strlen(j), out, sizeof out) == 0);
CHECK(strcmp(out, "{\"engine\":false,\"game\":null}") == 0);
j = "{\"enabled\":false,\"game_running\":true,\"game_title_id\":\"PSUP00001\"}";
CHECK(webui_cheats_status_to_json(j, strlen(j), out, sizeof out) == 0);
CHECK(strcmp(out, "{\"engine\":false,\"game\":null}") == 0);
/* a title id that is not plain letters and digits is never echoed */
j = "{\"enabled\":true,\"game_running\":true,\"game_title_id\":\"AB\\\"<x>\"}";
CHECK(webui_cheats_status_to_json(j, strlen(j), out, sizeof out) == 0);
CHECK(strcmp(out, "{\"engine\":true,\"game\":null}") == 0);
CHECK(webui_cheats_status_to_json("nope", 4, out, sizeof out) == -1);
CHECK(webui_cheats_status_to_json("{\"x\":1}", 7, out, sizeof out) == -1); /* no enabled */
CHECK(webui_cheats_status_to_json(j, strlen(j), out, 8) == -1); /* does not fit */
}
static void cheats_mods(void) {
static char out[4096];
char err[64];
const char *j =
"{\"title_id\":\"PPSA01650\",\"mods\":["
"{\"index\":0,\"name\":\"Infinite \\\"HP\\\"\",\"desc\":\"\",\"type\":\"checkbox\",\"on\":true},"
"{\"index\":1,\"name\":\"\xe7\x84\xa1\xe9\x99\x90\",\"desc\":\"UTF-8 kept\",\"type\":\"button\",\"on\":false},"
"{\"index\":2,\"desc\":\"no name: left out\",\"type\":\"checkbox\",\"on\":false},"
"{\"index\":3,\"name\":\"Max money\",\"desc\":\"a\\nb\",\"type\":\"checkbox\",\"on\":false}]}";
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, sizeof out, err, sizeof err) == 3);
CHECK(json_valid(out));
CHECK(strcmp(out, "{\"mods\":["
"{\"index\":0,\"name\":\"Infinite \\\"HP\\\"\",\"desc\":\"\",\"button\":false,\"on\":true},"
"{\"index\":1,\"name\":\"\xe7\x84\xa1\xe9\x99\x90\",\"desc\":\"UTF-8 kept\",\"button\":true,\"on\":false},"
"{\"index\":3,\"name\":\"Max money\",\"desc\":\"a\\nb\",\"button\":false,\"on\":false}],"
"\"truncated\":false}") == 0);
/* the helper's "no cheat files" answer: an empty list, not an error */
j = "{\"mods\":[],\"error\":\"no cheat files found\"}";
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, sizeof out, err, sizeof err) == 0);
CHECK(strcmp(out, "{\"mods\":[],\"truncated\":false}") == 0);
/* any other error is one */
j = "{\"mods\":[],\"error\":\"oom\"}";
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, sizeof out, err, sizeof err) == -1);
CHECK(strcmp(err, "oom") == 0);
CHECK(webui_cheats_mods_to_json("{\"x\":1}", 7, out, sizeof out, err, sizeof err) == -1);
/* the helper's 64 KiB reply cut mid-entry: the whole entries before the cut, truncated set */
j = "{\"title_id\":\"PPSA01650\",\"mods\":[{\"index\":0,\"name\":\"A\",\"desc\":\"\",\"type\":\"checkbox\",\"on\":false},"
"{\"index\":1,\"name\":\"B";
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, sizeof out, err, sizeof err) == 1);
CHECK(json_valid(out));
CHECK(strstr(out, "\"truncated\":true}") != NULL);
/* an output too small for every entry keeps the ones that fit and says truncated */
j = "{\"mods\":[{\"index\":0,\"name\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"desc\":\"\",\"type\":\"checkbox\",\"on\":false},"
"{\"index\":1,\"name\":\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\",\"desc\":\"\",\"type\":\"checkbox\",\"on\":false}]}";
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, 140, err, sizeof err) == 1);
CHECK(json_valid(out));
CHECK(strstr(out, "AAAA") && !strstr(out, "BBBB") && strstr(out, "\"truncated\":true}"));
CHECK(webui_cheats_mods_to_json(j, strlen(j), out, 10, err, sizeof err) == -1);
}
static void cheats_toggle_errors(void) {
const char *c = "{\"ok\":false,\"err\":\"no game is currently running\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "game_not_running") == 0);
c = "{\"ok\":false,\"err\":\"running game (PPSA00001) does not match cheat target (PPSA01650)\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "game_not_running") == 0);
c = "{\"ok\":false,\"err\":\"no cheat file for PPSA01650. Drop a .json/.shn/.mc4 into /data/etaHEN/cheats\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "no_cheats") == 0);
c = "{\"ok\":false,\"err\":\"cheat index out of range\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "bad_index") == 0);
c = "{\"ok\":false,\"err\":\"module \\\"x.prx\\\" is not loaded in the target process\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "module_missing") == 0);
c = "{\"ok\":false,\"err\":\"pt_attach failed (errno=1)\"}";
CHECK(strcmp(webui_cheats_toggle_error(c, strlen(c)), "toggle_failed") == 0);
CHECK(strcmp(webui_cheats_toggle_error(NULL, 0), "toggle_failed") == 0);
CHECK(strcmp(webui_cheats_toggle_error("busy", 4), "toggle_failed") == 0);
}
int main(void) {
unicode_path_round_trips();
array_span();
@@ -551,6 +639,9 @@ int main(void) {
kv_lines();
focus_running();
focus_running_fallback_games_only();
cheats_status();
cheats_mods();
cheats_toggle_errors();
if (failures) {
fprintf(stderr, "webui_codec_selftest: %d failure(s)\n", failures);
return 1;