denizka83 · 08-Июн-26 16:30(1 месяц 10 дней назад, ред. 08-Июн-26 19:40)
Ratchet & Clank Rift Apart Дата выпуска: 11.06.2021 Жанр: Платформер, шутер от третьего лица Разработчик: Insomniac Games Издательство: Sony Interactive Entertainment Локализатор: Sony Interactive Entertainment Код диска: PPSA01474 Регион игры: EUR Платформа: PS5 Тип издания: Лицензия Мультиплеер: Нет Версия игры: v01.005.003 Минимальная версия прошивки: 4.xx Работоспособность проверена релизером: Да; PS5 Digital Edition 10.01 + внешний USB-M2 SSD + Y2JB 1.5 + etaHEN 2.5B + Kstuff Lite 1.06 + ShadowMountPlus_1.6test15-fix2 Язык интерфейса: Русский, Английский, Хорватский, Чешский, Датский, Голландский, Финский, Французский, Немецкий, Греческий, Венгерский, Итальянский, Норвежский, Польскией, Португальский, Румынский, Испанский, Шведский, Турецкий Язык озвучки: Русский, Английский, Арабский, Датский, Голландский, Финский, Французский, Немецкий, Итальянский, Норвежский, Польский, Португальский, Испанский, Шведский Возраст: PEGI 7 - от 7 лет Описание:
Ratchet & Clank: Rift Apart рассказывает историю заглавных героев, которые путешествуют по разным мирам в нескольких реальностях, чтобы остановить своего врага, доктора Нефариуса. Нефариус воспользовался внезапным катастрофическим коллапсом измерений, чтобы окончательно осуществить свою мечту — полностью уничтожить органическую жизнь во вселенной. Причиной коллапса называют неисправный измеренитель, мощный артефакт, созданный ломбаксами, открывающий разломы в другие измерения. В процессе уничтожения измеренителя Рэтчет разлучается с Кланком, попадая в альтернативное измерение, где правит более способный двойник Нефариуса — император Нефариус. Они также встречают девушку-ломбакса по имени Ривет, возглавляющую группу сопротивления против императора Нефариуса. Рэтчету, Кланку и Ривет придётся работать вместе, чтобы помешать планам доктора Нефариуса и императора Нефариуса по завоеванию других реальностей.
Скриншоты
Скриншоты перевода
Установка и запуск (ОБЯЗАТЕЛЬНО К ПРОЧТЕНИЮ)
ЗАПРЕЩЕНО запускать игру с внутреннего SSD консоли/внутреннего M2. Это может привести к зависанию приставки и переходу в безопасный режим с потерей всех данных.
1. Разархивировать папку "Ratchet & Clank Rift Apart".zip на внешний USB-диск (желательно на USB-M2 SSD);
2. Запустить через ShadowMountPlus или другим способом. Размер распакованного архива примерно 78 GB.
Содержимое архива
История изменений
1. 07.06.2026 г. - создание раздачи в формате exFAT.
С внутреннего SSD или м2 дампы APR EMU лучше не запускать, есть очень большая вероятность почитать п.№8 recovery menu.
Да. А вот если упаковать игру в exfat, то можно запускать и с внутреннего, и с m2 без проблем. exfat при монтировании эмулирует внешний диск.
В целом, согласен, но есть нюанс. exFAT, также, не рекомендуется запускать с внутренней памяти. Прям в инструкции к дампам написано на dlpsgames.
Не спорю. Но, как я понимаю, это относится к самым ранним версиям прошивок. На них при монтировании exfat с внутреннего ssd бывают кп. В описании ShadowMountPlus тоже об этом сказано. У меня на 10.01 проблем нет.
89262172Добрый день, а когда нибудь появится возможность запустить с внутреннего ссд?
У меня всё работает с внутреннего SSD. В формате в exFAT. Прошивка 10.01. Запуск папками не рекомендуют может слететь прошивка если пользоваться на внутреннем.
касяк раздачи в том, что просто образ работать не будет.
нужно либо сначала распаковать в папку и тогда файлы при первом запуске проиндексируются, а затем можно обратно в exfat упаковать, но это долго.
другой способ это смонтировать образ на чтение-запись и запустить через командную строку файл build_ampr_index.py (буква диска):
создастся файл с индексами и образ можно размонтировать и отправить на плойку.
содержимое сохранить как build_ampr_index.py
скрытый текст
Код:
#!/usr/bin/env python3
"""
Build /app0/ampr_emu.index for the AMPR APR file resolver. The index is a compact binary file: AMPRIDX3 header
fixed-size sorted records
NUL-terminated /app0 path blob
prebuilt open-addressed hash slots Paths are stored with forward slashes and matched case-insensitively by the PRX.
The input root can be either a local directory or ftp://host[:port]/path/to/app0.
""" from __future__ import annotations import argparse
import ftplib
import os
import struct
import sys
from dataclasses import dataclass, field
from pathlib import Path
from urllib.parse import unquote, urlparse @dataclass
class HashCollisionStats:
probe_steps: int = 0
probed_entries: int = 0
max_probe: int = 0
duplicate_hash_groups: int = 0
duplicate_hash_entries: int = 0
duplicate_hash_samples: list[tuple[int, str, str]] = field(default_factory=list) def key_for(path: str) -> str:
return path.replace("\\", "/").lower() def fnv1a64_path_hash(path: str) -> int:
h = 1469598103934665603
for ch in key_for(path):
h ^= ord(ch)
h = (h * 1099511628211) & 0xFFFFFFFFFFFFFFFF
return h or 1 def hash_slot_count(entry_count: int) -> int:
if entry_count <= 0 or entry_count > 0xFFFFFFFE:
raise ValueError("invalid index entry count")
slots = 2
target = entry_count * 2
while slots < target:
slots <<= 1
return slots def build_hash_slots(rows: list[tuple[int, int, str]]) -> tuple[list[tuple[int, int, int]], HashCollisionStats]:
duplicate_flag = 1
slots = [(0, 0, 0) for _ in range(hash_slot_count(len(rows)))]
mask = len(slots) - 1
stats = HashCollisionStats()
duplicate_hashes: set[int] = set()
for index, (_, _, path) in enumerate(rows):
h = fnv1a64_path_hash(path)
pos = h & mask
duplicate = False
probe = 0
while slots[pos][1] != 0:
if slots[pos][0] == h:
old_hash, old_index_plus_one, old_flags = slots[pos]
slots[pos] = (old_hash, old_index_plus_one, old_flags | duplicate_flag)
if not duplicate:
if h not in duplicate_hashes:
duplicate_hashes.add(h)
stats.duplicate_hash_groups += 1
stats.duplicate_hash_entries += 2
else:
stats.duplicate_hash_entries += 1
if len(stats.duplicate_hash_samples) < 5:
stats.duplicate_hash_samples.append((h, rows[old_index_plus_one - 1][2], path))
duplicate = True
pos = (pos + 1) & mask
probe += 1
if probe:
stats.probed_entries += 1
stats.probe_steps += probe
stats.max_probe = max(stats.max_probe, probe)
slots[pos] = (h, index + 1, duplicate_flag if duplicate else 0)
return slots, stats def report_hash_collision_stats(stats: HashCollisionStats, slot_count: int, entry_count: int) -> None:
if stats.probe_steps:
print(
"info: AMPRIDX3 hash table probe stats: "
f"entries={entry_count} slots={slot_count} "
f"probedEntries={stats.probed_entries} "
f"probeSteps={stats.probe_steps} maxProbe={stats.max_probe}",
file=sys.stderr,
)
if stats.duplicate_hash_groups:
print(
"warning: AMPRIDX3 duplicate 64-bit path hashes: "
f"groups={stats.duplicate_hash_groups} entries={stats.duplicate_hash_entries}; "
"duplicate slots will force full path compare at runtime",
file=sys.stderr,
)
for h, first, second in stats.duplicate_hash_samples:
print(
f"warning: AMPRIDX3 duplicate hash sample hash=0x{h:016x}: {first} <-> {second}",
file=sys.stderr,
) def app0_path(root: Path, path: Path) -> str:
rel = path.relative_to(root).as_posix()
return "/app0/" + rel def report_progress(count: int) -> None:
if count and count % 10000 == 0:
print(f"indexed {count} files...", flush=True) def validate_and_add_row(
rows: list[tuple[int, int, str]],
seen: dict[str, str],
size: int,
mtime: int,
indexed_path: str,
allow_case_collisions: bool,
) -> bool:
if "\t" in indexed_path or "\n" in indexed_path or "\r" in indexed_path:
print(f"warning: skipping path with unsupported whitespace: {indexed_path}", file=sys.stderr)
return True key = key_for(indexed_path)
existing = seen.get(key)
if existing is not None:
msg = f"case-insensitive path collision: {existing} <-> {indexed_path}"
if not allow_case_collisions:
print(f"error: {msg}", file=sys.stderr)
return False
print(f"warning: keeping first collision entry: {msg}", file=sys.stderr)
return True seen[key] = indexed_path
rows.append((size, mtime, indexed_path))
report_progress(len(rows))
return True def write_index(rows: list[tuple[int, int, str]], output: Path) -> None:
output = output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
tmp = output.with_suffix(output.suffix + ".tmp")
rows = sorted(rows, key=lambda row: key_for(row[2])) record_struct = struct.Struct("<IIQq")
hash_slot_struct = struct.Struct("<QII")
header_struct = struct.Struct("<8sIIQQQII")
if len(rows) > 0xFFFFFFFE:
raise ValueError("index has too many records")
path_blob = bytearray()
records = bytearray()
for size, mtime, path in rows:
encoded = path.encode("utf-8") + b"\0"
offset = len(path_blob)
path_len = len(encoded) - 1
if offset > 0xFFFFFFFF or path_len > 0xFFFFFFFF:
raise ValueError("index path blob is too large")
records += record_struct.pack(offset, path_len, size, mtime)
path_blob += encoded hash_slots, hash_stats = build_hash_slots(rows)
report_hash_collision_stats(hash_stats, len(hash_slots), len(rows))
path_end = header_struct.size + len(records) + len(path_blob)
hash_offset = (path_end + (hash_slot_struct.size - 1)) & ~(hash_slot_struct.size - 1)
padding = b"\0" * (hash_offset - path_end) with tmp.open("wb") as f:
f.write(
header_struct.pack(
b"AMPRIDX3",
3,
record_struct.size,
len(rows),
len(path_blob),
hash_offset,
hash_slot_struct.size,
len(hash_slots),
)
)
f.write(records)
f.write(path_blob)
f.write(padding)
for h, index_plus_one, flags in hash_slots:
f.write(hash_slot_struct.pack(h, index_plus_one, flags))
tmp.replace(output) def build_index_local(root: Path, output: Path, allow_case_collisions: bool) -> int:
root = root.resolve()
if not root.is_dir():
print(f"error: root is not a directory: {root}", file=sys.stderr)
return 2 output = output.resolve()
output_tmp = output.with_suffix(output.suffix + ".tmp")
seen: dict[str, str] = {}
rows: list[tuple[int, int, str]] = [] for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort(key=str.lower)
filenames.sort(key=str.lower)
for filename in filenames:
path = Path(dirpath) / filename
try:
resolved = path.resolve()
except OSError as exc:
print(f"warning: skipping unresolved path {path}: {exc}", file=sys.stderr)
continue
if resolved == output or resolved == output_tmp:
continue
indexed_path = app0_path(root, path)
try:
st = path.stat()
except OSError as exc:
print(f"warning: skipping unreadable file {indexed_path}: {exc}", file=sys.stderr)
continue
if not path.is_file():
continue
if not validate_and_add_row(
rows,
seen,
st.st_size,
int(st.st_mtime),
indexed_path,
allow_case_collisions,
):
return 3 write_index(rows, output)
print(f"indexed {len(rows)} files from {root}")
print(f"wrote {output}")
return 0 def is_ftp_url(root: str) -> bool:
return urlparse(root).scheme.lower() == "ftp" def ftp_join(parent: str, name: str) -> str:
parent = parent.rstrip("/")
return f"{parent}/{name}" if parent else f"/{name}" def ftp_modify_to_int(value: str) -> int:
# MLSD modify is UTC YYYYMMDDHHMMSS. The emulator only uses mtime as
# cached metadata, so the compact integer is stable and timezone-free.
if len(value) >= 14 and value[:14].isdigit():
return int(value[:14])
return 0 def ftp_is_dir(ftp: ftplib.FTP, path: str) -> bool:
old = ftp.pwd()
try:
ftp.cwd(path)
ftp.cwd(old)
return True
except ftplib.all_errors:
try:
ftp.cwd(old)
except ftplib.all_errors:
pass
return False def ftp_file_facts(ftp: ftplib.FTP, path: str) -> tuple[int, int]:
size = 0
mtime = 0
try:
got_size = ftp.size(path)
if got_size is not None:
size = int(got_size)
except ftplib.all_errors:
pass
try:
resp = ftp.sendcmd(f"MDTM {path}")
parts = resp.split(maxsplit=1)
if len(parts) == 2:
mtime = ftp_modify_to_int(parts[1].strip())
except ftplib.all_errors:
pass
return size, mtime def ftp_list_entries(ftp: ftplib.FTP, current: str) -> list[tuple[str, dict[str, str]]]:
try:
return list(ftp.mlsd(current))
except ftplib.all_errors:
pass names = ftp.nlst(current)
result: list[tuple[str, dict[str, str]]] = []
prefix = current.rstrip("/") + "/"
for item in names:
name = item[len(prefix):] if item.startswith(prefix) else item.rsplit("/", 1)[-1]
if name in ("", ".", ".."):
continue
remote_path = ftp_join(current, name)
if ftp_is_dir(ftp, remote_path):
result.append((name, {"type": "dir"}))
else:
size, mtime = ftp_file_facts(ftp, remote_path)
result.append((name, {"type": "file", "size": str(size), "modify": str(mtime)}))
return result def parse_ftp_root(root_url: str) -> tuple[str, int, str, str, str]:
parsed = urlparse(root_url)
if not parsed.hostname:
raise ValueError(f"FTP URL has no host: {root_url}")
host = parsed.hostname
port = parsed.port or 21
user = unquote(parsed.username) if parsed.username else "anonymous"
password = unquote(parsed.password) if parsed.password else "anonymous@"
root = unquote(parsed.path or "/")
if not root.startswith("/"):
root = "/" + root
root = root.rstrip("/") or "/"
return host, port, user, password, root def collect_ftp_rows(
ftp: ftplib.FTP,
root: str,
allow_case_collisions: bool,
) -> tuple[int, list[tuple[int, int, str]]]:
seen: dict[str, str] = {}
rows: list[tuple[int, int, str]] = []
dirs_seen = 0
stack = [root]
while stack:
current = stack.pop()
dirs_seen += 1
try:
entries = ftp_list_entries(ftp, current)
except ftplib.error_perm as exc:
print(f"warning: skipping unreadable FTP directory {current}: {exc}", file=sys.stderr)
continue
entries.sort(key=lambda item: item[0].lower()) child_dirs: list[str] = []
for name, facts in entries:
if name in (".", ".."):
continue
typ = facts.get("type", "").lower()
remote_path = ftp_join(current, name)
if typ == "dir":
child_dirs.append(remote_path)
continue
if typ not in ("file", ""):
continue rel = remote_path[len(root):].lstrip("/") if root != "/" else remote_path.lstrip("/")
indexed_path = "/app0/" + rel.replace("\\", "/")
indexed_key = key_for(indexed_path)
if indexed_key in ("/app0/ampr_emu.index", "/app0/ampr_emu.index.tmp"):
continue size = int(facts.get("size", "0") or "0")
mtime = ftp_modify_to_int(facts.get("modify", ""))
if not validate_and_add_row(rows, seen, size, mtime, indexed_path, allow_case_collisions):
raise ValueError("case-insensitive path collision")
stack.extend(reversed(child_dirs))
return dirs_seen, rows def upload_index_to_ftp(ftp: ftplib.FTP, root: str, output: Path) -> None:
remote_tmp = ftp_join(root, "ampr_emu.index.tmp")
remote_dst = ftp_join(root, "ampr_emu.index")
with output.open("rb") as f:
ftp.storbinary(f"STOR {remote_tmp}", f)
try:
ftp.delete(remote_dst)
except ftplib.all_errors:
pass
ftp.rename(remote_tmp, remote_dst)
print(f"uploaded {remote_dst}") def build_index_ftp(root_url: str, output: Path, allow_case_collisions: bool, upload: bool) -> int:
ftp: ftplib.FTP | None = None
try:
host, port, user, password, root = parse_ftp_root(root_url)
ftp = ftplib.FTP()
ftp.connect(host, port, timeout=30)
ftp.login(user, password) dirs_seen, rows = collect_ftp_rows(ftp, root, allow_case_collisions)
write_index(rows, output)
print(f"indexed {len(rows)} files from {root_url} ({dirs_seen} directories)")
print(f"wrote {output.resolve()}")
if upload:
upload_index_to_ftp(ftp, root, output.resolve())
return 0
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
finally:
if ftp is not None:
try:
ftp.quit()
except ftplib.all_errors:
ftp.close() def main() -> int:
parser = argparse.ArgumentParser(description="Build AMPR /app0 case-insensitive file index")
parser.add_argument("root", help="local directory or ftp://[user[:pass]@]host[:port]/path mounted as /app0")
parser.add_argument(
"-o",
"--output",
type=Path,
help="index output path; default is <local-root>/ampr_emu.index or ./ampr_emu.index for FTP",
)
parser.add_argument(
"--upload",
action="store_true",
help="with FTP root, upload the generated index to <ftp-root>/ampr_emu.index",
)
parser.add_argument(
"--allow-case-collisions",
action="store_true",
help="do not fail when two files differ only by case; keep the first sorted entry",
)
args = parser.parse_args() if is_ftp_url(args.root):
output = args.output if args.output else Path("ampr_emu.index")
return build_index_ftp(args.root, output, args.allow_case_collisions, args.upload) if args.upload:
print("error: --upload is only valid for FTP roots", file=sys.stderr)
return 2
root = Path(args.root)
output = args.output if args.output else (root / "ampr_emu.index")
return build_index_local(root, output, args.allow_case_collisions) if __name__ == "__main__":
raise SystemExit(main())
а почему так?
вот Horizon Zero Dawn тоже имеет ARP emu, и у меня работает с voishell 3.0 + kstuff lite 1.05.
зачем тут обязательно нужен шадоумоунт и кейстафф 1.06 бета?
89262755касяк раздачи в том, что просто образ работать не будет.
нужно либо сначала распаковать в папку и тогда файлы при первом запуске проиндексируются, а затем можно обратно в exfat упаковать, но это долго.
другой способ это смонтировать образ на чтение-запись и запустить через командную строку файл build_ampr_index.py (буква диска):
создастся файл с индексами и образ можно размонтировать и отправить на плойку.
содержимое сохранить как build_ampr_index.py
скрытый текст
Код:
#!/usr/bin/env python3
"""
Build /app0/ampr_emu.index for the AMPR APR file resolver. The index is a compact binary file: AMPRIDX3 header
fixed-size sorted records
NUL-terminated /app0 path blob
prebuilt open-addressed hash slots Paths are stored with forward slashes and matched case-insensitively by the PRX.
The input root can be either a local directory or ftp://host[:port]/path/to/app0.
""" from __future__ import annotations import argparse
import ftplib
import os
import struct
import sys
from dataclasses import dataclass, field
from pathlib import Path
from urllib.parse import unquote, urlparse @dataclass
class HashCollisionStats:
probe_steps: int = 0
probed_entries: int = 0
max_probe: int = 0
duplicate_hash_groups: int = 0
duplicate_hash_entries: int = 0
duplicate_hash_samples: list[tuple[int, str, str]] = field(default_factory=list) def key_for(path: str) -> str:
return path.replace("\\", "/").lower() def fnv1a64_path_hash(path: str) -> int:
h = 1469598103934665603
for ch in key_for(path):
h ^= ord(ch)
h = (h * 1099511628211) & 0xFFFFFFFFFFFFFFFF
return h or 1 def hash_slot_count(entry_count: int) -> int:
if entry_count <= 0 or entry_count > 0xFFFFFFFE:
raise ValueError("invalid index entry count")
slots = 2
target = entry_count * 2
while slots < target:
slots <<= 1
return slots def build_hash_slots(rows: list[tuple[int, int, str]]) -> tuple[list[tuple[int, int, int]], HashCollisionStats]:
duplicate_flag = 1
slots = [(0, 0, 0) for _ in range(hash_slot_count(len(rows)))]
mask = len(slots) - 1
stats = HashCollisionStats()
duplicate_hashes: set[int] = set()
for index, (_, _, path) in enumerate(rows):
h = fnv1a64_path_hash(path)
pos = h & mask
duplicate = False
probe = 0
while slots[pos][1] != 0:
if slots[pos][0] == h:
old_hash, old_index_plus_one, old_flags = slots[pos]
slots[pos] = (old_hash, old_index_plus_one, old_flags | duplicate_flag)
if not duplicate:
if h not in duplicate_hashes:
duplicate_hashes.add(h)
stats.duplicate_hash_groups += 1
stats.duplicate_hash_entries += 2
else:
stats.duplicate_hash_entries += 1
if len(stats.duplicate_hash_samples) < 5:
stats.duplicate_hash_samples.append((h, rows[old_index_plus_one - 1][2], path))
duplicate = True
pos = (pos + 1) & mask
probe += 1
if probe:
stats.probed_entries += 1
stats.probe_steps += probe
stats.max_probe = max(stats.max_probe, probe)
slots[pos] = (h, index + 1, duplicate_flag if duplicate else 0)
return slots, stats def report_hash_collision_stats(stats: HashCollisionStats, slot_count: int, entry_count: int) -> None:
if stats.probe_steps:
print(
"info: AMPRIDX3 hash table probe stats: "
f"entries={entry_count} slots={slot_count} "
f"probedEntries={stats.probed_entries} "
f"probeSteps={stats.probe_steps} maxProbe={stats.max_probe}",
file=sys.stderr,
)
if stats.duplicate_hash_groups:
print(
"warning: AMPRIDX3 duplicate 64-bit path hashes: "
f"groups={stats.duplicate_hash_groups} entries={stats.duplicate_hash_entries}; "
"duplicate slots will force full path compare at runtime",
file=sys.stderr,
)
for h, first, second in stats.duplicate_hash_samples:
print(
f"warning: AMPRIDX3 duplicate hash sample hash=0x{h:016x}: {first} <-> {second}",
file=sys.stderr,
) def app0_path(root: Path, path: Path) -> str:
rel = path.relative_to(root).as_posix()
return "/app0/" + rel def report_progress(count: int) -> None:
if count and count % 10000 == 0:
print(f"indexed {count} files...", flush=True) def validate_and_add_row(
rows: list[tuple[int, int, str]],
seen: dict[str, str],
size: int,
mtime: int,
indexed_path: str,
allow_case_collisions: bool,
) -> bool:
if "\t" in indexed_path or "\n" in indexed_path or "\r" in indexed_path:
print(f"warning: skipping path with unsupported whitespace: {indexed_path}", file=sys.stderr)
return True key = key_for(indexed_path)
existing = seen.get(key)
if existing is not None:
msg = f"case-insensitive path collision: {existing} <-> {indexed_path}"
if not allow_case_collisions:
print(f"error: {msg}", file=sys.stderr)
return False
print(f"warning: keeping first collision entry: {msg}", file=sys.stderr)
return True seen[key] = indexed_path
rows.append((size, mtime, indexed_path))
report_progress(len(rows))
return True def write_index(rows: list[tuple[int, int, str]], output: Path) -> None:
output = output.resolve()
output.parent.mkdir(parents=True, exist_ok=True)
tmp = output.with_suffix(output.suffix + ".tmp")
rows = sorted(rows, key=lambda row: key_for(row[2])) record_struct = struct.Struct("<IIQq")
hash_slot_struct = struct.Struct("<QII")
header_struct = struct.Struct("<8sIIQQQII")
if len(rows) > 0xFFFFFFFE:
raise ValueError("index has too many records")
path_blob = bytearray()
records = bytearray()
for size, mtime, path in rows:
encoded = path.encode("utf-8") + b"\0"
offset = len(path_blob)
path_len = len(encoded) - 1
if offset > 0xFFFFFFFF or path_len > 0xFFFFFFFF:
raise ValueError("index path blob is too large")
records += record_struct.pack(offset, path_len, size, mtime)
path_blob += encoded hash_slots, hash_stats = build_hash_slots(rows)
report_hash_collision_stats(hash_stats, len(hash_slots), len(rows))
path_end = header_struct.size + len(records) + len(path_blob)
hash_offset = (path_end + (hash_slot_struct.size - 1)) & ~(hash_slot_struct.size - 1)
padding = b"\0" * (hash_offset - path_end) with tmp.open("wb") as f:
f.write(
header_struct.pack(
b"AMPRIDX3",
3,
record_struct.size,
len(rows),
len(path_blob),
hash_offset,
hash_slot_struct.size,
len(hash_slots),
)
)
f.write(records)
f.write(path_blob)
f.write(padding)
for h, index_plus_one, flags in hash_slots:
f.write(hash_slot_struct.pack(h, index_plus_one, flags))
tmp.replace(output) def build_index_local(root: Path, output: Path, allow_case_collisions: bool) -> int:
root = root.resolve()
if not root.is_dir():
print(f"error: root is not a directory: {root}", file=sys.stderr)
return 2 output = output.resolve()
output_tmp = output.with_suffix(output.suffix + ".tmp")
seen: dict[str, str] = {}
rows: list[tuple[int, int, str]] = [] for dirpath, dirnames, filenames in os.walk(root):
dirnames.sort(key=str.lower)
filenames.sort(key=str.lower)
for filename in filenames:
path = Path(dirpath) / filename
try:
resolved = path.resolve()
except OSError as exc:
print(f"warning: skipping unresolved path {path}: {exc}", file=sys.stderr)
continue
if resolved == output or resolved == output_tmp:
continue
indexed_path = app0_path(root, path)
try:
st = path.stat()
except OSError as exc:
print(f"warning: skipping unreadable file {indexed_path}: {exc}", file=sys.stderr)
continue
if not path.is_file():
continue
if not validate_and_add_row(
rows,
seen,
st.st_size,
int(st.st_mtime),
indexed_path,
allow_case_collisions,
):
return 3 write_index(rows, output)
print(f"indexed {len(rows)} files from {root}")
print(f"wrote {output}")
return 0 def is_ftp_url(root: str) -> bool:
return urlparse(root).scheme.lower() == "ftp" def ftp_join(parent: str, name: str) -> str:
parent = parent.rstrip("/")
return f"{parent}/{name}" if parent else f"/{name}" def ftp_modify_to_int(value: str) -> int:
# MLSD modify is UTC YYYYMMDDHHMMSS. The emulator only uses mtime as
# cached metadata, so the compact integer is stable and timezone-free.
if len(value) >= 14 and value[:14].isdigit():
return int(value[:14])
return 0 def ftp_is_dir(ftp: ftplib.FTP, path: str) -> bool:
old = ftp.pwd()
try:
ftp.cwd(path)
ftp.cwd(old)
return True
except ftplib.all_errors:
try:
ftp.cwd(old)
except ftplib.all_errors:
pass
return False def ftp_file_facts(ftp: ftplib.FTP, path: str) -> tuple[int, int]:
size = 0
mtime = 0
try:
got_size = ftp.size(path)
if got_size is not None:
size = int(got_size)
except ftplib.all_errors:
pass
try:
resp = ftp.sendcmd(f"MDTM {path}")
parts = resp.split(maxsplit=1)
if len(parts) == 2:
mtime = ftp_modify_to_int(parts[1].strip())
except ftplib.all_errors:
pass
return size, mtime def ftp_list_entries(ftp: ftplib.FTP, current: str) -> list[tuple[str, dict[str, str]]]:
try:
return list(ftp.mlsd(current))
except ftplib.all_errors:
pass names = ftp.nlst(current)
result: list[tuple[str, dict[str, str]]] = []
prefix = current.rstrip("/") + "/"
for item in names:
name = item[len(prefix):] if item.startswith(prefix) else item.rsplit("/", 1)[-1]
if name in ("", ".", ".."):
continue
remote_path = ftp_join(current, name)
if ftp_is_dir(ftp, remote_path):
result.append((name, {"type": "dir"}))
else:
size, mtime = ftp_file_facts(ftp, remote_path)
result.append((name, {"type": "file", "size": str(size), "modify": str(mtime)}))
return result def parse_ftp_root(root_url: str) -> tuple[str, int, str, str, str]:
parsed = urlparse(root_url)
if not parsed.hostname:
raise ValueError(f"FTP URL has no host: {root_url}")
host = parsed.hostname
port = parsed.port or 21
user = unquote(parsed.username) if parsed.username else "anonymous"
password = unquote(parsed.password) if parsed.password else "anonymous@"
root = unquote(parsed.path or "/")
if not root.startswith("/"):
root = "/" + root
root = root.rstrip("/") or "/"
return host, port, user, password, root def collect_ftp_rows(
ftp: ftplib.FTP,
root: str,
allow_case_collisions: bool,
) -> tuple[int, list[tuple[int, int, str]]]:
seen: dict[str, str] = {}
rows: list[tuple[int, int, str]] = []
dirs_seen = 0
stack = [root]
while stack:
current = stack.pop()
dirs_seen += 1
try:
entries = ftp_list_entries(ftp, current)
except ftplib.error_perm as exc:
print(f"warning: skipping unreadable FTP directory {current}: {exc}", file=sys.stderr)
continue
entries.sort(key=lambda item: item[0].lower()) child_dirs: list[str] = []
for name, facts in entries:
if name in (".", ".."):
continue
typ = facts.get("type", "").lower()
remote_path = ftp_join(current, name)
if typ == "dir":
child_dirs.append(remote_path)
continue
if typ not in ("file", ""):
continue rel = remote_path[len(root):].lstrip("/") if root != "/" else remote_path.lstrip("/")
indexed_path = "/app0/" + rel.replace("\\", "/")
indexed_key = key_for(indexed_path)
if indexed_key in ("/app0/ampr_emu.index", "/app0/ampr_emu.index.tmp"):
continue size = int(facts.get("size", "0") or "0")
mtime = ftp_modify_to_int(facts.get("modify", ""))
if not validate_and_add_row(rows, seen, size, mtime, indexed_path, allow_case_collisions):
raise ValueError("case-insensitive path collision")
stack.extend(reversed(child_dirs))
return dirs_seen, rows def upload_index_to_ftp(ftp: ftplib.FTP, root: str, output: Path) -> None:
remote_tmp = ftp_join(root, "ampr_emu.index.tmp")
remote_dst = ftp_join(root, "ampr_emu.index")
with output.open("rb") as f:
ftp.storbinary(f"STOR {remote_tmp}", f)
try:
ftp.delete(remote_dst)
except ftplib.all_errors:
pass
ftp.rename(remote_tmp, remote_dst)
print(f"uploaded {remote_dst}") def build_index_ftp(root_url: str, output: Path, allow_case_collisions: bool, upload: bool) -> int:
ftp: ftplib.FTP | None = None
try:
host, port, user, password, root = parse_ftp_root(root_url)
ftp = ftplib.FTP()
ftp.connect(host, port, timeout=30)
ftp.login(user, password) dirs_seen, rows = collect_ftp_rows(ftp, root, allow_case_collisions)
write_index(rows, output)
print(f"indexed {len(rows)} files from {root_url} ({dirs_seen} directories)")
print(f"wrote {output.resolve()}")
if upload:
upload_index_to_ftp(ftp, root, output.resolve())
return 0
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
finally:
if ftp is not None:
try:
ftp.quit()
except ftplib.all_errors:
ftp.close() def main() -> int:
parser = argparse.ArgumentParser(description="Build AMPR /app0 case-insensitive file index")
parser.add_argument("root", help="local directory or ftp://[user[:pass]@]host[:port]/path mounted as /app0")
parser.add_argument(
"-o",
"--output",
type=Path,
help="index output path; default is <local-root>/ampr_emu.index or ./ampr_emu.index for FTP",
)
parser.add_argument(
"--upload",
action="store_true",
help="with FTP root, upload the generated index to <ftp-root>/ampr_emu.index",
)
parser.add_argument(
"--allow-case-collisions",
action="store_true",
help="do not fail when two files differ only by case; keep the first sorted entry",
)
args = parser.parse_args() if is_ftp_url(args.root):
output = args.output if args.output else Path("ampr_emu.index")
return build_index_ftp(args.root, output, args.allow_case_collisions, args.upload) if args.upload:
print("error: --upload is only valid for FTP roots", file=sys.stderr)
return 2
root = Path(args.root)
output = args.output if args.output else (root / "ampr_emu.index")
return build_index_local(root, output, args.allow_case_collisions) if __name__ == "__main__":
raise SystemExit(main())
Можно подробнее действия описать?
открываю папку где у меня установлен питон.
Запускаю командную строку.
монтирую образ на чтение и запись, допустим он получил букву А при монтировании.
пишу в командной строке build_ampr_index.py А:
где то создастся фаил build_ampr_index.py, я так понимаю на смонтированном диске.
далее диск анмаунт и можно запускать уже на консоле?
еще какие-то файлы надо иметь? а то может данная команда не пройдёт в связи отсутствия какого файла.
Вобщем правильный процесс можно описать?
Разобрался, но раз всем пофиг, то мне так же))))))))))
DouleMint писал(а):
89261831
denizka83 писал(а):
89261780
ngiant писал(а):
89261730пробовал с 1.3, потом обновил до 1.5, не помогло.
Попробуйте распаковать в папочный вид.
Должна запуститься.
У авто таких версий не бывает. Обновите путём замены download0.dat с офишал репо Также, надо обновить shadowmountplus
На прошивке 4.50 игра запустилась (ShadowMountPlus_1.6test15-fix2 + kstuff lite dr 1.2-dr-test1 + BackPork 0.1) с внешнего USB-HDD, тормоза правда при подгрузке уровня
С внутреннего SSD или м2 дампы APR EMU лучше не запускать, есть очень большая вероятность почитать п.№8 recovery menu.
Да. А вот если упаковать игру в exfat, то можно запускать и с внутреннего, и с m2 без проблем. exfat при монтировании эмулирует внешний диск.
В целом, согласен, но есть нюанс. exFAT, также, не рекомендуется запускать с внутренней памяти. Прям в инструкции к дампам написано на dlpsgames.
Не спорю. Но, как я понимаю, это относится к самым ранним версиям прошивок. На них при монтировании exfat с внутреннего ssd бывают кп. В описании ShadowMountPlus тоже об этом сказано. У меня на 10.01 проблем нет.
Доброго времени суток. Не подскажешь как упаковать игру EXFAT? Хочу проверить с Marvel spider man 2 получится запустить с внутреннего SSD
С внутреннего SSD или м2 дампы APR EMU лучше не запускать, есть очень большая вероятность почитать п.№8 recovery menu.
Да. А вот если упаковать игру в exfat, то можно запускать и с внутреннего, и с m2 без проблем. exfat при монтировании эмулирует внешний диск.
В целом, согласен, но есть нюанс. exFAT, также, не рекомендуется запускать с внутренней памяти. Прям в инструкции к дампам написано на dlpsgames.
Не спорю. Но, как я понимаю, это относится к самым ранним версиям прошивок. На них при монтировании exfat с внутреннего ssd бывают кп. В описании ShadowMountPlus тоже об этом сказано. У меня на 10.01 проблем нет.
Доброго времени суток. Не подскажешь как упаковать игру EXFAT? Хочу проверить с Marvel spider man 2 получится запустить с внутреннего SSD
Программой exFAT Image Builder. В 3 клика соберет образ для тебя. Любую игру собирал на ней и вырезал ненужные языки (Для уменьшения размера игры). Запускаются с внутреннего диска без проблем [Прошивка 11.60, Y2JB+P2JB, Kstuff и ShadowMountPlus самые последние]. Еще собирал сжатые образы .ffpfsc формате, все без проблем работают, но вот Ratchet & Clank Rift Apart не запустилась, хотя и с внешнего запускал (С внутреннего не пробовал).
Игра с багом после того как битва с боссом Нефариусом заканчивается Рэтчета засасывает в портал он сначала попадает на рельсы после чего начинает бесконечно падать очевидно не прогружаются текстуры. Пробовал папкой и в exfat формате ,итог одинаковый с внешнего ssd... буду пробовать с внутреннего.Ps5 fat 11.00 poops 1.6 bdj Shadow mount 1.6 K stuff 1.07
С внутреннего SSD или м2 дампы APR EMU лучше не запускать, есть очень большая вероятность почитать п.№8 recovery menu.
Да. А вот если упаковать игру в exfat, то можно запускать и с внутреннего, и с m2 без проблем. exfat при монтировании эмулирует внешний диск.
В целом, согласен, но есть нюанс. exFAT, также, не рекомендуется запускать с внутренней памяти. Прям в инструкции к дампам написано на dlpsgames.
Не спорю. Но, как я понимаю, это относится к самым ранним версиям прошивок. На них при монтировании exfat с внутреннего ssd бывают кп. В описании ShadowMountPlus тоже об этом сказано. У меня на 10.01 проблем нет.
Доброго времени суток. Не подскажешь как упаковать игру EXFAT? Хочу проверить с Marvel spider man 2 получится запустить с внутреннего SSD
Программой exFAT Image Builder. В 3 клика соберет образ для тебя. Любую игру собирал на ней и вырезал ненужные языки (Для уменьшения размера игры). Запускаются с внутреннего диска без проблем [Прошивка 11.60, Y2JB+P2JB, Kstuff и ShadowMountPlus самые последние]. Еще собирал сжатые образы .ffpfsc формате, все без проблем работают, но вот Ratchet & Clank Rift Apart не запустилась, хотя и с внешнего запускал (С внутреннего не пробовал).
А есть вариант собирать в формат ffpfsc прямо на консоли? Или еще не внедрили ни в одну нагрузку такую функцию?