diff --git a/client/src/console/cheats/Cheats.test.tsx b/client/src/console/cheats/Cheats.test.tsx new file mode 100644 index 00000000..2deb3b3f --- /dev/null +++ b/client/src/console/cheats/Cheats.test.tsx @@ -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(); + 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 => ({ + 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, over: Partial = {}) => + renderToStaticMarkup( + {}} 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(/ + + ); +} + +function RefreshButton({ onRefresh }: { onRefresh: () => void }) { + return ( + + ); +} + +/** 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 ( +
+

{title}

+ {next ?

{next}

: null} + +
+ ); +} + +/** 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 =

{t("cw_cheats_loading")}

; + break; + case "error": + body = ; + break; + case "no_game": + body = ; + break; + case "exited": + body = ; + break; + case "none": + body = ; + break; + case "list": + body = ( + <> +
    + {phase.list.mods.map((m) => ( + + ))} +
+ {phase.list.truncated ?

{t("cw_cheats_truncated")}

: null} + + ); + break; + } + + return ( +
+

{t("cw_cheats_title")}

+ {title ? ( +
+ {p.cover ? ( + + ) : ( + + )} +
+
{t("cw_home_now_running")}
+
{title}
+
+ {s.status && !s.status.engine ? t("cw_cheats_engine_off") : t("cw_cheats_how")} +
+
+ +
+ ) : null} + {s.message ? ( +
+ {s.message} +
+ ) : null} + {body} +
+ ); +} + +/** □ 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(EMPTY_STATE); + const ctl = useRef(null); + const root = useRef(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(null); + const cover = loadedCover && coverFailed !== loadedCover ? loadedCover : null; + + return ( +
+ setCoverFailed(loadedCover)} + onToggle={(m) => void ctl.current?.toggle(m)} + onRefresh={() => void ctl.current?.refresh()} + /> +
+ ); +} diff --git a/client/src/console/cheats/model.ts b/client/src/console/cheats/model.ts new file mode 100644 index 00000000..28e6f431 --- /dev/null +++ b/client/src/console/cheats/model.ts @@ -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[]) { + 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; + /** Read the running game's cheats again. */ + loadList(): Promise; + /** Switch `mod` (a one-shot cheat is applied); ignored while another switch runs. */ + toggle(mod: CheatMod): Promise; + /** Create / coming back: the status and the cheats again. */ + refresh(): Promise; + /** 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) => { + 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("/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("/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, + }; +} diff --git a/client/src/console/console.css b/client/src/console/console.css index 54ff0767..44ebb5e5 100644 --- a/client/src/console/console.css +++ b/client/src/console/console.css @@ -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); +} diff --git a/client/src/console/home/Home.tsx b/client/src/console/home/Home.tsx index c3753c05..8b62594f 100644 --- a/client/src/console/home/Home.tsx +++ b/client/src/console/home/Home.tsx @@ -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(null); useEffect(() => { setUrl(null); diff --git a/client/src/console/sections.tsx b/client/src/console/sections.tsx index 847ca414..eb68e038 100644 --- a/client/src/console/sections.tsx +++ b/client/src/console/sections.tsx @@ -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) => , acceptsParams: true, }, + { + id: "cheats", + icon: Sparkles, + labelKey: "cw_cheats_title", + render: () => , + }, { id: "settings", icon: Settings, diff --git a/client/src/console/strings.gen.json b/client/src/console/strings.gen.json index 2442fed5..733df728 100644 --- a/client/src/console/strings.gen.json +++ b/client/src/console/strings.gen.json @@ -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}…", diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index 6c30c49f..e1a1a5ca 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -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; diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index 7a366e6f..e7af0232 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -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; diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index f95b0d56..c344031d 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -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; diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index 733a44cb..c0c37fcb 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -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; diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index 32d3ea7e..87dddc20 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -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; diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 6247d7ef..1e8bfd53 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -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; diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 5b77d4df..cdc68c6e 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -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; diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index 62cbc545..07d2d83f 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -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; diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index 4bec5336..9b021edb 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -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; diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index 283ce057..5732840f 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -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; diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index dc585a1c..8c63e499 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -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; diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index f383e964..2f8937d0 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -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; diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index 41e78dc9..afba72e6 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -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; diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index a8eeab19..cb1c3902 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -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; diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index d722d65c..a4c51293 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -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; diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index a5181fb4..3bdebc54 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -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; diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index 412f3e47..e415f680 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -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; diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index a6540e60..9bb3673d 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -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; diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index 2df42157..8d674f76 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -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; diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 7539ec7d..73a16b90 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -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; diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index 5cac78ac..6d07e7a4 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -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; diff --git a/payload/include/webui_codec.h b/payload/include/webui_codec.h index f6e3a611..4e0a4aec 100644 --- a/payload/include/webui_codec.h +++ b/payload/include/webui_codec.h @@ -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":""}) 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 diff --git a/payload/src/webui_api.c b/payload/src/webui_api.c index 04e0db52..8fb346ea 100644 --- a/payload/src/webui_api.c +++ b/payload/src/webui_api.c @@ -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), diff --git a/payload/src/webui_codec.c b/payload/src/webui_codec.c index 66d0d733..7382c8bf 100644 --- a/payload/src/webui_codec.c +++ b/payload/src/webui_codec.c @@ -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"; +} diff --git a/payload/tests/webui_codec_selftest.c b/payload/tests/webui_codec_selftest.c index 40310d10..98604b88 100644 --- a/payload/tests/webui_codec_selftest.c +++ b/payload/tests/webui_codec_selftest.c @@ -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\\\"\"}"; + 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;