First Commit
This commit is contained in:
75
tools/generate_fullscreen_ui_translation_strings.py
Executable file
75
tools/generate_fullscreen_ui_translation_strings.py
Executable 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)
|
||||
121
tools/generate_redump_yaml.py
Normal file
121
tools/generate_redump_yaml.py
Normal 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)
|
||||
108
tools/generate_update_fa_glyph_ranges.py
Executable file
108
tools/generate_update_fa_glyph_ranges.py
Executable 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))
|
||||
73
tools/merge_ws_ni_patches.py
Normal file
73
tools/merge_ws_ni_patches.py
Normal 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
13
tools/retry.sh
Executable 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
|
||||
163
tools/texture_dump_alpha_scaler.py
Executable file
163
tools/texture_dump_alpha_scaler.py
Executable 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)
|
||||
Reference in New Issue
Block a user