From dd16c7c47f536516cf09b36abebed1448b90eeb2 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 18 Oct 2026 11:40:49 -0700 Subject: [PATCH 1/2] console cheats: find and download cheats on the console The console page's Cheats section only listed cheat files already on the console. It can now find the running game's cheats in the community repos (read by the PS5 browser itself; a port of cheats.rs's repo list, filename parsing and content URLs), mark the ones for the installed version, and download one: the browser fetches it and POSTs it to the helper, which stores it atomically where the cheat engine looks. Helper: CSP connect-src allows raw.githubusercontent.com and api.github.com; new /api/cheats/version (appinfo CONTENT_VERSION/APP_VER) and streamed /api/cheats/store (strict name check, <= 4 MiB, same-folder temp + rename via wf_receive_file). cheats.c unchanged: the engine rescans its folders on every list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS --- Makefile | 8 + client/src/console/cheats/Cheats.test.tsx | 26 +- client/src/console/cheats/Cheats.tsx | 87 ++++- client/src/console/cheats/FindCheats.tsx | 152 ++++++++ client/src/console/cheats/finder.test.tsx | 257 ++++++++++++++ client/src/console/cheats/finder.ts | 168 +++++++++ client/src/console/cheats/repos.test.ts | 284 +++++++++++++++ client/src/console/cheats/repos.ts | 375 ++++++++++++++++++++ client/src/console/console.css | 75 ++++ client/src/console/locales/ar.json | 25 +- client/src/console/locales/bn.json | 25 +- client/src/console/locales/de.json | 25 +- client/src/console/locales/en.json | 25 +- client/src/console/locales/es.json | 25 +- client/src/console/locales/fa.json | 25 +- client/src/console/locales/fr.json | 25 +- client/src/console/locales/hi.json | 25 +- client/src/console/locales/hu.json | 25 +- client/src/console/locales/id.json | 25 +- client/src/console/locales/it.json | 25 +- client/src/console/locales/ja.json | 25 +- client/src/console/locales/ko.json | 25 +- client/src/console/locales/pl.json | 25 +- client/src/console/locales/pt-BR.json | 25 +- client/src/console/locales/ru.json | 25 +- client/src/console/locales/th.json | 25 +- client/src/console/locales/tr.json | 25 +- client/src/console/locales/vi.json | 25 +- client/src/console/locales/zh-CN.json | 25 +- client/src/console/locales/zh-TW.json | 25 +- client/src/i18n/locales/ar.ts | 25 +- client/src/i18n/locales/bn.ts | 25 +- client/src/i18n/locales/de.ts | 25 +- client/src/i18n/locales/en.ts | 25 +- client/src/i18n/locales/es.ts | 25 +- client/src/i18n/locales/fa.ts | 25 +- client/src/i18n/locales/fr.ts | 25 +- client/src/i18n/locales/hi.ts | 25 +- client/src/i18n/locales/hu.ts | 25 +- client/src/i18n/locales/id.ts | 25 +- client/src/i18n/locales/it.ts | 25 +- client/src/i18n/locales/ja.ts | 25 +- client/src/i18n/locales/ko.ts | 25 +- client/src/i18n/locales/pl.ts | 25 +- client/src/i18n/locales/pt-BR.ts | 25 +- client/src/i18n/locales/ru.ts | 25 +- client/src/i18n/locales/th.ts | 25 +- client/src/i18n/locales/tr.ts | 25 +- client/src/i18n/locales/vi.ts | 25 +- client/src/i18n/locales/zh-CN.ts | 25 +- client/src/i18n/locales/zh-TW.ts | 25 +- payload/Makefile | 2 +- payload/include/webui_cheats_store.h | 39 ++ payload/src/webui_api.c | 86 ++++- payload/src/webui_cheats_store.c | 151 ++++++++ payload/tests/webui_cheats_store_selftest.c | 220 ++++++++++++ 56 files changed, 2884 insertions(+), 96 deletions(-) create mode 100644 client/src/console/cheats/FindCheats.tsx create mode 100644 client/src/console/cheats/finder.test.tsx create mode 100644 client/src/console/cheats/finder.ts create mode 100644 client/src/console/cheats/repos.test.ts create mode 100644 client/src/console/cheats/repos.ts create mode 100644 payload/include/webui_cheats_store.h create mode 100644 payload/src/webui_cheats_store.c create mode 100644 payload/tests/webui_cheats_store_selftest.c diff --git a/Makefile b/Makefile index 499f611c..beb704ef 100644 --- a/Makefile +++ b/Makefile @@ -960,6 +960,14 @@ test-payload: payload $(PAYLOAD_DIR)/tests/webui_inputlog_selftest.c $(PAYLOAD_DIR)/src/webui_inputlog.c @/tmp/ps5upload-webui-inputlog-selftest @echo "✓ the controller input log takes only plain short lines and never grows past its 64 KiB ring" + @echo "Running webui cheat download store self-test (host build)..." + @cc -O2 -Wall -Wextra -Werror -pthread -I$(PAYLOAD_DIR)/include -I$(PAYLOAD_DIR)/ava1 -I$(PAYLOAD_DIR)/ava1/gen \ + -o /tmp/ps5upload-webui-cheats-store-selftest \ + $(PAYLOAD_DIR)/tests/webui_cheats_store_selftest.c $(PAYLOAD_DIR)/src/webui_cheats_store.c \ + $(PAYLOAD_DIR)/src/webui_files.c $(PAYLOAD_DIR)/src/webui_codec.c \ + $(PAYLOAD_DIR)/ava1/gen/ava1_gen.c $(PAYLOAD_DIR)/ava1/ava1_wire.c + @/tmp/ps5upload-webui-cheats-store-selftest + @echo "✓ a downloaded cheat is stored only under a plain name with its format's extension, through a same-folder temp + rename" @echo "Running AVA1 in-flight RPC gate self-test (host build)..." @cc -O2 -Wall -Wextra -Werror -pthread -I$(PAYLOAD_DIR)/ava1 \ -o /tmp/ps5upload-ava1-rpc-gate-selftest \ diff --git a/client/src/console/cheats/Cheats.test.tsx b/client/src/console/cheats/Cheats.test.tsx index b934f315..35919dfd 100644 --- a/client/src/console/cheats/Cheats.test.tsx +++ b/client/src/console/cheats/Cheats.test.tsx @@ -82,15 +82,35 @@ describe("cheats section", () => { expect(mockCall.mock.calls.map((x) => x[0])).toEqual(["/api/cheats/status"]); }); - it("no_cheats_points_to_the_pc_app", async () => { + it("no_cheats_offers_find_and_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()); + const html = view(c.get(), { onFind: () => {} }); + expect(html).toContain(t("cw_cheats_none")); + expect(t("cw_cheats_none")).toBe("No cheats for this game yet."); 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(t("cw_cheats_none_next")).toBe("Find cheats for it online, or add them from the PS5Upload app on your PC."); expect(html).toContain(GAME); + expect(html).toContain('data-action="cheats-find"'); + expect(html).toContain(t("cw_cheats_find")); + expect(html).not.toContain(t("cw_cheats_find_more")); + // Without a finder (no game known) there is no Find button. + expect(view(c.get())).not.toContain('data-action="cheats-find"'); + }); + + it("a_list_offers_find_more_and_shows_a_finished_download", async () => { + helper({ game: GAME, mods: [mod()] }); + const c = createCheatsController(() => {}); + await c.loadStatus(); + const html = view(c.get(), { onFind: () => {}, notice: "saved it" }); + expect(html).toContain(t("cw_cheats_find_more")); + expect(html).toContain("saved it"); + // no game: no Find + expect(view({ ...EMPTY_STATE, status: { engine: false, game: null } }, { onFind: () => {} })).not.toContain( + 'data-action="cheats-find"', + ); }); it("lists_the_running_games_cheats_as_switches", async () => { diff --git a/client/src/console/cheats/Cheats.tsx b/client/src/console/cheats/Cheats.tsx index eb871ce8..3e8cb389 100644 --- a/client/src/console/cheats/Cheats.tsx +++ b/client/src/console/cheats/Cheats.tsx @@ -1,9 +1,11 @@ import { useEffect, useRef, useState } from "react"; -import { Gamepad2, RefreshCw } from "lucide-react"; +import { Gamepad2, RefreshCw, Search } from "lucide-react"; import { useAppCover } from "../home/Home"; import { onNavAction } from "../nav"; import { t } from "../strings"; import { pollWhileVisible } from "../ui/visiblePoll"; +import { createFinder, FINDER_CLOSED, type Finder, type FinderState } from "./finder"; +import { FindCheats } from "./FindCheats"; import { cheatsPhase, createCheatsController, @@ -23,6 +25,10 @@ export interface CheatsViewProps { onCoverError?: () => void; onToggle: (mod: CheatMod) => void; onRefresh: () => void; + /** Find cheats online for the running game (absent: no Find button). */ + onFind?: () => void; + /** A finished download, one line (shown until the section changes). */ + notice?: string | null; } /** @@ -66,13 +72,37 @@ function RefreshButton({ onRefresh }: { onRefresh: () => void }) { ); } -/** 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" }) { +function FindButton({ onFind, more }: { onFind: () => void; more?: boolean }) { + return ( + + ); +} + +/** An empty state: what is true, what to do next, and Refresh (Find first when given). */ +function Empty({ + title, + next, + onRefresh, + onFind, + tone, +}: { + title: string; + next?: string; + onRefresh: () => void; + onFind?: () => void; + tone?: "error"; +}) { return (

{title}

{next ?

{next}

: null} - +
+ {onFind ? : null} + +
); } @@ -98,7 +128,7 @@ export function CheatsView(p: CheatsViewProps) { body = ; break; case "none": - body = ; + body = ; break; case "list": body = ( @@ -131,9 +161,17 @@ export function CheatsView(p: CheatsViewProps) { {s.status && !s.status.engine ? t("cw_cheats_engine_off") : t("cw_cheats_how")} - +
+ {phase.kind === "list" && p.onFind ? : null} + +
) : null} + {p.notice ? ( +
+ {p.notice} +
+ ) : null} {s.message ? (
{s.message} @@ -158,6 +196,22 @@ export function Cheats() { const [state, setState] = useState(EMPTY_STATE); const ctl = useRef(null); const root = useRef(null); + const [finderState, setFinderState] = useState(FINDER_CLOSED); + const finder = useRef(null); + const [notice, setNotice] = useState<{ title: string; text: string } | null>(null); + + // The finder: a stored file closes it, and the game's cheats are read again (they show at once). + useEffect(() => { + finder.current = createFinder(setFinderState, (entry, title) => { + setNotice({ title, text: t("cw_cheats_find_saved", { name: entry.filename }) }); + finder.current?.close(); + void ctl.current?.loadList(); + }); + return () => { + finder.current?.close(); + finder.current = 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. @@ -203,6 +257,10 @@ export function Cheats() { ); const titleId = state.status?.game?.title_id ?? null; + // The game went away (or another started): its finder and notice go with it. + useEffect(() => { + if (finderState.kind !== "closed" && finderState.title !== titleId) finder.current?.close(); + }, [titleId, finderState]); const loadedCover = useAppCover(titleId); const [coverFailed, setCoverFailed] = useState(null); const cover = loadedCover && coverFailed !== loadedCover ? loadedCover : null; @@ -215,6 +273,23 @@ export function Cheats() { onCoverError={() => setCoverFailed(loadedCover)} onToggle={(m) => void ctl.current?.toggle(m)} onRefresh={() => void ctl.current?.refresh()} + onFind={ + titleId + ? () => { + setNotice(null); + void finder.current?.open(titleId); + } + : undefined + } + notice={notice && notice.title === titleId ? notice.text : null} + /> + void finder.current?.download(e)} + onRetry={() => { + if (finderState.kind !== "closed") void finder.current?.open(finderState.title); + }} + onClose={() => finder.current?.close()} />
); diff --git a/client/src/console/cheats/FindCheats.tsx b/client/src/console/cheats/FindCheats.tsx new file mode 100644 index 00000000..df63e5b7 --- /dev/null +++ b/client/src/console/cheats/FindCheats.tsx @@ -0,0 +1,152 @@ +import { useEffect } from "react"; +import { Download, RefreshCw } from "lucide-react"; +import { focusElement } from "../nav"; +import { t } from "../strings"; +import { Modal } from "../ui/Modal"; +import type { FinderState } from "./finder"; +import { CHEAT_REPOS, sameVersion, type CheatEntry } from "./repos"; + +const repoName = (id: string) => CHEAT_REPOS.find((r) => r.id === id)?.name ?? id; + +function EntryRow({ + entry, + match, + busy, + disabled, + done, + first, + onDownload, +}: { + entry: CheatEntry; + match: boolean; + busy: boolean; + disabled: boolean; + done: boolean; + first: boolean; + onDownload: (e: CheatEntry) => void; +}) { + let tag: string | null = null; + if (busy) tag = t("cw_cheats_find_downloading"); + else if (done) tag = t("cw_cheats_find_downloaded"); + return ( +
  • + +
  • + ); +} + +/** The finder over the Cheats section: ✕ on a file downloads it to the console; ○ or Close leaves. */ +export function FindCheats({ + state, + onDownload, + onRetry, + onClose, +}: { + state: FinderState; + onDownload: (e: CheatEntry) => void; + onRetry: () => void; + onClose: () => void; +}) { + // The list (or Try again) replaces "Looking…" after the modal took focus: focus moves to it. + const ready = state.kind === "list" ? `list:${state.entries.length}` : state.kind; + useEffect(() => { + if (ready !== "offline" && !ready.startsWith("list:")) return; + const el = document.querySelector(".cw-finder [data-nav-default]"); + if (el) focusElement(el); + }, [ready]); + if (state.kind === "closed") return null; + const title = t("cw_cheats_find_title", { title: state.title }); + let body; + if (state.kind === "loading") { + body = ( +

    + {t("cw_cheats_find_loading")} +

    + ); + } else if (state.kind === "offline") { + body = ( +
    + {t("cw_cheats_find_offline")} +
    + ); + } else if (state.entries.length === 0) { + body = ( + <> +

    {t("cw_cheats_find_none")}

    + {state.partial ?

    {t("cw_cheats_find_partial")}

    : null} + + ); + } else { + const v = state.version; + const noMatch = !!v && !state.entries.some((e) => sameVersion(e.gameVersion, v)); + body = ( + <> +

    {t("cw_cheats_find_hint")}

    +

    + {v ? {t("cw_cheats_find_version", { v })} : null} + {t("cw_cheats_find_how")} +

    + {noMatch ?
    {t("cw_cheats_find_no_match", { v: v ?? "" })}
    : null} + {state.partial ?

    {t("cw_cheats_find_partial")}

    : null} +
      + {state.entries.map((e, i) => ( + + ))} +
    + + ); + } + const message = state.kind === "list" ? state.message : null; + return ( + +

    {title}

    + {body} + {message ? ( +
    + {message.text} +
    + ) : null} +
    + {state.kind === "offline" ? ( + + ) : null} + +
    +
    + ); +} diff --git a/client/src/console/cheats/finder.test.tsx b/client/src/console/cheats/finder.test.tsx new file mode 100644 index 00000000..2acb3e48 --- /dev/null +++ b/client/src/console/cheats/finder.test.tsx @@ -0,0 +1,257 @@ +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(), callRaw: vi.fn() }; +}); + +import { ApiError, call, callRaw } from "../api"; +import { t } from "../strings"; +import { createFinder, downloadErrorText, FINDER_CLOSED, storeRoute, type FinderState } from "./finder"; +import { FindCheats } from "./FindCheats"; +import { CHEAT_REPOS, contentUrls, parseCheatFilename, RepoError, storeFormat, type CheatEntry, type FetchLike, type RepoIndex } from "./repos"; + +const mockCall = vi.mocked(call); +const mockRaw = vi.mocked(callRaw); +const GAME = "CUSA15438"; +const MIX = CHEAT_REPOS[2]; + +const entry = (filename: string, repoId = "etahen", gameTitle = "Game"): CheatEntry => { + const [titleId, gameVersion] = parseCheatFilename(filename); + return { filename, gameTitle, format: storeFormat(filename), repoId, titleId, gameVersion }; +}; + +const INDEX: RepoIndex = { + entries: [ + entry("CUSA15438_01.00.json"), + entry("CUSA15438_01.02.shn", "goldhen"), + entry("CUSA15438_01.03.json", "henmix", ""), + entry("CUSA99999_01.00.json"), + entry("CUSA15438_01.02 copy.json"), // a name the helper would refuse: never listed + ], + failed: [], + offline: false, +}; + +/** The helper: /api/cheats/version answers `version`; /api/cheats/store records what it got. */ +function helper(opts: { version?: string | null; versionFails?: boolean; storeError?: ApiError } = {}) { + const stored: { route: string; body: string }[] = []; + mockCall.mockImplementation(async (route: string) => { + if (route === "/api/cheats/version") { + if (opts.versionFails) throw new ApiError(500, "internal"); + return { version: opts.version ?? null } as never; + } + throw new Error(`unexpected ${route}`); + }); + mockRaw.mockImplementation(async (route: string, body: Blob) => { + if (opts.storeError) throw opts.storeError; + stored.push({ route, body: await body.text() }); + return { ok: true } as never; + }); + return stored; +} + +/** GitHub for the content files: url → body; anything else 404. */ +function github(files: Record, down = false): FetchLike { + return async (url) => { + if (down) throw new TypeError("Load failed"); + return url in files ? new Response(files[url], { status: 200 }) : new Response("404", { status: 404 }); + }; +} + +function finder(opts: { index?: RepoIndex | Error; fetch?: FetchLike } = {}) { + const states: FinderState[] = []; + const stored: [CheatEntry, string][] = []; + const f = createFinder( + (s) => states.push(s), + (e, title) => stored.push([e, title]), + { + index: async () => { + if (opts.index instanceof Error) throw opts.index; + return opts.index ?? INDEX; + }, + fetch: opts.fetch ?? github({}), + }, + ); + return { f, states, stored }; +} + +const render = (state: FinderState) => + renderToStaticMarkup( {}} onRetry={() => {}} onClose={() => {}} />); + +beforeEach(() => { + mockCall.mockReset(); + mockRaw.mockReset(); +}); + +describe("find cheats", () => { + it("lists_the_running_games_files_installed_version_first", async () => { + helper({ version: "01.02" }); + const { f } = finder(); + await f.open(GAME); + const s = f.get(); + expect(s.kind).toBe("list"); + if (s.kind !== "list") return; + expect(s.version).toBe("01.02"); + expect(s.entries.map((e) => e.filename)).toEqual(["CUSA15438_01.02.shn", "CUSA15438_01.03.json", "CUSA15438_01.00.json"]); + expect(mockCall).toHaveBeenCalledWith("/api/cheats/version", { title_id: GAME }); + const html = render(s); + expect(html).toContain(t("cw_cheats_find_title", { title: GAME })); + expect(html).toContain(t("cw_cheats_find_your_version")); + expect(html).toContain("is-match"); + expect(html).toContain("GoldHEN/GoldHEN_Cheat_Repository"); + expect(html).toContain("SHN"); + expect(html).toContain(t("cw_cheats_find_version", { v: "01.02" })); + expect(html).not.toContain("CUSA99999"); + expect(html).not.toContain("copy.json"); + // the best file is where focus starts + expect(html.indexOf("data-nav-default")).toBeLessThan(html.indexOf("CUSA15438_01.03.json")); + }); + + it("an_unknown_version_lists_all_newest_first", async () => { + helper({ versionFails: true }); + const { f } = finder(); + await f.open(GAME); + const s = f.get(); + if (s.kind !== "list") throw new Error(s.kind); + expect(s.version).toBeNull(); + expect(s.entries.map((e) => e.gameVersion)).toEqual(["01.03", "01.02", "01.00"]); + const html = render(s); + expect(html).not.toContain(t("cw_cheats_find_your_version")); + expect(html).not.toContain(t("cw_cheats_find_no_match", { v: "" }).slice(0, 10)); + }); + + it("no_file_for_the_installed_version_warns", async () => { + helper({ version: "02.00" }); + const { f } = finder(); + await f.open(GAME); + expect(render(f.get())).toContain(t("cw_cheats_find_no_match", { v: "02.00" })); + }); + + it("nobody_published_any", async () => { + helper(); + const { f } = finder({ index: { entries: [], failed: ["henmix"], offline: false } }); + await f.open(GAME); + const html = render(f.get()); + expect(html).toContain(t("cw_cheats_find_none")); + expect(html).toContain(t("cw_cheats_find_partial")); + }); + + it("github_unreachable_says_what_to_do", async () => { + helper(); + const { f } = finder({ index: { entries: [], failed: ["etahen", "goldhen", "henmix"], offline: true } }); + await f.open(GAME); + expect(f.get()).toEqual({ kind: "offline", title: GAME }); + const html = render(f.get()); + expect(t("cw_cheats_find_offline")).toBe( + "This PS5 can't reach GitHub. Get cheats in the PS5Upload app on your PC, or allow raw.githubusercontent.com in your DNS settings.", + ); + expect(html).toContain("raw.githubusercontent.com"); + expect(html).toContain(t("cw_cheats_find_retry")); + }); + + it("download_fetches_in_the_browser_and_stores_on_the_console", async () => { + const stored = helper({ version: "01.03" }); + const e = INDEX.entries[2]; // henmix, nested under cheats/ + const urls = contentUrls(MIX, e.filename); + const { f, stored: done } = finder({ fetch: github({ [urls[0]]: '{"name":"Game","mods":[]}' }) }); + await f.open(GAME); + await f.download(e); + expect(stored).toEqual([ + { route: "/api/cheats/store?format=json&name=CUSA15438_01.03.json", body: '{"name":"Game","mods":[]}' }, + ]); + expect(done).toEqual([[e, GAME]]); + const s = f.get(); + if (s.kind !== "list") throw new Error(s.kind); + expect(s.busy).toBeNull(); + expect(s.done).toEqual([e.filename]); + expect(s.message).toEqual({ tone: "info", text: t("cw_cheats_find_saved", { name: e.filename }) }); + }); + + it("the_store_route_names_the_format_by_extension", () => { + expect(storeRoute(entry("CUSA15438_01.02.shn"))).toBe("/api/cheats/store?format=shn&name=CUSA15438_01.02.shn"); + expect(storeRoute(entry("CUSA15438_01.02.MC4"))).toBe("/api/cheats/store?format=mc4&name=CUSA15438_01.02.MC4"); + }); + + it("one_download_at_a_time", async () => { + helper(); + let release: () => void = () => {}; + const gate = new Promise((r) => (release = r)); + const fetchSlow: FetchLike = async () => { + await gate; + return new Response("x", { status: 200 }); + }; + const { f } = finder({ fetch: fetchSlow }); + await f.open(GAME); + const a = f.download(INDEX.entries[0]); + const s = f.get(); + expect(s.kind === "list" && s.busy).toBe(INDEX.entries[0].filename); + expect(render(s)).toContain(t("cw_cheats_find_downloading")); + await f.download(INDEX.entries[1]); // ignored + release(); + await a; + expect(mockRaw).toHaveBeenCalledTimes(1); + }); + + it("failures_are_one_line_and_keep_the_list", async () => { + helper({ storeError: new ApiError(500, "io_error") }); + const e = INDEX.entries[0]; + const { f, stored } = finder({ fetch: github({ [contentUrls(CHEAT_REPOS[0], e.filename)[0]]: "{}" }) }); + await f.open(GAME); + await f.download(e); + const s = f.get(); + if (s.kind !== "list") throw new Error(s.kind); + expect(s.message).toEqual({ tone: "error", text: t("cw_cheats_find_err_store", { name: e.filename, code: "io_error" }) }); + expect(s.entries).toHaveLength(3); + expect(stored).toEqual([]); + expect(render(s)).toContain('role="alert"'); + }); + + it("download_errors_read_well", () => { + expect(downloadErrorText(new RepoError(true, "unreachable"), "a.json")).toBe(t("cw_cheats_find_offline")); + expect(downloadErrorText(new RepoError(false, "too_large"), "a.json")).toBe(t("cw_cheats_find_too_large", { name: "a.json" })); + expect(downloadErrorText(new RepoError(false, "not_found"), "a.json")).toBe( + t("cw_cheats_find_err_download", { name: "a.json", code: "not_found" }), + ); + expect(downloadErrorText(new ApiError(401, "token"), "a.json")).toBeNull(); + expect(downloadErrorText(new ApiError(0, "offline"), "a.json")).toBe(t("cw_home_err_offline")); + expect(downloadErrorText(new ApiError(413, "too_large"), "a.json")).toBe(t("cw_cheats_find_too_large", { name: "a.json" })); + }); + + it("github_down_mid_download_says_unreachable", async () => { + helper(); + const { f } = finder({ fetch: github({}, true) }); + await f.open(GAME); + await f.download(INDEX.entries[0]); + const s = f.get(); + expect(s.kind === "list" && s.message?.text).toBe(t("cw_cheats_find_offline")); + expect(mockRaw).not.toHaveBeenCalled(); + }); + + it("closing_drops_a_late_answer", async () => { + helper(); + let release: (i: RepoIndex) => void = () => {}; + const states: FinderState[] = []; + const f = createFinder( + (s) => states.push(s), + () => {}, + { index: () => new Promise((r) => (release = r)) }, + ); + const p = f.open(GAME); + expect(f.get()).toEqual({ kind: "loading", title: GAME }); + expect(render(f.get())).toContain(t("cw_cheats_find_loading")); + f.close(); + release(INDEX); + await p; + expect(f.get()).toEqual(FINDER_CLOSED); + expect(render(f.get())).toBe(""); + }); + + it("an_index_read_that_throws_reads_as_offline", async () => { + helper(); + const { f } = finder({ index: new Error("boom") }); + await f.open(GAME); + expect(f.get().kind).toBe("offline"); + }); +}); diff --git a/client/src/console/cheats/finder.ts b/client/src/console/cheats/finder.ts new file mode 100644 index 00000000..ce346260 --- /dev/null +++ b/client/src/console/cheats/finder.ts @@ -0,0 +1,168 @@ +/** + * Find cheats: the running game's cheat files in the community repos (read by the PS5 browser, + * repos.ts), its installed version from the helper (/api/cheats/version), and a download that + * fetches the file in the browser and hands it to the helper (/api/cheats/store), which puts it + * where the cheat engine looks. Kept out of React so tests drive it directly. + */ +import { ApiError, call, callRaw, isOffline } from "../api"; +import { t } from "../strings"; +import { + downloadCheat, + entriesFor, + rankEntries, + RepoError, + repoIndex, + storeFormat, + type CheatEntry, + type FetchLike, + type RepoIndex, +} from "./repos"; + +export type FinderMessage = { tone: "error" | "info"; text: string }; + +export type FinderState = + | { kind: "closed" } + | { kind: "loading"; title: string } + | { kind: "offline"; title: string } + | { + kind: "list"; + title: string; + /** The installed version, or null when the helper could not say (every version is listed). */ + version: string | null; + /** Best first (rankEntries). */ + entries: CheatEntry[]; + /** Some repos could not be read. */ + partial: boolean; + /** The file being downloaded, or null. */ + busy: string | null; + /** Files downloaded while the finder was open. */ + done: string[]; + message: FinderMessage | null; + }; + +export const FINDER_CLOSED: FinderState = { kind: "closed" }; + +export interface FinderDeps { + index?: () => Promise; + fetch?: FetchLike; +} + +export interface Finder { + /** Look for `titleId`'s cheats (the repos are read once per page session). */ + open(titleId: string): Promise; + close(): void; + /** Download `entry` and store it on the console; ignored while another one runs. */ + download(entry: CheatEntry): Promise; + get(): FinderState; +} + +/** The game's installed version from the helper; null when it does not know (or cannot be asked). */ +export async function readVersion(titleId: string): Promise { + try { + const r = await call<{ version?: unknown }>("/api/cheats/version", { title_id: titleId }); + return r && typeof r.version === "string" && r.version ? r.version : null; + } catch { + return null; + } +} + +/** A failed download or store as one line; null for a 401 (the reload banner says it). */ +export function downloadErrorText(err: unknown, name: string): string | null { + if (err instanceof RepoError) { + if (err.network) return t("cw_cheats_find_offline"); + if (err.message === "too_large") return t("cw_cheats_find_too_large", { name }); + return t("cw_cheats_find_err_download", { name, code: err.message }); + } + if (err instanceof ApiError) { + if (err.status === 401) return null; + if (isOffline(err)) return t("cw_home_err_offline"); + if (err.code === "too_large") return t("cw_cheats_find_too_large", { name }); + return t("cw_cheats_find_err_store", { name, code: err.code }); + } + return t("cw_cheats_find_err_download", { name, code: err instanceof Error ? err.message : String(err) }); +} + +/** The helper route that stores `entry` (its name passed as is: only [A-Za-z0-9._-] are listed). */ +export function storeRoute(entry: CheatEntry): string { + return `/api/cheats/store?format=${storeFormat(entry.filename)}&name=${encodeURIComponent(entry.filename)}`; +} + +/** + * The finder's state machine. `onStored` runs after a file is on the console (the section reads + * the game's cheats again and closes the finder). + */ +export function createFinder( + onChange: (s: FinderState) => void, + onStored: (entry: CheatEntry, title: string) => void, + deps: FinderDeps = {}, +): Finder { + let s: FinderState = FINDER_CLOSED; + /** Every open gets a number: an older one's answer never lands on a newer finder. */ + let seq = 0; + const set = (next: FinderState) => { + s = next; + onChange(s); + }; + const index = deps.index ?? (() => repoIndex(deps.fetch)); + + return { + async open(titleId) { + const mine = ++seq; + set({ kind: "loading", title: titleId }); + let idx: RepoIndex; + let version: string | null; + try { + [idx, version] = await Promise.all([index(), readVersion(titleId)]); + } catch { + if (mine === seq) set({ kind: "offline", title: titleId }); + return; + } + if (mine !== seq) return; + if (idx.offline) { + set({ kind: "offline", title: titleId }); + return; + } + set({ + kind: "list", + title: titleId, + version, + entries: rankEntries(entriesFor(idx, titleId), version), + partial: idx.failed.length > 0, + busy: null, + done: [], + message: null, + }); + }, + close() { + seq++; + set(FINDER_CLOSED); + }, + async download(entry) { + if (s.kind !== "list" || s.busy !== null) return; + const mine = seq; + const title = s.title; + set({ ...s, busy: entry.filename, message: null }); + const still = () => mine === seq && s.kind === "list"; + try { + const bytes = await downloadCheat(entry, deps.fetch); + await callRaw(storeRoute(entry), new Blob([bytes], { type: "application/octet-stream" })); + } catch (e) { + if (still() && s.kind === "list") { + const text = downloadErrorText(e, entry.filename); + set({ ...s, busy: null, message: text ? { tone: "error", text } : null }); + } + return; + } + if (still() && s.kind === "list") { + set({ + ...s, + busy: null, + done: s.done.includes(entry.filename) ? s.done : [...s.done, entry.filename], + message: { tone: "info", text: t("cw_cheats_find_saved", { name: entry.filename }) }, + }); + } + onStored(entry, title); + }, + get: () => s, + }; +} diff --git a/client/src/console/cheats/repos.test.ts b/client/src/console/cheats/repos.test.ts new file mode 100644 index 00000000..545a1e28 --- /dev/null +++ b/client/src/console/cheats/repos.test.ts @@ -0,0 +1,284 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + CHEAT_FILE_MAX, + CHEAT_REPOS, + contentUrls, + downloadCheat, + entriesFor, + formatFromIndex, + parseCheatFilename, + parseIndexLine, + rankEntries, + readRepoIndex, + RepoError, + repoIndex, + resetRepoIndex, + sameVersion, + storableName, + storeFormat, + treeEntryFromPath, + type CheatEntry, + type FetchLike, +} from "./repos"; + +/** A fake network: url → body (string), status number, or "down" (the request throws). */ +function net(routes: Record) { + const calls: string[] = []; + const f: FetchLike = async (url) => { + calls.push(url); + const r = routes[url]; + if (r === undefined || r === "down") throw new TypeError("Load failed"); + if (typeof r === "number") return new Response("nope", { status: r }); + return new Response(r, { status: 200 }); + }; + return { f, calls }; +} + +const ETA = CHEAT_REPOS[0]; +const GOLD = CHEAT_REPOS[1]; +const MIX = CHEAT_REPOS[2]; + +describe("cheat file names (cheats.rs parse_cheat_filename)", () => { + it("reads the title and version from a published name", () => { + expect(parseCheatFilename("CUSA15438_01.00.json")).toEqual(["CUSA15438", "01.00"]); + expect(parseCheatFilename("CUSA09193_01.05_2.json")).toEqual(["CUSA09193", "01.05"]); + expect(parseCheatFilename("PPSA01650_01.000.016.shn")).toEqual(["PPSA01650", "01.000.016"]); + expect(parseCheatFilename("cusa34394_05.01.mc4")).toEqual(["CUSA34394", "05.01"]); + }); + it("keeps the title when the version is missing or not a version", () => { + expect(parseCheatFilename("CUSA15438.json")).toEqual(["CUSA15438", ""]); + expect(parseCheatFilename("CUSA15438_2.json")).toEqual(["CUSA15438", ""]); + expect(parseCheatFilename("CUSA15438_v1.00.json")).toEqual(["CUSA15438", ""]); + expect(parseCheatFilename("CUSA15438_c20ae0e8.json")).toEqual(["CUSA15438", ""]); + }); + it("gives nothing for a name that breaks the convention", () => { + expect(parseCheatFilename("README.md")).toEqual(["", ""]); + expect(parseCheatFilename("CUSA1543_01.00.json")).toEqual(["", ""]); + expect(parseCheatFilename("12345CUSA_01.00.json")).toEqual(["", ""]); + expect(parseCheatFilename("")).toEqual(["", ""]); + }); +}); + +describe("index files (cheats.rs parse_index_line / format_from_index)", () => { + it("splits filename=title and skips blanks and comments", () => { + expect(parseIndexLine("CUSA15438_01.00.json=Game name")).toEqual(["CUSA15438_01.00.json", "Game name"]); + expect(parseIndexLine(" CUSA1_1.0.shn = A = B ")).toEqual(["CUSA1_1.0.shn", "A = B"]); + expect(parseIndexLine("x.json=")).toEqual(["x.json", ""]); + expect(parseIndexLine("")).toBeNull(); + expect(parseIndexLine(" ")).toBeNull(); + expect(parseIndexLine("# comment=x")).toBeNull(); + expect(parseIndexLine("no equals")).toBeNull(); + expect(parseIndexLine("=title only")).toBeNull(); + }); + it("names the format by the index file", () => { + expect(formatFromIndex("json.txt")).toBe("json"); + expect(formatFromIndex("shn.txt")).toBe("shn"); + expect(formatFromIndex("mc4.txt")).toBe("mc4"); + expect(formatFromIndex("other.txt")).toBe("json"); + }); +}); + +describe("git tree paths (cheats.rs tree_entry_from_path)", () => { + it("accepts / with a matching extension", () => { + expect(treeEntryFromPath("cheats/json/CUSA00002_01.00_c20ae0e8.json", "cheats/")).toEqual(["CUSA00002_01.00_c20ae0e8.json", "json"]); + expect(treeEntryFromPath("cheats/shn/CUSA123.shn", "cheats/")).toEqual(["CUSA123.shn", "shn"]); + expect(treeEntryFromPath("cheats/mc4/CUSA123.MC4", "cheats/")).toEqual(["CUSA123.MC4", "mc4"]); + expect(treeEntryFromPath("json/CUSA1.json", "")).toEqual(["CUSA1.json", "json"]); + }); + it("refuses anything else", () => { + expect(treeEntryFromPath("docs/json/a.json", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/misc/a.json", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/json/sub/a.json", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/json/README.md", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/json/noext", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/json/", "cheats/")).toBeNull(); + expect(treeEntryFromPath("cheats/json", "cheats/")).toBeNull(); + }); +}); + +describe("content URLs (cheats.rs cheats_repo_download)", () => { + it("tries json, shn, mc4, misc under the content base, in that order", () => { + expect(contentUrls(MIX, "CUSA1_1.00.shn")).toEqual([ + "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/json/CUSA1_1.00.shn", + "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/shn/CUSA1_1.00.shn", + "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/mc4/CUSA1_1.00.shn", + "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/misc/CUSA1_1.00.shn", + ]); + expect(contentUrls(ETA, "a.json")[0]).toBe("https://raw.githubusercontent.com/etaHEN/PS5_Cheats/main/json/a.json"); + }); + it("matches the engine's repo list", () => { + expect(CHEAT_REPOS.map((r) => r.id)).toEqual(["etahen", "goldhen", "henmix"]); + for (const r of CHEAT_REPOS) { + expect(r.indexFiles.length > 0 || r.treeApi !== "").toBe(true); + expect(r.contentBase.endsWith("/")).toBe(true); + for (const u of [r.rawBase, r.contentBase, r.treeApi].filter(Boolean)) { + expect(u.startsWith("https://raw.githubusercontent.com/") || u.startsWith("https://api.github.com/")).toBe(true); + } + } + }); + it("stores by extension, only names the helper accepts", () => { + expect(storeFormat("a.SHN")).toBe("shn"); + expect(storeFormat("a.mc4")).toBe("mc4"); + expect(storeFormat("a.json")).toBe("json"); + expect(storableName("CUSA15438_01.00.json")).toBe(true); + expect(storableName("CUSA15438_01.00 (1).json")).toBe(false); + expect(storableName("../a.json")).toBe(false); + expect(storableName(".json")).toBe(false); + expect(storableName("a..json")).toBe(false); + expect(storableName("a.txt")).toBe(false); + expect(storableName(`${"a".repeat(123)}.json`)).toBe(true); + expect(storableName(`${"a".repeat(124)}.json`)).toBe(false); + }); +}); + +describe("versions", () => { + it("compares by number", () => { + expect(sameVersion("01.000.016", "1.0.16")).toBe(true); + expect(sameVersion("01.05", "1.5")).toBe(true); + expect(sameVersion("01.05", "01.10")).toBe(false); + expect(sameVersion("", "01.00")).toBe(false); + expect(sameVersion("01.00", null)).toBe(false); + }); + it("ranks the installed version first, then newest", () => { + const e = (filename: string): CheatEntry => { + const [titleId, gameVersion] = parseCheatFilename(filename); + return { filename, gameTitle: "", format: "json", repoId: "etahen", titleId, gameVersion }; + }; + const list = [e("CUSA00001_01.00.json"), e("CUSA00001_01.10.json"), e("CUSA00001_01.05.json"), e("CUSA00001.json"), e("CUSA00001_01.05_2.json")]; + expect(rankEntries(list, "01.05").map((x) => x.filename)).toEqual([ + "CUSA00001_01.05.json", + "CUSA00001_01.05_2.json", + "CUSA00001_01.10.json", + "CUSA00001_01.00.json", + "CUSA00001.json", + ]); + expect(rankEntries(list, null).map((x) => x.filename)).toEqual([ + "CUSA00001_01.10.json", + "CUSA00001_01.05.json", + "CUSA00001_01.05_2.json", + "CUSA00001_01.00.json", + "CUSA00001.json", + ]); + }); +}); + +const tree = (paths: string[]) => JSON.stringify({ tree: paths.map((path) => ({ type: "blob", path })).concat([{ type: "tree", path: "cheats/json" } as never]) }); + +describe("reading the repos", () => { + beforeEach(() => resetRepoIndex()); + + it("lists every repo, first repo wins a file name", async () => { + const { f } = net({ + [`${ETA.rawBase}json.txt`]: "CUSA15438_01.00.json=Game A\nCUSA15438_01.02.json=Game A\n", + [`${ETA.rawBase}shn.txt`]: "# none\n", + [`${ETA.rawBase}mc4.txt`]: 404, + [`${GOLD.rawBase}json.txt`]: "CUSA15438_01.00.json=Game A (gold)\r\nCUSA99999_01.00.json=Other\r\n", + [`${GOLD.rawBase}shn.txt`]: "CUSA15438_01.02.shn=Game A\n", + [`${GOLD.rawBase}mc4.txt`]: "CUSA15438_01.02.mc4=Game A\n", + [MIX.treeApi]: tree(["cheats/json/CUSA15438_01.03.json", "cheats/README.md", "cheats/json/CUSA15438_01.00.json"]), + }); + const idx = await readRepoIndex(f); + expect(idx.offline).toBe(false); + expect(idx.failed).toEqual([]); + const mine = entriesFor(idx, "cusa15438"); + expect(mine.map((e) => `${e.repoId}:${e.filename}:${e.format}`).sort()).toEqual( + [ + "etahen:CUSA15438_01.00.json:json", + "etahen:CUSA15438_01.02.json:json", + "goldhen:CUSA15438_01.02.shn:shn", + "goldhen:CUSA15438_01.02.mc4:mc4", + "henmix:CUSA15438_01.03.json:json", + ].sort(), + ); + expect(mine.find((e) => e.filename === "CUSA15438_01.00.json")?.gameTitle).toBe("Game A"); + expect(entriesFor(idx, "CUSA99999")).toHaveLength(1); + }); + + it("a rate-limited tree is that source unavailable, not offline", async () => { + const { f } = net({ + [`${ETA.rawBase}json.txt`]: "CUSA1_01.00.json=A\n", + [`${ETA.rawBase}shn.txt`]: 404, + [`${ETA.rawBase}mc4.txt`]: 404, + [`${GOLD.rawBase}json.txt`]: "down", + [`${GOLD.rawBase}shn.txt`]: "down", + [`${GOLD.rawBase}mc4.txt`]: "down", + [MIX.treeApi]: 403, + }); + const idx = await readRepoIndex(f); + expect(idx.offline).toBe(false); + expect(idx.failed.sort()).toEqual(["goldhen", "henmix"]); + expect(idx.entries.map((e) => e.filename)).toEqual(["CUSA1_01.00.json"]); + }); + + it("nothing answering is offline (no internet, or GitHub blocked by DNS)", async () => { + const { f } = net({}); + const idx = await readRepoIndex(f); + expect(idx.offline).toBe(true); + expect(idx.failed).toEqual(["etahen", "goldhen", "henmix"]); + }); + + it("a garbled tree reply is that source failing", async () => { + const { f } = net({ [`${ETA.rawBase}json.txt`]: "CUSA1_01.00.json=A\n", [MIX.treeApi]: "" }); + const idx = await readRepoIndex(f); + expect(idx.failed).toContain("henmix"); + expect(idx.offline).toBe(false); + }); + + it("is read once per session, again after a failed source", async () => { + const ok = net({ + [`${ETA.rawBase}json.txt`]: "CUSA1_01.00.json=A\n", + [`${ETA.rawBase}shn.txt`]: "", + [`${ETA.rawBase}mc4.txt`]: "", + [`${GOLD.rawBase}json.txt`]: "", + [`${GOLD.rawBase}shn.txt`]: "", + [`${GOLD.rawBase}mc4.txt`]: "", + [MIX.treeApi]: tree([]), + }); + await repoIndex(ok.f); + const n = ok.calls.length; + await repoIndex(ok.f); + expect(ok.calls.length).toBe(n); + + resetRepoIndex(); + const bad = net({ [`${ETA.rawBase}json.txt`]: "CUSA1_01.00.json=A\n" }); + await repoIndex(bad.f); + await new Promise((r) => setTimeout(r, 0)); + const m = bad.calls.length; + await repoIndex(bad.f); + expect(bad.calls.length).toBe(2 * m); + }); +}); + +describe("downloading a file", () => { + const entry: CheatEntry = { + filename: "CUSA1_01.00.shn", + gameTitle: "", + format: "shn", + repoId: "henmix", + titleId: "CUSA1", + gameVersion: "01.00", + }; + it("takes the first content URL that answers", async () => { + const urls = contentUrls(MIX, entry.filename); + const { f, calls } = net({ [urls[0]]: 404, [urls[1]]: "" }); + const buf = await downloadCheat(entry, f); + expect(new TextDecoder().decode(buf)).toBe(""); + expect(calls).toEqual(urls.slice(0, 2)); + }); + it("says not found when every URL answered an error", async () => { + const urls = contentUrls(MIX, entry.filename); + const { f } = net(Object.fromEntries(urls.map((u) => [u, 404]))); + await expect(downloadCheat(entry, f)).rejects.toMatchObject({ network: false, message: "not_found" }); + }); + it("says unreachable when nothing answered", async () => { + const { f } = net({}); + const err = await downloadCheat(entry, f).catch((e: unknown) => e); + expect(err).toBeInstanceOf(RepoError); + expect((err as RepoError).network).toBe(true); + }); + it("refuses a file over the helper's limit", async () => { + const urls = contentUrls(MIX, entry.filename); + const { f } = net({ [urls[0]]: new ArrayBuffer(CHEAT_FILE_MAX + 1) }); + await expect(downloadCheat(entry, f)).rejects.toMatchObject({ message: "too_large" }); + }); +}); diff --git a/client/src/console/cheats/repos.ts b/client/src/console/cheats/repos.ts new file mode 100644 index 00000000..5a1afd69 --- /dev/null +++ b/client/src/console/cheats/repos.ts @@ -0,0 +1,375 @@ +/** + * The community cheat repos, read by the PS5 browser itself (the console page has no PC engine to + * ask). A port of the PC engine's engine/crates/ps5upload-core/src/cheats.rs (cheat_repos, + * parse_cheat_filename, parse_index_line, format_from_index, tree_entry_from_path, + * cheats_repo_search's de-duplication, cheats_repo_download's content URLs): keep the two in step. + * + * Both hosts answer CORS-open (Access-Control-Allow-Origin: *), and the helper's CSP allows exactly + * these two in connect-src (payload/src/webui_api.c PAGE_HEADERS). + */ + +export type CheatFormat = "json" | "shn" | "mc4"; + +export interface CheatRepo { + id: string; + name: string; + /** Where the index files live (`json.txt`, ...). */ + rawBase: string; + /** Index files in the repo root; empty when the repo is listed through `treeApi`. */ + indexFiles: string[]; + /** Where the `json/`, `shn/`, `mc4/` folders live (not always the root). */ + contentBase: string; + /** GitHub git-trees API URL for a repo that publishes no index ("" = use indexFiles). */ + treeApi: string; + /** The folder inside the tree holding the format folders (`cheats/`). */ + treePrefix: string; +} + +/** cheats.rs cheat_repos(), in the same order (the first repo listing a file name wins). */ +export const CHEAT_REPOS: readonly CheatRepo[] = [ + { + id: "etahen", + name: "etaHEN/PS5_Cheats", + rawBase: "https://raw.githubusercontent.com/etaHEN/PS5_Cheats/main/", + indexFiles: ["json.txt", "shn.txt", "mc4.txt"], + contentBase: "https://raw.githubusercontent.com/etaHEN/PS5_Cheats/main/", + treeApi: "", + treePrefix: "", + }, + { + id: "goldhen", + name: "GoldHEN/GoldHEN_Cheat_Repository", + rawBase: "https://raw.githubusercontent.com/GoldHEN/GoldHEN_Cheat_Repository/main/", + indexFiles: ["json.txt", "shn.txt", "mc4.txt"], + contentBase: "https://raw.githubusercontent.com/GoldHEN/GoldHEN_Cheat_Repository/main/", + treeApi: "", + treePrefix: "", + }, + { + // No `*.txt` index, `master`, cheats nested under `cheats/`: listed through the git-trees API + // (60 requests an hour without a login, so a failure is "this source is unavailable"). + id: "henmix", + name: "TeeKay87/HEN-Cheats-Collection", + rawBase: "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/", + indexFiles: [], + contentBase: "https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/", + treeApi: "https://api.github.com/repos/TeeKay87/HEN-Cheats-Collection/git/trees/master?recursive=1", + treePrefix: "cheats/", + }, +]; + +export interface CheatEntry { + filename: string; + /** The game's name from the index ("" for a tree-listed repo). */ + gameTitle: string; + format: CheatFormat; + repoId: string; + /** From the file name; "" when it breaks the convention. */ + titleId: string; + /** The game version the cheat was made for; "" when the name does not say. */ + gameVersion: string; +} + +/** + * cheats.rs parse_cheat_filename: `_[_].` → [title id, version]. + * A title id is four letters then five digits; the version is the next segment when it is digits + * and dots with a dot (`_2` after it is a variant counter). Empty strings when it does not fit. + */ +export function parseCheatFilename(filename: string): [string, string] { + const dot = filename.lastIndexOf("."); + const stem = dot >= 0 ? filename.slice(0, dot) : filename; + const parts = stem.split("_"); + const title = parts[0] ?? ""; + if (!/^[A-Za-z]{4}[0-9]{5}$/.test(title)) return ["", ""]; + const v = parts[1]; + const version = v !== undefined && v !== "" && /^[0-9.]+$/.test(v) && v.includes(".") ? v : ""; + return [title.toUpperCase(), version]; +} + +/** cheats.rs parse_index_line: `filename=game title`; blank and `#` lines skipped. */ +export function parseIndexLine(line: string): [string, string] | null { + const l = line.trim(); + if (!l || l.startsWith("#")) return null; + const eq = l.indexOf("="); + if (eq < 0) return null; + const filename = l.slice(0, eq).trim(); + if (!filename) return null; + return [filename, l.slice(eq + 1).trim()]; +} + +/** cheats.rs format_from_index. */ +export function formatFromIndex(indexName: string): CheatFormat { + if (indexName === "shn.txt") return "shn"; + if (indexName === "mc4.txt") return "mc4"; + return "json"; +} + +/** + * cheats.rs tree_entry_from_path: exactly `/`, the format a known folder and + * the file's extension agreeing with it (any case); anything else (nested, README, unknown) null. + */ +export function treeEntryFromPath(path: string, prefix: string): [string, CheatFormat] | null { + if (!path.startsWith(prefix)) return null; + const rest = path.slice(prefix.length); + const slash = rest.indexOf("/"); + if (slash < 0) return null; + const dir = rest.slice(0, slash); + const filename = rest.slice(slash + 1); + if (!filename || filename.includes("/")) return null; + if (dir !== "json" && dir !== "shn" && dir !== "mc4") return null; + const dot = filename.lastIndexOf("."); + if (dot < 0) return null; + if (filename.slice(dot + 1).toLowerCase() !== dir) return null; + return [filename, dir]; +} + +/** + * cheats_repo_download's URLs, tried in this order until one answers: `/` + * for json, shn, mc4, misc (the same order as the engine, whatever the entry's own format). + */ +export function contentUrls(repo: CheatRepo, filename: string): string[] { + const base = repo.contentBase || repo.rawBase; + return ["json", "shn", "mc4", "misc"].map((d) => `${base}${d}/${filename}`); +} + +/** The format the file is stored as: by its extension (cheats_repo_download), json by default. */ +export function storeFormat(filename: string): CheatFormat { + const f = filename.toLowerCase(); + if (f.endsWith(".shn")) return "shn"; + if (f.endsWith(".mc4")) return "mc4"; + return "json"; +} + +/** The helper's rule for a stored name (webui_cheat_name_ok): only these can be downloaded here. */ +export function storableName(filename: string): boolean { + if (filename.length < 1 || filename.length > 128 || filename.startsWith(".") || filename.includes("..")) return false; + if (!/^[A-Za-z0-9._-]+$/.test(filename)) return false; + const fmt = storeFormat(filename); + return filename.toLowerCase().endsWith(`.${fmt}`) && filename.length > fmt.length + 1; +} + +// ---- versions (client/src/lib/cheatGames.ts, kept local: the console bundle stays small) ---- + +/** Version numbers: `01.000.016` equals `1.0.16`; non-numeric parts count as 0. */ +export function versionParts(v: string | null | undefined): number[] { + return (v ?? "") + .trim() + .split(".") + .filter((p) => p.length > 0) + .map((p) => { + const n = parseInt(p, 10); + return Number.isFinite(n) ? n : 0; + }); +} + +export function compareVersions(a: string, b: string): number { + const x = versionParts(a); + const y = versionParts(b); + for (let i = 0; i < Math.max(x.length, y.length); i++) { + const d = (x[i] ?? 0) - (y[i] ?? 0); + if (d !== 0) return d; + } + return 0; +} + +export function sameVersion(a: string | null | undefined, b: string | null | undefined): boolean { + if (!a || !b) return false; + if (versionParts(a).length === 0 || versionParts(b).length === 0) return false; + return compareVersions(a, b) === 0; +} + +/** A game's cheat files, best first: made for `installed` first, then newest version first. */ +export function rankEntries(entries: CheatEntry[], installed: string | null | undefined): CheatEntry[] { + return [...entries].sort((a, b) => { + const am = sameVersion(a.gameVersion, installed) ? 0 : 1; + const bm = sameVersion(b.gameVersion, installed) ? 0 : 1; + if (am !== bm) return am - bm; + const v = compareVersions(b.gameVersion, a.gameVersion); + if (v !== 0) return v; + return a.filename < b.filename ? -1 : a.filename > b.filename ? 1 : 0; + }); +} + +// ---- reading the repos ---- + +/** One source's files as (filename, game title, format). */ +type Listing = [string, string, CheatFormat][]; + +export type FetchLike = (url: string, init?: RequestInit) => Promise; + +/** A source this long without an answer counts as unreachable. */ +export const REPO_TIMEOUT_MS = 20_000; + +export class RepoError extends Error { + /** true: nothing answered (no internet, DNS blocked, timed out); false: an HTTP error or bad data. */ + readonly network: boolean; + constructor(network: boolean, message: string) { + super(message); + this.name = "RepoError"; + this.network = network; + } +} + +async function fetchWithTimeout(f: FetchLike, url: string, timeoutMs: number): Promise { + const stop = typeof AbortController === "function" ? new AbortController() : null; + const timer = setTimeout(() => stop?.abort(), timeoutMs); + try { + return await f(url, { method: "GET", cache: "no-store", credentials: "omit", signal: stop?.signal }); + } catch { + throw new RepoError(true, `unreachable ${url}`); + } finally { + clearTimeout(timer); + } +} + +async function fetchText(f: FetchLike, url: string, timeoutMs: number): Promise { + const res = await fetchWithTimeout(f, url, timeoutMs); + if (!res.ok) throw new RepoError(false, `http_${res.status}`); + try { + return await res.text(); + } catch { + throw new RepoError(true, `cut off ${url}`); + } +} + +/** One repo's listing: its index files (any that answers counts), or its git tree. */ +async function listRepo(repo: CheatRepo, f: FetchLike, timeoutMs: number): Promise { + if (repo.indexFiles.length === 0) { + const text = await fetchText(f, repo.treeApi, timeoutMs); + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + throw new RepoError(false, "bad_tree"); + } + const nodes = (parsed as { tree?: unknown } | null)?.tree; + if (!Array.isArray(nodes)) throw new RepoError(false, "bad_tree"); + const out: Listing = []; + for (const node of nodes as { type?: unknown; path?: unknown }[]) { + if (!node || node.type !== "blob" || typeof node.path !== "string") continue; + const e = treeEntryFromPath(node.path, repo.treePrefix); + if (e) out.push([e[0], "", e[1]]); + } + return out; + } + const results = await Promise.allSettled( + repo.indexFiles.map(async (idx) => { + const text = await fetchText(f, `${repo.rawBase}${idx}`, timeoutMs); + const fmt = formatFromIndex(idx); + const out: Listing = []; + for (const line of text.split(/\r?\n/)) { + const p = parseIndexLine(line); + if (p) out.push([p[0], p[1], fmt]); + } + return out; + }), + ); + const ok = results.filter((r): r is PromiseFulfilledResult => r.status === "fulfilled"); + if (ok.length === 0) throw (results[0] as PromiseRejectedResult).reason; + return ok.flatMap((r) => r.value); +} + +export interface RepoIndex { + entries: CheatEntry[]; + /** Repos that could not be read (their files are missing from entries). */ + failed: string[]; + /** Every repo failed and none of them answered at all: the PS5 cannot reach GitHub. */ + offline: boolean; +} + +/** + * Every repo's files, de-duplicated by file name across repos (the first repo wins, as in + * cheats_repo_search). Repos are read at the same time; one that fails is listed in `failed`. + */ +export async function readRepoIndex(f: FetchLike, repos: readonly CheatRepo[] = CHEAT_REPOS, timeoutMs = REPO_TIMEOUT_MS): Promise { + const results = await Promise.allSettled(repos.map((r) => listRepo(r, f, timeoutMs))); + const seen = new Set(); + const entries: CheatEntry[] = []; + const failed: string[] = []; + let answered = false; + results.forEach((r, i) => { + const repo = repos[i]; + if (r.status === "rejected") { + failed.push(repo.id); + if (!(r.reason instanceof RepoError && r.reason.network)) answered = true; + return; + } + answered = true; + for (const [filename, gameTitle, format] of r.value) { + if (seen.has(filename)) continue; + seen.add(filename); + const [titleId, gameVersion] = parseCheatFilename(filename); + entries.push({ filename, gameTitle, format, repoId: repo.id, titleId, gameVersion }); + } + }); + return { entries, failed, offline: !answered }; +} + +let cached: Promise | null = null; + +/** + * readRepoIndex once per page session (the git-trees API allows 60 reads an hour). A read where any + * repo failed is not kept: the next Find asks again. + */ +export function repoIndex(f: FetchLike = (u, i) => fetch(u, i)): Promise { + if (!cached) { + const p = readRepoIndex(f); + cached = p; + p.then( + (r) => { + if (r.failed.length && cached === p) cached = null; + }, + () => { + if (cached === p) cached = null; + }, + ); + } + return cached; +} + +/** Forget the session's index (tests). */ +export function resetRepoIndex(): void { + cached = null; +} + +/** The entries for one game (by title id, any case). Names the helper cannot store are left out. */ +export function entriesFor(index: RepoIndex, titleId: string): CheatEntry[] { + const id = titleId.toUpperCase(); + return index.entries.filter((e) => e.titleId === id && storableName(e.filename)); +} + +/** The helper's limit (cheats.c CHEAT_FILE_MAX): a larger file is never loaded. */ +export const CHEAT_FILE_MAX = 4 * 1024 * 1024; + +/** + * One cheat file's bytes from its repo: the content URLs in order (contentUrls), the first that + * answers 2xx. RepoError(network) when nothing answered; "too_large" over CHEAT_FILE_MAX; + * "not_found" when every URL answered an error. + */ +export async function downloadCheat(entry: CheatEntry, f: FetchLike = (u, i) => fetch(u, i), timeoutMs = REPO_TIMEOUT_MS): Promise { + const repo = CHEAT_REPOS.find((r) => r.id === entry.repoId); + if (!repo) throw new RepoError(false, "unknown_repo"); + let answered = false; + for (const url of contentUrls(repo, entry.filename)) { + let res: Response; + try { + res = await fetchWithTimeout(f, url, timeoutMs); + } catch { + continue; + } + answered = true; + if (!res.ok) continue; + const len = Number(res.headers.get("Content-Length")); + if (Number.isFinite(len) && len > CHEAT_FILE_MAX) throw new RepoError(false, "too_large"); + let buf: ArrayBuffer; + try { + buf = await res.arrayBuffer(); + } catch { + throw new RepoError(true, "cut_off"); + } + if (buf.byteLength > CHEAT_FILE_MAX) throw new RepoError(false, "too_large"); + if (buf.byteLength === 0) throw new RepoError(false, "empty"); + return buf; + } + throw new RepoError(!answered, answered ? "not_found" : "unreachable"); +} diff --git a/client/src/console/console.css b/client/src/console/console.css index ad5b0316..eb7425b0 100644 --- a/client/src/console/console.css +++ b/client/src/console/console.css @@ -972,6 +972,81 @@ button { .cw-cheats-empty.is-error .cw-cheats-empty-title { color: var(--color-bad); } +.cw-cheats-game-buttons { + display: flex; + flex: none; + margin-left: auto; +} +.cw-cheats-empty-buttons { + display: flex; +} +.cw-cheats-empty-buttons .cw-button + .cw-button { + margin-left: 0.7rem; +} + +/* ---- Cheats: Find cheats (a modal list of the repos' files for the running game) ---- */ +.cw-finder { + width: 52rem; +} +.cw-dialog p.cw-finder-hint { + margin: 0 0 0.6rem; + font-size: 0.9rem; +} +.cw-finder-version { + margin-right: 1.2rem; + color: var(--color-text); + font-weight: 600; +} +.cw-finder-list { + max-height: 50vh; + margin: 0 0 0.8rem; + padding: 0.2rem; + overflow-y: auto; + list-style: none; +} +.cw-finder-item + .cw-finder-item { + margin-top: 0.4rem; +} +.cw-finder-entry { + width: 100%; + min-width: 0; + padding: 0.45rem 0.9rem; + text-align: left; + border-radius: var(--radius-card); + background: var(--color-surface); + box-shadow: inset 0 0 0 1px var(--color-border); +} +.cw-finder-entry.is-match { + box-shadow: inset 0 0 0 1px var(--color-good); +} +.cw-finder-entry > svg { + flex: none; + margin-right: 0.8rem; +} +.cw-finder-entry[aria-disabled="true"] { + cursor: progress; +} +.cw-finder-entry[aria-disabled="true"]:not([aria-busy="true"]) { + opacity: 0.6; +} +.cw-finder-text { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; +} +.cw-finder-name { + font-weight: 600; +} +.cw-finder-meta { + overflow: hidden; + font-size: 0.8rem; + text-overflow: ellipsis; + white-space: nowrap; +} +.cw-finder-entry > .cw-tag { + flex: none; +} /* ---- rail: Help at the bottom ---- */ .cw-rail-help { diff --git a/client/src/console/locales/ar.json b/client/src/console/locales/ar.json index 3ff737b9..827e57ab 100644 --- a/client/src/console/locales/ar.json +++ b/client/src/console/locales/ar.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "تعذّر تبديل الشفرة ({code}).", "cw_cheats_exited": "تم إغلاق اللعبة.", "cw_cheats_exited_next": "شغّلها مرة أخرى وستعود شفراتها إلى هنا.", + "cw_cheats_find": "البحث عن شفرات", + "cw_cheats_find_any_version": "أي إصدار", + "cw_cheats_find_close": "إغلاق", + "cw_cheats_find_downloaded": "تم التنزيل", + "cw_cheats_find_downloading": "جارٍ التنزيل…", + "cw_cheats_find_err_download": "تعذّر تنزيل {name} ({code}).", + "cw_cheats_find_err_store": "تعذّر حفظ {name} على الجهاز ({code}).", + "cw_cheats_find_hint": "تُصنع الشفرات لإصدار واحد من اللعبة. اختر ما يطابق إصدارك.", + "cw_cheats_find_how": "يقوم ✕ بتنزيل الملف المحدد إلى الجهاز.", + "cw_cheats_find_loading": "جارٍ البحث عن شفرات على الإنترنت…", + "cw_cheats_find_more": "البحث عن مزيد من الشفرات", + "cw_cheats_find_no_match": "لا شيء للإصدار {v} بعد. هذه لإصدارات أخرى: عادةً لا تفعل شيئًا وقد تتسبب في تعطل اللعبة.", + "cw_cheats_find_none": "لم ينشر أحد شفرات لهذه اللعبة بعد.", + "cw_cheats_find_offline": "لا يستطيع جهاز PS5 هذا الوصول إلى GitHub. احصل على الشفرات من تطبيق PS5Upload على حاسوبك، أو اسمح بـ raw.githubusercontent.com في إعدادات DNS.", + "cw_cheats_find_partial": "تعذّر الوصول إلى بعض مصادر الشفرات، لذا قد تكون القائمة ناقصة.", + "cw_cheats_find_retry": "إعادة المحاولة", + "cw_cheats_find_saved": "أصبح {name} على الجهاز. شفراته موجودة الآن في القائمة.", + "cw_cheats_find_title": "شفرات {title} على الإنترنت", + "cw_cheats_find_too_large": "{name} كبير جدًا على ملف شفرات.", + "cw_cheats_find_version": "إصدار لعبتك: {v}", + "cw_cheats_find_your_version": "إصدارك", "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_none": "لا توجد شفرات لهذه اللعبة بعد.", + "cw_cheats_none_next": "ابحث عن شفرات لها عبر الإنترنت، أو أضفها من تطبيق PS5Upload على حاسوبك.", "cw_cheats_off": "إيقاف", "cw_cheats_on": "تشغيل", "cw_cheats_refresh": "تحديث", diff --git a/client/src/console/locales/bn.json b/client/src/console/locales/bn.json index 0c15eba2..06590ae2 100644 --- a/client/src/console/locales/bn.json +++ b/client/src/console/locales/bn.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "চিট বদলানো যায়নি ({code})।", "cw_cheats_exited": "গেমটি বন্ধ হয়ে গেছে।", "cw_cheats_exited_next": "আবার চালু করুন, এর চিটগুলো আবার এখানে দেখা যাবে।", + "cw_cheats_find": "চিট খুঁজুন", + "cw_cheats_find_any_version": "যেকোনো ভার্সন", + "cw_cheats_find_close": "বন্ধ করুন", + "cw_cheats_find_downloaded": "ডাউনলোড হয়েছে", + "cw_cheats_find_downloading": "ডাউনলোড হচ্ছে…", + "cw_cheats_find_err_download": "{name} ডাউনলোড করা যায়নি ({code})।", + "cw_cheats_find_err_store": "{name} কনসোলে সংরক্ষণ করা যায়নি ({code})।", + "cw_cheats_find_hint": "চিট গেমের একটি নির্দিষ্ট ভার্সনের জন্য তৈরি হয়। আপনার ভার্সনের সাথে মেলে এমনটি বেছে নিন।", + "cw_cheats_find_how": "✕ নির্বাচিত ফাইলটি কনসোলে ডাউনলোড করে।", + "cw_cheats_find_loading": "অনলাইনে চিট খোঁজা হচ্ছে…", + "cw_cheats_find_more": "আরও চিট খুঁজুন", + "cw_cheats_find_no_match": "ভার্সন {v}-এর জন্য এখনো কিছু নেই। এগুলো অন্য ভার্সনের জন্য: সাধারণত কিছুই করে না, আর গেম ক্র্যাশ করাতে পারে।", + "cw_cheats_find_none": "এই গেমের জন্য এখনো কেউ চিট প্রকাশ করেনি।", + "cw_cheats_find_offline": "এই PS5 GitHub-এ পৌঁছাতে পারছে না। আপনার PC-র PS5Upload অ্যাপ থেকে চিট নিন, অথবা DNS সেটিংসে raw.githubusercontent.com অনুমতি দিন।", + "cw_cheats_find_partial": "কিছু চিট উৎসে পৌঁছানো যায়নি, তাই তালিকাটি অসম্পূর্ণ হতে পারে।", + "cw_cheats_find_retry": "আবার চেষ্টা করুন", + "cw_cheats_find_saved": "{name} কনসোলে আছে। এর চিটগুলো এখন তালিকায় আছে।", + "cw_cheats_find_title": "{title}-এর অনলাইন চিট", + "cw_cheats_find_too_large": "{name} চিট ফাইলের জন্য খুব বড়।", + "cw_cheats_find_version": "আপনার গেমের ভার্সন: {v}", + "cw_cheats_find_your_version": "আপনার ভার্সন", "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_none": "এই গেমের জন্য এখনো কোনো চিট নেই।", + "cw_cheats_none_next": "অনলাইনে এর চিট খুঁজুন, অথবা আপনার PC-র PS5Upload অ্যাপ থেকে যোগ করুন।", "cw_cheats_off": "বন্ধ", "cw_cheats_on": "চালু", "cw_cheats_refresh": "রিফ্রেশ", diff --git a/client/src/console/locales/de.json b/client/src/console/locales/de.json index a10caa2e..2d6030ce 100644 --- a/client/src/console/locales/de.json +++ b/client/src/console/locales/de.json @@ -9,12 +9,33 @@ "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_find": "Cheats suchen", + "cw_cheats_find_any_version": "Jede Version", + "cw_cheats_find_close": "Schließen", + "cw_cheats_find_downloaded": "Geladen", + "cw_cheats_find_downloading": "Wird geladen…", + "cw_cheats_find_err_download": "{name} konnte nicht heruntergeladen werden ({code}).", + "cw_cheats_find_err_store": "{name} konnte nicht auf der Konsole gespeichert werden ({code}).", + "cw_cheats_find_hint": "Cheats sind für eine bestimmte Spielversion gemacht. Wähle die, die zu deiner passt.", + "cw_cheats_find_how": "✕ lädt die markierte Datei auf die Konsole.", + "cw_cheats_find_loading": "Suche online nach Cheats…", + "cw_cheats_find_more": "Weitere Cheats suchen", + "cw_cheats_find_no_match": "Noch nichts für Version {v}. Diese sind für andere Versionen: Meist bewirken sie nichts und können das Spiel abstürzen lassen.", + "cw_cheats_find_none": "Für dieses Spiel hat noch niemand Cheats veröffentlicht.", + "cw_cheats_find_offline": "Diese PS5 erreicht GitHub nicht. Hol dir Cheats in der PS5Upload-App auf deinem PC oder erlaube raw.githubusercontent.com in deinen DNS-Einstellungen.", + "cw_cheats_find_partial": "Einige Cheat-Quellen waren nicht erreichbar, die Liste ist daher eventuell unvollständig.", + "cw_cheats_find_retry": "Erneut versuchen", + "cw_cheats_find_saved": "{name} ist auf der Konsole. Seine Cheats stehen jetzt in der Liste.", + "cw_cheats_find_title": "Online-Cheats für {title}", + "cw_cheats_find_too_large": "{name} ist zu groß für eine Cheat-Datei.", + "cw_cheats_find_version": "Deine Spielversion: {v}", + "cw_cheats_find_your_version": "Deine Version", "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_none": "Noch keine Cheats für dieses Spiel.", + "cw_cheats_none_next": "Suche online nach Cheats dafür oder füge sie in der PS5Upload-App auf deinem PC hinzu.", "cw_cheats_off": "Aus", "cw_cheats_on": "An", "cw_cheats_refresh": "Aktualisieren", diff --git a/client/src/console/locales/en.json b/client/src/console/locales/en.json index 4f70831f..8324e5df 100644 --- a/client/src/console/locales/en.json +++ b/client/src/console/locales/en.json @@ -9,12 +9,33 @@ "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_find": "Find cheats", + "cw_cheats_find_any_version": "Any version", + "cw_cheats_find_close": "Close", + "cw_cheats_find_downloaded": "Downloaded", + "cw_cheats_find_downloading": "Downloading…", + "cw_cheats_find_err_download": "Could not download {name} ({code}).", + "cw_cheats_find_err_store": "Could not save {name} on the console ({code}).", + "cw_cheats_find_hint": "Cheats are made for one game version. Pick the one that matches yours.", + "cw_cheats_find_how": "✕ downloads the focused file to the console.", + "cw_cheats_find_loading": "Looking for cheats online…", + "cw_cheats_find_more": "Find more cheats", + "cw_cheats_find_no_match": "Nothing for version {v} yet. These are for other versions: they usually do nothing, and can crash the game.", + "cw_cheats_find_none": "Nobody has published cheats for this game yet.", + "cw_cheats_find_offline": "This PS5 can't reach GitHub. Get cheats in the PS5Upload app on your PC, or allow raw.githubusercontent.com in your DNS settings.", + "cw_cheats_find_partial": "Some cheat sources could not be reached, so the list may be incomplete.", + "cw_cheats_find_retry": "Try again", + "cw_cheats_find_saved": "{name} is on the console. Its cheats are in the list now.", + "cw_cheats_find_title": "Cheats online for {title}", + "cw_cheats_find_too_large": "{name} is too large for a cheat file.", + "cw_cheats_find_version": "Your game version: {v}", + "cw_cheats_find_your_version": "Your version", "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_none": "No cheats for this game yet.", + "cw_cheats_none_next": "Find cheats for it online, or add them from the PS5Upload app on your PC.", "cw_cheats_off": "Off", "cw_cheats_on": "On", "cw_cheats_refresh": "Refresh", diff --git a/client/src/console/locales/es.json b/client/src/console/locales/es.json index f197b378..1f64c5a6 100644 --- a/client/src/console/locales/es.json +++ b/client/src/console/locales/es.json @@ -9,12 +9,33 @@ "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_find": "Buscar trucos", + "cw_cheats_find_any_version": "Cualquier versión", + "cw_cheats_find_close": "Cerrar", + "cw_cheats_find_downloaded": "Descargado", + "cw_cheats_find_downloading": "Descargando…", + "cw_cheats_find_err_download": "No se pudo descargar {name} ({code}).", + "cw_cheats_find_err_store": "No se pudo guardar {name} en la consola ({code}).", + "cw_cheats_find_hint": "Los trucos se hacen para una versión concreta del juego. Elige el que coincida con la tuya.", + "cw_cheats_find_how": "✕ descarga el archivo seleccionado en la consola.", + "cw_cheats_find_loading": "Buscando trucos en línea…", + "cw_cheats_find_more": "Buscar más trucos", + "cw_cheats_find_no_match": "Aún no hay nada para la versión {v}. Estos son para otras versiones: normalmente no hacen nada y pueden cerrar el juego.", + "cw_cheats_find_none": "Nadie ha publicado trucos para este juego todavía.", + "cw_cheats_find_offline": "Esta PS5 no puede conectarse a GitHub. Consigue trucos en la app PS5Upload de tu PC o permite raw.githubusercontent.com en tus ajustes de DNS.", + "cw_cheats_find_partial": "No se pudo acceder a algunas fuentes de trucos, así que la lista puede estar incompleta.", + "cw_cheats_find_retry": "Reintentar", + "cw_cheats_find_saved": "{name} ya está en la consola. Sus trucos aparecen ahora en la lista.", + "cw_cheats_find_title": "Trucos en línea para {title}", + "cw_cheats_find_too_large": "{name} es demasiado grande para ser un archivo de trucos.", + "cw_cheats_find_version": "Versión de tu juego: {v}", + "cw_cheats_find_your_version": "Tu versión", "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_none": "Todavía no hay trucos para este juego.", + "cw_cheats_none_next": "Busca trucos en línea o añádelos desde la app PS5Upload en tu PC.", "cw_cheats_off": "Desactivado", "cw_cheats_on": "Activado", "cw_cheats_refresh": "Actualizar", diff --git a/client/src/console/locales/fa.json b/client/src/console/locales/fa.json index 8cb1627f..4a212bf2 100644 --- a/client/src/console/locales/fa.json +++ b/client/src/console/locales/fa.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "تغییر وضعیت چیت ممکن نشد ({code}).", "cw_cheats_exited": "بازی بسته شد.", "cw_cheats_exited_next": "دوباره اجرایش کنید تا چیت‌هایش دوباره اینجا نمایش داده شوند.", + "cw_cheats_find": "یافتن چیت", + "cw_cheats_find_any_version": "هر نسخه", + "cw_cheats_find_close": "بستن", + "cw_cheats_find_downloaded": "دانلود شد", + "cw_cheats_find_downloading": "در حال دانلود…", + "cw_cheats_find_err_download": "دانلود {name} ممکن نشد ({code}).", + "cw_cheats_find_err_store": "ذخیره {name} روی کنسول ممکن نشد ({code}).", + "cw_cheats_find_hint": "چیت‌ها برای یک نسخه از بازی ساخته می‌شوند. موردی را انتخاب کنید که با نسخه شما جور است.", + "cw_cheats_find_how": "✕ فایل انتخاب‌شده را روی کنسول دانلود می‌کند.", + "cw_cheats_find_loading": "در حال جست‌وجوی آنلاین چیت…", + "cw_cheats_find_more": "یافتن چیت‌های بیشتر", + "cw_cheats_find_no_match": "هنوز چیزی برای نسخه {v} نیست. این‌ها برای نسخه‌های دیگرند: معمولاً کاری نمی‌کنند و ممکن است بازی را از کار بیندازند.", + "cw_cheats_find_none": "هنوز کسی برای این بازی چیتی منتشر نکرده است.", + "cw_cheats_find_offline": "این PS5 به GitHub دسترسی ندارد. چیت‌ها را از برنامه PS5Upload روی رایانه‌تان بگیرید، یا raw.githubusercontent.com را در تنظیمات DNS مجاز کنید.", + "cw_cheats_find_partial": "به برخی منابع چیت دسترسی نبود، پس ممکن است فهرست کامل نباشد.", + "cw_cheats_find_retry": "تلاش دوباره", + "cw_cheats_find_saved": "{name} روی کنسول است. چیت‌هایش اکنون در فهرست هستند.", + "cw_cheats_find_title": "چیت‌های آنلاین برای {title}", + "cw_cheats_find_too_large": "{name} برای یک فایل چیت بیش از حد بزرگ است.", + "cw_cheats_find_version": "نسخه بازی شما: {v}", + "cw_cheats_find_your_version": "نسخه شما", "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_none": "هنوز برای این بازی چیتی وجود ندارد.", + "cw_cheats_none_next": "چیت‌های آن را آنلاین پیدا کنید، یا از برنامه PS5Upload روی رایانه‌تان اضافه کنید.", "cw_cheats_off": "خاموش", "cw_cheats_on": "روشن", "cw_cheats_refresh": "تازه‌سازی", diff --git a/client/src/console/locales/fr.json b/client/src/console/locales/fr.json index 63d89a3c..af7f3e5d 100644 --- a/client/src/console/locales/fr.json +++ b/client/src/console/locales/fr.json @@ -9,12 +9,33 @@ "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_find": "Chercher des cheats", + "cw_cheats_find_any_version": "Toutes versions", + "cw_cheats_find_close": "Fermer", + "cw_cheats_find_downloaded": "Téléchargé", + "cw_cheats_find_downloading": "Téléchargement…", + "cw_cheats_find_err_download": "Impossible de télécharger {name} ({code}).", + "cw_cheats_find_err_store": "Impossible d'enregistrer {name} sur la console ({code}).", + "cw_cheats_find_hint": "Les cheats sont faits pour une version précise du jeu. Choisissez celui qui correspond à la vôtre.", + "cw_cheats_find_how": "✕ télécharge le fichier sélectionné sur la console.", + "cw_cheats_find_loading": "Recherche de cheats en ligne…", + "cw_cheats_find_more": "Chercher d'autres cheats", + "cw_cheats_find_no_match": "Rien pour la version {v} pour l'instant. Ceux-ci visent d'autres versions : en général ils ne font rien et peuvent faire planter le jeu.", + "cw_cheats_find_none": "Personne n'a encore publié de cheats pour ce jeu.", + "cw_cheats_find_offline": "Cette PS5 n'arrive pas à joindre GitHub. Récupérez des cheats dans l'app PS5Upload sur votre PC, ou autorisez raw.githubusercontent.com dans vos réglages DNS.", + "cw_cheats_find_partial": "Certaines sources de cheats sont injoignables : la liste est peut-être incomplète.", + "cw_cheats_find_retry": "Réessayer", + "cw_cheats_find_saved": "{name} est sur la console. Ses cheats sont maintenant dans la liste.", + "cw_cheats_find_title": "Cheats en ligne pour {title}", + "cw_cheats_find_too_large": "{name} est trop volumineux pour un fichier de cheats.", + "cw_cheats_find_version": "Version de votre jeu : {v}", + "cw_cheats_find_your_version": "Votre version", "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_none": "Pas encore de cheats pour ce jeu.", + "cw_cheats_none_next": "Cherchez des cheats en ligne, ou ajoutez-les depuis l'app PS5Upload sur votre PC.", "cw_cheats_off": "Désactivé", "cw_cheats_on": "Activé", "cw_cheats_refresh": "Actualiser", diff --git a/client/src/console/locales/hi.json b/client/src/console/locales/hi.json index 0744606c..dd0edea9 100644 --- a/client/src/console/locales/hi.json +++ b/client/src/console/locales/hi.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "चीट नहीं बदला जा सका ({code})।", "cw_cheats_exited": "गेम बंद हो गया।", "cw_cheats_exited_next": "इसे फिर से शुरू करें, इसके चीट यहाँ वापस आ जाएँगे।", + "cw_cheats_find": "चीट ढूँढें", + "cw_cheats_find_any_version": "कोई भी वर्ज़न", + "cw_cheats_find_close": "बंद करें", + "cw_cheats_find_downloaded": "डाउनलोड हो गया", + "cw_cheats_find_downloading": "डाउनलोड हो रहा है…", + "cw_cheats_find_err_download": "{name} डाउनलोड नहीं हो सका ({code})।", + "cw_cheats_find_err_store": "{name} कंसोल पर सहेजा नहीं जा सका ({code})।", + "cw_cheats_find_hint": "चीट गेम के किसी एक वर्ज़न के लिए बनते हैं। अपने वर्ज़न से मेल खाने वाला चुनें।", + "cw_cheats_find_how": "✕ चुनी गई फ़ाइल को कंसोल पर डाउनलोड करता है।", + "cw_cheats_find_loading": "ऑनलाइन चीट ढूँढे जा रहे हैं…", + "cw_cheats_find_more": "और चीट ढूँढें", + "cw_cheats_find_no_match": "वर्ज़न {v} के लिए अभी कुछ नहीं है। ये दूसरे वर्ज़न के लिए हैं: आम तौर पर ये कुछ नहीं करते और गेम क्रैश कर सकते हैं।", + "cw_cheats_find_none": "इस गेम के लिए अभी तक किसी ने चीट प्रकाशित नहीं किए हैं।", + "cw_cheats_find_offline": "यह PS5 GitHub तक नहीं पहुँच पा रहा। अपने PC पर PS5Upload ऐप से चीट लें, या अपनी DNS सेटिंग में raw.githubusercontent.com को अनुमति दें।", + "cw_cheats_find_partial": "कुछ चीट स्रोतों तक नहीं पहुँचा जा सका, इसलिए सूची अधूरी हो सकती है।", + "cw_cheats_find_retry": "फिर कोशिश करें", + "cw_cheats_find_saved": "{name} कंसोल पर है। इसके चीट अब सूची में हैं।", + "cw_cheats_find_title": "{title} के ऑनलाइन चीट", + "cw_cheats_find_too_large": "{name} चीट फ़ाइल के लिए बहुत बड़ी है।", + "cw_cheats_find_version": "आपके गेम का वर्ज़न: {v}", + "cw_cheats_find_your_version": "आपका वर्ज़न", "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_none": "इस गेम के लिए अभी कोई चीट नहीं है।", + "cw_cheats_none_next": "इसके चीट ऑनलाइन ढूँढें, या अपने PC पर PS5Upload ऐप से जोड़ें।", "cw_cheats_off": "बंद", "cw_cheats_on": "चालू", "cw_cheats_refresh": "रीफ़्रेश करें", diff --git a/client/src/console/locales/hu.json b/client/src/console/locales/hu.json index fe44c466..70b6545c 100644 --- a/client/src/console/locales/hu.json +++ b/client/src/console/locales/hu.json @@ -9,12 +9,33 @@ "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_find": "Csalások keresése", + "cw_cheats_find_any_version": "Bármely verzió", + "cw_cheats_find_close": "Bezárás", + "cw_cheats_find_downloaded": "Letöltve", + "cw_cheats_find_downloading": "Letöltés…", + "cw_cheats_find_err_download": "Nem sikerült letölteni: {name} ({code}).", + "cw_cheats_find_err_store": "Nem sikerült menteni a konzolra: {name} ({code}).", + "cw_cheats_find_hint": "A csalások egy adott játékverzióhoz készülnek. Válaszd azt, amelyik a tiédhez illik.", + "cw_cheats_find_how": "Az ✕ a kijelölt fájlt letölti a konzolra.", + "cw_cheats_find_loading": "Csalások keresése online…", + "cw_cheats_find_more": "További csalások keresése", + "cw_cheats_find_no_match": "A(z) {v} verzióhoz még nincs semmi. Ezek más verziókhoz készültek: általában nem csinálnak semmit, és összeomolhat tőlük a játék.", + "cw_cheats_find_none": "Ehhez a játékhoz még senki sem tett közzé csalásokat.", + "cw_cheats_find_offline": "Ez a PS5 nem éri el a GitHubot. Szerezz csalásokat a PC-n futó PS5Upload alkalmazásban, vagy engedélyezd a raw.githubusercontent.com címet a DNS-beállításokban.", + "cw_cheats_find_partial": "Néhány csalásforrás nem érhető el, ezért a lista hiányos lehet.", + "cw_cheats_find_retry": "Újra", + "cw_cheats_find_saved": "A(z) {name} a konzolon van. A csalásai most már a listában vannak.", + "cw_cheats_find_title": "Online csalások ehhez: {title}", + "cw_cheats_find_too_large": "A(z) {name} túl nagy egy csalásfájlhoz.", + "cw_cheats_find_version": "A játékod verziója: {v}", + "cw_cheats_find_your_version": "A te verziód", "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_none": "Ehhez a játékhoz még nincsenek csalások.", + "cw_cheats_none_next": "Keress hozzá csalásokat online, vagy add hozzá őket a PC-n futó PS5Upload alkalmazásból.", "cw_cheats_off": "Ki", "cw_cheats_on": "Be", "cw_cheats_refresh": "Frissítés", diff --git a/client/src/console/locales/id.json b/client/src/console/locales/id.json index 3d95c4b3..86f8e85e 100644 --- a/client/src/console/locales/id.json +++ b/client/src/console/locales/id.json @@ -9,12 +9,33 @@ "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_find": "Cari cheat", + "cw_cheats_find_any_version": "Semua versi", + "cw_cheats_find_close": "Tutup", + "cw_cheats_find_downloaded": "Terunduh", + "cw_cheats_find_downloading": "Mengunduh…", + "cw_cheats_find_err_download": "Tidak dapat mengunduh {name} ({code}).", + "cw_cheats_find_err_store": "Tidak dapat menyimpan {name} di konsol ({code}).", + "cw_cheats_find_hint": "Cheat dibuat untuk satu versi game. Pilih yang cocok dengan versi Anda.", + "cw_cheats_find_how": "✕ mengunduh file yang dipilih ke konsol.", + "cw_cheats_find_loading": "Mencari cheat secara online…", + "cw_cheats_find_more": "Cari cheat lain", + "cw_cheats_find_no_match": "Belum ada untuk versi {v}. Ini untuk versi lain: biasanya tidak berfungsi dan bisa membuat game crash.", + "cw_cheats_find_none": "Belum ada yang menerbitkan cheat untuk game ini.", + "cw_cheats_find_offline": "PS5 ini tidak bisa menjangkau GitHub. Dapatkan cheat di aplikasi PS5Upload di PC Anda, atau izinkan raw.githubusercontent.com di pengaturan DNS Anda.", + "cw_cheats_find_partial": "Beberapa sumber cheat tidak dapat dijangkau, jadi daftarnya mungkin tidak lengkap.", + "cw_cheats_find_retry": "Coba lagi", + "cw_cheats_find_saved": "{name} sudah ada di konsol. Cheat-nya kini ada di daftar.", + "cw_cheats_find_title": "Cheat online untuk {title}", + "cw_cheats_find_too_large": "{name} terlalu besar untuk file cheat.", + "cw_cheats_find_version": "Versi game Anda: {v}", + "cw_cheats_find_your_version": "Versi Anda", "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_none": "Belum ada cheat untuk game ini.", + "cw_cheats_none_next": "Cari cheat secara online, atau tambahkan dari aplikasi PS5Upload di PC Anda.", "cw_cheats_off": "Mati", "cw_cheats_on": "Nyala", "cw_cheats_refresh": "Muat ulang", diff --git a/client/src/console/locales/it.json b/client/src/console/locales/it.json index 46f01f97..c2ce7243 100644 --- a/client/src/console/locales/it.json +++ b/client/src/console/locales/it.json @@ -9,12 +9,33 @@ "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_find": "Cerca cheat", + "cw_cheats_find_any_version": "Qualsiasi versione", + "cw_cheats_find_close": "Chiudi", + "cw_cheats_find_downloaded": "Scaricato", + "cw_cheats_find_downloading": "Download in corso…", + "cw_cheats_find_err_download": "Impossibile scaricare {name} ({code}).", + "cw_cheats_find_err_store": "Impossibile salvare {name} sulla console ({code}).", + "cw_cheats_find_hint": "I cheat sono fatti per una versione precisa del gioco. Scegli quello adatto alla tua.", + "cw_cheats_find_how": "✕ scarica sulla console il file selezionato.", + "cw_cheats_find_loading": "Ricerca dei cheat online…", + "cw_cheats_find_more": "Cerca altri cheat", + "cw_cheats_find_no_match": "Ancora niente per la versione {v}. Questi sono per altre versioni: di solito non fanno nulla e possono far chiudere il gioco.", + "cw_cheats_find_none": "Nessuno ha ancora pubblicato cheat per questo gioco.", + "cw_cheats_find_offline": "Questa PS5 non riesce a raggiungere GitHub. Ottieni i cheat dall'app PS5Upload sul tuo PC oppure consenti raw.githubusercontent.com nelle impostazioni DNS.", + "cw_cheats_find_partial": "Alcune fonti di cheat non sono raggiungibili, quindi l'elenco potrebbe essere incompleto.", + "cw_cheats_find_retry": "Riprova", + "cw_cheats_find_saved": "{name} è sulla console. I suoi cheat ora sono nell'elenco.", + "cw_cheats_find_title": "Cheat online per {title}", + "cw_cheats_find_too_large": "{name} è troppo grande per essere un file di cheat.", + "cw_cheats_find_version": "Versione del tuo gioco: {v}", + "cw_cheats_find_your_version": "La tua versione", "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_none": "Ancora nessun cheat per questo gioco.", + "cw_cheats_none_next": "Cerca i cheat online oppure aggiungili dall'app PS5Upload sul tuo PC.", "cw_cheats_off": "Disattivo", "cw_cheats_on": "Attivo", "cw_cheats_refresh": "Aggiorna", diff --git a/client/src/console/locales/ja.json b/client/src/console/locales/ja.json index 3ce33514..03f1b95a 100644 --- a/client/src/console/locales/ja.json +++ b/client/src/console/locales/ja.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "チートを切り替えられませんでした ({code})。", "cw_cheats_exited": "ゲームが終了しました。", "cw_cheats_exited_next": "もう一度起動すると、チートがここに戻ります。", + "cw_cheats_find": "チートを探す", + "cw_cheats_find_any_version": "全バージョン", + "cw_cheats_find_close": "閉じる", + "cw_cheats_find_downloaded": "ダウンロード済み", + "cw_cheats_find_downloading": "ダウンロード中…", + "cw_cheats_find_err_download": "{name} をダウンロードできませんでした ({code})。", + "cw_cheats_find_err_store": "{name} を本体に保存できませんでした ({code})。", + "cw_cheats_find_hint": "チートはゲームの特定のバージョン用に作られています。お使いのバージョンに合うものを選んでください。", + "cw_cheats_find_how": "✕ で選択中のファイルを本体にダウンロードします。", + "cw_cheats_find_loading": "オンラインでチートを探しています…", + "cw_cheats_find_more": "ほかのチートを探す", + "cw_cheats_find_no_match": "バージョン {v} 用のチートはまだありません。これらは別のバージョン用です。通常は効果がなく、ゲームがクラッシュすることがあります。", + "cw_cheats_find_none": "このゲームのチートはまだ誰も公開していません。", + "cw_cheats_find_offline": "この PS5 から GitHub に接続できません。PC の PS5Upload アプリでチートを入手するか、DNS 設定で raw.githubusercontent.com を許可してください。", + "cw_cheats_find_partial": "一部のチート配布元に接続できなかったため、リストが不完全な可能性があります。", + "cw_cheats_find_retry": "再試行", + "cw_cheats_find_saved": "{name} を本体に保存しました。チートがリストに表示されています。", + "cw_cheats_find_title": "{title} のオンラインチート", + "cw_cheats_find_too_large": "{name} はチートファイルとしては大きすぎます。", + "cw_cheats_find_version": "ゲームのバージョン: {v}", + "cw_cheats_find_your_version": "お使いのバージョン", "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_none": "このゲームのチートはまだありません。", + "cw_cheats_none_next": "オンラインでチートを探すか、PC の PS5Upload アプリから追加してください。", "cw_cheats_off": "オフ", "cw_cheats_on": "オン", "cw_cheats_refresh": "更新", diff --git a/client/src/console/locales/ko.json b/client/src/console/locales/ko.json index 91f6f12c..dd416106 100644 --- a/client/src/console/locales/ko.json +++ b/client/src/console/locales/ko.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "치트를 전환하지 못했습니다 ({code}).", "cw_cheats_exited": "게임이 종료되었습니다.", "cw_cheats_exited_next": "다시 시작하면 치트가 여기에 다시 나타납니다.", + "cw_cheats_find": "치트 찾기", + "cw_cheats_find_any_version": "모든 버전", + "cw_cheats_find_close": "닫기", + "cw_cheats_find_downloaded": "다운로드됨", + "cw_cheats_find_downloading": "다운로드 중…", + "cw_cheats_find_err_download": "{name}을(를) 다운로드할 수 없습니다 ({code}).", + "cw_cheats_find_err_store": "{name}을(를) 콘솔에 저장할 수 없습니다 ({code}).", + "cw_cheats_find_hint": "치트는 특정 게임 버전용으로 만들어집니다. 내 버전에 맞는 것을 고르세요.", + "cw_cheats_find_how": "✕ 버튼으로 선택한 파일을 콘솔에 다운로드합니다.", + "cw_cheats_find_loading": "온라인에서 치트를 찾는 중…", + "cw_cheats_find_more": "치트 더 찾기", + "cw_cheats_find_no_match": "{v} 버전용 치트는 아직 없습니다. 이것들은 다른 버전용이라 보통 효과가 없고 게임이 멈출 수 있습니다.", + "cw_cheats_find_none": "아직 이 게임의 치트를 공개한 사람이 없습니다.", + "cw_cheats_find_offline": "이 PS5에서 GitHub에 연결할 수 없습니다. PC의 PS5Upload 앱에서 치트를 받거나 DNS 설정에서 raw.githubusercontent.com을 허용하세요.", + "cw_cheats_find_partial": "일부 치트 출처에 연결할 수 없어 목록이 불완전할 수 있습니다.", + "cw_cheats_find_retry": "다시 시도", + "cw_cheats_find_saved": "{name}이(가) 콘솔에 저장되었습니다. 이제 치트가 목록에 있습니다.", + "cw_cheats_find_title": "{title}의 온라인 치트", + "cw_cheats_find_too_large": "{name}은(는) 치트 파일로는 너무 큽니다.", + "cw_cheats_find_version": "게임 버전: {v}", + "cw_cheats_find_your_version": "내 버전", "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_none": "이 게임의 치트가 아직 없습니다.", + "cw_cheats_none_next": "온라인에서 치트를 찾거나 PC의 PS5Upload 앱에서 추가하세요.", "cw_cheats_off": "꺼짐", "cw_cheats_on": "켜짐", "cw_cheats_refresh": "새로 고침", diff --git a/client/src/console/locales/pl.json b/client/src/console/locales/pl.json index 6023c7a6..ab80f06c 100644 --- a/client/src/console/locales/pl.json +++ b/client/src/console/locales/pl.json @@ -9,12 +9,33 @@ "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_find": "Szukaj kodów", + "cw_cheats_find_any_version": "Dowolna wersja", + "cw_cheats_find_close": "Zamknij", + "cw_cheats_find_downloaded": "Pobrano", + "cw_cheats_find_downloading": "Pobieranie…", + "cw_cheats_find_err_download": "Nie udało się pobrać {name} ({code}).", + "cw_cheats_find_err_store": "Nie udało się zapisać {name} na konsoli ({code}).", + "cw_cheats_find_hint": "Kody są tworzone dla jednej wersji gry. Wybierz ten, który pasuje do Twojej.", + "cw_cheats_find_how": "✕ pobiera zaznaczony plik na konsolę.", + "cw_cheats_find_loading": "Szukanie kodów w internecie…", + "cw_cheats_find_more": "Szukaj więcej kodów", + "cw_cheats_find_no_match": "Na razie nic dla wersji {v}. Te są dla innych wersji: zwykle nic nie robią i mogą zawiesić grę.", + "cw_cheats_find_none": "Nikt jeszcze nie opublikował kodów dla tej gry.", + "cw_cheats_find_offline": "Ta konsola PS5 nie może połączyć się z GitHubem. Pobierz kody w aplikacji PS5Upload na komputerze albo zezwól na raw.githubusercontent.com w ustawieniach DNS.", + "cw_cheats_find_partial": "Niektóre źródła kodów są nieosiągalne, więc lista może być niepełna.", + "cw_cheats_find_retry": "Spróbuj ponownie", + "cw_cheats_find_saved": "{name} jest już na konsoli. Jego kody są teraz na liście.", + "cw_cheats_find_title": "Kody online dla {title}", + "cw_cheats_find_too_large": "{name} jest za duży jak na plik z kodami.", + "cw_cheats_find_version": "Wersja Twojej gry: {v}", + "cw_cheats_find_your_version": "Twoja wersja", "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_none": "Na razie brak kodów dla tej gry.", + "cw_cheats_none_next": "Poszukaj kodów w internecie albo dodaj je w aplikacji PS5Upload na komputerze.", "cw_cheats_off": "Wył.", "cw_cheats_on": "Wł.", "cw_cheats_refresh": "Odśwież", diff --git a/client/src/console/locales/pt-BR.json b/client/src/console/locales/pt-BR.json index 427ce112..0635b9e2 100644 --- a/client/src/console/locales/pt-BR.json +++ b/client/src/console/locales/pt-BR.json @@ -9,12 +9,33 @@ "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_find": "Procurar cheats", + "cw_cheats_find_any_version": "Qualquer versão", + "cw_cheats_find_close": "Fechar", + "cw_cheats_find_downloaded": "Baixado", + "cw_cheats_find_downloading": "Baixando…", + "cw_cheats_find_err_download": "Não foi possível baixar {name} ({code}).", + "cw_cheats_find_err_store": "Não foi possível salvar {name} no console ({code}).", + "cw_cheats_find_hint": "Cheats são feitos para uma versão específica do jogo. Escolha o que corresponde à sua.", + "cw_cheats_find_how": "✕ baixa o arquivo selecionado para o console.", + "cw_cheats_find_loading": "Procurando cheats on-line…", + "cw_cheats_find_more": "Procurar mais cheats", + "cw_cheats_find_no_match": "Ainda não há nada para a versão {v}. Estes são para outras versões: geralmente não fazem nada e podem travar o jogo.", + "cw_cheats_find_none": "Ninguém publicou cheats para este jogo ainda.", + "cw_cheats_find_offline": "Este PS5 não consegue acessar o GitHub. Obtenha cheats no app PS5Upload do seu PC ou libere raw.githubusercontent.com nas suas configurações de DNS.", + "cw_cheats_find_partial": "Algumas fontes de cheats não puderam ser acessadas, então a lista pode estar incompleta.", + "cw_cheats_find_retry": "Tentar de novo", + "cw_cheats_find_saved": "{name} está no console. Os cheats dele já estão na lista.", + "cw_cheats_find_title": "Cheats on-line para {title}", + "cw_cheats_find_too_large": "{name} é grande demais para um arquivo de cheats.", + "cw_cheats_find_version": "Versão do seu jogo: {v}", + "cw_cheats_find_your_version": "Sua versão", "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_none": "Ainda não há cheats para este jogo.", + "cw_cheats_none_next": "Procure cheats on-line ou adicione-os pelo app PS5Upload no seu PC.", "cw_cheats_off": "Desligado", "cw_cheats_on": "Ligado", "cw_cheats_refresh": "Atualizar", diff --git a/client/src/console/locales/ru.json b/client/src/console/locales/ru.json index 7e483faf..73597106 100644 --- a/client/src/console/locales/ru.json +++ b/client/src/console/locales/ru.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "Не удалось переключить чит ({code}).", "cw_cheats_exited": "Игра закрыта.", "cw_cheats_exited_next": "Запустите её снова, и её читы снова появятся здесь.", + "cw_cheats_find": "Найти читы", + "cw_cheats_find_any_version": "Любая версия", + "cw_cheats_find_close": "Закрыть", + "cw_cheats_find_downloaded": "Загружено", + "cw_cheats_find_downloading": "Загрузка…", + "cw_cheats_find_err_download": "Не удалось скачать {name} ({code}).", + "cw_cheats_find_err_store": "Не удалось сохранить {name} на консоли ({code}).", + "cw_cheats_find_hint": "Читы делаются под одну версию игры. Выберите тот, что подходит к вашей.", + "cw_cheats_find_how": "✕ загружает выбранный файл на консоль.", + "cw_cheats_find_loading": "Ищем читы в интернете…", + "cw_cheats_find_more": "Найти ещё читы", + "cw_cheats_find_no_match": "Для версии {v} пока ничего нет. Эти — для других версий: обычно они ничего не делают и могут привести к сбою игры.", + "cw_cheats_find_none": "Для этой игры пока никто не опубликовал читы.", + "cw_cheats_find_offline": "Эта PS5 не может подключиться к GitHub. Возьмите читы в приложении PS5Upload на ПК или разрешите raw.githubusercontent.com в настройках DNS.", + "cw_cheats_find_partial": "Некоторые источники читов недоступны, поэтому список может быть неполным.", + "cw_cheats_find_retry": "Повторить", + "cw_cheats_find_saved": "{name} теперь на консоли. Его читы уже в списке.", + "cw_cheats_find_title": "Читы в сети для {title}", + "cw_cheats_find_too_large": "{name} слишком большой для файла читов.", + "cw_cheats_find_version": "Версия вашей игры: {v}", + "cw_cheats_find_your_version": "Ваша версия", "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_none": "Для этой игры пока нет читов.", + "cw_cheats_none_next": "Найдите читы в интернете или добавьте их в приложении PS5Upload на ПК.", "cw_cheats_off": "Выкл.", "cw_cheats_on": "Вкл.", "cw_cheats_refresh": "Обновить", diff --git a/client/src/console/locales/th.json b/client/src/console/locales/th.json index 157b0e1a..0d6737a0 100644 --- a/client/src/console/locales/th.json +++ b/client/src/console/locales/th.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "สลับสูตรโกงไม่ได้ ({code})", "cw_cheats_exited": "เกมถูกปิดแล้ว", "cw_cheats_exited_next": "เริ่มเกมอีกครั้ง แล้วสูตรโกงจะกลับมาที่นี่", + "cw_cheats_find": "ค้นหาสูตรโกง", + "cw_cheats_find_any_version": "ทุกเวอร์ชัน", + "cw_cheats_find_close": "ปิด", + "cw_cheats_find_downloaded": "ดาวน์โหลดแล้ว", + "cw_cheats_find_downloading": "กำลังดาวน์โหลด…", + "cw_cheats_find_err_download": "ดาวน์โหลด {name} ไม่ได้ ({code})", + "cw_cheats_find_err_store": "บันทึก {name} ลงเครื่องไม่ได้ ({code})", + "cw_cheats_find_hint": "สูตรโกงทำมาสำหรับเกมเวอร์ชันเดียว เลือกอันที่ตรงกับเวอร์ชันของคุณ", + "cw_cheats_find_how": "✕ ดาวน์โหลดไฟล์ที่เลือกลงเครื่อง", + "cw_cheats_find_loading": "กำลังค้นหาสูตรโกงทางออนไลน์…", + "cw_cheats_find_more": "ค้นหาสูตรโกงเพิ่ม", + "cw_cheats_find_no_match": "ยังไม่มีสำหรับเวอร์ชัน {v} อันเหล่านี้สำหรับเวอร์ชันอื่น: มักไม่มีผลและอาจทำให้เกมค้าง", + "cw_cheats_find_none": "ยังไม่มีใครเผยแพร่สูตรโกงสำหรับเกมนี้", + "cw_cheats_find_offline": "PS5 เครื่องนี้เชื่อมต่อ GitHub ไม่ได้ รับสูตรโกงจากแอป PS5Upload บนพีซีของคุณ หรืออนุญาต raw.githubusercontent.com ในการตั้งค่า DNS", + "cw_cheats_find_partial": "เข้าถึงแหล่งสูตรโกงบางแห่งไม่ได้ รายการจึงอาจไม่ครบ", + "cw_cheats_find_retry": "ลองอีกครั้ง", + "cw_cheats_find_saved": "{name} อยู่บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", + "cw_cheats_find_title": "สูตรโกงออนไลน์สำหรับ {title}", + "cw_cheats_find_too_large": "{name} ใหญ่เกินกว่าจะเป็นไฟล์สูตรโกง", + "cw_cheats_find_version": "เวอร์ชันเกมของคุณ: {v}", + "cw_cheats_find_your_version": "เวอร์ชันของคุณ", "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_none": "ยังไม่มีสูตรโกงสำหรับเกมนี้", + "cw_cheats_none_next": "ค้นหาสูตรโกงทางออนไลน์ หรือเพิ่มจากแอป PS5Upload บนพีซีของคุณ", "cw_cheats_off": "ปิด", "cw_cheats_on": "เปิด", "cw_cheats_refresh": "รีเฟรช", diff --git a/client/src/console/locales/tr.json b/client/src/console/locales/tr.json index 3934fbc1..a3514825 100644 --- a/client/src/console/locales/tr.json +++ b/client/src/console/locales/tr.json @@ -9,12 +9,33 @@ "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_find": "Hile ara", + "cw_cheats_find_any_version": "Her sürüm", + "cw_cheats_find_close": "Kapat", + "cw_cheats_find_downloaded": "İndirildi", + "cw_cheats_find_downloading": "İndiriliyor…", + "cw_cheats_find_err_download": "{name} indirilemedi ({code}).", + "cw_cheats_find_err_store": "{name} konsola kaydedilemedi ({code}).", + "cw_cheats_find_hint": "Hileler oyunun belirli bir sürümü için yapılır. Sizinkine uyanı seçin.", + "cw_cheats_find_how": "✕ seçili dosyayı konsola indirir.", + "cw_cheats_find_loading": "Hileler internette aranıyor…", + "cw_cheats_find_more": "Daha fazla hile ara", + "cw_cheats_find_no_match": "{v} sürümü için henüz bir şey yok. Bunlar başka sürümler için: genellikle işe yaramaz ve oyunun çökmesine yol açabilir.", + "cw_cheats_find_none": "Bu oyun için henüz kimse hile yayımlamadı.", + "cw_cheats_find_offline": "Bu PS5 GitHub'a ulaşamıyor. Hileleri bilgisayarınızdaki PS5Upload uygulamasından alın veya DNS ayarlarınızda raw.githubusercontent.com adresine izin verin.", + "cw_cheats_find_partial": "Bazı hile kaynaklarına ulaşılamadı, bu yüzden liste eksik olabilir.", + "cw_cheats_find_retry": "Tekrar dene", + "cw_cheats_find_saved": "{name} konsolda. Hileleri artık listede.", + "cw_cheats_find_title": "{title} için çevrimiçi hileler", + "cw_cheats_find_too_large": "{name} bir hile dosyası için fazla büyük.", + "cw_cheats_find_version": "Oyununuzun sürümü: {v}", + "cw_cheats_find_your_version": "Sizin sürümünüz", "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_none": "Bu oyun için henüz hile yok.", + "cw_cheats_none_next": "Hileleri internette arayın ya da bilgisayarınızdaki PS5Upload uygulamasından ekleyin.", "cw_cheats_off": "Kapalı", "cw_cheats_on": "Açık", "cw_cheats_refresh": "Yenile", diff --git a/client/src/console/locales/vi.json b/client/src/console/locales/vi.json index d1dd9ef4..82837131 100644 --- a/client/src/console/locales/vi.json +++ b/client/src/console/locales/vi.json @@ -9,12 +9,33 @@ "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_find": "Tìm cheat", + "cw_cheats_find_any_version": "Mọi phiên bản", + "cw_cheats_find_close": "Đóng", + "cw_cheats_find_downloaded": "Đã tải xuống", + "cw_cheats_find_downloading": "Đang tải xuống…", + "cw_cheats_find_err_download": "Không tải xuống được {name} ({code}).", + "cw_cheats_find_err_store": "Không lưu được {name} lên máy ({code}).", + "cw_cheats_find_hint": "Cheat được làm cho một phiên bản trò chơi. Hãy chọn cái khớp với phiên bản của bạn.", + "cw_cheats_find_how": "✕ tải tệp đang chọn xuống máy.", + "cw_cheats_find_loading": "Đang tìm cheat trực tuyến…", + "cw_cheats_find_more": "Tìm thêm cheat", + "cw_cheats_find_no_match": "Chưa có gì cho phiên bản {v}. Những cái này dành cho phiên bản khác: thường không có tác dụng và có thể làm trò chơi bị lỗi.", + "cw_cheats_find_none": "Chưa ai đăng cheat cho trò chơi này.", + "cw_cheats_find_offline": "PS5 này không kết nối được GitHub. Hãy lấy cheat bằng ứng dụng PS5Upload trên PC, hoặc cho phép raw.githubusercontent.com trong cài đặt DNS.", + "cw_cheats_find_partial": "Không truy cập được một số nguồn cheat, nên danh sách có thể chưa đầy đủ.", + "cw_cheats_find_retry": "Thử lại", + "cw_cheats_find_saved": "{name} đã có trên máy. Các cheat của nó giờ đã ở trong danh sách.", + "cw_cheats_find_title": "Cheat trực tuyến cho {title}", + "cw_cheats_find_too_large": "{name} quá lớn để là tệp cheat.", + "cw_cheats_find_version": "Phiên bản trò chơi của bạn: {v}", + "cw_cheats_find_your_version": "Phiên bản của bạn", "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_none": "Chưa có cheat cho trò chơi này.", + "cw_cheats_none_next": "Tìm cheat trực tuyến, hoặc thêm 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", diff --git a/client/src/console/locales/zh-CN.json b/client/src/console/locales/zh-CN.json index 7b333d7b..ba453b0c 100644 --- a/client/src/console/locales/zh-CN.json +++ b/client/src/console/locales/zh-CN.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "无法切换金手指({code})。", "cw_cheats_exited": "游戏已关闭。", "cw_cheats_exited_next": "再次启动游戏,它的金手指会重新显示在这里。", + "cw_cheats_find": "查找金手指", + "cw_cheats_find_any_version": "任意版本", + "cw_cheats_find_close": "关闭", + "cw_cheats_find_downloaded": "已下载", + "cw_cheats_find_downloading": "正在下载…", + "cw_cheats_find_err_download": "无法下载 {name}({code})。", + "cw_cheats_find_err_store": "无法将 {name} 保存到主机({code})。", + "cw_cheats_find_hint": "金手指只适用于特定的游戏版本。请选择与你的版本匹配的那个。", + "cw_cheats_find_how": "按 ✕ 将选中的文件下载到主机。", + "cw_cheats_find_loading": "正在在线查找金手指…", + "cw_cheats_find_more": "查找更多金手指", + "cw_cheats_find_no_match": "暂时没有适用于版本 {v} 的金手指。这些适用于其他版本:通常没有效果,还可能让游戏崩溃。", + "cw_cheats_find_none": "还没有人发布此游戏的金手指。", + "cw_cheats_find_offline": "这台 PS5 无法连接 GitHub。请在电脑上的 PS5Upload 应用中获取金手指,或在 DNS 设置中允许 raw.githubusercontent.com。", + "cw_cheats_find_partial": "部分金手指来源无法访问,列表可能不完整。", + "cw_cheats_find_retry": "重试", + "cw_cheats_find_saved": "{name} 已保存到主机。它的金手指现在已在列表中。", + "cw_cheats_find_title": "{title} 的在线金手指", + "cw_cheats_find_too_large": "{name} 太大,不像是金手指文件。", + "cw_cheats_find_version": "你的游戏版本:{v}", + "cw_cheats_find_your_version": "你的版本", "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_none": "此游戏还没有金手指。", + "cw_cheats_none_next": "可以在线查找金手指,或在电脑上的 PS5Upload 应用中添加。", "cw_cheats_off": "关", "cw_cheats_on": "开", "cw_cheats_refresh": "刷新", diff --git a/client/src/console/locales/zh-TW.json b/client/src/console/locales/zh-TW.json index 7029ee9b..d3399932 100644 --- a/client/src/console/locales/zh-TW.json +++ b/client/src/console/locales/zh-TW.json @@ -9,12 +9,33 @@ "cw_cheats_err_toggle": "無法切換金手指({code})。", "cw_cheats_exited": "遊戲已關閉。", "cw_cheats_exited_next": "再次啟動遊戲,它的金手指會重新顯示在這裡。", + "cw_cheats_find": "尋找金手指", + "cw_cheats_find_any_version": "任何版本", + "cw_cheats_find_close": "關閉", + "cw_cheats_find_downloaded": "已下載", + "cw_cheats_find_downloading": "正在下載…", + "cw_cheats_find_err_download": "無法下載 {name}({code})。", + "cw_cheats_find_err_store": "無法將 {name} 儲存到主機({code})。", + "cw_cheats_find_hint": "金手指只適用於特定的遊戲版本。請選擇與你的版本相符的那個。", + "cw_cheats_find_how": "按 ✕ 將選取的檔案下載到主機。", + "cw_cheats_find_loading": "正在線上尋找金手指…", + "cw_cheats_find_more": "尋找更多金手指", + "cw_cheats_find_no_match": "目前沒有適用於版本 {v} 的金手指。這些適用於其他版本:通常沒有效果,還可能讓遊戲當機。", + "cw_cheats_find_none": "還沒有人發布此遊戲的金手指。", + "cw_cheats_find_offline": "這台 PS5 無法連線到 GitHub。請在電腦上的 PS5Upload 應用程式中取得金手指,或在 DNS 設定中允許 raw.githubusercontent.com。", + "cw_cheats_find_partial": "部分金手指來源無法連線,清單可能不完整。", + "cw_cheats_find_retry": "重試", + "cw_cheats_find_saved": "{name} 已儲存到主機。它的金手指現在已在清單中。", + "cw_cheats_find_title": "{title} 的線上金手指", + "cw_cheats_find_too_large": "{name} 太大,不像是金手指檔案。", + "cw_cheats_find_version": "你的遊戲版本:{v}", + "cw_cheats_find_your_version": "你的版本", "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_none": "此遊戲還沒有金手指。", + "cw_cheats_none_next": "可以在線上尋找金手指,或在電腦上的 PS5Upload 應用程式中新增。", "cw_cheats_off": "關", "cw_cheats_on": "開", "cw_cheats_refresh": "重新整理", diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index 3d70f8d2..29ef3ef8 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -4303,8 +4303,8 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB: "cw_cheats_loading": "جارٍ تحميل الشفرات…", "cw_cheats_no_game": "لا توجد لعبة قيد التشغيل.", "cw_cheats_no_game_next": "شغّل لعبة، ثم افتح الشفرات.", -"cw_cheats_none": "لا توجد شفرات لهذه اللعبة.", -"cw_cheats_none_next": "أضف شفرات لهذه اللعبة من تطبيق PS5Upload على حاسوبك.", +"cw_cheats_none": "لا توجد شفرات لهذه اللعبة بعد.", +"cw_cheats_none_next": "ابحث عن شفرات لها عبر الإنترنت، أو أضفها من تطبيق PS5Upload على حاسوبك.", "cw_cheats_exited": "تم إغلاق اللعبة.", "cw_cheats_exited_next": "شغّلها مرة أخرى وستعود شفراتها إلى هنا.", "cw_cheats_refresh": "تحديث", @@ -4314,6 +4314,27 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB: "cw_cheats_switching": "جارٍ التبديل…", "cw_cheats_engine_off": "محرك الشفرات متوقف. يبدأ عند تشغيل أي شفرة.", "cw_cheats_truncated": "لا تتسع هذه الصفحة لكل الشفرات. يعرضها تطبيق PS5Upload على حاسوبك كلها.", +"cw_cheats_find": "البحث عن شفرات", +"cw_cheats_find_more": "البحث عن مزيد من الشفرات", +"cw_cheats_find_title": "شفرات {title} على الإنترنت", +"cw_cheats_find_loading": "جارٍ البحث عن شفرات على الإنترنت…", +"cw_cheats_find_hint": "تُصنع الشفرات لإصدار واحد من اللعبة. اختر ما يطابق إصدارك.", +"cw_cheats_find_version": "إصدار لعبتك: {v}", +"cw_cheats_find_your_version": "إصدارك", +"cw_cheats_find_any_version": "أي إصدار", +"cw_cheats_find_none": "لم ينشر أحد شفرات لهذه اللعبة بعد.", +"cw_cheats_find_no_match": "لا شيء للإصدار {v} بعد. هذه لإصدارات أخرى: عادةً لا تفعل شيئًا وقد تتسبب في تعطل اللعبة.", +"cw_cheats_find_offline": "لا يستطيع جهاز PS5 هذا الوصول إلى GitHub. احصل على الشفرات من تطبيق PS5Upload على حاسوبك، أو اسمح بـ raw.githubusercontent.com في إعدادات DNS.", +"cw_cheats_find_partial": "تعذّر الوصول إلى بعض مصادر الشفرات، لذا قد تكون القائمة ناقصة.", +"cw_cheats_find_downloading": "جارٍ التنزيل…", +"cw_cheats_find_downloaded": "تم التنزيل", +"cw_cheats_find_saved": "أصبح {name} على الجهاز. شفراته موجودة الآن في القائمة.", +"cw_cheats_find_err_download": "تعذّر تنزيل {name} ({code}).", +"cw_cheats_find_err_store": "تعذّر حفظ {name} على الجهاز ({code}).", +"cw_cheats_find_too_large": "{name} كبير جدًا على ملف شفرات.", +"cw_cheats_find_retry": "إعادة المحاولة", +"cw_cheats_find_close": "إغلاق", +"cw_cheats_find_how": "يقوم ✕ بتنزيل الملف المحدد إلى الجهاز.", "cw_cheats_err_load": "تعذّرت قراءة الشفرات ({code}). اضغط تحديث للمحاولة مرة أخرى.", "cw_cheats_err_module": "تحتاج هذه الشفرة إلى جزء من اللعبة لم يُحمَّل بعد. حاول لاحقًا أثناء اللعب.", "cw_cheats_err_changed": "تغيّرت ملفات الشفرات. أُعيدت قراءة القائمة.", diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index 5763b12c..fd9729eb 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -4303,8 +4303,8 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা "cw_cheats_loading": "চিট লোড হচ্ছে…", "cw_cheats_no_game": "কোনো গেম চলছে না।", "cw_cheats_no_game_next": "একটি গেম চালু করুন, তারপর চিট খুলুন।", -"cw_cheats_none": "এই গেমের জন্য কোনো চিট নেই।", -"cw_cheats_none_next": "আপনার PC-র PS5Upload অ্যাপ থেকে এই গেমের চিট যোগ করুন।", +"cw_cheats_none": "এই গেমের জন্য এখনো কোনো চিট নেই।", +"cw_cheats_none_next": "অনলাইনে এর চিট খুঁজুন, অথবা আপনার PC-র PS5Upload অ্যাপ থেকে যোগ করুন।", "cw_cheats_exited": "গেমটি বন্ধ হয়ে গেছে।", "cw_cheats_exited_next": "আবার চালু করুন, এর চিটগুলো আবার এখানে দেখা যাবে।", "cw_cheats_refresh": "রিফ্রেশ", @@ -4314,6 +4314,27 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা "cw_cheats_switching": "বদলানো হচ্ছে…", "cw_cheats_engine_off": "চিট ইঞ্জিন বন্ধ আছে। কোনো চিট চালু করলে এটি শুরু হয়।", "cw_cheats_truncated": "সব চিট এখানে ধরে না। আপনার PC-র PS5Upload অ্যাপ সবগুলো দেখায়।", +"cw_cheats_find": "চিট খুঁজুন", +"cw_cheats_find_more": "আরও চিট খুঁজুন", +"cw_cheats_find_title": "{title}-এর অনলাইন চিট", +"cw_cheats_find_loading": "অনলাইনে চিট খোঁজা হচ্ছে…", +"cw_cheats_find_hint": "চিট গেমের একটি নির্দিষ্ট ভার্সনের জন্য তৈরি হয়। আপনার ভার্সনের সাথে মেলে এমনটি বেছে নিন।", +"cw_cheats_find_version": "আপনার গেমের ভার্সন: {v}", +"cw_cheats_find_your_version": "আপনার ভার্সন", +"cw_cheats_find_any_version": "যেকোনো ভার্সন", +"cw_cheats_find_none": "এই গেমের জন্য এখনো কেউ চিট প্রকাশ করেনি।", +"cw_cheats_find_no_match": "ভার্সন {v}-এর জন্য এখনো কিছু নেই। এগুলো অন্য ভার্সনের জন্য: সাধারণত কিছুই করে না, আর গেম ক্র্যাশ করাতে পারে।", +"cw_cheats_find_offline": "এই PS5 GitHub-এ পৌঁছাতে পারছে না। আপনার PC-র PS5Upload অ্যাপ থেকে চিট নিন, অথবা DNS সেটিংসে raw.githubusercontent.com অনুমতি দিন।", +"cw_cheats_find_partial": "কিছু চিট উৎসে পৌঁছানো যায়নি, তাই তালিকাটি অসম্পূর্ণ হতে পারে।", +"cw_cheats_find_downloading": "ডাউনলোড হচ্ছে…", +"cw_cheats_find_downloaded": "ডাউনলোড হয়েছে", +"cw_cheats_find_saved": "{name} কনসোলে আছে। এর চিটগুলো এখন তালিকায় আছে।", +"cw_cheats_find_err_download": "{name} ডাউনলোড করা যায়নি ({code})।", +"cw_cheats_find_err_store": "{name} কনসোলে সংরক্ষণ করা যায়নি ({code})।", +"cw_cheats_find_too_large": "{name} চিট ফাইলের জন্য খুব বড়।", +"cw_cheats_find_retry": "আবার চেষ্টা করুন", +"cw_cheats_find_close": "বন্ধ করুন", +"cw_cheats_find_how": "✕ নির্বাচিত ফাইলটি কনসোলে ডাউনলোড করে।", "cw_cheats_err_load": "চিট পড়া যায়নি ({code})। আবার চেষ্টা করতে রিফ্রেশ চাপুন।", "cw_cheats_err_module": "এই চিটের জন্য গেমের এমন একটি অংশ দরকার যা এখনো লোড হয়নি। গেমে পরে আবার চেষ্টা করুন।", "cw_cheats_err_changed": "চিট ফাইল বদলে গেছে। তালিকা আবার পড়া হয়েছে।", diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index ae13d5ed..ab7bd193 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -4311,8 +4311,8 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}", "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_none": "Noch keine Cheats für dieses Spiel.", +"cw_cheats_none_next": "Suche online nach Cheats dafür oder füge sie 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", @@ -4322,6 +4322,27 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}", "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_find": "Cheats suchen", +"cw_cheats_find_more": "Weitere Cheats suchen", +"cw_cheats_find_title": "Online-Cheats für {title}", +"cw_cheats_find_loading": "Suche online nach Cheats…", +"cw_cheats_find_hint": "Cheats sind für eine bestimmte Spielversion gemacht. Wähle die, die zu deiner passt.", +"cw_cheats_find_version": "Deine Spielversion: {v}", +"cw_cheats_find_your_version": "Deine Version", +"cw_cheats_find_any_version": "Jede Version", +"cw_cheats_find_none": "Für dieses Spiel hat noch niemand Cheats veröffentlicht.", +"cw_cheats_find_no_match": "Noch nichts für Version {v}. Diese sind für andere Versionen: Meist bewirken sie nichts und können das Spiel abstürzen lassen.", +"cw_cheats_find_offline": "Diese PS5 erreicht GitHub nicht. Hol dir Cheats in der PS5Upload-App auf deinem PC oder erlaube raw.githubusercontent.com in deinen DNS-Einstellungen.", +"cw_cheats_find_partial": "Einige Cheat-Quellen waren nicht erreichbar, die Liste ist daher eventuell unvollständig.", +"cw_cheats_find_downloading": "Wird geladen…", +"cw_cheats_find_downloaded": "Geladen", +"cw_cheats_find_saved": "{name} ist auf der Konsole. Seine Cheats stehen jetzt in der Liste.", +"cw_cheats_find_err_download": "{name} konnte nicht heruntergeladen werden ({code}).", +"cw_cheats_find_err_store": "{name} konnte nicht auf der Konsole gespeichert werden ({code}).", +"cw_cheats_find_too_large": "{name} ist zu groß für eine Cheat-Datei.", +"cw_cheats_find_retry": "Erneut versuchen", +"cw_cheats_find_close": "Schließen", +"cw_cheats_find_how": "✕ lädt die markierte Datei auf die Konsole.", "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.", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index a4acec93..f7d538bb 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -4626,8 +4626,8 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}", "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_none": "No cheats for this game yet.", +"cw_cheats_none_next": "Find cheats for it online, or add them 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", @@ -4637,6 +4637,27 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}", "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_find": "Find cheats", +"cw_cheats_find_more": "Find more cheats", +"cw_cheats_find_title": "Cheats online for {title}", +"cw_cheats_find_loading": "Looking for cheats online…", +"cw_cheats_find_hint": "Cheats are made for one game version. Pick the one that matches yours.", +"cw_cheats_find_version": "Your game version: {v}", +"cw_cheats_find_your_version": "Your version", +"cw_cheats_find_any_version": "Any version", +"cw_cheats_find_none": "Nobody has published cheats for this game yet.", +"cw_cheats_find_no_match": "Nothing for version {v} yet. These are for other versions: they usually do nothing, and can crash the game.", +"cw_cheats_find_offline": "This PS5 can't reach GitHub. Get cheats in the PS5Upload app on your PC, or allow raw.githubusercontent.com in your DNS settings.", +"cw_cheats_find_partial": "Some cheat sources could not be reached, so the list may be incomplete.", +"cw_cheats_find_downloading": "Downloading…", +"cw_cheats_find_downloaded": "Downloaded", +"cw_cheats_find_saved": "{name} is on the console. Its cheats are in the list now.", +"cw_cheats_find_err_download": "Could not download {name} ({code}).", +"cw_cheats_find_err_store": "Could not save {name} on the console ({code}).", +"cw_cheats_find_too_large": "{name} is too large for a cheat file.", +"cw_cheats_find_retry": "Try again", +"cw_cheats_find_close": "Close", +"cw_cheats_find_how": "✕ downloads the focused file to the console.", "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.", diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index c027680a..22d35137 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -4253,8 +4253,8 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}", "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_none": "Todavía no hay trucos para este juego.", +"cw_cheats_none_next": "Busca trucos en línea o añádelos 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", @@ -4264,6 +4264,27 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}", "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_find": "Buscar trucos", +"cw_cheats_find_more": "Buscar más trucos", +"cw_cheats_find_title": "Trucos en línea para {title}", +"cw_cheats_find_loading": "Buscando trucos en línea…", +"cw_cheats_find_hint": "Los trucos se hacen para una versión concreta del juego. Elige el que coincida con la tuya.", +"cw_cheats_find_version": "Versión de tu juego: {v}", +"cw_cheats_find_your_version": "Tu versión", +"cw_cheats_find_any_version": "Cualquier versión", +"cw_cheats_find_none": "Nadie ha publicado trucos para este juego todavía.", +"cw_cheats_find_no_match": "Aún no hay nada para la versión {v}. Estos son para otras versiones: normalmente no hacen nada y pueden cerrar el juego.", +"cw_cheats_find_offline": "Esta PS5 no puede conectarse a GitHub. Consigue trucos en la app PS5Upload de tu PC o permite raw.githubusercontent.com en tus ajustes de DNS.", +"cw_cheats_find_partial": "No se pudo acceder a algunas fuentes de trucos, así que la lista puede estar incompleta.", +"cw_cheats_find_downloading": "Descargando…", +"cw_cheats_find_downloaded": "Descargado", +"cw_cheats_find_saved": "{name} ya está en la consola. Sus trucos aparecen ahora en la lista.", +"cw_cheats_find_err_download": "No se pudo descargar {name} ({code}).", +"cw_cheats_find_err_store": "No se pudo guardar {name} en la consola ({code}).", +"cw_cheats_find_too_large": "{name} es demasiado grande para ser un archivo de trucos.", +"cw_cheats_find_retry": "Reintentar", +"cw_cheats_find_close": "Cerrar", +"cw_cheats_find_how": "✕ descarga el archivo seleccionado en la consola.", "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.", diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 8c5dd565..da879da8 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -4604,8 +4604,8 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}", "cw_cheats_loading": "در حال بارگیری چیت‌ها…", "cw_cheats_no_game": "هیچ بازی‌ای در حال اجرا نیست.", "cw_cheats_no_game_next": "یک بازی را اجرا کنید، سپس چیت‌ها را باز کنید.", -"cw_cheats_none": "برای این بازی چیتی وجود ندارد.", -"cw_cheats_none_next": "چیت‌های این بازی را از برنامه PS5Upload روی رایانه‌تان اضافه کنید.", +"cw_cheats_none": "هنوز برای این بازی چیتی وجود ندارد.", +"cw_cheats_none_next": "چیت‌های آن را آنلاین پیدا کنید، یا از برنامه PS5Upload روی رایانه‌تان اضافه کنید.", "cw_cheats_exited": "بازی بسته شد.", "cw_cheats_exited_next": "دوباره اجرایش کنید تا چیت‌هایش دوباره اینجا نمایش داده شوند.", "cw_cheats_refresh": "تازه‌سازی", @@ -4615,6 +4615,27 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}", "cw_cheats_switching": "در حال تغییر…", "cw_cheats_engine_off": "موتور چیت خاموش است. با روشن کردن یک چیت راه‌اندازی می‌شود.", "cw_cheats_truncated": "همه چیت‌ها اینجا جا نمی‌شوند. برنامه PS5Upload روی رایانه‌تان همه را نشان می‌دهد.", +"cw_cheats_find": "یافتن چیت", +"cw_cheats_find_more": "یافتن چیت‌های بیشتر", +"cw_cheats_find_title": "چیت‌های آنلاین برای {title}", +"cw_cheats_find_loading": "در حال جست‌وجوی آنلاین چیت…", +"cw_cheats_find_hint": "چیت‌ها برای یک نسخه از بازی ساخته می‌شوند. موردی را انتخاب کنید که با نسخه شما جور است.", +"cw_cheats_find_version": "نسخه بازی شما: {v}", +"cw_cheats_find_your_version": "نسخه شما", +"cw_cheats_find_any_version": "هر نسخه", +"cw_cheats_find_none": "هنوز کسی برای این بازی چیتی منتشر نکرده است.", +"cw_cheats_find_no_match": "هنوز چیزی برای نسخه {v} نیست. این‌ها برای نسخه‌های دیگرند: معمولاً کاری نمی‌کنند و ممکن است بازی را از کار بیندازند.", +"cw_cheats_find_offline": "این PS5 به GitHub دسترسی ندارد. چیت‌ها را از برنامه PS5Upload روی رایانه‌تان بگیرید، یا raw.githubusercontent.com را در تنظیمات DNS مجاز کنید.", +"cw_cheats_find_partial": "به برخی منابع چیت دسترسی نبود، پس ممکن است فهرست کامل نباشد.", +"cw_cheats_find_downloading": "در حال دانلود…", +"cw_cheats_find_downloaded": "دانلود شد", +"cw_cheats_find_saved": "{name} روی کنسول است. چیت‌هایش اکنون در فهرست هستند.", +"cw_cheats_find_err_download": "دانلود {name} ممکن نشد ({code}).", +"cw_cheats_find_err_store": "ذخیره {name} روی کنسول ممکن نشد ({code}).", +"cw_cheats_find_too_large": "{name} برای یک فایل چیت بیش از حد بزرگ است.", +"cw_cheats_find_retry": "تلاش دوباره", +"cw_cheats_find_close": "بستن", +"cw_cheats_find_how": "✕ فایل انتخاب‌شده را روی کنسول دانلود می‌کند.", "cw_cheats_err_load": "خواندن چیت‌ها ممکن نشد ({code}). برای تلاش دوباره تازه‌سازی را بزنید.", "cw_cheats_err_module": "این چیت به بخشی از بازی نیاز دارد که هنوز بارگیری نشده است. بعداً در بازی دوباره امتحان کنید.", "cw_cheats_err_changed": "فایل‌های چیت تغییر کردند. فهرست دوباره خوانده شد.", diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 10edc36a..6b8ecd42 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -4321,8 +4321,8 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}", "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_none": "Pas encore de cheats pour ce jeu.", +"cw_cheats_none_next": "Cherchez des cheats en ligne, ou ajoutez-les 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", @@ -4332,6 +4332,27 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}", "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_find": "Chercher des cheats", +"cw_cheats_find_more": "Chercher d'autres cheats", +"cw_cheats_find_title": "Cheats en ligne pour {title}", +"cw_cheats_find_loading": "Recherche de cheats en ligne…", +"cw_cheats_find_hint": "Les cheats sont faits pour une version précise du jeu. Choisissez celui qui correspond à la vôtre.", +"cw_cheats_find_version": "Version de votre jeu : {v}", +"cw_cheats_find_your_version": "Votre version", +"cw_cheats_find_any_version": "Toutes versions", +"cw_cheats_find_none": "Personne n'a encore publié de cheats pour ce jeu.", +"cw_cheats_find_no_match": "Rien pour la version {v} pour l'instant. Ceux-ci visent d'autres versions : en général ils ne font rien et peuvent faire planter le jeu.", +"cw_cheats_find_offline": "Cette PS5 n'arrive pas à joindre GitHub. Récupérez des cheats dans l'app PS5Upload sur votre PC, ou autorisez raw.githubusercontent.com dans vos réglages DNS.", +"cw_cheats_find_partial": "Certaines sources de cheats sont injoignables : la liste est peut-être incomplète.", +"cw_cheats_find_downloading": "Téléchargement…", +"cw_cheats_find_downloaded": "Téléchargé", +"cw_cheats_find_saved": "{name} est sur la console. Ses cheats sont maintenant dans la liste.", +"cw_cheats_find_err_download": "Impossible de télécharger {name} ({code}).", +"cw_cheats_find_err_store": "Impossible d'enregistrer {name} sur la console ({code}).", +"cw_cheats_find_too_large": "{name} est trop volumineux pour un fichier de cheats.", +"cw_cheats_find_retry": "Réessayer", +"cw_cheats_find_close": "Fermer", +"cw_cheats_find_how": "✕ télécharge le fichier sélectionné sur la console.", "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.", diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index cdaaaab4..d4f4155b 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -4319,8 +4319,8 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ "cw_cheats_loading": "चीट लोड हो रहे हैं…", "cw_cheats_no_game": "कोई गेम नहीं चल रहा है।", "cw_cheats_no_game_next": "कोई गेम शुरू करें, फिर चीट खोलें।", -"cw_cheats_none": "इस गेम के लिए कोई चीट नहीं है।", -"cw_cheats_none_next": "इस गेम के चीट अपने PC पर PS5Upload ऐप से जोड़ें।", +"cw_cheats_none": "इस गेम के लिए अभी कोई चीट नहीं है।", +"cw_cheats_none_next": "इसके चीट ऑनलाइन ढूँढें, या अपने PC पर PS5Upload ऐप से जोड़ें।", "cw_cheats_exited": "गेम बंद हो गया।", "cw_cheats_exited_next": "इसे फिर से शुरू करें, इसके चीट यहाँ वापस आ जाएँगे।", "cw_cheats_refresh": "रीफ़्रेश करें", @@ -4330,6 +4330,27 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ "cw_cheats_switching": "बदला जा रहा है…", "cw_cheats_engine_off": "चीट इंजन बंद है। कोई चीट चालू करने पर यह शुरू हो जाता है।", "cw_cheats_truncated": "सभी चीट यहाँ नहीं समाते। आपके PC पर PS5Upload ऐप सभी दिखाता है।", +"cw_cheats_find": "चीट ढूँढें", +"cw_cheats_find_more": "और चीट ढूँढें", +"cw_cheats_find_title": "{title} के ऑनलाइन चीट", +"cw_cheats_find_loading": "ऑनलाइन चीट ढूँढे जा रहे हैं…", +"cw_cheats_find_hint": "चीट गेम के किसी एक वर्ज़न के लिए बनते हैं। अपने वर्ज़न से मेल खाने वाला चुनें।", +"cw_cheats_find_version": "आपके गेम का वर्ज़न: {v}", +"cw_cheats_find_your_version": "आपका वर्ज़न", +"cw_cheats_find_any_version": "कोई भी वर्ज़न", +"cw_cheats_find_none": "इस गेम के लिए अभी तक किसी ने चीट प्रकाशित नहीं किए हैं।", +"cw_cheats_find_no_match": "वर्ज़न {v} के लिए अभी कुछ नहीं है। ये दूसरे वर्ज़न के लिए हैं: आम तौर पर ये कुछ नहीं करते और गेम क्रैश कर सकते हैं।", +"cw_cheats_find_offline": "यह PS5 GitHub तक नहीं पहुँच पा रहा। अपने PC पर PS5Upload ऐप से चीट लें, या अपनी DNS सेटिंग में raw.githubusercontent.com को अनुमति दें।", +"cw_cheats_find_partial": "कुछ चीट स्रोतों तक नहीं पहुँचा जा सका, इसलिए सूची अधूरी हो सकती है।", +"cw_cheats_find_downloading": "डाउनलोड हो रहा है…", +"cw_cheats_find_downloaded": "डाउनलोड हो गया", +"cw_cheats_find_saved": "{name} कंसोल पर है। इसके चीट अब सूची में हैं।", +"cw_cheats_find_err_download": "{name} डाउनलोड नहीं हो सका ({code})।", +"cw_cheats_find_err_store": "{name} कंसोल पर सहेजा नहीं जा सका ({code})।", +"cw_cheats_find_too_large": "{name} चीट फ़ाइल के लिए बहुत बड़ी है।", +"cw_cheats_find_retry": "फिर कोशिश करें", +"cw_cheats_find_close": "बंद करें", +"cw_cheats_find_how": "✕ चुनी गई फ़ाइल को कंसोल पर डाउनलोड करता है।", "cw_cheats_err_load": "चीट नहीं पढ़े जा सके ({code})। फिर से कोशिश करने के लिए रीफ़्रेश दबाएँ।", "cw_cheats_err_module": "इस चीट को गेम का ऐसा हिस्सा चाहिए जो अभी लोड नहीं हुआ है। गेम में आगे चलकर फिर से कोशिश करें।", "cw_cheats_err_changed": "चीट फ़ाइलें बदल गईं। सूची फिर से पढ़ी गई।", diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index 65857672..b0fdb67c 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -4618,8 +4618,8 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}", "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_none": "Ehhez a játékhoz még nincsenek csalások.", +"cw_cheats_none_next": "Keress hozzá csalásokat online, vagy add hozzá őket 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", @@ -4629,6 +4629,27 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}", "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_find": "Csalások keresése", +"cw_cheats_find_more": "További csalások keresése", +"cw_cheats_find_title": "Online csalások ehhez: {title}", +"cw_cheats_find_loading": "Csalások keresése online…", +"cw_cheats_find_hint": "A csalások egy adott játékverzióhoz készülnek. Válaszd azt, amelyik a tiédhez illik.", +"cw_cheats_find_version": "A játékod verziója: {v}", +"cw_cheats_find_your_version": "A te verziód", +"cw_cheats_find_any_version": "Bármely verzió", +"cw_cheats_find_none": "Ehhez a játékhoz még senki sem tett közzé csalásokat.", +"cw_cheats_find_no_match": "A(z) {v} verzióhoz még nincs semmi. Ezek más verziókhoz készültek: általában nem csinálnak semmit, és összeomolhat tőlük a játék.", +"cw_cheats_find_offline": "Ez a PS5 nem éri el a GitHubot. Szerezz csalásokat a PC-n futó PS5Upload alkalmazásban, vagy engedélyezd a raw.githubusercontent.com címet a DNS-beállításokban.", +"cw_cheats_find_partial": "Néhány csalásforrás nem érhető el, ezért a lista hiányos lehet.", +"cw_cheats_find_downloading": "Letöltés…", +"cw_cheats_find_downloaded": "Letöltve", +"cw_cheats_find_saved": "A(z) {name} a konzolon van. A csalásai most már a listában vannak.", +"cw_cheats_find_err_download": "Nem sikerült letölteni: {name} ({code}).", +"cw_cheats_find_err_store": "Nem sikerült menteni a konzolra: {name} ({code}).", +"cw_cheats_find_too_large": "A(z) {name} túl nagy egy csalásfájlhoz.", +"cw_cheats_find_retry": "Újra", +"cw_cheats_find_close": "Bezárás", +"cw_cheats_find_how": "Az ✕ a kijelölt fájlt letölti a konzolra.", "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.", diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index 8ae2af09..e67f3821 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -4312,8 +4312,8 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}", "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_none": "Belum ada cheat untuk game ini.", +"cw_cheats_none_next": "Cari cheat secara online, atau tambahkan 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", @@ -4323,6 +4323,27 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}", "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_find": "Cari cheat", +"cw_cheats_find_more": "Cari cheat lain", +"cw_cheats_find_title": "Cheat online untuk {title}", +"cw_cheats_find_loading": "Mencari cheat secara online…", +"cw_cheats_find_hint": "Cheat dibuat untuk satu versi game. Pilih yang cocok dengan versi Anda.", +"cw_cheats_find_version": "Versi game Anda: {v}", +"cw_cheats_find_your_version": "Versi Anda", +"cw_cheats_find_any_version": "Semua versi", +"cw_cheats_find_none": "Belum ada yang menerbitkan cheat untuk game ini.", +"cw_cheats_find_no_match": "Belum ada untuk versi {v}. Ini untuk versi lain: biasanya tidak berfungsi dan bisa membuat game crash.", +"cw_cheats_find_offline": "PS5 ini tidak bisa menjangkau GitHub. Dapatkan cheat di aplikasi PS5Upload di PC Anda, atau izinkan raw.githubusercontent.com di pengaturan DNS Anda.", +"cw_cheats_find_partial": "Beberapa sumber cheat tidak dapat dijangkau, jadi daftarnya mungkin tidak lengkap.", +"cw_cheats_find_downloading": "Mengunduh…", +"cw_cheats_find_downloaded": "Terunduh", +"cw_cheats_find_saved": "{name} sudah ada di konsol. Cheat-nya kini ada di daftar.", +"cw_cheats_find_err_download": "Tidak dapat mengunduh {name} ({code}).", +"cw_cheats_find_err_store": "Tidak dapat menyimpan {name} di konsol ({code}).", +"cw_cheats_find_too_large": "{name} terlalu besar untuk file cheat.", +"cw_cheats_find_retry": "Coba lagi", +"cw_cheats_find_close": "Tutup", +"cw_cheats_find_how": "✕ mengunduh file yang dipilih ke konsol.", "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.", diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index e5cd616b..c1c91a01 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -4253,8 +4253,8 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}", "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_none": "Ancora nessun cheat per questo gioco.", +"cw_cheats_none_next": "Cerca i cheat online oppure aggiungili 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", @@ -4264,6 +4264,27 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}", "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_find": "Cerca cheat", +"cw_cheats_find_more": "Cerca altri cheat", +"cw_cheats_find_title": "Cheat online per {title}", +"cw_cheats_find_loading": "Ricerca dei cheat online…", +"cw_cheats_find_hint": "I cheat sono fatti per una versione precisa del gioco. Scegli quello adatto alla tua.", +"cw_cheats_find_version": "Versione del tuo gioco: {v}", +"cw_cheats_find_your_version": "La tua versione", +"cw_cheats_find_any_version": "Qualsiasi versione", +"cw_cheats_find_none": "Nessuno ha ancora pubblicato cheat per questo gioco.", +"cw_cheats_find_no_match": "Ancora niente per la versione {v}. Questi sono per altre versioni: di solito non fanno nulla e possono far chiudere il gioco.", +"cw_cheats_find_offline": "Questa PS5 non riesce a raggiungere GitHub. Ottieni i cheat dall'app PS5Upload sul tuo PC oppure consenti raw.githubusercontent.com nelle impostazioni DNS.", +"cw_cheats_find_partial": "Alcune fonti di cheat non sono raggiungibili, quindi l'elenco potrebbe essere incompleto.", +"cw_cheats_find_downloading": "Download in corso…", +"cw_cheats_find_downloaded": "Scaricato", +"cw_cheats_find_saved": "{name} è sulla console. I suoi cheat ora sono nell'elenco.", +"cw_cheats_find_err_download": "Impossibile scaricare {name} ({code}).", +"cw_cheats_find_err_store": "Impossibile salvare {name} sulla console ({code}).", +"cw_cheats_find_too_large": "{name} è troppo grande per essere un file di cheat.", +"cw_cheats_find_retry": "Riprova", +"cw_cheats_find_close": "Chiudi", +"cw_cheats_find_how": "✕ scarica sulla console il file selezionato.", "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.", diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index 741fe159..03f68493 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -4324,8 +4324,8 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail "cw_cheats_loading": "チートを読み込み中…", "cw_cheats_no_game": "ゲームが起動していません。", "cw_cheats_no_game_next": "ゲームを起動してから、チートを開いてください。", -"cw_cheats_none": "このゲームのチートはありません。", -"cw_cheats_none_next": "このゲームのチートは PC の PS5Upload アプリから追加してください。", +"cw_cheats_none": "このゲームのチートはまだありません。", +"cw_cheats_none_next": "オンラインでチートを探すか、PC の PS5Upload アプリから追加してください。", "cw_cheats_exited": "ゲームが終了しました。", "cw_cheats_exited_next": "もう一度起動すると、チートがここに戻ります。", "cw_cheats_refresh": "更新", @@ -4335,6 +4335,27 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail "cw_cheats_switching": "切り替え中…", "cw_cheats_engine_off": "チートエンジンはオフです。チートをオンにすると起動します。", "cw_cheats_truncated": "すべてのチートはここに表示しきれません。PC の PS5Upload アプリですべて確認できます。", +"cw_cheats_find": "チートを探す", +"cw_cheats_find_more": "ほかのチートを探す", +"cw_cheats_find_title": "{title} のオンラインチート", +"cw_cheats_find_loading": "オンラインでチートを探しています…", +"cw_cheats_find_hint": "チートはゲームの特定のバージョン用に作られています。お使いのバージョンに合うものを選んでください。", +"cw_cheats_find_version": "ゲームのバージョン: {v}", +"cw_cheats_find_your_version": "お使いのバージョン", +"cw_cheats_find_any_version": "全バージョン", +"cw_cheats_find_none": "このゲームのチートはまだ誰も公開していません。", +"cw_cheats_find_no_match": "バージョン {v} 用のチートはまだありません。これらは別のバージョン用です。通常は効果がなく、ゲームがクラッシュすることがあります。", +"cw_cheats_find_offline": "この PS5 から GitHub に接続できません。PC の PS5Upload アプリでチートを入手するか、DNS 設定で raw.githubusercontent.com を許可してください。", +"cw_cheats_find_partial": "一部のチート配布元に接続できなかったため、リストが不完全な可能性があります。", +"cw_cheats_find_downloading": "ダウンロード中…", +"cw_cheats_find_downloaded": "ダウンロード済み", +"cw_cheats_find_saved": "{name} を本体に保存しました。チートがリストに表示されています。", +"cw_cheats_find_err_download": "{name} をダウンロードできませんでした ({code})。", +"cw_cheats_find_err_store": "{name} を本体に保存できませんでした ({code})。", +"cw_cheats_find_too_large": "{name} はチートファイルとしては大きすぎます。", +"cw_cheats_find_retry": "再試行", +"cw_cheats_find_close": "閉じる", +"cw_cheats_find_how": "✕ で選択中のファイルを本体にダウンロードします。", "cw_cheats_err_load": "チートを読み込めませんでした ({code})。更新を押してもう一度お試しください。", "cw_cheats_err_module": "このチートには、まだ読み込まれていないゲームの部分が必要です。ゲームを進めてから再度お試しください。", "cw_cheats_err_changed": "チートファイルが変更されました。一覧を読み込み直しました。", diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index 0948f90e..bfdd1b8e 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -4322,8 +4322,8 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det "cw_cheats_loading": "치트를 불러오는 중…", "cw_cheats_no_game": "실행 중인 게임이 없습니다.", "cw_cheats_no_game_next": "게임을 시작한 다음 치트를 여세요.", -"cw_cheats_none": "이 게임의 치트가 없습니다.", -"cw_cheats_none_next": "PC의 PS5Upload 앱에서 이 게임의 치트를 추가하세요.", +"cw_cheats_none": "이 게임의 치트가 아직 없습니다.", +"cw_cheats_none_next": "온라인에서 치트를 찾거나 PC의 PS5Upload 앱에서 추가하세요.", "cw_cheats_exited": "게임이 종료되었습니다.", "cw_cheats_exited_next": "다시 시작하면 치트가 여기에 다시 나타납니다.", "cw_cheats_refresh": "새로 고침", @@ -4333,6 +4333,27 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det "cw_cheats_switching": "전환 중…", "cw_cheats_engine_off": "치트 엔진이 꺼져 있습니다. 치트를 켜면 시작됩니다.", "cw_cheats_truncated": "모든 치트를 여기에 표시할 수 없습니다. PC의 PS5Upload 앱에서 모두 볼 수 있습니다.", +"cw_cheats_find": "치트 찾기", +"cw_cheats_find_more": "치트 더 찾기", +"cw_cheats_find_title": "{title}의 온라인 치트", +"cw_cheats_find_loading": "온라인에서 치트를 찾는 중…", +"cw_cheats_find_hint": "치트는 특정 게임 버전용으로 만들어집니다. 내 버전에 맞는 것을 고르세요.", +"cw_cheats_find_version": "게임 버전: {v}", +"cw_cheats_find_your_version": "내 버전", +"cw_cheats_find_any_version": "모든 버전", +"cw_cheats_find_none": "아직 이 게임의 치트를 공개한 사람이 없습니다.", +"cw_cheats_find_no_match": "{v} 버전용 치트는 아직 없습니다. 이것들은 다른 버전용이라 보통 효과가 없고 게임이 멈출 수 있습니다.", +"cw_cheats_find_offline": "이 PS5에서 GitHub에 연결할 수 없습니다. PC의 PS5Upload 앱에서 치트를 받거나 DNS 설정에서 raw.githubusercontent.com을 허용하세요.", +"cw_cheats_find_partial": "일부 치트 출처에 연결할 수 없어 목록이 불완전할 수 있습니다.", +"cw_cheats_find_downloading": "다운로드 중…", +"cw_cheats_find_downloaded": "다운로드됨", +"cw_cheats_find_saved": "{name}이(가) 콘솔에 저장되었습니다. 이제 치트가 목록에 있습니다.", +"cw_cheats_find_err_download": "{name}을(를) 다운로드할 수 없습니다 ({code}).", +"cw_cheats_find_err_store": "{name}을(를) 콘솔에 저장할 수 없습니다 ({code}).", +"cw_cheats_find_too_large": "{name}은(는) 치트 파일로는 너무 큽니다.", +"cw_cheats_find_retry": "다시 시도", +"cw_cheats_find_close": "닫기", +"cw_cheats_find_how": "✕ 버튼으로 선택한 파일을 콘솔에 다운로드합니다.", "cw_cheats_err_load": "치트를 읽지 못했습니다 ({code}). 새로 고침을 눌러 다시 시도하세요.", "cw_cheats_err_module": "이 치트에는 아직 로드되지 않은 게임 부분이 필요합니다. 게임을 더 진행한 뒤 다시 시도하세요.", "cw_cheats_err_changed": "치트 파일이 바뀌었습니다. 목록을 다시 읽었습니다.", diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index 12134915..1eb638c8 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -4558,8 +4558,8 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}", "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_none": "Na razie brak kodów dla tej gry.", +"cw_cheats_none_next": "Poszukaj kodów w internecie albo dodaj je 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ż", @@ -4569,6 +4569,27 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}", "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_find": "Szukaj kodów", +"cw_cheats_find_more": "Szukaj więcej kodów", +"cw_cheats_find_title": "Kody online dla {title}", +"cw_cheats_find_loading": "Szukanie kodów w internecie…", +"cw_cheats_find_hint": "Kody są tworzone dla jednej wersji gry. Wybierz ten, który pasuje do Twojej.", +"cw_cheats_find_version": "Wersja Twojej gry: {v}", +"cw_cheats_find_your_version": "Twoja wersja", +"cw_cheats_find_any_version": "Dowolna wersja", +"cw_cheats_find_none": "Nikt jeszcze nie opublikował kodów dla tej gry.", +"cw_cheats_find_no_match": "Na razie nic dla wersji {v}. Te są dla innych wersji: zwykle nic nie robią i mogą zawiesić grę.", +"cw_cheats_find_offline": "Ta konsola PS5 nie może połączyć się z GitHubem. Pobierz kody w aplikacji PS5Upload na komputerze albo zezwól na raw.githubusercontent.com w ustawieniach DNS.", +"cw_cheats_find_partial": "Niektóre źródła kodów są nieosiągalne, więc lista może być niepełna.", +"cw_cheats_find_downloading": "Pobieranie…", +"cw_cheats_find_downloaded": "Pobrano", +"cw_cheats_find_saved": "{name} jest już na konsoli. Jego kody są teraz na liście.", +"cw_cheats_find_err_download": "Nie udało się pobrać {name} ({code}).", +"cw_cheats_find_err_store": "Nie udało się zapisać {name} na konsoli ({code}).", +"cw_cheats_find_too_large": "{name} jest za duży jak na plik z kodami.", +"cw_cheats_find_retry": "Spróbuj ponownie", +"cw_cheats_find_close": "Zamknij", +"cw_cheats_find_how": "✕ pobiera zaznaczony plik na konsolę.", "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.", diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index 996f8ff2..51824dfa 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -4320,8 +4320,8 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}", "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_none": "Ainda não há cheats para este jogo.", +"cw_cheats_none_next": "Procure cheats on-line ou adicione-os 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", @@ -4331,6 +4331,27 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}", "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_find": "Procurar cheats", +"cw_cheats_find_more": "Procurar mais cheats", +"cw_cheats_find_title": "Cheats on-line para {title}", +"cw_cheats_find_loading": "Procurando cheats on-line…", +"cw_cheats_find_hint": "Cheats são feitos para uma versão específica do jogo. Escolha o que corresponde à sua.", +"cw_cheats_find_version": "Versão do seu jogo: {v}", +"cw_cheats_find_your_version": "Sua versão", +"cw_cheats_find_any_version": "Qualquer versão", +"cw_cheats_find_none": "Ninguém publicou cheats para este jogo ainda.", +"cw_cheats_find_no_match": "Ainda não há nada para a versão {v}. Estes são para outras versões: geralmente não fazem nada e podem travar o jogo.", +"cw_cheats_find_offline": "Este PS5 não consegue acessar o GitHub. Obtenha cheats no app PS5Upload do seu PC ou libere raw.githubusercontent.com nas suas configurações de DNS.", +"cw_cheats_find_partial": "Algumas fontes de cheats não puderam ser acessadas, então a lista pode estar incompleta.", +"cw_cheats_find_downloading": "Baixando…", +"cw_cheats_find_downloaded": "Baixado", +"cw_cheats_find_saved": "{name} está no console. Os cheats dele já estão na lista.", +"cw_cheats_find_err_download": "Não foi possível baixar {name} ({code}).", +"cw_cheats_find_err_store": "Não foi possível salvar {name} no console ({code}).", +"cw_cheats_find_too_large": "{name} é grande demais para um arquivo de cheats.", +"cw_cheats_find_retry": "Tentar de novo", +"cw_cheats_find_close": "Fechar", +"cw_cheats_find_how": "✕ baixa o arquivo selecionado para o console.", "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.", diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index 7d9efb59..1f6ef51f 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -4253,8 +4253,8 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла: "cw_cheats_loading": "Загрузка читов…", "cw_cheats_no_game": "Игра не запущена.", "cw_cheats_no_game_next": "Запустите игру, затем откройте «Читы».", -"cw_cheats_none": "Для этой игры нет читов.", -"cw_cheats_none_next": "Добавьте читы для этой игры в приложении PS5Upload на ПК.", +"cw_cheats_none": "Для этой игры пока нет читов.", +"cw_cheats_none_next": "Найдите читы в интернете или добавьте их в приложении PS5Upload на ПК.", "cw_cheats_exited": "Игра закрыта.", "cw_cheats_exited_next": "Запустите её снова, и её читы снова появятся здесь.", "cw_cheats_refresh": "Обновить", @@ -4264,6 +4264,27 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла: "cw_cheats_switching": "Переключение…", "cw_cheats_engine_off": "Движок читов выключен. Он запустится, когда вы включите чит.", "cw_cheats_truncated": "Здесь помещаются не все читы. Приложение PS5Upload на ПК показывает все.", +"cw_cheats_find": "Найти читы", +"cw_cheats_find_more": "Найти ещё читы", +"cw_cheats_find_title": "Читы в сети для {title}", +"cw_cheats_find_loading": "Ищем читы в интернете…", +"cw_cheats_find_hint": "Читы делаются под одну версию игры. Выберите тот, что подходит к вашей.", +"cw_cheats_find_version": "Версия вашей игры: {v}", +"cw_cheats_find_your_version": "Ваша версия", +"cw_cheats_find_any_version": "Любая версия", +"cw_cheats_find_none": "Для этой игры пока никто не опубликовал читы.", +"cw_cheats_find_no_match": "Для версии {v} пока ничего нет. Эти — для других версий: обычно они ничего не делают и могут привести к сбою игры.", +"cw_cheats_find_offline": "Эта PS5 не может подключиться к GitHub. Возьмите читы в приложении PS5Upload на ПК или разрешите raw.githubusercontent.com в настройках DNS.", +"cw_cheats_find_partial": "Некоторые источники читов недоступны, поэтому список может быть неполным.", +"cw_cheats_find_downloading": "Загрузка…", +"cw_cheats_find_downloaded": "Загружено", +"cw_cheats_find_saved": "{name} теперь на консоли. Его читы уже в списке.", +"cw_cheats_find_err_download": "Не удалось скачать {name} ({code}).", +"cw_cheats_find_err_store": "Не удалось сохранить {name} на консоли ({code}).", +"cw_cheats_find_too_large": "{name} слишком большой для файла читов.", +"cw_cheats_find_retry": "Повторить", +"cw_cheats_find_close": "Закрыть", +"cw_cheats_find_how": "✕ загружает выбранный файл на консоль.", "cw_cheats_err_load": "Не удалось прочитать читы ({code}). Нажмите «Обновить», чтобы повторить.", "cw_cheats_err_module": "Этому читу нужна часть игры, которая ещё не загружена. Попробуйте позже в игре.", "cw_cheats_err_changed": "Файлы читов изменились. Список прочитан заново.", diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index 19fed8ca..b9c9f2e8 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -4312,8 +4312,8 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ "cw_cheats_loading": "กำลังโหลดสูตรโกง…", "cw_cheats_no_game": "ไม่มีเกมที่กำลังทำงาน", "cw_cheats_no_game_next": "เริ่มเกมก่อน แล้วจึงเปิดเมนูโกง", -"cw_cheats_none": "ไม่มีสูตรโกงสำหรับเกมนี้", -"cw_cheats_none_next": "เพิ่มสูตรโกงสำหรับเกมนี้จากแอป PS5Upload บนพีซีของคุณ", +"cw_cheats_none": "ยังไม่มีสูตรโกงสำหรับเกมนี้", +"cw_cheats_none_next": "ค้นหาสูตรโกงทางออนไลน์ หรือเพิ่มจากแอป PS5Upload บนพีซีของคุณ", "cw_cheats_exited": "เกมถูกปิดแล้ว", "cw_cheats_exited_next": "เริ่มเกมอีกครั้ง แล้วสูตรโกงจะกลับมาที่นี่", "cw_cheats_refresh": "รีเฟรช", @@ -4323,6 +4323,27 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ "cw_cheats_switching": "กำลังสลับ…", "cw_cheats_engine_off": "เอนจินสูตรโกงปิดอยู่ จะเริ่มทำงานเมื่อคุณเปิดสูตรโกง", "cw_cheats_truncated": "แสดงสูตรโกงได้ไม่ครบที่นี่ แอป PS5Upload บนพีซีของคุณแสดงได้ทั้งหมด", +"cw_cheats_find": "ค้นหาสูตรโกง", +"cw_cheats_find_more": "ค้นหาสูตรโกงเพิ่ม", +"cw_cheats_find_title": "สูตรโกงออนไลน์สำหรับ {title}", +"cw_cheats_find_loading": "กำลังค้นหาสูตรโกงทางออนไลน์…", +"cw_cheats_find_hint": "สูตรโกงทำมาสำหรับเกมเวอร์ชันเดียว เลือกอันที่ตรงกับเวอร์ชันของคุณ", +"cw_cheats_find_version": "เวอร์ชันเกมของคุณ: {v}", +"cw_cheats_find_your_version": "เวอร์ชันของคุณ", +"cw_cheats_find_any_version": "ทุกเวอร์ชัน", +"cw_cheats_find_none": "ยังไม่มีใครเผยแพร่สูตรโกงสำหรับเกมนี้", +"cw_cheats_find_no_match": "ยังไม่มีสำหรับเวอร์ชัน {v} อันเหล่านี้สำหรับเวอร์ชันอื่น: มักไม่มีผลและอาจทำให้เกมค้าง", +"cw_cheats_find_offline": "PS5 เครื่องนี้เชื่อมต่อ GitHub ไม่ได้ รับสูตรโกงจากแอป PS5Upload บนพีซีของคุณ หรืออนุญาต raw.githubusercontent.com ในการตั้งค่า DNS", +"cw_cheats_find_partial": "เข้าถึงแหล่งสูตรโกงบางแห่งไม่ได้ รายการจึงอาจไม่ครบ", +"cw_cheats_find_downloading": "กำลังดาวน์โหลด…", +"cw_cheats_find_downloaded": "ดาวน์โหลดแล้ว", +"cw_cheats_find_saved": "{name} อยู่บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", +"cw_cheats_find_err_download": "ดาวน์โหลด {name} ไม่ได้ ({code})", +"cw_cheats_find_err_store": "บันทึก {name} ลงเครื่องไม่ได้ ({code})", +"cw_cheats_find_too_large": "{name} ใหญ่เกินกว่าจะเป็นไฟล์สูตรโกง", +"cw_cheats_find_retry": "ลองอีกครั้ง", +"cw_cheats_find_close": "ปิด", +"cw_cheats_find_how": "✕ ดาวน์โหลดไฟล์ที่เลือกลงเครื่อง", "cw_cheats_err_load": "อ่านสูตรโกงไม่ได้ ({code}) กดรีเฟรชเพื่อลองอีกครั้ง", "cw_cheats_err_module": "สูตรโกงนี้ต้องใช้ส่วนของเกมที่ยังไม่ได้โหลด ลองอีกครั้งภายหลังในเกม", "cw_cheats_err_changed": "ไฟล์สูตรโกงเปลี่ยนไป อ่านรายการใหม่แล้ว", diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index d85cd7f9..91f9a5ef 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -4310,8 +4310,8 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}", "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_none": "Bu oyun için henüz hile yok.", +"cw_cheats_none_next": "Hileleri internette arayın ya da 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", @@ -4321,6 +4321,27 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}", "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_find": "Hile ara", +"cw_cheats_find_more": "Daha fazla hile ara", +"cw_cheats_find_title": "{title} için çevrimiçi hileler", +"cw_cheats_find_loading": "Hileler internette aranıyor…", +"cw_cheats_find_hint": "Hileler oyunun belirli bir sürümü için yapılır. Sizinkine uyanı seçin.", +"cw_cheats_find_version": "Oyununuzun sürümü: {v}", +"cw_cheats_find_your_version": "Sizin sürümünüz", +"cw_cheats_find_any_version": "Her sürüm", +"cw_cheats_find_none": "Bu oyun için henüz kimse hile yayımlamadı.", +"cw_cheats_find_no_match": "{v} sürümü için henüz bir şey yok. Bunlar başka sürümler için: genellikle işe yaramaz ve oyunun çökmesine yol açabilir.", +"cw_cheats_find_offline": "Bu PS5 GitHub'a ulaşamıyor. Hileleri bilgisayarınızdaki PS5Upload uygulamasından alın veya DNS ayarlarınızda raw.githubusercontent.com adresine izin verin.", +"cw_cheats_find_partial": "Bazı hile kaynaklarına ulaşılamadı, bu yüzden liste eksik olabilir.", +"cw_cheats_find_downloading": "İndiriliyor…", +"cw_cheats_find_downloaded": "İndirildi", +"cw_cheats_find_saved": "{name} konsolda. Hileleri artık listede.", +"cw_cheats_find_err_download": "{name} indirilemedi ({code}).", +"cw_cheats_find_err_store": "{name} konsola kaydedilemedi ({code}).", +"cw_cheats_find_too_large": "{name} bir hile dosyası için fazla büyük.", +"cw_cheats_find_retry": "Tekrar dene", +"cw_cheats_find_close": "Kapat", +"cw_cheats_find_how": "✕ seçili dosyayı konsola indirir.", "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.", diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index a1c68395..ad76c53e 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -4305,8 +4305,8 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}", "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_none": "Chưa có cheat cho trò chơi này.", +"cw_cheats_none_next": "Tìm cheat trực tuyến, hoặc thêm 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", @@ -4316,6 +4316,27 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}", "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_find": "Tìm cheat", +"cw_cheats_find_more": "Tìm thêm cheat", +"cw_cheats_find_title": "Cheat trực tuyến cho {title}", +"cw_cheats_find_loading": "Đang tìm cheat trực tuyến…", +"cw_cheats_find_hint": "Cheat được làm cho một phiên bản trò chơi. Hãy chọn cái khớp với phiên bản của bạn.", +"cw_cheats_find_version": "Phiên bản trò chơi của bạn: {v}", +"cw_cheats_find_your_version": "Phiên bản của bạn", +"cw_cheats_find_any_version": "Mọi phiên bản", +"cw_cheats_find_none": "Chưa ai đăng cheat cho trò chơi này.", +"cw_cheats_find_no_match": "Chưa có gì cho phiên bản {v}. Những cái này dành cho phiên bản khác: thường không có tác dụng và có thể làm trò chơi bị lỗi.", +"cw_cheats_find_offline": "PS5 này không kết nối được GitHub. Hãy lấy cheat bằng ứng dụng PS5Upload trên PC, hoặc cho phép raw.githubusercontent.com trong cài đặt DNS.", +"cw_cheats_find_partial": "Không truy cập được một số nguồn cheat, nên danh sách có thể chưa đầy đủ.", +"cw_cheats_find_downloading": "Đang tải xuống…", +"cw_cheats_find_downloaded": "Đã tải xuống", +"cw_cheats_find_saved": "{name} đã có trên máy. Các cheat của nó giờ đã ở trong danh sách.", +"cw_cheats_find_err_download": "Không tải xuống được {name} ({code}).", +"cw_cheats_find_err_store": "Không lưu được {name} lên máy ({code}).", +"cw_cheats_find_too_large": "{name} quá lớn để là tệp cheat.", +"cw_cheats_find_retry": "Thử lại", +"cw_cheats_find_close": "Đóng", +"cw_cheats_find_how": "✕ tải tệp đang chọn xuống máy.", "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.", diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index f5c60eec..33c623b2 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -4259,8 +4259,8 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}", "cw_cheats_loading": "正在加载金手指…", "cw_cheats_no_game": "没有正在运行的游戏。", "cw_cheats_no_game_next": "请先启动游戏,再打开金手指。", -"cw_cheats_none": "此游戏没有金手指。", -"cw_cheats_none_next": "请在电脑上的 PS5Upload 应用中为此游戏添加金手指。", +"cw_cheats_none": "此游戏还没有金手指。", +"cw_cheats_none_next": "可以在线查找金手指,或在电脑上的 PS5Upload 应用中添加。", "cw_cheats_exited": "游戏已关闭。", "cw_cheats_exited_next": "再次启动游戏,它的金手指会重新显示在这里。", "cw_cheats_refresh": "刷新", @@ -4270,6 +4270,27 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}", "cw_cheats_switching": "正在切换…", "cw_cheats_engine_off": "金手指引擎已关闭。开启任一金手指时会自动启动。", "cw_cheats_truncated": "此处无法显示全部金手指。电脑上的 PS5Upload 应用可显示全部。", +"cw_cheats_find": "查找金手指", +"cw_cheats_find_more": "查找更多金手指", +"cw_cheats_find_title": "{title} 的在线金手指", +"cw_cheats_find_loading": "正在在线查找金手指…", +"cw_cheats_find_hint": "金手指只适用于特定的游戏版本。请选择与你的版本匹配的那个。", +"cw_cheats_find_version": "你的游戏版本:{v}", +"cw_cheats_find_your_version": "你的版本", +"cw_cheats_find_any_version": "任意版本", +"cw_cheats_find_none": "还没有人发布此游戏的金手指。", +"cw_cheats_find_no_match": "暂时没有适用于版本 {v} 的金手指。这些适用于其他版本:通常没有效果,还可能让游戏崩溃。", +"cw_cheats_find_offline": "这台 PS5 无法连接 GitHub。请在电脑上的 PS5Upload 应用中获取金手指,或在 DNS 设置中允许 raw.githubusercontent.com。", +"cw_cheats_find_partial": "部分金手指来源无法访问,列表可能不完整。", +"cw_cheats_find_downloading": "正在下载…", +"cw_cheats_find_downloaded": "已下载", +"cw_cheats_find_saved": "{name} 已保存到主机。它的金手指现在已在列表中。", +"cw_cheats_find_err_download": "无法下载 {name}({code})。", +"cw_cheats_find_err_store": "无法将 {name} 保存到主机({code})。", +"cw_cheats_find_too_large": "{name} 太大,不像是金手指文件。", +"cw_cheats_find_retry": "重试", +"cw_cheats_find_close": "关闭", +"cw_cheats_find_how": "按 ✕ 将选中的文件下载到主机。", "cw_cheats_err_load": "无法读取金手指({code})。请按刷新重试。", "cw_cheats_err_module": "此金手指需要游戏中尚未加载的部分。请在游戏中稍后再试。", "cw_cheats_err_changed": "金手指文件已更改,已重新读取列表。", diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index e2157e2c..730980aa 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -4253,8 +4253,8 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}", "cw_cheats_loading": "正在載入金手指…", "cw_cheats_no_game": "沒有正在執行的遊戲。", "cw_cheats_no_game_next": "請先啟動遊戲,再開啟金手指。", -"cw_cheats_none": "此遊戲沒有金手指。", -"cw_cheats_none_next": "請在電腦上的 PS5Upload 應用程式中為此遊戲新增金手指。", +"cw_cheats_none": "此遊戲還沒有金手指。", +"cw_cheats_none_next": "可以在線上尋找金手指,或在電腦上的 PS5Upload 應用程式中新增。", "cw_cheats_exited": "遊戲已關閉。", "cw_cheats_exited_next": "再次啟動遊戲,它的金手指會重新顯示在這裡。", "cw_cheats_refresh": "重新整理", @@ -4264,6 +4264,27 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}", "cw_cheats_switching": "正在切換…", "cw_cheats_engine_off": "金手指引擎已關閉。開啟任一金手指時會自動啟動。", "cw_cheats_truncated": "此處無法顯示全部金手指。電腦上的 PS5Upload 應用程式可顯示全部。", +"cw_cheats_find": "尋找金手指", +"cw_cheats_find_more": "尋找更多金手指", +"cw_cheats_find_title": "{title} 的線上金手指", +"cw_cheats_find_loading": "正在線上尋找金手指…", +"cw_cheats_find_hint": "金手指只適用於特定的遊戲版本。請選擇與你的版本相符的那個。", +"cw_cheats_find_version": "你的遊戲版本:{v}", +"cw_cheats_find_your_version": "你的版本", +"cw_cheats_find_any_version": "任何版本", +"cw_cheats_find_none": "還沒有人發布此遊戲的金手指。", +"cw_cheats_find_no_match": "目前沒有適用於版本 {v} 的金手指。這些適用於其他版本:通常沒有效果,還可能讓遊戲當機。", +"cw_cheats_find_offline": "這台 PS5 無法連線到 GitHub。請在電腦上的 PS5Upload 應用程式中取得金手指,或在 DNS 設定中允許 raw.githubusercontent.com。", +"cw_cheats_find_partial": "部分金手指來源無法連線,清單可能不完整。", +"cw_cheats_find_downloading": "正在下載…", +"cw_cheats_find_downloaded": "已下載", +"cw_cheats_find_saved": "{name} 已儲存到主機。它的金手指現在已在清單中。", +"cw_cheats_find_err_download": "無法下載 {name}({code})。", +"cw_cheats_find_err_store": "無法將 {name} 儲存到主機({code})。", +"cw_cheats_find_too_large": "{name} 太大,不像是金手指檔案。", +"cw_cheats_find_retry": "重試", +"cw_cheats_find_close": "關閉", +"cw_cheats_find_how": "按 ✕ 將選取的檔案下載到主機。", "cw_cheats_err_load": "無法讀取金手指({code})。請按重新整理再試一次。", "cw_cheats_err_module": "此金手指需要遊戲中尚未載入的部分。請在遊戲中稍後再試。", "cw_cheats_err_changed": "金手指檔案已變更,已重新讀取清單。", diff --git a/payload/Makefile b/payload/Makefile index 69cae94c..cf63c343 100644 --- a/payload/Makefile +++ b/payload/Makefile @@ -194,7 +194,7 @@ SRCS := src/main.c src/runtime.c src/shell_builtin.c src/takeover.c src/takeover src/tmdb.c src/fw_spoof.c src/focus_probe.c \ src/content_db.c src/mgmt_rpc.c src/fs_jobs.c src/net_probe.c src/net_ports.c src/mgmt_fs.c src/path_policy.c \ src/webui_http.c src/webui_auth.c src/webui_server.c src/webui_codec.c src/webui_api.c \ - src/webui_settings.c src/webui_inputlog.c src/webui_files.c src/webui_tile.c src/webui_strings.c src/webui_offline.c src/files_prefs.c src/files_handoff.c $(WEBUI_TILE_ICON_C) \ + src/webui_settings.c src/webui_inputlog.c src/webui_files.c src/webui_cheats_store.c src/webui_tile.c src/webui_strings.c src/webui_offline.c src/files_prefs.c src/files_handoff.c $(WEBUI_TILE_ICON_C) \ $(WEBUI_PAGE_C) $(WEBUI_STRINGS_C) $(WEBUI_OFFLINE_C) \ third_party/tiny_aes/aes.c \ $(BLAKE3_SRCS) diff --git a/payload/include/webui_cheats_store.h b/payload/include/webui_cheats_store.h new file mode 100644 index 00000000..19b41c91 --- /dev/null +++ b/payload/include/webui_cheats_store.h @@ -0,0 +1,39 @@ +/* The console page's cheat download (webui_api.c is the glue): the page fetches a cheat file from a + * community repo in the browser and POSTs it to /api/cheats/store?format=&name=; these pure + * pieces check that request and place the file where the cheat engine looks (cheats.c scans + * CHEATS_{JSON,SHN,MC4}_DIR on every list/get, so a new file needs no reload). The bytes themselves + * go through wf_receive_file (webui_files.h): a same-folder temp file renamed after a same-device + * check, never across a mount. Host-tested by payload/tests/webui_cheats_store_selftest.c. */ +#ifndef PS5UPLOAD_WEBUI_CHEATS_STORE_H +#define PS5UPLOAD_WEBUI_CHEATS_STORE_H + +#include + +#define WEBUI_CHEATS_ROOT "/data/ps5upload/cheats" /* cheats.c CHEATS_ROOT */ +#define WEBUI_CHEAT_FILE_MAX (4u * 1024u * 1024u) /* cheats.c CHEAT_FILE_MAX: a larger file never loads */ +#define WEBUI_CHEAT_NAME_MAX 128u /* bytes, NUL not counted */ +#define WEBUI_CHEAT_FMT_CAP 8u + +/* "json", "shn" or "mc4" (exactly, lower case): 1, else 0. */ +int webui_cheat_format_ok(const char *fmt); + +/* A file name the store accepts for `fmt`: a plain base name of 1..WEBUI_CHEAT_NAME_MAX bytes from + * [A-Za-z0-9._-], not starting with '.', no "..", and ending in "." (any case) after a non-empty + * stem. 1, else 0. */ +int webui_cheat_name_ok(const char *fmt, const char *name); + +/* The query "format=&name=" (either order; other keys ignored; no percent-escapes: every + * byte a name may hold is sent as is) into `fmt` and `name`. NULL when usable, else the error token + * ("bad_format" or "bad_name"; a key given twice is refused too). */ +const char *webui_cheat_store_query(const char *query, char fmt[WEBUI_CHEAT_FMT_CAP], char name[WEBUI_CHEAT_NAME_MAX + 1]); + +/* "//" into `out`, creating and / (0777) when missing. 0, or -1 + * when it does not fit or the folder cannot be made (or is not a real folder: a link is refused). */ +int webui_cheat_target(const char *root, const char *fmt, const char *name, char *out, size_t cap); + +/* The game's version from an APP_INFO_QUERY reply ({"ok":true,"rows":[{"key":..,"val":..}..]}): + * CONTENT_VERSION (PS5, "01.000.016") first, else APP_VER (PS4, "01.05"), the same forms the cheat + * files name their target version in. Only digits and dots, 1..cap-1 bytes. 1 found, else 0 and "". */ +int webui_cheat_appinfo_version(const char *json, size_t n, char *out, size_t cap); + +#endif diff --git a/payload/src/webui_api.c b/payload/src/webui_api.c index 6c914782..f92d1166 100644 --- a/payload/src/webui_api.c +++ b/payload/src/webui_api.c @@ -32,6 +32,7 @@ #include "path_policy.h" #include "proc_identity.h" #include "ps5_firmware.h" +#include "webui_cheats_store.h" #include "webui_codec.h" #include "webui_files.h" #include "webui_inputlog.h" @@ -1274,6 +1275,24 @@ static void r_cheats_toggle(const char *b, size_t n, uint8_t *out, int fd) { } } +/* {title_id}: the game's installed version, read like the PC app reads it (APP_INFO_QUERY's + * CONTENT_VERSION, else APP_VER): {"version":"01.05"} or {"version":null} when appinfo.db does not + * say (the page then lists every version's cheats). Never an error for an unknown version. */ +static void r_cheats_version(const char *b, size_t n, uint8_t *out, int fd) { + char title[32], req[96], ver[32], json[64]; + const uint8_t *t; + size_t ol, tl; + int st, k; + if (need_title(fd, b, n, title, sizeof title) != 0) return; + k = snprintf(req, sizeof req, "{\"title_id\":\"%s\",\"keys\":\"CONTENT_VERSION,APP_VER\"}", title); + st = call_text(AVA1_METHOD_APP_INFO_QUERY, req, (size_t)k, out, &ol, &t, &tl); + if (st == AVA1_STATUS_OK && webui_cheat_appinfo_version((const char *)t, tl, ver, sizeof ver) == 1) + k = snprintf(json, sizeof json, "{\"version\":\"%s\"}", ver); /* digits and dots only */ + else + k = snprintf(json, sizeof json, "{\"version\":null}"); + send_json(fd, 200, json, (size_t)k); +} + /* ---- search: the helper's file index (INDEX_START / STATUS / SEARCH / CANCEL, runtime.c) ---- * * One index for the whole helper (its own thread walks the roots: at most 200,000 files and 32 MiB of @@ -1797,6 +1816,64 @@ static void s_save_text(const webui_req_t *r, const uint8_t *pre, size_t pre_len pthread_mutex_unlock(&g_text_mu); } +/* One cheat store at a time: two pages saving the same name would race their renames. */ +static pthread_mutex_t g_cheat_mu = PTHREAD_MUTEX_INITIALIZER; +#define CHEAT_STORE_BUF (256u * 1024u) + +/* A cheat file the page downloaded from a community repo (stream): ?format=json|shn|mc4&name=, + * the raw bytes as the body (1..WEBUI_CHEAT_FILE_MAX). Stored as WEBUI_CHEATS_ROOT// + * (folders made when missing) through a same-folder temp + rename, replacing a file of that name. The + * cheat engine reads its folders on every list, so the game's cheats show on the next read. Answers + * {"ok":true,"format":..,"name":..,"size":N}. */ +static void s_cheats_store(const webui_req_t *r, const uint8_t *pre, size_t pre_len, int fd) { + char fmt[WEBUI_CHEAT_FMT_CAP], name[WEBUI_CHEAT_NAME_MAX + 1], target[API_PATH], dir[API_PATH], json[256]; + const char *bad; + uint8_t *buf; + uint64_t total = r->content_length; + uint64_t left = total > pre_len ? total - pre_len : 0; + int rc; + bad = webui_cheat_store_query(r->query, fmt, name); + if (bad) { + drain_then(fd, left); + send_bad(fd, bad); + return; + } + if (total == 0) { + send_bad(fd, "empty"); + return; + } + if (total > WEBUI_CHEAT_FILE_MAX) { + drain_then(fd, left); + send_error(fd, 413, "too_large", 9); + return; + } + if (pthread_mutex_trylock(&g_cheat_mu) != 0) { + drain_then(fd, left); + send_error(fd, 409, "busy", 4); + return; + } + buf = malloc(CHEAT_STORE_BUF); + if (!buf || webui_cheat_target(WEBUI_CHEATS_ROOT, fmt, name, target, sizeof target) != 0) { + const char *why = buf ? "io_error" : "out_of_memory"; + pthread_mutex_unlock(&g_cheat_mu); + free(buf); + drain_then(fd, left); + send_error(fd, 500, why, strlen(why)); + return; + } + if (folder_of(target, dir, sizeof dir) == 0) (void)wf_sweep_stale(dir, time(NULL), WF_STALE_PART_S); + rc = wf_receive_file(fd, pre, pre_len, total, target, 1, 0777, WF_IDLE_MS, NULL, buf, CHEAT_STORE_BUF); + free(buf); + pthread_mutex_unlock(&g_cheat_mu); + if (rc != WF_OK) { + send_wf(fd, rc); + return; + } + rc = snprintf(json, sizeof json, "{\"ok\":true,\"format\":\"%s\",\"name\":\"%s\",\"size\":%llu}", fmt, name, + (unsigned long long)total); /* both checked: [A-Za-z0-9._-] only */ + send_json(fd, 200, json, (size_t)rc); +} + /* A URL path segment: unreserved bytes as they are, the rest %XX. */ static size_t url_segment(char *out, size_t cap, const char *s) { static const char hex[] = "0123456789ABCDEF"; @@ -2084,13 +2161,16 @@ static const struct { { "/api/cheats/status", r_cheats_status }, { "/api/cheats/list", r_cheats_list }, { "/api/cheats/toggle", r_cheats_toggle }, + { "/api/cheats/version", r_cheats_version }, { "/api/index/start", r_index_start }, { "/api/index/status", r_index_status }, { "/api/index/search", r_index_search }, { "/api/index/cancel", r_index_cancel }, }; -/* The page's own headers: only same-origin fetches, inline script/style (the page is one file), +/* The page's own headers: same-origin fetches plus the two GitHub hosts the cheat finder reads + * (raw.githubusercontent.com for the repos' index and cheat files, api.github.com for the one repo + * listed through the git-trees API; both answer CORS-open, and only GET), inline script/style (the page is one file), * no-cache rather than no-store (revalidated on every visit, but storable: the appcache keeps it as * the offline copy; webui_offline.h), * data:/blob: images; nosniff is on every response already. Never framed (frame-ancestors plus @@ -2099,7 +2179,8 @@ static const struct { #define PAGE_HEADERS \ "Content-Encoding: gzip\r\n" \ "Cache-Control: no-cache\r\n" \ - "Content-Security-Policy: default-src 'self' 'unsafe-inline' data: blob:; connect-src 'self'; " \ + "Content-Security-Policy: default-src 'self' 'unsafe-inline' data: blob:; " \ + "connect-src 'self' https://raw.githubusercontent.com https://api.github.com; " \ "frame-ancestors 'none'\r\n" \ "X-Frame-Options: DENY\r\n" /* webui_send_ex builds the head in 512 bytes and sends nothing when it does not fit: the fixed @@ -2164,6 +2245,7 @@ static const struct { } k_streams[] = { { "/api/fs/save-text", s_save_text }, { "/api/upload/data", s_upload_data }, + { "/api/cheats/store", s_cheats_store }, }; int webui_api_stream_for(const webui_req_t *r) { diff --git a/payload/src/webui_cheats_store.c b/payload/src/webui_cheats_store.c new file mode 100644 index 00000000..a4eb8d02 --- /dev/null +++ b/payload/src/webui_cheats_store.c @@ -0,0 +1,151 @@ +/* See include/webui_cheats_store.h. */ +#include "webui_cheats_store.h" + +#include +#include +#include +#include +#include + +#include "webui_codec.h" + +int webui_cheat_format_ok(const char *fmt) { + return fmt && (strcmp(fmt, "json") == 0 || strcmp(fmt, "shn") == 0 || strcmp(fmt, "mc4") == 0); +} + +static int name_byte(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || + c == '-'; +} + +int webui_cheat_name_ok(const char *fmt, const char *name) { + size_t n, fl, i; + if (!webui_cheat_format_ok(fmt) || !name) return 0; + n = strlen(name); + fl = strlen(fmt); + if (n == 0 || n > WEBUI_CHEAT_NAME_MAX || name[0] == '.' || strstr(name, "..")) return 0; + for (i = 0; i < n; i++) + if (!name_byte(name[i])) return 0; + /* a stem, then ".": the engine picks files by that extension (cheats.c find_cheat_files) */ + if (n < fl + 2 || name[n - fl - 1] != '.' || strcasecmp(name + n - fl, fmt) != 0) return 0; + return 1; +} + +/* The value of `key` in the query into `out`: 1 found once, 0 absent, -1 twice or too long. */ +static int query_value(const char *q, const char *key, char *out, size_t cap) { + size_t kl = strlen(key); + int found = 0; + const char *p = q; + while (p && *p) { + const char *end = strchr(p, '&'); + size_t len = end ? (size_t)(end - p) : strlen(p); + if (len > kl && strncmp(p, key, kl) == 0 && p[kl] == '=') { + size_t vl = len - kl - 1; + if (found || vl >= cap) return -1; + memcpy(out, p + kl + 1, vl); + out[vl] = '\0'; + found = 1; + } else if (len == kl && strncmp(p, key, kl) == 0) { + return -1; /* "key" without a value */ + } + p = end ? end + 1 : NULL; + } + return found; +} + +const char *webui_cheat_store_query(const char *query, char fmt[WEBUI_CHEAT_FMT_CAP], + char name[WEBUI_CHEAT_NAME_MAX + 1]) { + fmt[0] = '\0'; + name[0] = '\0'; + if (!query) return "bad_format"; + if (query_value(query, "format", fmt, WEBUI_CHEAT_FMT_CAP) != 1 || !webui_cheat_format_ok(fmt)) return "bad_format"; + if (query_value(query, "name", name, WEBUI_CHEAT_NAME_MAX + 1) != 1 || !webui_cheat_name_ok(fmt, name)) + return "bad_name"; + return NULL; +} + +/* `path` is a real folder after this (made when missing): 0, else -1. */ +static int ensure_dir(const char *path) { + struct stat st; + if (mkdir(path, 0777) != 0 && errno != EEXIST) return -1; + if (lstat(path, &st) != 0 || !S_ISDIR(st.st_mode)) return -1; + (void)chmod(path, 0777); /* the umask must not take the bits the helper's folders always have */ + return 0; +} + +int webui_cheat_target(const char *root, const char *fmt, const char *name, char *out, size_t cap) { + char dir[512]; + int k; + if (!root || !webui_cheat_name_ok(fmt, name)) return -1; + k = snprintf(dir, sizeof dir, "%s/%s", root, fmt); + if (k <= 0 || (size_t)k >= sizeof dir) return -1; + k = snprintf(out, cap, "%s/%s", dir, name); + if (k <= 0 || (size_t)k >= cap) return -1; + if (ensure_dir(root) != 0 || ensure_dir(dir) != 0) return -1; + return 0; +} + +/* The end of the JSON value at s[0] (an object): the index past its '}', strings skipped; 0 if cut. */ +static size_t object_end(const char *s, size_t n) { + size_t i; + int depth = 0, in_str = 0; + for (i = 0; i < n; i++) { + char c = s[i]; + if (in_str) { + if (c == '\\') i++; + else if (c == '"') in_str = 0; + } else if (c == '"') { + in_str = 1; + } else if (c == '{' || c == '[') { + depth++; + } else if (c == '}' || c == ']') { + if (--depth == 0) return i + 1; + } + } + return 0; +} + +/* `v` trimmed of spaces in place; 1 when it is then digits and dots with a digit first. */ +static int version_ok(char *v) { + size_t i, n, a = 0; + while (v[a] == ' ') a++; + if (a) memmove(v, v + a, strlen(v + a) + 1); + n = strlen(v); + while (n && v[n - 1] == ' ') v[--n] = '\0'; + if (n == 0 || v[0] < '0' || v[0] > '9') return 0; + for (i = 0; i < n; i++) + if (v[i] != '.' && (v[i] < '0' || v[i] > '9')) return 0; + return 1; +} + +int webui_cheat_appinfo_version(const char *json, size_t n, char *out, size_t cap) { + const char *arr; + size_t alen, i = 1; + char key[32], val[64], app_ver[64] = ""; + if (cap) out[0] = '\0'; + if (!json || cap < 2 || webui_json_array(json, n, "rows", &arr, &alen) != 1) return 0; + while (i < alen) { + size_t len; + char c = arr[i]; + if (c != '{') { + i++; + continue; + } + len = object_end(arr + i, alen - i); + if (!len) break; + if (webui_json_str(arr + i, len, "key", key, sizeof key) == 1 && + webui_json_str(arr + i, len, "val", val, sizeof val) == 1 && version_ok(val) && strlen(val) < cap) { + if (strcmp(key, "CONTENT_VERSION") == 0) { + memcpy(out, val, strlen(val) + 1); + return 1; + } + if (strcmp(key, "APP_VER") == 0) memcpy(app_ver, val, strlen(val) + 1); + } + i += len; + } + if (app_ver[0]) { + memcpy(out, app_ver, strlen(app_ver) + 1); + return 1; + } + return 0; +} diff --git a/payload/tests/webui_cheats_store_selftest.c b/payload/tests/webui_cheats_store_selftest.c new file mode 100644 index 00000000..ee215526 --- /dev/null +++ b/payload/tests/webui_cheats_store_selftest.c @@ -0,0 +1,220 @@ +/* Host-side tests for the console page's cheat download store (webui_cheats_store.c): the + * /api/cheats/store query and file name are checked strictly (a plain base name with the format's + * extension, never a path), the target folders are made when missing, and the bytes land through + * wf_receive_file as a same-folder temp + rename: a cut-off body leaves the old file and no temp. + * The appinfo reply reader prefers CONTENT_VERSION over APP_VER. File tests run in a fresh mkdtemp + * directory. Build: see the root Makefile test-payload. */ +#include +#include +#include +#include +#include +#include +#include + +#include "webui_cheats_store.h" +#include "webui_files.h" + +static int failures = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \ + failures++; \ + } \ + } while (0) + +static void formats(void) { + CHECK(webui_cheat_format_ok("json")); + CHECK(webui_cheat_format_ok("shn")); + CHECK(webui_cheat_format_ok("mc4")); + CHECK(!webui_cheat_format_ok("JSON")); + CHECK(!webui_cheat_format_ok("txt")); + CHECK(!webui_cheat_format_ok("")); + CHECK(!webui_cheat_format_ok(NULL)); +} + +static void names(void) { + char longname[WEBUI_CHEAT_NAME_MAX + 8]; + CHECK(webui_cheat_name_ok("json", "CUSA15438_01.00.json")); + CHECK(webui_cheat_name_ok("json", "CUSA09193_01.05_2.json")); + CHECK(webui_cheat_name_ok("shn", "PPSA01650_01.000.016.shn")); + CHECK(webui_cheat_name_ok("mc4", "CUSA00002_01.00_c20ae0e8.MC4")); + CHECK(webui_cheat_name_ok("json", "a-b.json")); + /* the extension must be the format's */ + CHECK(!webui_cheat_name_ok("json", "CUSA15438_01.00.shn")); + CHECK(!webui_cheat_name_ok("shn", "CUSA15438_01.00.json")); + CHECK(!webui_cheat_name_ok("json", "CUSA15438json")); + CHECK(!webui_cheat_name_ok("json", ".json")); + CHECK(!webui_cheat_name_ok("json", "json")); + /* never a path, a hidden file, or a byte outside the set */ + CHECK(!webui_cheat_name_ok("json", "../x.json")); + CHECK(!webui_cheat_name_ok("json", "a/b.json")); + CHECK(!webui_cheat_name_ok("json", "a\\b.json")); + CHECK(!webui_cheat_name_ok("json", "a..b.json")); + CHECK(!webui_cheat_name_ok("json", ".ps5u-0011.part.json")); + CHECK(!webui_cheat_name_ok("json", "a b.json")); + CHECK(!webui_cheat_name_ok("json", "a%2Fb.json")); + CHECK(!webui_cheat_name_ok("json", "\xc3\xa9.json")); + CHECK(!webui_cheat_name_ok("json", "")); + CHECK(!webui_cheat_name_ok("json", NULL)); + CHECK(!webui_cheat_name_ok("txt", "a.txt")); + /* 128 bytes is the longest */ + memset(longname, 'a', sizeof longname); + memcpy(longname + WEBUI_CHEAT_NAME_MAX - 5, ".json", 6); + CHECK(strlen(longname) == WEBUI_CHEAT_NAME_MAX); + CHECK(webui_cheat_name_ok("json", longname)); + memset(longname, 'a', sizeof longname); + memcpy(longname + WEBUI_CHEAT_NAME_MAX - 4, ".json", 6); + CHECK(!webui_cheat_name_ok("json", longname)); +} + +static void queries(void) { + char fmt[WEBUI_CHEAT_FMT_CAP], name[WEBUI_CHEAT_NAME_MAX + 1]; + CHECK(webui_cheat_store_query("format=json&name=CUSA15438_01.00.json", fmt, name) == NULL); + CHECK(strcmp(fmt, "json") == 0 && strcmp(name, "CUSA15438_01.00.json") == 0); + CHECK(webui_cheat_store_query("name=X1234_1.0.shn&format=shn&v=2", fmt, name) == NULL); + CHECK(strcmp(fmt, "shn") == 0 && strcmp(name, "X1234_1.0.shn") == 0); + CHECK(strcmp(webui_cheat_store_query("", fmt, name), "bad_format") == 0); + CHECK(strcmp(webui_cheat_store_query("format=txt&name=a.txt", fmt, name), "bad_format") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json", fmt, name), "bad_name") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json&name=", fmt, name), "bad_name") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json&name=..%2Fx.json", fmt, name), "bad_name") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json&name=a.json&name=b.json", fmt, name), "bad_name") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json&format=shn&name=a.json", fmt, name), "bad_format") == 0); + CHECK(strcmp(webui_cheat_store_query("format=json&name=a.shn", fmt, name), "bad_name") == 0); + CHECK(strcmp(webui_cheat_store_query("formatx=json&name=a.json", fmt, name), "bad_format") == 0); +} + +static int count_entries(const char *dir) { + DIR *d = opendir(dir); + struct dirent *e; + int n = 0; + if (!d) return -1; + while ((e = readdir(d)) != NULL) + if (e->d_name[0] != '.' || (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0)) n++; + closedir(d); + return n; +} + +static int file_is(const char *path, const char *want) { + char buf[256]; + FILE *f = fopen(path, "rb"); + size_t n; + if (!f) return 0; + n = fread(buf, 1, sizeof buf, f); + fclose(f); + return n == strlen(want) && memcmp(buf, want, n) == 0; +} + +/* `body` (split in two: the part that came with the headers, then the socket) into `target`; `send` + * bytes of it actually sent before the peer closes. */ +static int receive(const char *target, const char *body, size_t send, size_t declared) { + int sv[2], rc; + uint8_t buf[64]; + size_t pre = send < 4 ? send : 4; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) return -100; + if (send > pre && write(sv[1], body + pre, send - pre) != (ssize_t)(send - pre)) return -101; + close(sv[1]); + rc = wf_receive_file(sv[0], (const uint8_t *)body, pre, declared, target, 1, 0777, 2000, NULL, buf, sizeof buf); + close(sv[0]); + return rc; +} + +static void store(void) { + char tmpl[] = "/tmp/ps5u-cheats-store-XXXXXX"; + char root[256], target[512], dir[512]; + const char *root_dir = mkdtemp(tmpl); + const char *v1 = "{\"name\":\"Game\",\"mods\":[]}"; + const char *v2 = "{\"name\":\"Game v2\",\"mods\":[{\"name\":\"Inf HP\"}]}"; + struct stat st; + CHECK(root_dir != NULL); + if (!root_dir) return; + snprintf(root, sizeof root, "%s/cheats", root_dir); + snprintf(dir, sizeof dir, "%s/json", root); + + /* refused names never make a folder or a path */ + CHECK(webui_cheat_target(root, "json", "../x.json", target, sizeof target) == -1); + CHECK(stat(root, &st) != 0); + CHECK(webui_cheat_target(root, "txt", "x.txt", target, sizeof target) == -1); + CHECK(webui_cheat_target(root, "json", "x.json", target, 8) == -1); + + /* the root and the format folder are made when missing */ + CHECK(webui_cheat_target(root, "json", "CUSA15438_01.00.json", target, sizeof target) == 0); + CHECK(stat(dir, &st) == 0 && S_ISDIR(st.st_mode)); + CHECK(strcmp(target + strlen(root), "/json/CUSA15438_01.00.json") == 0); + /* and found again when there */ + CHECK(webui_cheat_target(root, "json", "CUSA15438_01.00.json", target, sizeof target) == 0); + + /* a full body lands as the file, nothing else left in the folder */ + CHECK(receive(target, v1, strlen(v1), strlen(v1)) == WF_OK); + CHECK(file_is(target, v1)); + CHECK(count_entries(dir) == 1); + + /* the same name again replaces it */ + CHECK(receive(target, v2, strlen(v2), strlen(v2)) == WF_OK); + CHECK(file_is(target, v2)); + CHECK(count_entries(dir) == 1); + + /* a body cut short keeps the old file and leaves no temp */ + CHECK(receive(target, v1, 10, strlen(v1)) == WF_E_SHORT); + CHECK(file_is(target, v2)); + CHECK(count_entries(dir) == 1); + + /* a folder in the file's place is never replaced */ + { + char other[512]; + CHECK(webui_cheat_target(root, "shn", "CUSA1_1.0.shn", other, sizeof other) == 0); + CHECK(mkdir(other, 0777) == 0); + CHECK(receive(other, v1, strlen(v1), strlen(v1)) == WF_E_NOT_FILE); + rmdir(other); + } + + /* a link in the format folder's place is refused */ + { + char mc4[512], t2[512]; + snprintf(mc4, sizeof mc4, "%s/mc4", root); + CHECK(symlink(root_dir, mc4) == 0); + CHECK(webui_cheat_target(root, "mc4", "CUSA1_1.0.mc4", t2, sizeof t2) == -1); + unlink(mc4); + } + + { + char cmd[600]; + snprintf(cmd, sizeof cmd, "rm -rf '%s'", root_dir); + CHECK(system(cmd) == 0); + } +} + +static void versions(void) { + char v[32]; + const char *ps5 = + "{\"ok\":true,\"title_id\":\"PPSA01650\",\"rows\":[{\"key\":\"APP_VER\",\"val\":\"01.00\"}," + "{\"key\":\"CONTENT_VERSION\",\"val\":\"01.000.016\"}],\"source\":\"x\"}"; + const char *ps4 = "{\"ok\":true,\"rows\":[{\"key\":\"APP_VER\",\"val\":\" 01.05 \"}]}"; + const char *none = "{\"ok\":true,\"rows\":[]}"; + const char *junk = "{\"ok\":true,\"rows\":[{\"key\":\"CONTENT_VERSION\",\"val\":\"abc\"},{\"key\":\"APP_VER\",\"val\":\"1.\\\"}\"}]}"; + const char *nested = "{\"ok\":true,\"rows\":[{\"key\":\"X\",\"val\":\"{]}\"},{\"key\":\"APP_VER\",\"val\":\"02.10\"}]}"; + CHECK(webui_cheat_appinfo_version(ps5, strlen(ps5), v, sizeof v) == 1 && strcmp(v, "01.000.016") == 0); + CHECK(webui_cheat_appinfo_version(ps4, strlen(ps4), v, sizeof v) == 1 && strcmp(v, "01.05") == 0); + CHECK(webui_cheat_appinfo_version(none, strlen(none), v, sizeof v) == 0 && v[0] == '\0'); + CHECK(webui_cheat_appinfo_version(junk, strlen(junk), v, sizeof v) == 0); + CHECK(webui_cheat_appinfo_version(nested, strlen(nested), v, sizeof v) == 1 && strcmp(v, "02.10") == 0); + CHECK(webui_cheat_appinfo_version("{\"ok\":false,\"error\":\"x\"}", 25, v, sizeof v) == 0); + CHECK(webui_cheat_appinfo_version(ps5, strlen(ps5), v, 4) == 0); /* does not fit */ +} + +int main(void) { + formats(); + names(); + queries(); + store(); + versions(); + if (failures) { + fprintf(stderr, "webui_cheats_store_selftest: %d failure(s)\n", failures); + return 1; + } + printf("webui_cheats_store_selftest: ok\n"); + return 0; +} From ef0d59b681269e33ac11eef6a09d4a6ffa429cb8 Mon Sep 17 00:00:00 2001 From: phantomptr Date: Sun, 18 Oct 2026 11:48:06 -0700 Subject: [PATCH 2/2] console cheats: download review fixes - One deadline covers the reply and its body, and the body is read in chunks (getReader; arrayBuffer when there is no stream) and abandoned past 4 MiB: a stalled or oversized body can no longer hang the finder. - The helper reports "replaced" when a file of that name was already stored; the page says so (cw_cheats_find_replaced, 21 locales). - A stored file closes only the finder that asked for it, and only while its game is running. - The finder stays open through a transient empty status: it closes when another game is reported or no game on two reads in a row. - downloadCheat refuses a name the helper would refuse before fetching. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS --- client/src/console/cheats/Cheats.test.tsx | 10 +- client/src/console/cheats/Cheats.tsx | 36 +++++-- client/src/console/cheats/finder.test.tsx | 46 +++++++- client/src/console/cheats/finder.ts | 26 ++++- client/src/console/cheats/repos.test.ts | 58 ++++++++++ client/src/console/cheats/repos.ts | 122 ++++++++++++++++------ client/src/console/locales/ar.json | 1 + client/src/console/locales/bn.json | 1 + client/src/console/locales/de.json | 1 + client/src/console/locales/en.json | 1 + client/src/console/locales/es.json | 1 + client/src/console/locales/fa.json | 1 + client/src/console/locales/fr.json | 1 + client/src/console/locales/hi.json | 1 + client/src/console/locales/hu.json | 1 + client/src/console/locales/id.json | 1 + client/src/console/locales/it.json | 1 + client/src/console/locales/ja.json | 1 + client/src/console/locales/ko.json | 1 + client/src/console/locales/pl.json | 1 + client/src/console/locales/pt-BR.json | 1 + client/src/console/locales/ru.json | 1 + client/src/console/locales/th.json | 1 + client/src/console/locales/tr.json | 1 + client/src/console/locales/vi.json | 1 + client/src/console/locales/zh-CN.json | 1 + client/src/console/locales/zh-TW.json | 1 + client/src/i18n/locales/ar.ts | 1 + client/src/i18n/locales/bn.ts | 1 + client/src/i18n/locales/de.ts | 1 + client/src/i18n/locales/en.ts | 1 + client/src/i18n/locales/es.ts | 1 + client/src/i18n/locales/fa.ts | 1 + client/src/i18n/locales/fr.ts | 1 + client/src/i18n/locales/hi.ts | 1 + client/src/i18n/locales/hu.ts | 1 + client/src/i18n/locales/id.ts | 1 + client/src/i18n/locales/it.ts | 1 + client/src/i18n/locales/ja.ts | 1 + client/src/i18n/locales/ko.ts | 1 + client/src/i18n/locales/pl.ts | 1 + client/src/i18n/locales/pt-BR.ts | 1 + client/src/i18n/locales/ru.ts | 1 + client/src/i18n/locales/th.ts | 1 + client/src/i18n/locales/tr.ts | 1 + client/src/i18n/locales/vi.ts | 1 + client/src/i18n/locales/zh-CN.ts | 1 + client/src/i18n/locales/zh-TW.ts | 1 + payload/src/webui_api.c | 10 +- 49 files changed, 297 insertions(+), 53 deletions(-) diff --git a/client/src/console/cheats/Cheats.test.tsx b/client/src/console/cheats/Cheats.test.tsx index 35919dfd..76941d54 100644 --- a/client/src/console/cheats/Cheats.test.tsx +++ b/client/src/console/cheats/Cheats.test.tsx @@ -9,7 +9,7 @@ vi.mock("../api", async (importOriginal) => { import { ApiError, call } from "../api"; import { SECTIONS } from "../sections"; import { t } from "../strings"; -import { CheatsView, focusedCheatIndex, type CheatsViewProps } from "./Cheats"; +import { CheatsView, finderClosesFor, focusedCheatIndex, type CheatsViewProps } from "./Cheats"; import { cheatsPhase, createCheatsController, @@ -311,4 +311,12 @@ describe("cheats section", () => { expect(focusedCheatIndex(root2, notACheat)).toBeNull(); expect(focusedCheatIndex(null, row)).toBeNull(); }); + + it("the_finder_closes_only_when_its_game_is_confirmed_gone", () => { + expect(finderClosesFor(GAME, GAME, 0)).toBe(false); + expect(finderClosesFor(GAME, "CUSA00001", 0)).toBe(true); + // one empty status read is not enough (a transient answer); two in a row are + expect(finderClosesFor(GAME, null, 1)).toBe(false); + expect(finderClosesFor(GAME, null, 2)).toBe(true); + }); }); diff --git a/client/src/console/cheats/Cheats.tsx b/client/src/console/cheats/Cheats.tsx index 3e8cb389..cf08336b 100644 --- a/client/src/console/cheats/Cheats.tsx +++ b/client/src/console/cheats/Cheats.tsx @@ -4,7 +4,7 @@ import { useAppCover } from "../home/Home"; import { onNavAction } from "../nav"; import { t } from "../strings"; import { pollWhileVisible } from "../ui/visiblePoll"; -import { createFinder, FINDER_CLOSED, type Finder, type FinderState } from "./finder"; +import { createFinder, FINDER_CLOSED, savedText, type Finder, type FinderState } from "./finder"; import { FindCheats } from "./FindCheats"; import { cheatsPhase, @@ -182,6 +182,13 @@ export function CheatsView(p: CheatsViewProps) { ); } +/** Does a finder open for `finderTitle` close, with `running` the game the last status read reported + * (null: none) and `noGameReads` the status reads in a row that reported none? */ +export function finderClosesFor(finderTitle: string, running: string | null, noGameReads: number): boolean { + if (running) return running !== finderTitle; + return noGameReads >= 2; +} + /** □ 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; @@ -200,11 +207,14 @@ export function Cheats() { const finder = useRef(null); const [notice, setNotice] = useState<{ title: string; text: string } | null>(null); - // The finder: a stored file closes it, and the game's cheats are read again (they show at once). + // The finder: a stored file is read at once (the game's cheats again) and closes the finder that + // asked for it, never one opened since (another game, or opened again). useEffect(() => { - finder.current = createFinder(setFinderState, (entry, title) => { - setNotice({ title, text: t("cw_cheats_find_saved", { name: entry.filename }) }); - finder.current?.close(); + finder.current = createFinder(setFinderState, ({ entry, title, replaced, current }) => { + const running = ctl.current?.get().status?.game?.title_id ?? null; + if (running !== title) return; + setNotice({ title, text: savedText(entry.filename, replaced) }); + if (current) finder.current?.close(); void ctl.current?.loadList(); }); return () => { @@ -257,10 +267,20 @@ export function Cheats() { ); const titleId = state.status?.game?.title_id ?? null; - // The game went away (or another started): its finder and notice go with it. + // The finder goes when its game is confirmed gone: another game reported running, or no game on two + // status reads in a row. A status not read yet (or a failed read, which keeps the last one) or a + // single empty read keeps it open. + const noGameReads = useRef(0); + const status = state.status; useEffect(() => { - if (finderState.kind !== "closed" && finderState.title !== titleId) finder.current?.close(); - }, [titleId, finderState]); + if (!status) return; + noGameReads.current = status.game ? 0 : noGameReads.current + 1; + }, [status]); + useEffect(() => { + if (finderState.kind === "closed" || !status) return; + const now = status.game?.title_id ?? null; + if (finderClosesFor(finderState.title, now, noGameReads.current)) finder.current?.close(); + }, [status, finderState]); const loadedCover = useAppCover(titleId); const [coverFailed, setCoverFailed] = useState(null); const cover = loadedCover && coverFailed !== loadedCover ? loadedCover : null; diff --git a/client/src/console/cheats/finder.test.tsx b/client/src/console/cheats/finder.test.tsx index 2acb3e48..deeb52d9 100644 --- a/client/src/console/cheats/finder.test.tsx +++ b/client/src/console/cheats/finder.test.tsx @@ -8,7 +8,7 @@ vi.mock("../api", async (importOriginal) => { import { ApiError, call, callRaw } from "../api"; import { t } from "../strings"; -import { createFinder, downloadErrorText, FINDER_CLOSED, storeRoute, type FinderState } from "./finder"; +import { createFinder, downloadErrorText, FINDER_CLOSED, storeRoute, type FinderState, type StoredFile } from "./finder"; import { FindCheats } from "./FindCheats"; import { CHEAT_REPOS, contentUrls, parseCheatFilename, RepoError, storeFormat, type CheatEntry, type FetchLike, type RepoIndex } from "./repos"; @@ -35,7 +35,7 @@ const INDEX: RepoIndex = { }; /** The helper: /api/cheats/version answers `version`; /api/cheats/store records what it got. */ -function helper(opts: { version?: string | null; versionFails?: boolean; storeError?: ApiError } = {}) { +function helper(opts: { version?: string | null; versionFails?: boolean; storeError?: ApiError; replaced?: boolean } = {}) { const stored: { route: string; body: string }[] = []; mockCall.mockImplementation(async (route: string) => { if (route === "/api/cheats/version") { @@ -47,7 +47,7 @@ function helper(opts: { version?: string | null; versionFails?: boolean; storeEr mockRaw.mockImplementation(async (route: string, body: Blob) => { if (opts.storeError) throw opts.storeError; stored.push({ route, body: await body.text() }); - return { ok: true } as never; + return { ok: true, replaced: opts.replaced === true } as never; }); return stored; } @@ -63,9 +63,13 @@ function github(files: Record, down = false): FetchLike { function finder(opts: { index?: RepoIndex | Error; fetch?: FetchLike } = {}) { const states: FinderState[] = []; const stored: [CheatEntry, string][] = []; + const full: StoredFile[] = []; const f = createFinder( (s) => states.push(s), - (e, title) => stored.push([e, title]), + (x) => { + stored.push([x.entry, x.title]); + full.push(x); + }, { index: async () => { if (opts.index instanceof Error) throw opts.index; @@ -74,7 +78,7 @@ function finder(opts: { index?: RepoIndex | Error; fetch?: FetchLike } = {}) { fetch: opts.fetch ?? github({}), }, ); - return { f, states, stored }; + return { f, states, stored, full }; } const render = (state: FinderState) => @@ -254,4 +258,36 @@ describe("find cheats", () => { await f.open(GAME); expect(f.get().kind).toBe("offline"); }); + + it("a_file_already_there_says_replaced", async () => { + helper({ replaced: true }); + const e = INDEX.entries[0]; + const { f, full } = finder({ fetch: github({ [contentUrls(CHEAT_REPOS[0], e.filename)[0]]: "{}" }) }); + await f.open(GAME); + await f.download(e); + const s = f.get(); + expect(s.kind === "list" && s.message?.text).toBe(t("cw_cheats_find_replaced", { name: e.filename })); + expect(t("cw_cheats_find_replaced", { name: "x" })).toBe("x was replaced on the console. Its cheats are in the list now."); + expect(full[0]).toMatchObject({ replaced: true, current: true, title: GAME }); + }); + + it("a_download_finishing_after_the_finder_was_reopened_is_not_current", async () => { + helper(); + let release: () => void = () => {}; + const gate = new Promise((r) => (release = r)); + const slow: FetchLike = async () => { + await gate; + return new Response("x", { status: 200 }); + }; + const { f, full } = finder({ fetch: slow }); + await f.open(GAME); + const p = f.download(INDEX.entries[0]); + f.close(); + await f.open("CUSA99999"); + release(); + await p; + expect(full).toHaveLength(1); + expect(full[0]).toMatchObject({ title: GAME, current: false }); + expect(f.get()).toMatchObject({ kind: "list", title: "CUSA99999" }); + }); }); diff --git a/client/src/console/cheats/finder.ts b/client/src/console/cheats/finder.ts index ce346260..b354e18e 100644 --- a/client/src/console/cheats/finder.ts +++ b/client/src/console/cheats/finder.ts @@ -87,13 +87,27 @@ export function storeRoute(entry: CheatEntry): string { return `/api/cheats/store?format=${storeFormat(entry.filename)}&name=${encodeURIComponent(entry.filename)}`; } +/** A file the helper stored: for which game, whether it replaced one of the same name, and whether + * the finder that asked is still the open one (`current`: only then may the section close it). */ +export interface StoredFile { + entry: CheatEntry; + title: string; + replaced: boolean; + current: boolean; +} + +/** The saved line: "replaced" when a file of that name was already on the console. */ +export function savedText(name: string, replaced: boolean): string { + return t(replaced ? "cw_cheats_find_replaced" : "cw_cheats_find_saved", { name }); +} + /** * The finder's state machine. `onStored` runs after a file is on the console (the section reads - * the game's cheats again and closes the finder). + * the game's cheats again, and closes the finder when it is still the one that asked). */ export function createFinder( onChange: (s: FinderState) => void, - onStored: (entry: CheatEntry, title: string) => void, + onStored: (stored: StoredFile) => void, deps: FinderDeps = {}, ): Finder { let s: FinderState = FINDER_CLOSED; @@ -143,9 +157,11 @@ export function createFinder( const title = s.title; set({ ...s, busy: entry.filename, message: null }); const still = () => mine === seq && s.kind === "list"; + let replaced: boolean; try { const bytes = await downloadCheat(entry, deps.fetch); - await callRaw(storeRoute(entry), new Blob([bytes], { type: "application/octet-stream" })); + const r = await callRaw<{ replaced?: unknown } | null>(storeRoute(entry), new Blob([bytes], { type: "application/octet-stream" })); + replaced = r?.replaced === true; } catch (e) { if (still() && s.kind === "list") { const text = downloadErrorText(e, entry.filename); @@ -158,10 +174,10 @@ export function createFinder( ...s, busy: null, done: s.done.includes(entry.filename) ? s.done : [...s.done, entry.filename], - message: { tone: "info", text: t("cw_cheats_find_saved", { name: entry.filename }) }, + message: { tone: "info", text: savedText(entry.filename, replaced) }, }); } - onStored(entry, title); + onStored({ entry, title, replaced, current: still() }); }, get: () => s, }; diff --git a/client/src/console/cheats/repos.test.ts b/client/src/console/cheats/repos.test.ts index 545a1e28..9363b37a 100644 --- a/client/src/console/cheats/repos.test.ts +++ b/client/src/console/cheats/repos.test.ts @@ -5,6 +5,7 @@ import { contentUrls, downloadCheat, entriesFor, + fetchLimited, formatFromIndex, parseCheatFilename, parseIndexLine, @@ -281,4 +282,61 @@ describe("downloading a file", () => { const { f } = net({ [urls[0]]: new ArrayBuffer(CHEAT_FILE_MAX + 1) }); await expect(downloadCheat(entry, f)).rejects.toMatchObject({ message: "too_large" }); }); + it("never_fetches_a_name_the_helper_would_refuse", async () => { + const { f, calls } = net({}); + await expect(downloadCheat({ ...entry, filename: "CUSA1 copy.shn" }, f)).rejects.toMatchObject({ message: "bad_name" }); + expect(calls).toEqual([]); + }); + it("a_stalled_body_times_out_instead_of_hanging", async () => { + const urls = contentUrls(MIX, entry.filename); + const f: FetchLike = async (url) => + url === urls[0] + ? new Response(new ReadableStream({ start: (c) => c.enqueue(new Uint8Array([1, 2])) }), { status: 200 }) + : new Response("404", { status: 404 }); + const err = await downloadCheat(entry, f, 50).catch((e: unknown) => e); + expect(err).toMatchObject({ network: true, message: "cut_off" }); + }); +}); + +describe("bounded reads (fetchLimited)", () => { + const chunked = (sizes: number[]) => { + let pulled = 0; + const body = new ReadableStream({ + pull(c) { + if (pulled >= sizes.length) return c.close(); + c.enqueue(new Uint8Array(sizes[pulled++])); + }, + }); + return { body, pulled: () => pulled }; + }; + it("stops reading past the limit", async () => { + const big = chunked([600, 600, 600, 600, 600]); + const f: FetchLike = async () => new Response(big.body, { status: 200 }); + await expect(fetchLimited(f, "u", 1000, 1000)).rejects.toMatchObject({ message: "too_large", network: false }); + expect(big.pulled()).toBeLessThan(5); + }); + it("reads a body in chunks", async () => { + const f: FetchLike = async () => new Response(chunked([3, 4]).body, { status: 200 }); + const r = await fetchLimited(f, "u", 1000, 7); + expect(r.ok && r.body.byteLength).toBe(7); + }); + it("refuses a declared length over the limit without reading", async () => { + const f: FetchLike = async () => new Response("x".repeat(10), { status: 200, headers: { "Content-Length": "5000" } }); + await expect(fetchLimited(f, "u", 1000, 100)).rejects.toMatchObject({ message: "too_large" }); + }); + it("falls back to arrayBuffer without a body stream", async () => { + const res = { ok: true, status: 200, headers: new Headers(), body: null, arrayBuffer: async () => new ArrayBuffer(9) } as unknown as Response; + const r = await fetchLimited(async () => res, "u", 1000, 10); + expect(r.ok && r.body.byteLength).toBe(9); + const big = { ...res, arrayBuffer: async () => new ArrayBuffer(11) } as unknown as Response; + await expect(fetchLimited(async () => big, "u", 1000, 10)).rejects.toMatchObject({ message: "too_large" }); + }); + it("an_http_error_is_not_ok", async () => { + const r = await fetchLimited(async () => new Response("no", { status: 403 }), "u", 1000, 10); + expect(r).toEqual({ ok: false, status: 403 }); + }); + it("a_reply_that_never_comes_times_out", async () => { + const r = fetchLimited(() => new Promise(() => {}), "u", 30, 10); + await expect(r).rejects.toMatchObject({ network: true }); + }); }); diff --git a/client/src/console/cheats/repos.ts b/client/src/console/cheats/repos.ts index 5a1afd69..23df2e4e 100644 --- a/client/src/console/cheats/repos.ts +++ b/client/src/console/cheats/repos.ts @@ -210,26 +210,93 @@ export class RepoError extends Error { } } -async function fetchWithTimeout(f: FetchLike, url: string, timeoutMs: number): Promise { +/** The longest index or tree reply read (the git tree is ~2 MB today). */ +export const LISTING_MAX = 16 * 1024 * 1024; + +/** A whole GET (headers and body) that must finish within `timeoutMs`. */ +export type Fetched = { ok: true; body: ArrayBuffer } | { ok: false; status: number }; + +/** + * GET `url` with one deadline for the reply AND its body (a body that stalls cannot hang the + * caller), reading the body in chunks and giving up past `max` bytes (RepoError "too_large"). + * Nothing answering (or the deadline) is RepoError(network); an HTTP error is { ok: false }. + */ +export async function fetchLimited(f: FetchLike, url: string, timeoutMs: number, max: number): Promise { const stop = typeof AbortController === "function" ? new AbortController() : null; - const timer = setTimeout(() => stop?.abort(), timeoutMs); + let timer: ReturnType | undefined; + const deadline = new Promise((_, reject) => { + timer = setTimeout(() => { + stop?.abort(); + reject(new RepoError(true, `timed out ${url}`)); + }, timeoutMs); + }); + // Every await below races the deadline: a fetch that ignores its signal still cannot hang us. + const within = (p: Promise): Promise => Promise.race([p, deadline]); + deadline.catch(() => {}); + let res: Response; try { - return await f(url, { method: "GET", cache: "no-store", credentials: "omit", signal: stop?.signal }); - } catch { - throw new RepoError(true, `unreachable ${url}`); + try { + res = await within(f(url, { method: "GET", cache: "no-store", credentials: "omit", signal: stop?.signal })); + } catch (e) { + throw e instanceof RepoError ? e : new RepoError(true, `unreachable ${url}`); + } + if (!res.ok) { + void res.body?.cancel().catch(() => {}); + return { ok: false, status: res.status }; + } + const tooLarge = () => { + stop?.abort(); + return new RepoError(false, "too_large"); + }; + const len = Number(res.headers.get("Content-Length")); + if (Number.isFinite(len) && len > max) { + void res.body?.cancel().catch(() => {}); + throw tooLarge(); + } + const reader = res.body && typeof res.body.getReader === "function" ? res.body.getReader() : null; + if (!reader) { + let buf: ArrayBuffer; + try { + buf = await within(res.arrayBuffer()); + } catch (e) { + throw e instanceof RepoError ? e : new RepoError(true, `cut off ${url}`); + } + if (buf.byteLength > max) throw tooLarge(); + return { ok: true, body: buf }; + } + const chunks: Uint8Array[] = []; + let total = 0; + try { + for (;;) { + const { done, value } = await within(reader.read()); + if (done) break; + total += value.byteLength; + if (total > max) { + void reader.cancel().catch(() => {}); + throw tooLarge(); + } + chunks.push(value); + } + } catch (e) { + void reader.cancel().catch(() => {}); + throw e instanceof RepoError ? e : new RepoError(true, `cut off ${url}`); + } + const out = new Uint8Array(total); + let at = 0; + for (const c of chunks) { + out.set(c, at); + at += c.byteLength; + } + return { ok: true, body: out.buffer }; } finally { clearTimeout(timer); } } async function fetchText(f: FetchLike, url: string, timeoutMs: number): Promise { - const res = await fetchWithTimeout(f, url, timeoutMs); - if (!res.ok) throw new RepoError(false, `http_${res.status}`); - try { - return await res.text(); - } catch { - throw new RepoError(true, `cut off ${url}`); - } + const r = await fetchLimited(f, url, timeoutMs, LISTING_MAX); + if (!r.ok) throw new RepoError(false, `http_${r.status}`); + return new TextDecoder().decode(r.body); } /** One repo's listing: its index files (any that answers counts), or its git tree. */ @@ -343,33 +410,28 @@ export const CHEAT_FILE_MAX = 4 * 1024 * 1024; /** * One cheat file's bytes from its repo: the content URLs in order (contentUrls), the first that - * answers 2xx. RepoError(network) when nothing answered; "too_large" over CHEAT_FILE_MAX; - * "not_found" when every URL answered an error. + * answers 2xx, read within the timeout and never past CHEAT_FILE_MAX. RepoError(network) when + * nothing answered or the body stalled; "too_large"; "not_found" when every URL answered an error; + * "bad_name" for a name the helper would refuse (never fetched). */ export async function downloadCheat(entry: CheatEntry, f: FetchLike = (u, i) => fetch(u, i), timeoutMs = REPO_TIMEOUT_MS): Promise { + if (!storableName(entry.filename)) throw new RepoError(false, "bad_name"); const repo = CHEAT_REPOS.find((r) => r.id === entry.repoId); if (!repo) throw new RepoError(false, "unknown_repo"); let answered = false; for (const url of contentUrls(repo, entry.filename)) { - let res: Response; + let r: Fetched; try { - res = await fetchWithTimeout(f, url, timeoutMs); - } catch { - continue; + r = await fetchLimited(f, url, timeoutMs, CHEAT_FILE_MAX); + } catch (e) { + // Nothing answered at this URL: try the next. A stalled or oversized body ends the download. + if (e instanceof RepoError && e.network && /^unreachable /.test(e.message)) continue; + throw e instanceof RepoError && e.network ? new RepoError(true, "cut_off") : e; } answered = true; - if (!res.ok) continue; - const len = Number(res.headers.get("Content-Length")); - if (Number.isFinite(len) && len > CHEAT_FILE_MAX) throw new RepoError(false, "too_large"); - let buf: ArrayBuffer; - try { - buf = await res.arrayBuffer(); - } catch { - throw new RepoError(true, "cut_off"); - } - if (buf.byteLength > CHEAT_FILE_MAX) throw new RepoError(false, "too_large"); - if (buf.byteLength === 0) throw new RepoError(false, "empty"); - return buf; + if (!r.ok) continue; + if (r.body.byteLength === 0) throw new RepoError(false, "empty"); + return r.body; } throw new RepoError(!answered, answered ? "not_found" : "unreachable"); } diff --git a/client/src/console/locales/ar.json b/client/src/console/locales/ar.json index 827e57ab..20814381 100644 --- a/client/src/console/locales/ar.json +++ b/client/src/console/locales/ar.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "لم ينشر أحد شفرات لهذه اللعبة بعد.", "cw_cheats_find_offline": "لا يستطيع جهاز PS5 هذا الوصول إلى GitHub. احصل على الشفرات من تطبيق PS5Upload على حاسوبك، أو اسمح بـ raw.githubusercontent.com في إعدادات DNS.", "cw_cheats_find_partial": "تعذّر الوصول إلى بعض مصادر الشفرات، لذا قد تكون القائمة ناقصة.", + "cw_cheats_find_replaced": "تم استبدال {name} على الجهاز. شفراته موجودة الآن في القائمة.", "cw_cheats_find_retry": "إعادة المحاولة", "cw_cheats_find_saved": "أصبح {name} على الجهاز. شفراته موجودة الآن في القائمة.", "cw_cheats_find_title": "شفرات {title} على الإنترنت", diff --git a/client/src/console/locales/bn.json b/client/src/console/locales/bn.json index 06590ae2..2b9ddfdd 100644 --- a/client/src/console/locales/bn.json +++ b/client/src/console/locales/bn.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "এই গেমের জন্য এখনো কেউ চিট প্রকাশ করেনি।", "cw_cheats_find_offline": "এই PS5 GitHub-এ পৌঁছাতে পারছে না। আপনার PC-র PS5Upload অ্যাপ থেকে চিট নিন, অথবা DNS সেটিংসে raw.githubusercontent.com অনুমতি দিন।", "cw_cheats_find_partial": "কিছু চিট উৎসে পৌঁছানো যায়নি, তাই তালিকাটি অসম্পূর্ণ হতে পারে।", + "cw_cheats_find_replaced": "কনসোলে {name} প্রতিস্থাপন করা হয়েছে। এর চিটগুলো এখন তালিকায় আছে।", "cw_cheats_find_retry": "আবার চেষ্টা করুন", "cw_cheats_find_saved": "{name} কনসোলে আছে। এর চিটগুলো এখন তালিকায় আছে।", "cw_cheats_find_title": "{title}-এর অনলাইন চিট", diff --git a/client/src/console/locales/de.json b/client/src/console/locales/de.json index 2d6030ce..9cc9dcdd 100644 --- a/client/src/console/locales/de.json +++ b/client/src/console/locales/de.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Für dieses Spiel hat noch niemand Cheats veröffentlicht.", "cw_cheats_find_offline": "Diese PS5 erreicht GitHub nicht. Hol dir Cheats in der PS5Upload-App auf deinem PC oder erlaube raw.githubusercontent.com in deinen DNS-Einstellungen.", "cw_cheats_find_partial": "Einige Cheat-Quellen waren nicht erreichbar, die Liste ist daher eventuell unvollständig.", + "cw_cheats_find_replaced": "{name} wurde auf der Konsole ersetzt. Seine Cheats stehen jetzt in der Liste.", "cw_cheats_find_retry": "Erneut versuchen", "cw_cheats_find_saved": "{name} ist auf der Konsole. Seine Cheats stehen jetzt in der Liste.", "cw_cheats_find_title": "Online-Cheats für {title}", diff --git a/client/src/console/locales/en.json b/client/src/console/locales/en.json index 8324e5df..8334077c 100644 --- a/client/src/console/locales/en.json +++ b/client/src/console/locales/en.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Nobody has published cheats for this game yet.", "cw_cheats_find_offline": "This PS5 can't reach GitHub. Get cheats in the PS5Upload app on your PC, or allow raw.githubusercontent.com in your DNS settings.", "cw_cheats_find_partial": "Some cheat sources could not be reached, so the list may be incomplete.", + "cw_cheats_find_replaced": "{name} was replaced on the console. Its cheats are in the list now.", "cw_cheats_find_retry": "Try again", "cw_cheats_find_saved": "{name} is on the console. Its cheats are in the list now.", "cw_cheats_find_title": "Cheats online for {title}", diff --git a/client/src/console/locales/es.json b/client/src/console/locales/es.json index 1f64c5a6..3601935f 100644 --- a/client/src/console/locales/es.json +++ b/client/src/console/locales/es.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Nadie ha publicado trucos para este juego todavía.", "cw_cheats_find_offline": "Esta PS5 no puede conectarse a GitHub. Consigue trucos en la app PS5Upload de tu PC o permite raw.githubusercontent.com en tus ajustes de DNS.", "cw_cheats_find_partial": "No se pudo acceder a algunas fuentes de trucos, así que la lista puede estar incompleta.", + "cw_cheats_find_replaced": "{name} se ha reemplazado en la consola. Sus trucos aparecen ahora en la lista.", "cw_cheats_find_retry": "Reintentar", "cw_cheats_find_saved": "{name} ya está en la consola. Sus trucos aparecen ahora en la lista.", "cw_cheats_find_title": "Trucos en línea para {title}", diff --git a/client/src/console/locales/fa.json b/client/src/console/locales/fa.json index 4a212bf2..ce5f692c 100644 --- a/client/src/console/locales/fa.json +++ b/client/src/console/locales/fa.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "هنوز کسی برای این بازی چیتی منتشر نکرده است.", "cw_cheats_find_offline": "این PS5 به GitHub دسترسی ندارد. چیت‌ها را از برنامه PS5Upload روی رایانه‌تان بگیرید، یا raw.githubusercontent.com را در تنظیمات DNS مجاز کنید.", "cw_cheats_find_partial": "به برخی منابع چیت دسترسی نبود، پس ممکن است فهرست کامل نباشد.", + "cw_cheats_find_replaced": "{name} روی کنسول جایگزین شد. چیت‌هایش اکنون در فهرست هستند.", "cw_cheats_find_retry": "تلاش دوباره", "cw_cheats_find_saved": "{name} روی کنسول است. چیت‌هایش اکنون در فهرست هستند.", "cw_cheats_find_title": "چیت‌های آنلاین برای {title}", diff --git a/client/src/console/locales/fr.json b/client/src/console/locales/fr.json index af7f3e5d..20a1d98c 100644 --- a/client/src/console/locales/fr.json +++ b/client/src/console/locales/fr.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Personne n'a encore publié de cheats pour ce jeu.", "cw_cheats_find_offline": "Cette PS5 n'arrive pas à joindre GitHub. Récupérez des cheats dans l'app PS5Upload sur votre PC, ou autorisez raw.githubusercontent.com dans vos réglages DNS.", "cw_cheats_find_partial": "Certaines sources de cheats sont injoignables : la liste est peut-être incomplète.", + "cw_cheats_find_replaced": "{name} a été remplacé sur la console. Ses cheats sont maintenant dans la liste.", "cw_cheats_find_retry": "Réessayer", "cw_cheats_find_saved": "{name} est sur la console. Ses cheats sont maintenant dans la liste.", "cw_cheats_find_title": "Cheats en ligne pour {title}", diff --git a/client/src/console/locales/hi.json b/client/src/console/locales/hi.json index dd0edea9..09fafdcf 100644 --- a/client/src/console/locales/hi.json +++ b/client/src/console/locales/hi.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "इस गेम के लिए अभी तक किसी ने चीट प्रकाशित नहीं किए हैं।", "cw_cheats_find_offline": "यह PS5 GitHub तक नहीं पहुँच पा रहा। अपने PC पर PS5Upload ऐप से चीट लें, या अपनी DNS सेटिंग में raw.githubusercontent.com को अनुमति दें।", "cw_cheats_find_partial": "कुछ चीट स्रोतों तक नहीं पहुँचा जा सका, इसलिए सूची अधूरी हो सकती है।", + "cw_cheats_find_replaced": "कंसोल पर {name} को बदल दिया गया। इसके चीट अब सूची में हैं।", "cw_cheats_find_retry": "फिर कोशिश करें", "cw_cheats_find_saved": "{name} कंसोल पर है। इसके चीट अब सूची में हैं।", "cw_cheats_find_title": "{title} के ऑनलाइन चीट", diff --git a/client/src/console/locales/hu.json b/client/src/console/locales/hu.json index 70b6545c..ce6a59a8 100644 --- a/client/src/console/locales/hu.json +++ b/client/src/console/locales/hu.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Ehhez a játékhoz még senki sem tett közzé csalásokat.", "cw_cheats_find_offline": "Ez a PS5 nem éri el a GitHubot. Szerezz csalásokat a PC-n futó PS5Upload alkalmazásban, vagy engedélyezd a raw.githubusercontent.com címet a DNS-beállításokban.", "cw_cheats_find_partial": "Néhány csalásforrás nem érhető el, ezért a lista hiányos lehet.", + "cw_cheats_find_replaced": "A(z) {name} le lett cserélve a konzolon. A csalásai most már a listában vannak.", "cw_cheats_find_retry": "Újra", "cw_cheats_find_saved": "A(z) {name} a konzolon van. A csalásai most már a listában vannak.", "cw_cheats_find_title": "Online csalások ehhez: {title}", diff --git a/client/src/console/locales/id.json b/client/src/console/locales/id.json index 86f8e85e..4307b651 100644 --- a/client/src/console/locales/id.json +++ b/client/src/console/locales/id.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Belum ada yang menerbitkan cheat untuk game ini.", "cw_cheats_find_offline": "PS5 ini tidak bisa menjangkau GitHub. Dapatkan cheat di aplikasi PS5Upload di PC Anda, atau izinkan raw.githubusercontent.com di pengaturan DNS Anda.", "cw_cheats_find_partial": "Beberapa sumber cheat tidak dapat dijangkau, jadi daftarnya mungkin tidak lengkap.", + "cw_cheats_find_replaced": "{name} sudah diganti di konsol. Cheat-nya kini ada di daftar.", "cw_cheats_find_retry": "Coba lagi", "cw_cheats_find_saved": "{name} sudah ada di konsol. Cheat-nya kini ada di daftar.", "cw_cheats_find_title": "Cheat online untuk {title}", diff --git a/client/src/console/locales/it.json b/client/src/console/locales/it.json index c2ce7243..8e07f5a2 100644 --- a/client/src/console/locales/it.json +++ b/client/src/console/locales/it.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Nessuno ha ancora pubblicato cheat per questo gioco.", "cw_cheats_find_offline": "Questa PS5 non riesce a raggiungere GitHub. Ottieni i cheat dall'app PS5Upload sul tuo PC oppure consenti raw.githubusercontent.com nelle impostazioni DNS.", "cw_cheats_find_partial": "Alcune fonti di cheat non sono raggiungibili, quindi l'elenco potrebbe essere incompleto.", + "cw_cheats_find_replaced": "{name} è stato sostituito sulla console. I suoi cheat ora sono nell'elenco.", "cw_cheats_find_retry": "Riprova", "cw_cheats_find_saved": "{name} è sulla console. I suoi cheat ora sono nell'elenco.", "cw_cheats_find_title": "Cheat online per {title}", diff --git a/client/src/console/locales/ja.json b/client/src/console/locales/ja.json index 03f1b95a..e94ac6e6 100644 --- a/client/src/console/locales/ja.json +++ b/client/src/console/locales/ja.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "このゲームのチートはまだ誰も公開していません。", "cw_cheats_find_offline": "この PS5 から GitHub に接続できません。PC の PS5Upload アプリでチートを入手するか、DNS 設定で raw.githubusercontent.com を許可してください。", "cw_cheats_find_partial": "一部のチート配布元に接続できなかったため、リストが不完全な可能性があります。", + "cw_cheats_find_replaced": "本体の {name} を置き換えました。チートがリストに表示されています。", "cw_cheats_find_retry": "再試行", "cw_cheats_find_saved": "{name} を本体に保存しました。チートがリストに表示されています。", "cw_cheats_find_title": "{title} のオンラインチート", diff --git a/client/src/console/locales/ko.json b/client/src/console/locales/ko.json index dd416106..449ec139 100644 --- a/client/src/console/locales/ko.json +++ b/client/src/console/locales/ko.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "아직 이 게임의 치트를 공개한 사람이 없습니다.", "cw_cheats_find_offline": "이 PS5에서 GitHub에 연결할 수 없습니다. PC의 PS5Upload 앱에서 치트를 받거나 DNS 설정에서 raw.githubusercontent.com을 허용하세요.", "cw_cheats_find_partial": "일부 치트 출처에 연결할 수 없어 목록이 불완전할 수 있습니다.", + "cw_cheats_find_replaced": "콘솔의 {name}을(를) 교체했습니다. 이제 치트가 목록에 있습니다.", "cw_cheats_find_retry": "다시 시도", "cw_cheats_find_saved": "{name}이(가) 콘솔에 저장되었습니다. 이제 치트가 목록에 있습니다.", "cw_cheats_find_title": "{title}의 온라인 치트", diff --git a/client/src/console/locales/pl.json b/client/src/console/locales/pl.json index ab80f06c..d1f37839 100644 --- a/client/src/console/locales/pl.json +++ b/client/src/console/locales/pl.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Nikt jeszcze nie opublikował kodów dla tej gry.", "cw_cheats_find_offline": "Ta konsola PS5 nie może połączyć się z GitHubem. Pobierz kody w aplikacji PS5Upload na komputerze albo zezwól na raw.githubusercontent.com w ustawieniach DNS.", "cw_cheats_find_partial": "Niektóre źródła kodów są nieosiągalne, więc lista może być niepełna.", + "cw_cheats_find_replaced": "{name} został zastąpiony na konsoli. Jego kody są teraz na liście.", "cw_cheats_find_retry": "Spróbuj ponownie", "cw_cheats_find_saved": "{name} jest już na konsoli. Jego kody są teraz na liście.", "cw_cheats_find_title": "Kody online dla {title}", diff --git a/client/src/console/locales/pt-BR.json b/client/src/console/locales/pt-BR.json index 0635b9e2..f646a970 100644 --- a/client/src/console/locales/pt-BR.json +++ b/client/src/console/locales/pt-BR.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Ninguém publicou cheats para este jogo ainda.", "cw_cheats_find_offline": "Este PS5 não consegue acessar o GitHub. Obtenha cheats no app PS5Upload do seu PC ou libere raw.githubusercontent.com nas suas configurações de DNS.", "cw_cheats_find_partial": "Algumas fontes de cheats não puderam ser acessadas, então a lista pode estar incompleta.", + "cw_cheats_find_replaced": "{name} foi substituído no console. Os cheats dele já estão na lista.", "cw_cheats_find_retry": "Tentar de novo", "cw_cheats_find_saved": "{name} está no console. Os cheats dele já estão na lista.", "cw_cheats_find_title": "Cheats on-line para {title}", diff --git a/client/src/console/locales/ru.json b/client/src/console/locales/ru.json index 73597106..64d93ef7 100644 --- a/client/src/console/locales/ru.json +++ b/client/src/console/locales/ru.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Для этой игры пока никто не опубликовал читы.", "cw_cheats_find_offline": "Эта PS5 не может подключиться к GitHub. Возьмите читы в приложении PS5Upload на ПК или разрешите raw.githubusercontent.com в настройках DNS.", "cw_cheats_find_partial": "Некоторые источники читов недоступны, поэтому список может быть неполным.", + "cw_cheats_find_replaced": "{name} заменён на консоли. Его читы уже в списке.", "cw_cheats_find_retry": "Повторить", "cw_cheats_find_saved": "{name} теперь на консоли. Его читы уже в списке.", "cw_cheats_find_title": "Читы в сети для {title}", diff --git a/client/src/console/locales/th.json b/client/src/console/locales/th.json index 0d6737a0..c12b399a 100644 --- a/client/src/console/locales/th.json +++ b/client/src/console/locales/th.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "ยังไม่มีใครเผยแพร่สูตรโกงสำหรับเกมนี้", "cw_cheats_find_offline": "PS5 เครื่องนี้เชื่อมต่อ GitHub ไม่ได้ รับสูตรโกงจากแอป PS5Upload บนพีซีของคุณ หรืออนุญาต raw.githubusercontent.com ในการตั้งค่า DNS", "cw_cheats_find_partial": "เข้าถึงแหล่งสูตรโกงบางแห่งไม่ได้ รายการจึงอาจไม่ครบ", + "cw_cheats_find_replaced": "แทนที่ {name} บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", "cw_cheats_find_retry": "ลองอีกครั้ง", "cw_cheats_find_saved": "{name} อยู่บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", "cw_cheats_find_title": "สูตรโกงออนไลน์สำหรับ {title}", diff --git a/client/src/console/locales/tr.json b/client/src/console/locales/tr.json index a3514825..7b2d3c88 100644 --- a/client/src/console/locales/tr.json +++ b/client/src/console/locales/tr.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Bu oyun için henüz kimse hile yayımlamadı.", "cw_cheats_find_offline": "Bu PS5 GitHub'a ulaşamıyor. Hileleri bilgisayarınızdaki PS5Upload uygulamasından alın veya DNS ayarlarınızda raw.githubusercontent.com adresine izin verin.", "cw_cheats_find_partial": "Bazı hile kaynaklarına ulaşılamadı, bu yüzden liste eksik olabilir.", + "cw_cheats_find_replaced": "{name} konsolda değiştirildi. Hileleri artık listede.", "cw_cheats_find_retry": "Tekrar dene", "cw_cheats_find_saved": "{name} konsolda. Hileleri artık listede.", "cw_cheats_find_title": "{title} için çevrimiçi hileler", diff --git a/client/src/console/locales/vi.json b/client/src/console/locales/vi.json index 82837131..1c7644df 100644 --- a/client/src/console/locales/vi.json +++ b/client/src/console/locales/vi.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "Chưa ai đăng cheat cho trò chơi này.", "cw_cheats_find_offline": "PS5 này không kết nối được GitHub. Hãy lấy cheat bằng ứng dụng PS5Upload trên PC, hoặc cho phép raw.githubusercontent.com trong cài đặt DNS.", "cw_cheats_find_partial": "Không truy cập được một số nguồn cheat, nên danh sách có thể chưa đầy đủ.", + "cw_cheats_find_replaced": "{name} đã được thay thế trên máy. Các cheat của nó giờ đã ở trong danh sách.", "cw_cheats_find_retry": "Thử lại", "cw_cheats_find_saved": "{name} đã có trên máy. Các cheat của nó giờ đã ở trong danh sách.", "cw_cheats_find_title": "Cheat trực tuyến cho {title}", diff --git a/client/src/console/locales/zh-CN.json b/client/src/console/locales/zh-CN.json index ba453b0c..a8eb4b94 100644 --- a/client/src/console/locales/zh-CN.json +++ b/client/src/console/locales/zh-CN.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "还没有人发布此游戏的金手指。", "cw_cheats_find_offline": "这台 PS5 无法连接 GitHub。请在电脑上的 PS5Upload 应用中获取金手指,或在 DNS 设置中允许 raw.githubusercontent.com。", "cw_cheats_find_partial": "部分金手指来源无法访问,列表可能不完整。", + "cw_cheats_find_replaced": "已替换主机上的 {name}。它的金手指现在已在列表中。", "cw_cheats_find_retry": "重试", "cw_cheats_find_saved": "{name} 已保存到主机。它的金手指现在已在列表中。", "cw_cheats_find_title": "{title} 的在线金手指", diff --git a/client/src/console/locales/zh-TW.json b/client/src/console/locales/zh-TW.json index d3399932..de82f2b0 100644 --- a/client/src/console/locales/zh-TW.json +++ b/client/src/console/locales/zh-TW.json @@ -24,6 +24,7 @@ "cw_cheats_find_none": "還沒有人發布此遊戲的金手指。", "cw_cheats_find_offline": "這台 PS5 無法連線到 GitHub。請在電腦上的 PS5Upload 應用程式中取得金手指,或在 DNS 設定中允許 raw.githubusercontent.com。", "cw_cheats_find_partial": "部分金手指來源無法連線,清單可能不完整。", + "cw_cheats_find_replaced": "已取代主機上的 {name}。它的金手指現在已在清單中。", "cw_cheats_find_retry": "重試", "cw_cheats_find_saved": "{name} 已儲存到主機。它的金手指現在已在清單中。", "cw_cheats_find_title": "{title} 的線上金手指", diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index 29ef3ef8..850e8f8b 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -4328,6 +4328,7 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB: "cw_cheats_find_partial": "تعذّر الوصول إلى بعض مصادر الشفرات، لذا قد تكون القائمة ناقصة.", "cw_cheats_find_downloading": "جارٍ التنزيل…", "cw_cheats_find_downloaded": "تم التنزيل", +"cw_cheats_find_replaced": "تم استبدال {name} على الجهاز. شفراته موجودة الآن في القائمة.", "cw_cheats_find_saved": "أصبح {name} على الجهاز. شفراته موجودة الآن في القائمة.", "cw_cheats_find_err_download": "تعذّر تنزيل {name} ({code}).", "cw_cheats_find_err_store": "تعذّر حفظ {name} على الجهاز ({code}).", diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index fd9729eb..884f310f 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -4328,6 +4328,7 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা "cw_cheats_find_partial": "কিছু চিট উৎসে পৌঁছানো যায়নি, তাই তালিকাটি অসম্পূর্ণ হতে পারে।", "cw_cheats_find_downloading": "ডাউনলোড হচ্ছে…", "cw_cheats_find_downloaded": "ডাউনলোড হয়েছে", +"cw_cheats_find_replaced": "কনসোলে {name} প্রতিস্থাপন করা হয়েছে। এর চিটগুলো এখন তালিকায় আছে।", "cw_cheats_find_saved": "{name} কনসোলে আছে। এর চিটগুলো এখন তালিকায় আছে।", "cw_cheats_find_err_download": "{name} ডাউনলোড করা যায়নি ({code})।", "cw_cheats_find_err_store": "{name} কনসোলে সংরক্ষণ করা যায়নি ({code})।", diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index ab7bd193..93f63ba7 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -4336,6 +4336,7 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}", "cw_cheats_find_partial": "Einige Cheat-Quellen waren nicht erreichbar, die Liste ist daher eventuell unvollständig.", "cw_cheats_find_downloading": "Wird geladen…", "cw_cheats_find_downloaded": "Geladen", +"cw_cheats_find_replaced": "{name} wurde auf der Konsole ersetzt. Seine Cheats stehen jetzt in der Liste.", "cw_cheats_find_saved": "{name} ist auf der Konsole. Seine Cheats stehen jetzt in der Liste.", "cw_cheats_find_err_download": "{name} konnte nicht heruntergeladen werden ({code}).", "cw_cheats_find_err_store": "{name} konnte nicht auf der Konsole gespeichert werden ({code}).", diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index f7d538bb..3ade3619 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -4651,6 +4651,7 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}", "cw_cheats_find_partial": "Some cheat sources could not be reached, so the list may be incomplete.", "cw_cheats_find_downloading": "Downloading…", "cw_cheats_find_downloaded": "Downloaded", +"cw_cheats_find_replaced": "{name} was replaced on the console. Its cheats are in the list now.", "cw_cheats_find_saved": "{name} is on the console. Its cheats are in the list now.", "cw_cheats_find_err_download": "Could not download {name} ({code}).", "cw_cheats_find_err_store": "Could not save {name} on the console ({code}).", diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index 22d35137..954edda6 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -4278,6 +4278,7 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}", "cw_cheats_find_partial": "No se pudo acceder a algunas fuentes de trucos, así que la lista puede estar incompleta.", "cw_cheats_find_downloading": "Descargando…", "cw_cheats_find_downloaded": "Descargado", +"cw_cheats_find_replaced": "{name} se ha reemplazado en la consola. Sus trucos aparecen ahora en la lista.", "cw_cheats_find_saved": "{name} ya está en la consola. Sus trucos aparecen ahora en la lista.", "cw_cheats_find_err_download": "No se pudo descargar {name} ({code}).", "cw_cheats_find_err_store": "No se pudo guardar {name} en la consola ({code}).", diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index da879da8..291fd343 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -4629,6 +4629,7 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}", "cw_cheats_find_partial": "به برخی منابع چیت دسترسی نبود، پس ممکن است فهرست کامل نباشد.", "cw_cheats_find_downloading": "در حال دانلود…", "cw_cheats_find_downloaded": "دانلود شد", +"cw_cheats_find_replaced": "{name} روی کنسول جایگزین شد. چیت‌هایش اکنون در فهرست هستند.", "cw_cheats_find_saved": "{name} روی کنسول است. چیت‌هایش اکنون در فهرست هستند.", "cw_cheats_find_err_download": "دانلود {name} ممکن نشد ({code}).", "cw_cheats_find_err_store": "ذخیره {name} روی کنسول ممکن نشد ({code}).", diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 6b8ecd42..97e59416 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -4346,6 +4346,7 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}", "cw_cheats_find_partial": "Certaines sources de cheats sont injoignables : la liste est peut-être incomplète.", "cw_cheats_find_downloading": "Téléchargement…", "cw_cheats_find_downloaded": "Téléchargé", +"cw_cheats_find_replaced": "{name} a été remplacé sur la console. Ses cheats sont maintenant dans la liste.", "cw_cheats_find_saved": "{name} est sur la console. Ses cheats sont maintenant dans la liste.", "cw_cheats_find_err_download": "Impossible de télécharger {name} ({code}).", "cw_cheats_find_err_store": "Impossible d'enregistrer {name} sur la console ({code}).", diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index d4f4155b..b21f8304 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -4344,6 +4344,7 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ "cw_cheats_find_partial": "कुछ चीट स्रोतों तक नहीं पहुँचा जा सका, इसलिए सूची अधूरी हो सकती है।", "cw_cheats_find_downloading": "डाउनलोड हो रहा है…", "cw_cheats_find_downloaded": "डाउनलोड हो गया", +"cw_cheats_find_replaced": "कंसोल पर {name} को बदल दिया गया। इसके चीट अब सूची में हैं।", "cw_cheats_find_saved": "{name} कंसोल पर है। इसके चीट अब सूची में हैं।", "cw_cheats_find_err_download": "{name} डाउनलोड नहीं हो सका ({code})।", "cw_cheats_find_err_store": "{name} कंसोल पर सहेजा नहीं जा सका ({code})।", diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index b0fdb67c..672f64a2 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -4643,6 +4643,7 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}", "cw_cheats_find_partial": "Néhány csalásforrás nem érhető el, ezért a lista hiányos lehet.", "cw_cheats_find_downloading": "Letöltés…", "cw_cheats_find_downloaded": "Letöltve", +"cw_cheats_find_replaced": "A(z) {name} le lett cserélve a konzolon. A csalásai most már a listában vannak.", "cw_cheats_find_saved": "A(z) {name} a konzolon van. A csalásai most már a listában vannak.", "cw_cheats_find_err_download": "Nem sikerült letölteni: {name} ({code}).", "cw_cheats_find_err_store": "Nem sikerült menteni a konzolra: {name} ({code}).", diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index e67f3821..aa4bb10b 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -4337,6 +4337,7 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}", "cw_cheats_find_partial": "Beberapa sumber cheat tidak dapat dijangkau, jadi daftarnya mungkin tidak lengkap.", "cw_cheats_find_downloading": "Mengunduh…", "cw_cheats_find_downloaded": "Terunduh", +"cw_cheats_find_replaced": "{name} sudah diganti di konsol. Cheat-nya kini ada di daftar.", "cw_cheats_find_saved": "{name} sudah ada di konsol. Cheat-nya kini ada di daftar.", "cw_cheats_find_err_download": "Tidak dapat mengunduh {name} ({code}).", "cw_cheats_find_err_store": "Tidak dapat menyimpan {name} di konsol ({code}).", diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index c1c91a01..8d4f6465 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -4278,6 +4278,7 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}", "cw_cheats_find_partial": "Alcune fonti di cheat non sono raggiungibili, quindi l'elenco potrebbe essere incompleto.", "cw_cheats_find_downloading": "Download in corso…", "cw_cheats_find_downloaded": "Scaricato", +"cw_cheats_find_replaced": "{name} è stato sostituito sulla console. I suoi cheat ora sono nell'elenco.", "cw_cheats_find_saved": "{name} è sulla console. I suoi cheat ora sono nell'elenco.", "cw_cheats_find_err_download": "Impossibile scaricare {name} ({code}).", "cw_cheats_find_err_store": "Impossibile salvare {name} sulla console ({code}).", diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index 03f68493..6702a634 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -4349,6 +4349,7 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail "cw_cheats_find_partial": "一部のチート配布元に接続できなかったため、リストが不完全な可能性があります。", "cw_cheats_find_downloading": "ダウンロード中…", "cw_cheats_find_downloaded": "ダウンロード済み", +"cw_cheats_find_replaced": "本体の {name} を置き換えました。チートがリストに表示されています。", "cw_cheats_find_saved": "{name} を本体に保存しました。チートがリストに表示されています。", "cw_cheats_find_err_download": "{name} をダウンロードできませんでした ({code})。", "cw_cheats_find_err_store": "{name} を本体に保存できませんでした ({code})。", diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index bfdd1b8e..031e12d4 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -4347,6 +4347,7 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det "cw_cheats_find_partial": "일부 치트 출처에 연결할 수 없어 목록이 불완전할 수 있습니다.", "cw_cheats_find_downloading": "다운로드 중…", "cw_cheats_find_downloaded": "다운로드됨", +"cw_cheats_find_replaced": "콘솔의 {name}을(를) 교체했습니다. 이제 치트가 목록에 있습니다.", "cw_cheats_find_saved": "{name}이(가) 콘솔에 저장되었습니다. 이제 치트가 목록에 있습니다.", "cw_cheats_find_err_download": "{name}을(를) 다운로드할 수 없습니다 ({code}).", "cw_cheats_find_err_store": "{name}을(를) 콘솔에 저장할 수 없습니다 ({code}).", diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index 1eb638c8..d973ae41 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -4583,6 +4583,7 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}", "cw_cheats_find_partial": "Niektóre źródła kodów są nieosiągalne, więc lista może być niepełna.", "cw_cheats_find_downloading": "Pobieranie…", "cw_cheats_find_downloaded": "Pobrano", +"cw_cheats_find_replaced": "{name} został zastąpiony na konsoli. Jego kody są teraz na liście.", "cw_cheats_find_saved": "{name} jest już na konsoli. Jego kody są teraz na liście.", "cw_cheats_find_err_download": "Nie udało się pobrać {name} ({code}).", "cw_cheats_find_err_store": "Nie udało się zapisać {name} na konsoli ({code}).", diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index 51824dfa..8c90699d 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -4345,6 +4345,7 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}", "cw_cheats_find_partial": "Algumas fontes de cheats não puderam ser acessadas, então a lista pode estar incompleta.", "cw_cheats_find_downloading": "Baixando…", "cw_cheats_find_downloaded": "Baixado", +"cw_cheats_find_replaced": "{name} foi substituído no console. Os cheats dele já estão na lista.", "cw_cheats_find_saved": "{name} está no console. Os cheats dele já estão na lista.", "cw_cheats_find_err_download": "Não foi possível baixar {name} ({code}).", "cw_cheats_find_err_store": "Não foi possível salvar {name} no console ({code}).", diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index 1f6ef51f..1bdd7fed 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -4278,6 +4278,7 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла: "cw_cheats_find_partial": "Некоторые источники читов недоступны, поэтому список может быть неполным.", "cw_cheats_find_downloading": "Загрузка…", "cw_cheats_find_downloaded": "Загружено", +"cw_cheats_find_replaced": "{name} заменён на консоли. Его читы уже в списке.", "cw_cheats_find_saved": "{name} теперь на консоли. Его читы уже в списке.", "cw_cheats_find_err_download": "Не удалось скачать {name} ({code}).", "cw_cheats_find_err_store": "Не удалось сохранить {name} на консоли ({code}).", diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index b9c9f2e8..e4bf392b 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -4337,6 +4337,7 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ "cw_cheats_find_partial": "เข้าถึงแหล่งสูตรโกงบางแห่งไม่ได้ รายการจึงอาจไม่ครบ", "cw_cheats_find_downloading": "กำลังดาวน์โหลด…", "cw_cheats_find_downloaded": "ดาวน์โหลดแล้ว", +"cw_cheats_find_replaced": "แทนที่ {name} บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", "cw_cheats_find_saved": "{name} อยู่บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว", "cw_cheats_find_err_download": "ดาวน์โหลด {name} ไม่ได้ ({code})", "cw_cheats_find_err_store": "บันทึก {name} ลงเครื่องไม่ได้ ({code})", diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index 91f9a5ef..bf7d6c57 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -4335,6 +4335,7 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}", "cw_cheats_find_partial": "Bazı hile kaynaklarına ulaşılamadı, bu yüzden liste eksik olabilir.", "cw_cheats_find_downloading": "İndiriliyor…", "cw_cheats_find_downloaded": "İndirildi", +"cw_cheats_find_replaced": "{name} konsolda değiştirildi. Hileleri artık listede.", "cw_cheats_find_saved": "{name} konsolda. Hileleri artık listede.", "cw_cheats_find_err_download": "{name} indirilemedi ({code}).", "cw_cheats_find_err_store": "{name} konsola kaydedilemedi ({code}).", diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index ad76c53e..2c6a7c5a 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -4330,6 +4330,7 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}", "cw_cheats_find_partial": "Không truy cập được một số nguồn cheat, nên danh sách có thể chưa đầy đủ.", "cw_cheats_find_downloading": "Đang tải xuống…", "cw_cheats_find_downloaded": "Đã tải xuống", +"cw_cheats_find_replaced": "{name} đã được thay thế trên máy. Các cheat của nó giờ đã ở trong danh sách.", "cw_cheats_find_saved": "{name} đã có trên máy. Các cheat của nó giờ đã ở trong danh sách.", "cw_cheats_find_err_download": "Không tải xuống được {name} ({code}).", "cw_cheats_find_err_store": "Không lưu được {name} lên máy ({code}).", diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index 33c623b2..07bbc6c3 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -4284,6 +4284,7 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}", "cw_cheats_find_partial": "部分金手指来源无法访问,列表可能不完整。", "cw_cheats_find_downloading": "正在下载…", "cw_cheats_find_downloaded": "已下载", +"cw_cheats_find_replaced": "已替换主机上的 {name}。它的金手指现在已在列表中。", "cw_cheats_find_saved": "{name} 已保存到主机。它的金手指现在已在列表中。", "cw_cheats_find_err_download": "无法下载 {name}({code})。", "cw_cheats_find_err_store": "无法将 {name} 保存到主机({code})。", diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index 730980aa..1dd648a8 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -4278,6 +4278,7 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}", "cw_cheats_find_partial": "部分金手指來源無法連線,清單可能不完整。", "cw_cheats_find_downloading": "正在下載…", "cw_cheats_find_downloaded": "已下載", +"cw_cheats_find_replaced": "已取代主機上的 {name}。它的金手指現在已在清單中。", "cw_cheats_find_saved": "{name} 已儲存到主機。它的金手指現在已在清單中。", "cw_cheats_find_err_download": "無法下載 {name}({code})。", "cw_cheats_find_err_store": "無法將 {name} 儲存到主機({code})。", diff --git a/payload/src/webui_api.c b/payload/src/webui_api.c index f92d1166..667f4dda 100644 --- a/payload/src/webui_api.c +++ b/payload/src/webui_api.c @@ -1824,14 +1824,15 @@ static pthread_mutex_t g_cheat_mu = PTHREAD_MUTEX_INITIALIZER; * the raw bytes as the body (1..WEBUI_CHEAT_FILE_MAX). Stored as WEBUI_CHEATS_ROOT// * (folders made when missing) through a same-folder temp + rename, replacing a file of that name. The * cheat engine reads its folders on every list, so the game's cheats show on the next read. Answers - * {"ok":true,"format":..,"name":..,"size":N}. */ + * {"ok":true,"format":..,"name":..,"size":N,"replaced":bool} (replaced: a file of that name was there). */ static void s_cheats_store(const webui_req_t *r, const uint8_t *pre, size_t pre_len, int fd) { char fmt[WEBUI_CHEAT_FMT_CAP], name[WEBUI_CHEAT_NAME_MAX + 1], target[API_PATH], dir[API_PATH], json[256]; const char *bad; uint8_t *buf; uint64_t total = r->content_length; uint64_t left = total > pre_len ? total - pre_len : 0; - int rc; + struct stat old; + int rc, replaced; bad = webui_cheat_store_query(r->query, fmt, name); if (bad) { drain_then(fd, left); @@ -1862,6 +1863,7 @@ static void s_cheats_store(const webui_req_t *r, const uint8_t *pre, size_t pre_ return; } if (folder_of(target, dir, sizeof dir) == 0) (void)wf_sweep_stale(dir, time(NULL), WF_STALE_PART_S); + replaced = lstat(target, &old) == 0 && S_ISREG(old.st_mode); /* stores are serialized (g_cheat_mu) */ rc = wf_receive_file(fd, pre, pre_len, total, target, 1, 0777, WF_IDLE_MS, NULL, buf, CHEAT_STORE_BUF); free(buf); pthread_mutex_unlock(&g_cheat_mu); @@ -1869,8 +1871,8 @@ static void s_cheats_store(const webui_req_t *r, const uint8_t *pre, size_t pre_ send_wf(fd, rc); return; } - rc = snprintf(json, sizeof json, "{\"ok\":true,\"format\":\"%s\",\"name\":\"%s\",\"size\":%llu}", fmt, name, - (unsigned long long)total); /* both checked: [A-Za-z0-9._-] only */ + rc = snprintf(json, sizeof json, "{\"ok\":true,\"format\":\"%s\",\"name\":\"%s\",\"size\":%llu,\"replaced\":%s}", fmt, + name, (unsigned long long)total, replaced ? "true" : "false"); /* both checked: [A-Za-z0-9._-] only */ send_json(fd, 200, json, (size_t)rc); }