mirror of
https://github.com/kamb205/KB-RPCS3.git
synced 2026-10-17 21:00:50 +02:00
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Make a PS5 home-screen tile (a ShadowMountPlus folder title) for one PS3 game in RPCS3 PS5.
|
|
|
|
The tile's eboot is the launcher (tools/launcher/main.c): it starts RPCS3 PS5 with "--boot <path>".
|
|
|
|
make-game-tile.py --serial BLES01439 --name "WWE '12" --boot /app0/rpcs3/games/WWE12 \
|
|
--icon0 ICON0.PNG --pic1 PIC1.PNG [--cover cover.jpg] --launcher ~/work/tile-launcher/app --out ~/work/tiles
|
|
|
|
Art: icon0.png 512x512 (the tile) and pic0.png 3840x2160 (the background), as real PS5 games have.
|
|
With --cover (box art), the tile shows the cover; otherwise the game's ICON0 on its own PIC1.
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import shutil
|
|
import zlib
|
|
|
|
from PIL import Image, ImageFilter, ImageEnhance
|
|
|
|
|
|
def tile_title_id(serial):
|
|
# PPSA8xxxx, stable per PS3 serial (PS5 title IDs are 4 letters + 5 digits)
|
|
return "PPSA8%04d" % (zlib.crc32(serial.encode()) % 10000)
|
|
|
|
|
|
def cover_crop(img, w, h):
|
|
"""Scale and centre-crop img to fill w x h."""
|
|
scale = max(w / img.width, h / img.height)
|
|
img = img.resize((max(w, round(img.width * scale)), max(h, round(img.height * scale))), Image.LANCZOS)
|
|
left, top = (img.width - w) // 2, (img.height - h) // 2
|
|
return img.crop((left, top, left + w, top + h))
|
|
|
|
|
|
def fit(img, w, h):
|
|
scale = min(w / img.width, h / img.height)
|
|
return img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)
|
|
|
|
|
|
def backdrop(img, size):
|
|
bg = cover_crop(img, size, size).filter(ImageFilter.GaussianBlur(size // 24))
|
|
return ImageEnhance.Brightness(bg).enhance(0.55)
|
|
|
|
|
|
def make_icon(icon0, pic1, cover):
|
|
size = 512
|
|
if cover is not None:
|
|
art = cover.convert("RGB")
|
|
canvas = backdrop(art, size)
|
|
front = fit(art, size, size)
|
|
else:
|
|
art = icon0.convert("RGBA")
|
|
canvas = backdrop((pic1 or icon0).convert("RGB"), size)
|
|
front = fit(art, size - 24, size - 24)
|
|
canvas = canvas.convert("RGBA")
|
|
canvas.alpha_composite(front.convert("RGBA"), ((size - front.width) // 2, (size - front.height) // 2))
|
|
return canvas.convert("RGB")
|
|
|
|
|
|
def make_pic0(icon0, pic1, cover):
|
|
w, h = 3840, 2160
|
|
if pic1 is not None:
|
|
return cover_crop(pic1.convert("RGB"), w, h)
|
|
src = (cover or icon0).convert("RGB")
|
|
return ImageEnhance.Brightness(cover_crop(src, w, h).filter(ImageFilter.GaussianBlur(40))).enhance(0.6)
|
|
|
|
|
|
def param_json(title_id, name):
|
|
return {
|
|
"ageLevel": {"default": 0},
|
|
"applicationCategoryType": 0,
|
|
"applicationDrmType": "free",
|
|
"attribute": 0,
|
|
"attribute2": 0,
|
|
"attribute3": 524352,
|
|
"conceptId": title_id[4:],
|
|
"contentBadgeType": 1,
|
|
"contentId": "UP9000-%s_00-RPCS3TILE0000000" % title_id,
|
|
"contentVersion": "01.000.000",
|
|
"downloadDataSize": 0,
|
|
"gameIntent": {"permittedIntents": [{"intentType": "launchActivity"}]},
|
|
"localizedParameters": {"defaultLanguage": "en-US", "en-US": {"titleName": name}},
|
|
"masterVersion": "01.00",
|
|
"pubtools": {"creationDate": "2026-10-07 00:00:00", "loudnessSnd0": "-28.00", "toolVersion": "2.00"},
|
|
"requiredSystemSoftwareVersion": "0x0000000000000000",
|
|
"sdkVersion": "0x0000000000000000",
|
|
"titleId": title_id,
|
|
"versionFileUri": "",
|
|
}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--serial", required=True)
|
|
ap.add_argument("--name", required=True)
|
|
ap.add_argument("--boot", required=True, help="path RPCS3 PS5 boots, as its library lists it")
|
|
ap.add_argument("--icon0", required=True)
|
|
ap.add_argument("--pic1")
|
|
ap.add_argument("--cover")
|
|
ap.add_argument("--launcher", required=True, help="folder with eboot.bin and sce_module/")
|
|
ap.add_argument("--out", required=True)
|
|
a = ap.parse_args()
|
|
|
|
title_id = tile_title_id(a.serial)
|
|
content_id = "UP9000-%s_00-RPCS3TILE0000000" % title_id
|
|
assert len(content_id) == 36, content_id
|
|
out = os.path.join(a.out, title_id)
|
|
shutil.rmtree(out, ignore_errors=True)
|
|
os.makedirs(os.path.join(out, "sce_sys"))
|
|
shutil.copy(os.path.join(a.launcher, "eboot.bin"), out)
|
|
shutil.copytree(os.path.join(a.launcher, "sce_module"), os.path.join(out, "sce_module"))
|
|
|
|
icon0 = Image.open(a.icon0)
|
|
pic1 = Image.open(a.pic1) if a.pic1 else None
|
|
cover = Image.open(a.cover) if a.cover else None
|
|
make_icon(icon0, pic1, cover).save(os.path.join(out, "sce_sys", "icon0.png"), optimize=True)
|
|
make_pic0(icon0, pic1, cover).save(os.path.join(out, "sce_sys", "pic0.png"), optimize=True)
|
|
with open(os.path.join(out, "sce_sys", "param.json"), "w") as f:
|
|
json.dump(param_json(title_id, a.name), f, indent=2)
|
|
with open(os.path.join(out, "rpcs3-launch.txt"), "w") as f:
|
|
f.write(a.boot + "\n")
|
|
print("%s: %s (%s) -> %s" % (title_id, a.name, a.serial, out))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|