Coverage for src/ph/platforms.py: 93.5%

44 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 08:17 +0000

1"""EmulationStation platform metadata and ROM-folder mappings.""" 

2 

3from collections.abc import Iterable, Sequence 

4from pathlib import Path 

5 

6from ph.compatibility import is_unsupported_system 

7from ph.models import Platform 

8from ph.targets import LinuxTarget 

9 

10 

11def _platform( 

12 name: str, 

13 slug: str, 

14 alias: str, 

15 folder: str, 

16 code: str = "", 

17 *alternate_folders: str, 

18) -> Platform: 

19 return Platform(name, slug, code, alias, folder, alternate_folders) 

20 

21 

22# The initial folder profile follows DarkOS EmulationStation. Console 

23# families outside the supported handheld profile are intentionally omitted. 

24# Empty service codes use all-platform search while preserving the ROM destination. 

25DARKOS_PLATFORMS: tuple[Platform, ...] = ( 

26 Platform("All platforms", "all", "", "ALL"), 

27 _platform("3DO", "3do", "3DO", "3do"), 

28 _platform("Adventure Vision", "adventure-vision", "ADV", "advision"), 

29 _platform("American Laser Games", "american-laser-games", "ALG", "alg"), 

30 _platform("Amiga", "amiga", "AMIGA", "amiga"), 

31 _platform("Amiga CD32", "amiga-cd32", "CD32", "amigacd32"), 

32 _platform("Amstrad CPC", "amstrad-cpc", "CPC", "amstradcpc"), 

33 _platform("Amstrad GX4000", "amstrad-gx4000", "GX4000", "gx4000"), 

34 _platform("Apple II", "apple-ii", "APPLE2", "apple2"), 

35 _platform("Apple Macintosh", "apple-macintosh", "MAC", "vmac"), 

36 _platform("Aquaplus P/ECE", "aquaplus-piece", "PIECE", "piece"), 

37 _platform("Arcade", "arcade", "ARCADE", "arcade"), 

38 _platform("Arduboy", "arduboy", "ARDUBOY", "arduboy"), 

39 _platform("Astrocade", "astrocade", "ASTRO", "astrocde"), 

40 _platform("Atomiswave", "atomiswave", "ATOM", "atomiswave"), 

41 _platform("Atari 800", "atari-800", "A800", "atari800"), 

42 _platform("Atari 2600", "atari-2600", "A2600", "atari2600", "Atari2600"), 

43 _platform("Atari 5200", "atari-5200", "A5200", "atari5200", "Atari5200"), 

44 _platform("Atari 7800", "atari-7800", "A7800", "atari7800", "Atari7800"), 

45 _platform("Atari Jaguar", "atari-jaguar", "JAG", "atarijaguar", "Jaguar"), 

46 _platform("Atari Lynx", "atari-lynx", "LYNX", "atarilynx", "Lynx"), 

47 _platform("Atari ST", "atari-st", "AST", "atarist"), 

48 _platform("Atari XEGS", "atari-xegs", "XEGS", "atarixegs"), 

49 _platform("ColecoVision", "colecovision", "COLECO", "coleco"), 

50 _platform("Commodore 16", "commodore-16", "C16", "c16"), 

51 _platform("Commodore 64 / PET", "commodore-64", "C64", "c64"), 

52 _platform("Commodore 128", "commodore-128", "C128", "c128"), 

53 _platform("Commodore VIC-20", "commodore-vic-20", "VIC20", "vic20"), 

54 _platform("Capcom Play System I", "cps-1", "CPS1", "cps1"), 

55 _platform("Capcom Play System II", "cps-2", "CPS2", "cps2"), 

56 _platform("Capcom Play System III", "cps-3", "CPS3", "cps3"), 

57 _platform("Daphne", "daphne", "DAPHNE", "daphne"), 

58 _platform("Doom", "doom", "DOOM", "doom"), 

59 _platform("Dreamcast", "dreamcast", "DC", "dreamcast", "Dreamcast"), 

60 _platform("Dreamcast VMU", "dreamcast-vmu", "VMU", "vmu"), 

61 _platform("EasyRPG", "easyrpg", "EASYRPG", "easyrpg"), 

62 _platform("Enterprise 64/128", "enterprise", "EP64", "enterprise"), 

63 _platform("Fairchild Channel F", "fairchild-channel-f", "CHF", "channelf"), 

64 _platform("Famicom Disk System", "famicom-disk-system", "FDS", "fds"), 

65 _platform("Game Boy", "game-boy", "GB", "gb", "GB"), 

66 _platform("Game Boy Advance", "game-boy-advance", "GBA", "gba", "GBA"), 

67 _platform("Game & Watch", "game-and-watch", "GW", "gameandwatch"), 

68 _platform("Game Boy Color", "game-boy-color", "GBC", "gbc", "GBC"), 

69 _platform("Game Gear", "game-gear", "GG", "gamegear", "GG"), 

70 _platform("Genesis / Mega Drive", "genesis", "GEN", "megadrive", "Genesis", "genesis"), 

71 _platform("Intellivision", "intellivision", "INTV", "intellivision"), 

72 _platform("LÖVE", "love2d", "LOVE", "love2d"), 

73 _platform("LowRes NX", "lowres-nx", "LRNX", "lowresnx"), 

74 _platform("MAME 2003", "mame-2003", "MAME03", "mame2003"), 

75 _platform("MAME 2010", "mame-2010", "MAME", "mame"), 

76 _platform("Master System", "master-system", "SMS", "mastersystem", "SMS"), 

77 _platform("Mega Drive MSU", "megadrive-msu", "MSUMD", "msumd"), 

78 _platform("Mega Duck", "mega-duck", "MDUCK", "megaduck"), 

79 _platform("Microvision", "microvision", "MV", "mv"), 

80 _platform("MSX", "msx", "MSX", "msx"), 

81 _platform("MSX2", "msx2", "MSX2", "msx2"), 

82 _platform("Naomi", "naomi", "NAOMI", "naomi"), 

83 _platform("Neo Geo", "neo-geo", "NEOGEO", "neogeo"), 

84 _platform("Neo Geo CD", "neo-geo-cd", "NGCD", "neogeocd"), 

85 _platform("Neo Geo Pocket", "neo-geo-pocket", "NGP", "ngp"), 

86 _platform("Neo Geo Pocket Color", "neo-geo-pocket-color", "NGPC", "ngpc"), 

87 _platform("Nintendo 64", "nintendo-64", "N64", "n64", "N64"), 

88 _platform("Nintendo 64DD", "nintendo-64dd", "N64DD", "n64dd"), 

89 _platform("Nintendo DS", "nintendo-ds", "NDS", "nds", "DS"), 

90 _platform("Nintendo Entertainment System", "nintendo", "NES", "nes", "NES", "famicom"), 

91 _platform("Odyssey²", "odyssey-2", "O2", "odyssey2"), 

92 _platform("OpenBOR", "openbor", "OPENBOR", "openbor"), 

93 _platform("Palm OS", "palm-os", "PALM", "palm"), 

94 _platform("PC-98", "pc-98", "PC98", "pc98"), 

95 _platform("PC / MS-DOS", "dos", "DOS", "dos"), 

96 _platform("PC Engine / TurboGrafx-16", "pc-engine", "PCE", "pcengine", "TG16", "turbografx"), 

97 _platform( 

98 "PC Engine CD / TurboGrafx-CD", 

99 "pc-engine-cd", 

100 "PCECD", 

101 "pcenginecd", 

102 "TGCD", 

103 "turbografxcd", 

104 ), 

105 _platform("PC-FX", "pc-fx", "PCFX", "pcfx"), 

106 _platform("PICO-8", "pico-8", "PICO8", "pico-8/carts"), 

107 _platform("PlayStation", "playstation", "PS1", "psx", "PS1"), 

108 _platform("PlayStation Portable", "ps-portable", "PSP", "psp", "PSP"), 

109 _platform("PSP Minis", "psp-minis", "PSPM", "pspminis"), 

110 _platform("Pokémon Mini", "pokemon-mini", "PKMINI", "pokemonmini"), 

111 _platform("PuzzleScript", "puzzlescript", "PUZZLE", "puzzlescript"), 

112 _platform("Satellaview", "satellaview", "BSX", "satellaview"), 

113 _platform("ScummVM", "scummvm", "SCUMM", "scummvm"), 

114 _platform("Sega 32X", "sega-32x", "32X", "sega32x", "32X"), 

115 _platform("Sega CD", "sega-cd", "SCD", "segacd", "SegaCD"), 

116 _platform("Sega Pico", "sega-pico", "PICO", "pico"), 

117 _platform("Sega Saturn", "saturn", "SAT", "saturn", "Saturn"), 

118 _platform("SG-1000", "sg-1000", "SG1K", "sg-1000"), 

119 _platform("Sharp X1", "sharp-x1", "X1", "x1"), 

120 _platform("Sharp X68000", "sharp-x68000", "X68K", "x68000"), 

121 _platform("Solarus", "solarus", "SOLARUS", "solarus"), 

122 _platform("SuFami Turbo", "sufami-turbo", "SUFAMI", "sufami"), 

123 _platform("Super Cassette Vision", "super-cassette-vision", "SCV", "scv"), 

124 _platform("Super Game Boy", "super-game-boy", "SGB", "sgb"), 

125 _platform("SuperGrafx", "supergrafx", "SGFX", "supergrafx"), 

126 _platform("Super Nintendo", "super-nintendo", "SNES", "snes", "SNES", "sfc"), 

127 _platform("Super Nintendo MSU1", "super-nintendo-msu1", "MSU1", "snesmsu1"), 

128 _platform("Super Nintendo Hacks", "super-nintendo-hacks", "SNESH", "snes-hacks"), 

129 _platform("Tandy Color Computer 3", "coco-3", "COCO3", "coco3"), 

130 _platform("Thomson", "thomson", "THOM", "thomson"), 

131 _platform("TIC-80", "tic-80", "TIC80", "tic80"), 

132 _platform("TI-99/4A", "ti-99", "TI99", "ti99"), 

133 _platform("Uzebox", "uzebox", "UZE", "uzebox"), 

134 _platform("Vectrex", "vectrex", "VECT", "vectrex"), 

135 _platform("Videopac", "videopac", "VIDPAC", "videopac"), 

136 _platform("Videoton TV-Computer", "videoton-tvc", "TVC", "tvc"), 

137 _platform("Vircon32", "vircon32", "V32", "vircon32"), 

138 _platform("Virtual Boy", "virtual-boy", "VB", "virtualboy", "VB"), 

139 _platform("ONScripter", "onscripter", "ONS", "onscripter"), 

140 _platform("WASM-4", "wasm-4", "WASM4", "wasm4"), 

141 _platform("Watara Supervision", "watara-supervision", "SUPERV", "supervision"), 

142 _platform("Wolfenstein", "wolfenstein", "WOLF", "wolf"), 

143 _platform("WonderSwan", "wonderswan", "WS", "wonderswan"), 

144 _platform("WonderSwan Color", "wonderswan-color", "WSC", "wonderswancolor"), 

145 _platform("ZX81", "zx81", "ZX81", "zx81"), 

146 _platform("ZX Spectrum", "zx-spectrum", "ZXS", "zxspectrum"), 

147 _platform("Ports", "ports", "PORTS", "ports"), 

148) 

149 

150NON_PLATFORM_DIRECTORIES = { 

151 "backup", 

152 "bios", 

153 "bgmusic", 

154 "downloads", 

155 "launchimages", 

156 "lost+found", 

157 "movies", 

158 "screensavers", 

159 "splash", 

160 "themes", 

161 "tools", 

162 "videos", 

163} 

164 

165 

166def discover_platforms( 

167 roms_directories: Iterable[Path], known: Sequence[Platform] = DARKOS_PLATFORMS 

168) -> tuple[Platform, ...]: 

169 """Add image-specific ROM folders found on SD1 or SD2 to the known set.""" 

170 

171 discovered = list(known) 

172 known_roots = { 

173 folder.split("/", maxsplit=1)[0].casefold() 

174 for platform in known 

175 for folder in platform.rom_folders 

176 } 

177 custom_names: set[str] = set() 

178 for root in roms_directories: 

179 try: 

180 folders = tuple(path for path in root.iterdir() if path.is_dir()) 

181 except OSError: 

182 continue 

183 for folder in folders: 

184 folded = folder.name.casefold() 

185 if ( 

186 folded.startswith(".") 

187 or folded in known_roots 

188 or folded in NON_PLATFORM_DIRECTORIES 

189 or is_unsupported_system(folder.name) 

190 ): 

191 continue 

192 custom_names.add(folder.name) 

193 for name in sorted(custom_names, key=str.casefold): 

194 discovered.append( 

195 Platform( 

196 name=f"Detected: {name}", 

197 slug=f"detected-{name.casefold()}", 

198 code="", 

199 alias=name, 

200 rom_folder=name, 

201 ) 

202 ) 

203 return tuple(discovered) 

204 

205 

206def _build_aliases() -> dict[str, Platform]: 

207 aliases: dict[str, Platform] = {} 

208 for platform in DARKOS_PLATFORMS: 

209 values = ( 

210 platform.name, 

211 platform.slug, 

212 platform.alias, 

213 platform.code, 

214 *platform.rom_folders, 

215 ) 

216 for value in values: 

217 if value: 

218 aliases[value.casefold()] = platform 

219 return aliases 

220 

221 

222_ALIASES = _build_aliases() 

223 

224 

225def platform_catalogue(target: LinuxTarget) -> tuple[Platform, ...]: 

226 """Return the ROM-folder catalogue owned by an OS target profile.""" 

227 

228 if target.platform_profile == "darkos": 

229 return DARKOS_PLATFORMS 

230 raise ValueError(f"No platform catalogue is registered for {target.display_name}.") 

231 

232 

233def resolve_platform( 

234 value: str, 

235 known: Sequence[Platform] = DARKOS_PLATFORMS, 

236) -> Platform | None: 

237 """Resolve a slug, code, short alias, or ROM folder case-insensitively.""" 

238 

239 normalized = value.strip().casefold() 

240 if known is DARKOS_PLATFORMS: 240 ↛ 242line 240 didn't jump to line 242 because the condition on line 240 was always true

241 return _ALIASES.get(normalized) 

242 return next( 

243 ( 

244 platform 

245 for platform in known 

246 if normalized 

247 in { 

248 platform.name.casefold(), 

249 platform.slug.casefold(), 

250 platform.alias.casefold(), 

251 platform.code.casefold(), 

252 *(folder.casefold() for folder in platform.rom_folders), 

253 } 

254 ), 

255 None, 

256 )