mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-18 18:10:05 -05:00
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:
commit
12a61d6199
56 files changed
+3129
-97
No files matched your search
@@ -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 \
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": "تحديث",
|
||||
|
||||
@@ -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": "রিফ্রেশ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "تازهسازی",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "रीफ़्रेश करें",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "更新",
|
||||
|
||||
@@ -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": "새로 고침",
|
||||
|
||||
@@ -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ż",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Обновить",
|
||||
|
||||
@@ -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": "รีเฟรช",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "刷新",
|
||||
|
||||
@@ -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": "重新整理",
|
||||
|
||||
@@ -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": "تغيّرت ملفات الشفرات. أُعيدت قراءة القائمة.",
|
||||
|
||||
@@ -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": "চিট ফাইল বদলে গেছে। তালিকা আবার পড়া হয়েছে।",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "فایلهای چیت تغییر کردند. فهرست دوباره خوانده شد.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "चीट फ़ाइलें बदल गईं। सूची फिर से पढ़ी गई।",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "チートファイルが変更されました。一覧を読み込み直しました。",
|
||||
|
||||
@@ -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": "치트 파일이 바뀌었습니다. 목록을 다시 읽었습니다.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Файлы читов изменились. Список прочитан заново.",
|
||||
|
||||
@@ -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": "ไฟล์สูตรโกงเปลี่ยนไป อ่านรายการใหม่แล้ว",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "金手指文件已更改,已重新读取列表。",
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user