Merge console cheat downloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
phantomptrandClaude Opus 5.5 committed 2026-10-18 11:56:12 -07:00
commit 12a61d6199
56 files changed
+3129 -97

No files matched your search

+8
View File
@@ -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 \
+32 -4
View File
@@ -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,
@@ -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 () => {
@@ -291,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);
});
});
+101 -6
View File
@@ -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, savedText, 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 (
<button type="button" className={`cw-focusable cw-button${more ? "" : " is-primary"}`} data-action="cheats-find" onClick={onFind}>
<Search aria-hidden />
<span>{t(more ? "cw_cheats_find_more" : "cw_cheats_find")}</span>
</button>
);
}
/** 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 (
<div className={`cw-card cw-cheats-empty${tone === "error" ? " is-error" : ""}`} role={tone === "error" ? "alert" : "status"}>
<p className="cw-cheats-empty-title">{title}</p>
{next ? <p className="cw-dim">{next}</p> : null}
<RefreshButton onRefresh={onRefresh} />
<div className="cw-cheats-empty-buttons">
{onFind ? <FindButton onFind={onFind} /> : null}
<RefreshButton onRefresh={onRefresh} />
</div>
</div>
);
}
@@ -98,7 +128,7 @@ export function CheatsView(p: CheatsViewProps) {
body = <Empty title={t("cw_cheats_exited")} next={t("cw_cheats_exited_next")} onRefresh={p.onRefresh} />;
break;
case "none":
body = <Empty title={t("cw_cheats_none")} next={t("cw_cheats_none_next")} onRefresh={p.onRefresh} />;
body = <Empty title={t("cw_cheats_none")} next={t("cw_cheats_none_next")} onRefresh={p.onRefresh} onFind={p.onFind} />;
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")}
</div>
</div>
<RefreshButton onRefresh={p.onRefresh} />
<div className="cw-cheats-game-buttons">
{phase.kind === "list" && p.onFind ? <FindButton onFind={p.onFind} more /> : null}
<RefreshButton onRefresh={p.onRefresh} />
</div>
</section>
) : null}
{p.notice ? (
<div className="cw-msg is-info" role="status">
{p.notice}
</div>
) : null}
{s.message ? (
<div className="cw-msg is-error" role="alert">
{s.message}
@@ -144,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;
@@ -158,6 +203,25 @@ export function Cheats() {
const [state, setState] = useState<CheatsState>(EMPTY_STATE);
const ctl = useRef<CheatsController | null>(null);
const root = useRef<HTMLDivElement>(null);
const [finderState, setFinderState] = useState<FinderState>(FINDER_CLOSED);
const finder = useRef<Finder | null>(null);
const [notice, setNotice] = useState<{ title: string; text: string } | null>(null);
// 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, 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 () => {
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 +267,20 @@ export function Cheats() {
);
const titleId = state.status?.game?.title_id ?? null;
// 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 (!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<string | null>(null);
const cover = loadedCover && coverFailed !== loadedCover ? loadedCover : null;
@@ -215,6 +293,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}
/>
<FindCheats
state={finderState}
onDownload={(e) => void finder.current?.download(e)}
onRetry={() => {
if (finderState.kind !== "closed") void finder.current?.open(finderState.title);
}}
onClose={() => finder.current?.close()}
/>
</div>
);
+152
View File
@@ -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 (
<li className="cw-finder-item">
<button
type="button"
className={`cw-focusable cw-button cw-finder-entry${match ? " is-match" : ""}`}
aria-busy={busy || undefined}
aria-disabled={disabled || undefined}
data-cheat-file={entry.filename}
data-nav-default={first ? "" : undefined}
onClick={() => onDownload(entry)}
>
<Download aria-hidden />
<span className="cw-finder-text">
<span className="cw-finder-name">
{entry.gameVersion ? `v${entry.gameVersion}` : t("cw_cheats_find_any_version")}
{match ? <span className="cw-tag is-on">{t("cw_cheats_find_your_version")}</span> : null}
</span>
<span className="cw-dim cw-finder-meta">
{entry.filename} · {entry.format.toUpperCase()} · {repoName(entry.repoId)}
{entry.gameTitle ? ` · ${entry.gameTitle}` : ""}
</span>
</span>
{tag ? <span className="cw-tag">{tag}</span> : null}
</button>
</li>
);
}
/** 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<HTMLElement>(".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 = (
<p className="cw-dim" role="status">
{t("cw_cheats_find_loading")}
</p>
);
} else if (state.kind === "offline") {
body = (
<div className="cw-msg is-error" role="alert">
{t("cw_cheats_find_offline")}
</div>
);
} else if (state.entries.length === 0) {
body = (
<>
<p role="status">{t("cw_cheats_find_none")}</p>
{state.partial ? <p className="cw-dim">{t("cw_cheats_find_partial")}</p> : null}
</>
);
} else {
const v = state.version;
const noMatch = !!v && !state.entries.some((e) => sameVersion(e.gameVersion, v));
body = (
<>
<p className="cw-finder-hint">{t("cw_cheats_find_hint")}</p>
<p className="cw-dim cw-finder-hint">
{v ? <span className="cw-finder-version">{t("cw_cheats_find_version", { v })}</span> : null}
{t("cw_cheats_find_how")}
</p>
{noMatch ? <div className="cw-msg">{t("cw_cheats_find_no_match", { v: v ?? "" })}</div> : null}
{state.partial ? <p className="cw-dim cw-finder-hint">{t("cw_cheats_find_partial")}</p> : null}
<ul className="cw-finder-list">
{state.entries.map((e, i) => (
<EntryRow
key={`${e.repoId}/${e.filename}`}
entry={e}
match={sameVersion(e.gameVersion, v)}
busy={state.busy === e.filename}
disabled={state.busy !== null}
done={state.done.includes(e.filename)}
first={i === 0}
onDownload={onDownload}
/>
))}
</ul>
</>
);
}
const message = state.kind === "list" ? state.message : null;
return (
<Modal label={title} className="cw-finder" onBack={onClose}>
<h2 className="cw-sheet-title">{title}</h2>
{body}
{message ? (
<div className={`cw-msg ${message.tone === "error" ? "is-error" : "is-info"}`} role={message.tone === "error" ? "alert" : "status"}>
{message.text}
</div>
) : null}
<div className="cw-dialog-buttons">
{state.kind === "offline" ? (
<button type="button" className="cw-focusable cw-button" data-nav-default="" onClick={onRetry}>
<RefreshCw aria-hidden />
<span>{t("cw_cheats_find_retry")}</span>
</button>
) : null}
<button type="button" className="cw-focusable cw-button" data-action="finder-close" onClick={onClose}>
{t("cw_cheats_find_close")}
</button>
</div>
</Modal>
);
}
+293
View File
@@ -0,0 +1,293 @@
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../api", async (importOriginal) => {
const real = await importOriginal<typeof import("../api")>();
return { ...real, call: vi.fn(), callRaw: vi.fn() };
});
import { ApiError, call, callRaw } from "../api";
import { t } from "../strings";
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";
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; replaced?: boolean } = {}) {
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, replaced: opts.replaced === true } as never;
});
return stored;
}
/** GitHub for the content files: url → body; anything else 404. */
function github(files: Record<string, string>, 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 full: StoredFile[] = [];
const f = createFinder(
(s) => states.push(s),
(x) => {
stored.push([x.entry, x.title]);
full.push(x);
},
{
index: async () => {
if (opts.index instanceof Error) throw opts.index;
return opts.index ?? INDEX;
},
fetch: opts.fetch ?? github({}),
},
);
return { f, states, stored, full };
}
const render = (state: FinderState) =>
renderToStaticMarkup(<FindCheats state={state} onDownload={() => {}} 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<void>((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<RepoIndex>((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");
});
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<void>((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" });
});
});
+184
View File
@@ -0,0 +1,184 @@
/**
* 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<RepoIndex>;
fetch?: FetchLike;
}
export interface Finder {
/** Look for `titleId`'s cheats (the repos are read once per page session). */
open(titleId: string): Promise<void>;
close(): void;
/** Download `entry` and store it on the console; ignored while another one runs. */
download(entry: CheatEntry): Promise<void>;
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<string | null> {
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)}`;
}
/** 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 when it is still the one that asked).
*/
export function createFinder(
onChange: (s: FinderState) => void,
onStored: (stored: StoredFile) => 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";
let replaced: boolean;
try {
const bytes = await downloadCheat(entry, deps.fetch);
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);
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: savedText(entry.filename, replaced) },
});
}
onStored({ entry, title, replaced, current: still() });
},
get: () => s,
};
}
+342
View File
@@ -0,0 +1,342 @@
import { beforeEach, describe, expect, it } from "vitest";
import {
CHEAT_FILE_MAX,
CHEAT_REPOS,
contentUrls,
downloadCheat,
entriesFor,
fetchLimited,
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<string, string | number | "down" | ArrayBuffer>) {
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 <prefix><format>/<file> 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]: "<html>" });
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]]: "<Trainer/>" });
const buf = await downloadCheat(entry, f);
expect(new TextDecoder().decode(buf)).toBe("<Trainer/>");
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" });
});
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<Uint8Array>({
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<Response>(() => {}), "u", 30, 10);
await expect(r).rejects.toMatchObject({ network: true });
});
});
+437
View File
@@ -0,0 +1,437 @@
/**
* 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>[_<variant>].<ext>` → [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 `<prefix><format>/<filename>`, 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: `<content base><dir>/<file>`
* 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<Response>;
/** 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;
}
}
/** 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<Fetched> {
const stop = typeof AbortController === "function" ? new AbortController() : null;
let timer: ReturnType<typeof setTimeout> | undefined;
const deadline = new Promise<never>((_, 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 = <T,>(p: Promise<T>): Promise<T> => Promise.race([p, deadline]);
deadline.catch(() => {});
let res: Response;
try {
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<string> {
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. */
async function listRepo(repo: CheatRepo, f: FetchLike, timeoutMs: number): Promise<Listing> {
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<Listing> => 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<RepoIndex> {
const results = await Promise.allSettled(repos.map((r) => listRepo(r, f, timeoutMs)));
const seen = new Set<string>();
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<RepoIndex> | 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<RepoIndex> {
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, 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<ArrayBuffer> {
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 r: Fetched;
try {
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 (!r.ok) continue;
if (r.body.byteLength === 0) throw new RepoError(false, "empty");
return r.body;
}
throw new RepoError(!answered, answered ? "not_found" : "unreachable");
}
+75
View File
@@ -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 {
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "تم استبدال {name} على الجهاز. شفراته موجودة الآن في القائمة.",
"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": "تحديث",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "কনসোলে {name} প্রতিস্থাপন করা হয়েছে। এর চিটগুলো এখন তালিকায় আছে।",
"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": "রিফ্রেশ",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "{name} روی کنسول جایگزین شد. چیت‌هایش اکنون در فهرست هستند.",
"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": "تازه‌سازی",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "कंसोल पर {name} को बदल दिया गया। इसके चीट अब सूची में हैं।",
"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": "रीफ़्रेश करें",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "本体の {name} を置き換えました。チートがリストに表示されています。",
"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": "更新",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "콘솔의 {name}을(를) 교체했습니다. 이제 치트가 목록에 있습니다.",
"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": "새로 고침",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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ż",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "{name} заменён на консоли. Его читы уже в списке.",
"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": "Обновить",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "แทนที่ {name} บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว",
"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": "รีเฟรช",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_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}",
"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",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "已替换主机上的 {name}。它的金手指现在已在列表中。",
"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": "刷新",
+24 -2
View File
@@ -9,12 +9,34 @@
"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_replaced": "已取代主機上的 {name}。它的金手指現在已在清單中。",
"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": "重新整理",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "تم استبدال {name} على الجهاز. شفراته موجودة الآن في القائمة.",
"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": "تغيّرت ملفات الشفرات. أُعيدت قراءة القائمة.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "কনসোলে {name} প্রতিস্থাপন করা হয়েছে। এর চিটগুলো এখন তালিকায় আছে।",
"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": "চিট ফাইল বদলে গেছে। তালিকা আবার পড়া হয়েছে।",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "{name} روی کنسول جایگزین شد. چیت‌هایش اکنون در فهرست هستند.",
"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": "فایل‌های چیت تغییر کردند. فهرست دوباره خوانده شد.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "कंसोल पर {name} को बदल दिया गया। इसके चीट अब सूची में हैं।",
"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": "चीट फ़ाइलें बदल गईं। सूची फिर से पढ़ी गई।",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "本体の {name} を置き換えました。チートがリストに表示されています。",
"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": "チートファイルが変更されました。一覧を読み込み直しました。",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "콘솔의 {name}을(를) 교체했습니다. 이제 치트가 목록에 있습니다.",
"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": "치트 파일이 바뀌었습니다. 목록을 다시 읽었습니다.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "{name} заменён на консоли. Его читы уже в списке.",
"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": "Файлы читов изменились. Список прочитан заново.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "แทนที่ {name} บนเครื่องแล้ว สูตรโกงของมันอยู่ในรายการแล้ว",
"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": "ไฟล์สูตรโกงเปลี่ยนไป อ่านรายการใหม่แล้ว",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_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}).",
"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.",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "已替换主机上的 {name}。它的金手指现在已在列表中。",
"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": "金手指文件已更改,已重新读取列表。",
+24 -2
View File
@@ -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,28 @@ 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_replaced": "已取代主機上的 {name}。它的金手指現在已在清單中。",
"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": "金手指檔案已變更,已重新讀取清單。",
+1 -1
View File
@@ -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)
+39
View File
@@ -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=<f>&name=<file>; 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 <stddef.h>
#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 ".<fmt>" (any case) after a non-empty
* stem. 1, else 0. */
int webui_cheat_name_ok(const char *fmt, const char *name);
/* The query "format=<f>&name=<file>" (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]);
/* "<root>/<fmt>/<name>" into `out`, creating <root> and <root>/<fmt> (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
+86 -2
View File
@@ -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,66 @@ 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=<file>,
* the raw bytes as the body (1..WEBUI_CHEAT_FILE_MAX). Stored as WEBUI_CHEATS_ROOT/<format>/<name>
* (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,"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;
struct stat old;
int rc, replaced;
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);
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);
if (rc != WF_OK) {
send_wf(fd, rc);
return;
}
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);
}
/* 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 +2163,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 +2181,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 +2247,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) {
+151
View File
@@ -0,0 +1,151 @@
/* See include/webui_cheats_store.h. */
#include "webui_cheats_store.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#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 ".<fmt>": 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;
}
+220
View File
@@ -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 <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#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;
}