Files
ppsspp/Tools/update_gamecontrollerdb.py
T
Henrik RydgårdandClaude Opus 5.5 2363a10a77 Re-sync gamecontrollerdb.txt with SDL_GameControllerDB, add a sync script
- Fixes #18091

The upstream part hadn't been synced since 2023. Tools/update_gamecontrollerdb.py
replaces it with the current upstream file and keeps PPSSPP's own mappings,
dropping those whose GUID upstream now has (after SDL's old-GUID conversion),
since a later line overrides an earlier one. 193 of 207 were covered,
including the PS3 and 8BitDo Ultimate 2C one-offs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 13:49:49 +02:00

136 lines
4.6 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2012- PPSSPP Project.
# This program 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 Foundation, version 2.0 or later versions.
# This program 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 2.0 for more details.
# A copy of the GPL 2.0 should have been included with the program.
# If not, see http://www.gnu.org/licenses/
# Official git repository and contact information can be found at
# https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
"""Re-sync assets/gamecontrollerdb.txt with the community SDL_GameControllerDB.
The file is upstream's database followed by PPSSPP's own mappings, which start at
PPSSPP_MARKER. This replaces the upstream part with the current upstream file and
keeps the PPSSPP mappings, except those whose GUID (on the same platform) upstream
now has. SDL lets a later mapping replace an earlier one, so a stale PPSSPP line
would otherwise override upstream's.
Usage: python Tools/update_gamecontrollerdb.py [--upstream FILE]
"""
import argparse
import os
import sys
import urllib.request
UPSTREAM_URL = "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt"
PPSSPP_MARKER = "# The below is from PPSSPP's old gamecontrollerdb.txt."
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "assets", "gamecontrollerdb.txt")
# Section comments in the PPSSPP part, for lines without a platform: field.
SECTION_PLATFORMS = {
"# Windows": "Windows",
"# OS X": "Mac OS X",
"# Mac OS X": "Mac OS X",
"# Linux": "Linux",
}
def normalize_guid(guid, platform):
"""Converts pre-2.0.5 GUIDs the way SDL_PrivateGetGamepadGUIDFromMappingString() does."""
guid = guid.lower()
if len(guid) != 32:
return guid
if platform == "Windows" and guid[20:] == "504944564944":
return "03000000" + guid[0:4] + "0000" + guid[4:8] + "000000000000"
if platform == "Mac OS X" and guid[4:16] == "000000000000" and guid[20:] == "000000000000":
return "03000000" + guid[0:4] + "0000" + guid[16:20] + "000000000000"
return guid
def parse_mapping(line, section_platform):
"""Returns (guid, platform) for a mapping line, or None for comments and blanks."""
line = line.strip()
if not line or line.startswith("#"):
return None
fields = line.split(",")
platform = section_platform
for field in fields[2:]:
if field.startswith("platform:"):
platform = field[len("platform:"):]
return (normalize_guid(fields[0], platform), platform)
def section_platform_of(line, current):
for prefix, platform in SECTION_PLATFORMS.items():
if line.startswith(prefix):
return platform
return current
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--upstream", help="Use this local copy of upstream's gamecontrollerdb.txt instead of downloading it")
args = parser.parse_args()
with open(DB_PATH, "r", encoding="utf-8", newline="") as f:
current = f.read()
newline = "\r\n" if "\r\n" in current else "\n"
current_lines = current.splitlines()
if PPSSPP_MARKER not in current_lines:
print("Marker line not found in %s, giving up" % DB_PATH, file=sys.stderr)
return 1
ppsspp_lines = current_lines[current_lines.index(PPSSPP_MARKER):]
if args.upstream:
with open(args.upstream, "r", encoding="utf-8") as f:
upstream = f.read()
else:
with urllib.request.urlopen(UPSTREAM_URL) as response:
upstream = response.read().decode("utf-8")
upstream_lines = upstream.splitlines()
while upstream_lines and not upstream_lines[-1].strip():
upstream_lines.pop()
upstream_keys = set()
platform = None
for line in upstream_lines:
platform = section_platform_of(line, platform)
key = parse_mapping(line, platform)
if key:
upstream_keys.add(key)
kept = []
dropped = []
platform = None
for line in ppsspp_lines:
platform = section_platform_of(line, platform)
key = parse_mapping(line, platform)
if key and key in upstream_keys:
dropped.append(line)
else:
kept.append(line)
with open(DB_PATH, "w", encoding="utf-8", newline="") as f:
f.write(newline.join(upstream_lines + [""] + kept) + newline)
print("Upstream: %d mappings. PPSSPP: kept %d lines, dropped %d now covered upstream:" % (len(upstream_keys), len(kept), len(dropped)))
for line in dropped:
print(" " + line.split(",")[0] + " " + line.split(",")[1])
return 0
if __name__ == "__main__":
sys.exit(main())