First Commit

This commit is contained in:
2025-11-18 14:18:26 -07:00
parent 33eb6e3707
commit 27277ec342
6106 changed files with 3571167 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
import os
START_IDENT = "// TRANSLATION-STRING-AREA-BEGIN"
END_IDENT = "// TRANSLATION-STRING-AREA-END"
src_file = os.path.join(os.path.dirname(__file__), "..", "pcsx2", "ImGui", "FullscreenUI.cpp")
with open(src_file, "r") as f:
full_source = f.read()
strings = []
for token in ["FSUI_STR", "FSUI_CSTR", "FSUI_FSTR", "FSUI_NSTR", "FSUI_VSTR", "FSUI_ICONSTR", "FSUI_ICONSTR_S"]:
token_len = len(token)
last_pos = 0
while True:
last_pos = full_source.find(token, last_pos)
if last_pos < 0:
break
if last_pos >= 8 and full_source[last_pos - 8:last_pos] == "#define ":
last_pos += len(token)
continue
if full_source[last_pos + token_len] == '(':
start_pos = last_pos + token_len + 1
end_pos = full_source.find("\")", start_pos)
s = full_source[start_pos:end_pos+1]
# remove "
pos = s.find('"')
new_s = ""
while pos >= 0:
if pos == 0 or s[pos - 1] != '\\':
epos = pos
while True:
epos = s.find('"', epos + 1)
assert epos > pos
if s[epos - 1] == '\\':
continue
else:
break
assert epos > pos
new_s += s[pos+1:epos]
cpos = s.find(',', epos + 1)
pos = s.find('"', epos + 1)
if cpos >= 0 and pos >= 0 and cpos < pos:
break
else:
pos = s.find('"', pos + 1)
assert len(new_s) > 0
#assert (end_pos - start_pos) < 300
#if (end_pos - start_pos) >= 300:
# print("WARNING: Long string")
# print(new_s)
if new_s not in strings:
strings.append(new_s)
last_pos += len(token)
print(f"Found {len(strings)} unique strings.")
start = full_source.find(START_IDENT)
end = full_source.find(END_IDENT)
assert start >= 0 and end > start
new_area = ""
for string in list(strings):
new_area += f"TRANSLATE_NOOP(\"FullscreenUI\", \"{string}\");\n"
full_source = full_source[:start+len(START_IDENT)+1] + new_area + full_source[end:]
with open(src_file, "w") as f:
f.write(full_source)

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python3
import sys
import xml.etree.ElementTree as ET
import re
import yaml
# Database downloadable from http://redump.org/datfile/ps2/serial,version,description
def parse_serials(serials_text):
serials = []
serials_text = serials_text.replace("&", ",")
serials_text = serials_text.replace("/", ",")
for serial in serials_text.split(","):
serial = serial.strip()
if len(serial) < 3:
continue
matches = re.match("([A-Z0-9a-z]+)[\- ]([0-9]+)\-([0-9]+).*", serial)
if matches is not None:
rlen = len(matches[3])
base = matches[2][:-rlen]
start = int(matches[2][-rlen:])
end = int(matches[3])
fmt = "%0" + str(rlen) + "d"
for rbit in range(start, end + 1):
code = matches[1] + "-" + base + (fmt % rbit)
if code in serials:
continue
serials.append(code)
else:
matches = re.match("([A-Z0-9a-z]+)[\- ]([0-9]+).*", serial)
if matches is None:
continue
code = matches[1] + "-" + matches[2]
if code in serials:
continue
serials.append(code)
return serials
def parse_redump(filename):
games = []
tree = ET.parse(filename)
for child in tree.getroot():
if (child.tag != "game"):
continue
name = child.get("name")
name = name.strip() if name is not None else ""
node = child.find("version")
version = node.text.strip() if node is not None else ""
node = child.find("serial")
serials_text = node.text.strip() if node is not None else ""
serials = parse_serials(serials_text)
# remove version from title if it exists
sversion = "(" + version + ")"
name = name.replace(sversion, "")
hashes = []
for grandchild in child:
if grandchild.tag != "rom":
continue
tname = grandchild.get("name")
if ".cue" in tname:
continue
tsize = int(grandchild.get("size"))
tmd5 = grandchild.get("md5")
track = 1
matches = re.match(".*\(Track ([0-9]+)\)", tname)
if matches is not None:
track = int(matches[1])
expected_track = len(hashes) + 1
if track != expected_track:
print("Expected track %d got track %d" % (expected_track, track))
hashes.append({"size": tsize,
"md5": tmd5
})
if len(hashes) == 0:
print("No hashes for %s" % name)
continue
game = {
"name": name,
"hashes": hashes
}
if len(version) > 0:
game["version"] = version
if len(serials) > 0:
game["serial"] = serials[0]
games.append(game)
return games
def write_yaml(games, filename):
with open(filename, "w") as f:
f.write(yaml.dump(games))
if __name__ == "__main__":
if len(sys.argv) < 3:
print("usage: %s <redump xml> <output yaml>" % sys.argv[0])
sys.exit(1)
print("Loading %s..." % sys.argv[1])
games = parse_redump(sys.argv[1])
if len(games) == 0:
print("No games found in dat file")
sys.exit(1)
print("Writing %s..." % sys.argv[2])
write_yaml(games, sys.argv[2])
sys.exit(0)

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
import sys
import os
import glob
import re
import functools
# PCSX2 - PS2 Emulator for PCs
# Copyright (C) 2002-2025 PCSX2 Dev Team
#
# PCSX2 is free software: you can redistribute it and/or modify it under the terms
# of the GNU General Public License as published by the Free Software Found-
# ation, either version 3 of the License, or (at your option) any later version.
#
# PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with PCSX2.
# If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=bare-except, disable=missing-function-docstring
src_dirs = [os.path.join(os.path.dirname(__file__), "..", "pcsx2"), os.path.join(os.path.dirname(__file__), "..", "pcsx2-qt")]
fa_file = os.path.join(os.path.dirname(__file__), "..", "3rdparty", "include", "IconsFontAwesome5.h")
pf_file = os.path.join(os.path.dirname(__file__), "..", "3rdparty", "include", "IconsPromptFont.h")
dst_file = os.path.join(os.path.dirname(__file__), "..", "pcsx2", "ImGui", "ImGuiManager.cpp")
all_source_files = list(functools.reduce(lambda prev, src_dir: prev + glob.glob(os.path.join(src_dir, "**", "*.cpp"), recursive=True) + \
glob.glob(os.path.join(src_dir, "**", "*.h"), recursive=True) + \
glob.glob(os.path.join(src_dir, "**", "*.inl"), recursive=True), src_dirs, []))
tokens = set()
pf_tokens = set()
for filename in all_source_files:
data = None
with open(filename, "r") as f:
try:
data = f.read()
except:
continue
tokens = tokens.union(set(re.findall("(ICON_FA_[a-zA-Z0-9_]+)", data)))
pf_tokens = pf_tokens.union(set(re.findall("(ICON_PF_[a-zA-Z0-9_]+)", data)))
print("{}/{} tokens found.".format(len(tokens), len(pf_tokens)))
if len(tokens) == 0 and len(pf_tokens) == 0:
sys.exit(0)
u8_encodings = {}
with open(fa_file, "r") as f:
for line in f.readlines():
match = re.match("#define (ICON_FA_[^ ]+) \"([^\"]+)\"", line)
if match is None:
continue
u8_encodings[match[1]] = bytes.fromhex(match[2].replace("\\x", ""))
with open(pf_file, "r") as f:
for line in f.readlines():
match = re.match("#define (ICON_PF_[^ ]+) \"([^\"]+)\"", line)
if match is None:
continue
u8_encodings[match[1]] = bytes.fromhex(match[2].replace("\\x", ""))
out_pattern = "(static constexpr ImWchar range_fa\[\] = \{)[0-9A-Z_a-z, \n]+(\};)"
out_pf_pattern = "(static constexpr ImWchar range_pf\[\] = \{)[0-9A-Z_a-z, \n]+(\};)"
def get_pairs(tokens):
codepoints = list()
for token in tokens:
u8_bytes = u8_encodings[token]
u8 = str(u8_bytes, "utf-8")
u16 = u8.encode("utf-16le")
if len(u16) > 2:
raise ValueError("{} {} too long".format(u8_bytes, token))
codepoint = int.from_bytes(u16, byteorder="little", signed=False)
codepoints.append(codepoint)
codepoints.sort()
codepoints.append(0) # null terminator
startc = codepoints[0]
endc = None
pairs = [startc]
for codepoint in codepoints:
if endc is not None and (endc + 1) != codepoint:
pairs.append(endc)
pairs.append(codepoint)
startc = codepoint
endc = codepoint
else:
endc = codepoint
pairs.append(endc)
pairs_str = ",".join(list(map("0x{:x}".format, pairs)))
return pairs_str
with open(dst_file, "r") as f:
original = f.read()
updated = re.sub(out_pattern, "\\1 " + get_pairs(tokens) + " \\2", original)
updated = re.sub(out_pf_pattern, "\\1 " + get_pairs(pf_tokens) + " \\2", updated)
if original != updated:
with open(dst_file, "w") as f:
f.write(updated)
print("Updated {}".format(dst_file))
else:
print("Skipping updating {}".format(dst_file))

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# PCSX2 - PS2 Emulator for PCs
# Copyright (C) 2002-2025 PCSX2 Dev Team
#
# PCSX2 is free software: you can redistribute it and/or modify it under the terms
# of the GNU General Public License as published by the Free Software Found-
# ation, either version 3 of the License, or (at your option) any later version.
#
# PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with PCSX2.
# If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=bare-except, disable=missing-function-docstring
import glob
import os
import sys
def merge_patches(srcdir, dstdir, label, desc, extralines=None):
for file in glob.glob(os.path.join(srcdir, "*.pnach")):
print(f"Reading {file}...")
name = os.path.basename(file)
with open(file, "rb") as f:
lines = f.read().decode().strip().split("\n")
gametitle_line = None
comment_line = None
for line in lines:
line = line.strip()
if line.startswith("gametitle=") and gametitle_line is None:
gametitle_line = line
elif line.startswith("comment=") and comment_line is None:
comment_line = line[8:]
# ignore gametitle if file already exists
outname = os.path.join(dstdir, name)
if os.path.exists(outname):
gametitle_line = None
with open(outname, "ab") as f:
if gametitle_line is not None:
f.write((gametitle_line + "\n\n").encode())
f.write(f"[{label}]\n".encode())
if desc is not None and comment_line is None:
f.write(f"description={desc}\n".encode())
if extralines is not None:
f.write(f"{extralines}\n".encode())
for line in lines:
line = line.strip()
if not line.startswith("gametitle="):
f.write((line + "\n").encode())
f.write("\n\n".encode())
print(f"Wrote/updated {outname}")
if __name__ == "__main__":
if len(sys.argv) < 4:
print(f"Usage: {sys.argv[0]} <ws directory> <ni directory> <output directory>")
sys.exit(1)
outdir = sys.argv[3]
if not os.path.isdir(outdir):
os.mkdir(outdir)
merge_patches(sys.argv[1], outdir, "Widescreen 16:9", "Renders the game in 16:9 aspect ratio, instead of 4:3.", "gsaspectratio=16:9")
merge_patches(sys.argv[2], outdir, "No-Interlacing", "Attempts to disable interlaced offset rendering.", "gsinterlacemode=1")

13
tools/retry.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
RETRIES=10
for i in $(seq 1 "$RETRIES"); do
"$@" && break
if [ "$i" == "$RETRIES" ]; then
echo "Command \"$@\" failed after ${RETRIES} retries."
exit 1
fi
done
exit 0

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
import sys
import glob
import os
import argparse
from PIL import Image
# PCSX2 - PS2 Emulator for PCs
# Copyright (C) 2002-2025 PCSX2 Dev Team
#
# PCSX2 is free software: you can redistribute it and/or modify it under the terms
# of the GNU General Public License as published by the Free Software Found-
# ation, either version 3 of the License, or (at your option) any later version.
#
# PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with PCSX2.
# If not, see <http://www.gnu.org/licenses/>.
DESCRIPTION = """Quick script to scale alpha values commonly seen in PCSX2 texture dumps.
This script will scale textures with a maximum alpha intensity of 128 to 255, and then back
again with the unscale command, suitable for use as replacements. Not unscaling after editing
may result in broken rendering!
Example usage:
python3 texture_dump_alpha_scaler.py scale path/to/serial/dumps
<edit your images, move to replacements, including the index txt>
python3 texture_dump_alpha_scaler.py unscale path/to/serial/replacements
"""
# pylint: disable=bare-except, disable=missing-function-docstring
def get_index_path(idir):
return os.path.join(idir, "__scaled_images__.txt")
def scale_image(path, relpath):
try:
img = Image.open(path, "r")
except:
return False
print("Processing '%s'" % relpath)
if img.mode != "RGBA":
print(" Skipping because it's not RGBA (%s)" % img.mode)
return False
data = img.getdata()
max_alpha = max(map(lambda p: p[3], data))
print(" max alpha %u" % max_alpha)
if max_alpha > 128:
print(" skipping because of large alpha value")
return False
new_pixels = list(map(lambda p: (p[0], p[1], p[2], min(p[3] * 2 - 1, 255)), data))
img.putdata(new_pixels)
img.save(path)
print(" scaled!")
return True
def unscale_image(path, relpath):
try:
img = Image.open(path, "r")
except:
return False
print("Processing '%s'" % relpath)
if img.mode != "RGBA":
print(" Skipping because it's not RGBA (%s)" % img.mode)
return False
data = img.getdata()
new_pixels = list(map(lambda p: (p[0], p[1], p[2], max((p[3] + 1) // 2, 0)), data))
img.putdata(new_pixels)
img.save(path)
print(" unscaled!")
return True
def get_scaled_images(idir):
try:
scaled_images = set()
with open(get_index_path(idir), "r") as ifile:
for line in ifile.readlines():
line = line.strip()
if len(line) == 0:
continue
scaled_images.add(line)
return scaled_images
except:
return set()
def put_scaled_images(idir, scaled_images):
if len(scaled_images) > 0:
with open(get_index_path(idir), "w") as ifile:
ifile.writelines(map(lambda s: s + "\n", scaled_images))
elif os.path.exists(get_index_path(idir)):
os.remove(get_index_path(idir))
def scale_images(idir, force):
scaled_images = get_scaled_images(idir)
for path in glob.glob(idir + "/**", recursive=True):
relpath = os.path.relpath(path, idir)
if not path.endswith(".png"):
continue
if relpath in scaled_images and not force:
continue
if not scale_image(path, relpath):
continue
scaled_images.add(relpath)
put_scaled_images(idir, scaled_images)
def unscale_images(idir, force):
scaled_images = get_scaled_images(idir)
if force:
for path in glob.glob(idir + "/**", recursive=True):
relpath = os.path.relpath(path, idir)
if not path.endswith(".png"):
continue
scaled_images.add(relpath)
for relpath in list(scaled_images):
if unscale_image(os.path.join(idir, relpath), relpath):
scaled_images.remove(relpath)
put_scaled_images(idir, scaled_images)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=DESCRIPTION,
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("command", type=str,
help="Command, should be scale or unscale")
parser.add_argument("directory", type=str,
help="Directory containing images, searched recursively")
parser.add_argument("--force",
help="Scale images regardless of whether it's in the index",
action="store_true", required=False)
args = parser.parse_args()
if args.command == "scale":
scale_images(args.directory, args.force)
sys.exit(0)
elif args.command == "unscale":
unscale_images(args.directory, args.force)
sys.exit(0)
else:
print("Unknown command, should be scale or unscale")
sys.exit(1)