DuckStation (PS1) set up like ARMSX2

- From the rail: -batch -bigpicture -fullscreen (the same flags as PCSX2's,
  src/duckstation-qt/qthost.cpp) - straight into the game, gone when it is quit.
- bannerlator-pad-defaults: a new player starts on DuckStation's first-time setup
  with the BIOS folder in the ROMs folder (psx/bios, created, or an existing one),
  psx as the game list, Vulkan, the controller on Player 1 and the guide button
  on the pause menu (not the analog toggle). An existing install gets the guide
  and game-list changes, and once, its own empty BIOS folder moved to the ROMs
  folder and Automatic made Vulkan; later choices are kept. Users bring their own
  BIOS and games.
This commit is contained in:
The412Banner committed 2026-09-24 09:03:34 -04:00
1 parent 21be683768
commit 274f3d0c7a
2 files changed
+116 -2

No files matched your search

@@ -191,6 +191,8 @@ object Library {
// Straight into the game in its controller-driven full-screen UI (first-time setup there
// too), and gone when the game is quit from its pause menu (guide button): -batch.
"armsx2" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath)
// DuckStation shares PCSX2's flags (src/duckstation-qt/qthost.cpp): the same launch and exit.
"duckstation" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath)
"dolphin" -> listOf("-e", guestPath)
"cemu" -> listOf("-g", guestPath)
else -> listOf(guestPath)
@@ -180,9 +180,121 @@ SmallMotor = SDL-0/SmallMotor
"""
def ini_get(path, section, key):
try:
with open(path, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
except OSError:
return None
span = ini_section(lines, section)
if not span:
return None
for l in lines[span[0]:span[1]]:
if "=" in l and l.split("=", 1)[0].strip() == key:
return l.split("=", 1)[1].strip()
return None
def ini_set(path, section, key, value):
"""Sets one key (the first of that name) in [section], adding either as needed."""
with open(path, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
span = ini_section(lines, section)
if span is None:
lines += ["", f"[{section}]", f"{key} = {value}"]
else:
for i in range(span[0], span[1]):
if "=" in lines[i] and lines[i].split("=", 1)[0].strip() == key:
lines[i] = f"{key} = {value}"
break
else:
end = span[1]
while end > span[0] and not lines[end - 1].strip():
end -= 1
lines.insert(end, f"{key} = {value}")
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
os.replace(path + ".tmp", path)
def drop_line(path, line):
with open(path, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
kept = [l for l in lines if l.replace(" ", "") != line.replace(" ", "")]
if len(kept) != len(lines):
with open(path + ".tmp", "w", encoding="utf-8") as f:
f.write("\n".join(kept) + "\n")
os.replace(path + ".tmp", path)
def system_folder(names, default):
for name in names:
for cand in (name, name.upper()):
d = os.path.join(ROMS, cand)
if os.path.isdir(d):
return d
return os.path.join(ROMS, default)
def bios_folder(base):
for name in ("bios", "BIOS", "firmware"):
d = os.path.join(base, name)
if os.path.isdir(d):
return d
d = os.path.join(base, "bios")
if os.path.isdir(ROMS):
try:
os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS
except OSError:
pass
return d
# DuckStation (PS1) - set up like ARMSX2: users bring their own BIOS and games, in the ROMs folder.
# Front-end runs use -batch -bigpicture -fullscreen, so the guide button is the pause menu
# (Hotkeys OpenPauseMenu, src/core/hotkeys.cpp), not the analog toggle. Keys from
# src/core/settings.cpp: [BIOS] SearchDirectory, [GPU] Renderer ("Vulkan"), [Main]
# SetupWizardIncomplete (src/duckstation-qt/qthost.cpp).
DUCKSTATION_PAD1_GUIDE_FREE = "\n".join(l for l in DUCKSTATION_PAD1.splitlines() if not l.startswith("Analog ")) + "\n"
def duckstation():
for d in (os.path.join(CONFIG, "duckstation"), os.path.join(HOME, ".local/share/duckstation")):
patch_ini_section(os.path.join(d, "settings.ini"), "Pad1", DUCKSTATION_PAD1, "DuckStation")
ps1 = system_folder(("psx", "ps1"), "psx")
d = os.path.join(CONFIG, "duckstation")
ini = os.path.join(d, "settings.ini")
legacy = os.path.join(HOME, ".local/share/duckstation/settings.ini")
if not os.path.isfile(ini) and os.path.isfile(legacy):
ini = legacy
if not os.path.isfile(ini):
# A new player: DuckStation's own first-time setup still runs, looking in the ROMs folder.
bios = bios_folder(ps1)
seed(ini, "[Main]\nSetupWizardIncomplete = true\n\n"
f"[BIOS]\nSearchDirectory = {bios}\n\n"
f"[GameList]\nRecursivePaths = {ps1}\n\n"
"[GPU]\nRenderer = Vulkan\n\n"
"[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n"
"[Pad1]\n" + DUCKSTATION_PAD1_GUIDE_FREE, "DuckStation", "first-time setup, BIOS from " + bios)
return
patch_ini_section(ini, "Pad1", DUCKSTATION_PAD1_GUIDE_FREE, "DuckStation")
drop_line(ini, "Analog = SDL-0/Guide")
patch_ini_section(ini, "Hotkeys", "OpenPauseMenu = SDL-0/Guide\n", "DuckStation", needle="SDL-0/Guide",
done="the guide button opens the pause menu")
patch_ini_section(ini, "GameList", f"RecursivePaths = {ps1}\n", "DuckStation", needle=ROMS,
done="games listed from the ROMs folder")
# Once, for a DuckStation set up before this: its own empty BIOS folder moves to the ROMs folder,
# and Automatic becomes Vulkan. A later choice in DuckStation's settings is kept.
marker = os.path.join(os.path.dirname(ini), ".droiddeck-setup")
if os.path.exists(marker):
return
current = ini_get(ini, "BIOS", "SearchDirectory") or "bios"
own = current if os.path.isabs(current) else os.path.join(os.path.dirname(ini), current)
if not (os.path.isdir(own) and os.listdir(own)):
ini_set(ini, "BIOS", "SearchDirectory", bios_folder(ps1))
print(f"bannerlator-pad-defaults: DuckStation: BIOS from {bios_folder(ps1)}")
if (ini_get(ini, "GPU", "Renderer") or "Automatic") == "Automatic":
ini_set(ini, "GPU", "Renderer", "Vulkan")
print("bannerlator-pad-defaults: DuckStation: renderer Vulkan")
open(marker, "w").close()
# PCSX2: FaceSouth/... names (pcsx2/Input/SDLInputSource.cpp), keys from SIO/Pad/PadDualshock2.cpp.