mirror of
https://github.com/lowbit/ps5-openrct2.git
synced 2026-10-17 23:00:58 +02:00
The import page and screen take GOG's offline installer: the setup .exe and its .bin file, chosen
or dropped together, or the whole download folder dropped. src/port/inno.c reads Inno Setup 5.5 to
6.4 (Unicode) after innoextract's reading of the format, including GOG Galaxy's layout, where each
game file sits under {tmp} in deflated parts named after their MD5. Chunks are read once, front to
back, with zlib or liblzma (xz 5.8.1, decoders only, from tools/build-deps.sh), and every file is
checked against its SHA-1. tools/test-inno.sh builds the reader for Linux and compares it with
innoextract: all 2457 game files of the Triple Thrill Pack installer match.
On the console the 554 MB installer arrives in about 50 s and unpacks on a thread in about 85 to
100 s. Creating files in the title's folder is the cost (about 35 ms each), so files are written in
place, and unfinished-import in the game folder keeps a half-unpacked folder from counting as
complete. A missing .bin is named and the .exe kept until it arrives; the screen clears its notes
when a new send starts and wraps them. The upload server also checks for a stop before every recv.
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
"""Sends the RollerCoaster Tycoon 2 game folder (or a ZIP file of it, or the GOG installer) to the
|
|
import screen, the way its web page does, and prints what the console said.
|
|
|
|
uv run --no-project python tools/send.py http://192.168.0.90:9666/ path/to/rct2
|
|
uv run --no-project python tools/send.py ADDRESS FOLDER --only Data/g1.dat Data/css1.dat
|
|
uv run --no-project python tools/send.py ADDRESS game.zip
|
|
uv run --no-project python tools/send.py ADDRESS setup_rollercoaster_tycoon_2_....exe
|
|
|
|
The screen shows the address. Only files below Data, ObjData, Scenarios, Tracks and Landscapes
|
|
are sent; --only limits that to the paths given (relative to the folder). An installer's .exe is
|
|
sent with the .bin files next to it that share its name. Used with test/console-import.cfg for
|
|
hands-free tests.
|
|
"""
|
|
import argparse
|
|
import http.client
|
|
import json
|
|
import time
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
|
|
GAME_FOLDERS = {"data", "objdata", "scenarios", "tracks", "landscapes"}
|
|
|
|
|
|
def request(address, method, path, body=None):
|
|
url = urllib.parse.urlsplit(address)
|
|
connection = http.client.HTTPConnection(url.hostname, url.port, timeout=60)
|
|
connection.request(method, path, body=body)
|
|
response = connection.getresponse()
|
|
reply = response.read().decode(errors="replace")
|
|
connection.close()
|
|
return response.status, reply
|
|
|
|
|
|
def game_path(relative):
|
|
parts = relative.split("/")
|
|
for start, part in enumerate(parts[:-1]):
|
|
if part.lower() in GAME_FOLDERS:
|
|
return "/".join(parts[start:])
|
|
return None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("address", help="the address the screen shows")
|
|
ap.add_argument("source", type=Path, help="the game folder or a ZIP file of it")
|
|
ap.add_argument("--only", nargs="+", help="send only these paths inside the folder")
|
|
ap.add_argument("--wait", type=int, default=60, help="seconds to wait for the screen to answer")
|
|
args = ap.parse_args()
|
|
|
|
deadline = time.time() + args.wait
|
|
while True:
|
|
try:
|
|
request(args.address, "GET", "/status")
|
|
break
|
|
except OSError:
|
|
if time.time() > deadline:
|
|
raise SystemExit("the screen does not answer")
|
|
time.sleep(2)
|
|
|
|
if args.source.suffix.lower() == ".exe":
|
|
parts = sorted(args.source.parent.glob(args.source.stem + "-*.bin"))
|
|
files = [(file.name, file) for file in [args.source, *parts]]
|
|
elif args.source.is_file():
|
|
files = [(args.source.name, args.source)]
|
|
else:
|
|
files = []
|
|
for file in sorted(args.source.rglob("*")):
|
|
relative = file.relative_to(args.source).as_posix()
|
|
path = game_path(relative)
|
|
if file.is_file() and path and (not args.only or relative in args.only):
|
|
files.append((path, file))
|
|
total = sum(file.stat().st_size for _, file in files)
|
|
started = time.time()
|
|
sent = 0
|
|
for index, (path, file) in enumerate(files):
|
|
try:
|
|
status, reply = request(args.address, "PUT", "/upload/" + urllib.parse.quote(path), file.read_bytes())
|
|
except OSError as error:
|
|
status, reply = 0, str(error)
|
|
sent += file.stat().st_size
|
|
if status != 200:
|
|
print(f"{path}: {status} {reply}")
|
|
elif index % 100 == 0 or index == len(files) - 1:
|
|
seconds = time.time() - started
|
|
print(f"{index + 1}/{len(files)} files, {sent >> 20} of {total >> 20} MB, "
|
|
f"{sent / max(seconds, 0.001) / 1048576:.1f} MB/s", flush=True)
|
|
request(args.address, "POST", "/done")
|
|
time.sleep(3)
|
|
try:
|
|
_, reply = request(args.address, "GET", "/status")
|
|
for item in json.loads(reply)["notes"]:
|
|
print(("ok " if item["ok"] else "fail ") + item["text"])
|
|
except OSError:
|
|
print("the screen has closed (the game is starting)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|