1# FSGS - Common functionality for FS Game System. 2# Copyright (C) 2013-2019 Frode Solheim <frode@solheim.dev> 3# 4# This program is free software; you can redistribute it and/or modify it 5# under the terms of the GNU General Public License as published by the Free 6# Software Foundation; either version 2 of the License, or (at your option) 7# any later version. 8# 9# This program is distributed in the hope that it will be useful, but WITHOUT 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12# more details. 13# 14# You should have received a copy of the GNU General Public License along 15# with this program; if not, write to the Free Software Foundation, Inc., 16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17""" 18FSGS Game Driver for Commodore 64 (C64). 19 20TODO: 21 22* Handle V-SYNC 23* Gamepad select button -> toggle status bar? 24* Cleanup temp files 25* Consider gamepad mappings for tape control 26* Ability to select input devices, and control device type via database config 27* Config key to set C64 model 28* Gamepad button to open attack disk / tape menu? 29* Handle save games properly (did C64 games save to tape? disk only?) 30* Alt+S for screenshot and store screenshots to Screenshots dir 31* Mouse support (need a use case / test game) 32 33FIXME: 34 35* FIXME: Positional keyboard support 36 Keyboard: Error - Cannot load keymap `sdl_pos.vkm'. 37 38""" 39import json 40import os 41 42import shutil 43 44from fsbc.system import windows, macosx 45from fsgs.drivers.gamedriver import GameDriver 46from fsgs.input.mapper import InputMapper 47from fsgs.util import sdl2 48from fsgs.option import Option 49from fsgs.platform import Platform 50from fsgs.platforms.loader import SimpleLoader 51 52C64_MODEL_C64 = "c64" 53C64_MODEL_C64C = "c64c" 54C64_MODEL_C64C_1541_II = "c64c/1541-ii" 55C64_JOYSTICK = { 56 "type": "joystick", 57 "description": "Joystick", 58 "mapping_name": "c64", 59} 60C64_PORTS = [ 61 { 62 "description": "Port 2", 63 "types": [C64_JOYSTICK], 64 "type_option": "c64_port_2_type", 65 "device_option": "c64_port_2", 66 }, 67 { 68 "description": "Port 1", 69 "types": [C64_JOYSTICK], 70 "type_option": "c64_port_1_type", 71 "device_option": "c64_port_1", 72 }, 73] 74C64_VIDEO_WIDTH = 384 75C64_VIDEO_HEIGHT = 272 76 77VICE_KEY_SET_A = 2 78VICE_KEY_SET_B = 3 79VICE_JOYSTICK = 4 80 81 82class Commodore64Platform(Platform): 83 PLATFORM_NAME = "Commodore 64" 84 85 def __init__(self): 86 super().__init__(Commodore64Loader, Commodore64ViceDriver) 87 88 def driver(self, fsgc): 89 return Commodore64ViceDriver(fsgc) 90 91 def loader(self, fsgc): 92 return Commodore64Loader(fsgc) 93 94 95class Commodore64Loader(SimpleLoader): 96 def load_files(self, values): 97 file_list = json.loads(values["file_list"]) 98 # assert len(file_list) == 1 99 for i, item in enumerate(file_list): 100 _, ext = os.path.splitext(item["name"]) 101 ext = ext.upper() 102 if ext in [".TAP", ".T64"]: 103 if i == 0: 104 self.config["tape_drive_0"] = "sha1://{}/{}".format( 105 item["sha1"], item["name"] 106 ) 107 self.config[ 108 "tape_image_{0}".format(i) 109 ] = "sha1://{}/{}".format(item["sha1"], item["name"]) 110 elif ext in [".D64"]: 111 if i == 0: 112 self.config["floppy_drive_0"] = "sha1://{}/{}".format( 113 item["sha1"], item["name"] 114 ) 115 self.config[ 116 "floppy_image_{}".format(i) 117 ] = "sha1://{0}/{1}".format(item["sha1"], item["name"]) 118 119 def load_extra(self, values): 120 # FIXME: Replace with c64_model? 121 self.config[Option.C64_MODEL] = values["model"] 122 if not self.config[Option.C64_MODEL]: 123 self.config[Option.C64_MODEL] = C64_MODEL_C64C 124 self.config["c64_port_1_type"] = values["c64_port_1_type"] 125 self.config["c64_port_2_type"] = values["c64_port_2_type"] 126 # FIXME: Remove? 127 self.config["model"] = "" 128 129 130class Commodore64ViceDriver(GameDriver): 131 PORTS = C64_PORTS 132 133 def __init__(self, fsgs): 134 super().__init__(fsgs) 135 self.emulator.name = "x64sc-fs" 136 self.helper = Commodore64Helper(self.options) 137 138 def prepare(self): 139 dot_vice_dir = os.path.join(self.home.path, ".vice") 140 if os.path.exists(dot_vice_dir): 141 shutil.rmtree(dot_vice_dir) 142 os.makedirs(dot_vice_dir) 143 dot_vice_dir = os.path.join(self.home.path, ".config", ".vice") 144 if os.path.exists(dot_vice_dir): 145 shutil.rmtree(dot_vice_dir) 146 os.makedirs(dot_vice_dir) 147 148 # noinspection SpellCheckingInspection 149 joymap_file = self.temp_file("joymap.vjm").path 150 with open(joymap_file, "w", encoding="UTF-8") as f: 151 self.create_joymap_file(f) 152 # noinspection SpellCheckingInspection 153 if windows: 154 # Not using normpath because os.sep can be "/" on MSYS2, 155 # and Vice on Windows really requires backslashes here. 156 joymap_file = joymap_file.replace("/", "\\") 157 self.emulator.args.extend(["-joymap", joymap_file]) 158 159 hotkey_file = self.temp_file("hotkey.vkm").path 160 with open(hotkey_file, "w", encoding="UTF-8") as f: 161 self.create_hotkey_file(f) 162 # noinspection SpellCheckingInspection 163 if windows: 164 hotkey_file = hotkey_file.replace("/", "\\") 165 self.emulator.args.extend(["-hotkeyfile", hotkey_file]) 166 167 config_file = self.temp_file("vice.cfg").path 168 with open(config_file, "w", encoding="UTF-8") as f: 169 self.create_vice_cfg(f) 170 self.configure_audio(f) 171 self.configure_input(f) 172 self.configure_video(f) 173 self.emulator.args.extend(["-config", config_file]) 174 175 # self.emulator.args.extend(["-model", "c64"]) 176 if self.helper.model() == C64_MODEL_C64: 177 self.set_model_name("Commodore C64") 178 self.emulator.args.extend(["-model", "c64"]) 179 elif self.helper.model() == C64_MODEL_C64C_1541_II: 180 self.set_model_name("Commodore C64C 1541-II") 181 self.emulator.args.extend(["-model", "c64c"]) 182 else: 183 self.set_model_name("Commodore C64C") 184 self.emulator.args.extend(["-model", "c64c"]) 185 186 media_keys = ["floppy_drive_0", "tape_drive_0"] 187 for i in range(20): 188 media_keys.append("floppy_image_{0}".format(i)) 189 media_keys.append("tape_image_{0}".format(i)) 190 unique_uris = set() 191 for key in media_keys: 192 if self.options[key]: 193 unique_uris.add(self.options[key]) 194 # media_dir = self.temp_dir("media") 195 # VICE uses CWD as default directory for media files 196 media_dir = self.cwd 197 for file_uri in unique_uris: 198 input_stream = self.fsgc.file.open(file_uri) 199 game_file = os.path.join(media_dir.path, file_uri.split("/")[-1]) 200 with open(game_file, "wb") as f: 201 f.write(input_stream.read()) 202 203 if self.options[Option.TAPE_DRIVE_0]: 204 file_uri = self.options[Option.TAPE_DRIVE_0] 205 else: 206 file_uri = self.options[Option.FLOPPY_DRIVE_0] 207 208 autostart_file = os.path.join(media_dir.path, file_uri.split("/")[-1]) 209 self.emulator.args.extend(["-autostart", autostart_file]) 210 211 def finish(self): 212 pass 213 214 def create_vice_cfg(self, f): 215 f.write("[C64SC]\n") 216 f.write("ConfirmOnExit=0\n") 217 # f.write("KeepAspectRatio=1\n") 218 f.write("VICIIDoubleScan=1\n") 219 f.write("HwScalePossible=1\n") 220 # 0 means SDL_FULLSCREEN_DESKTOP - 1 means SDL_FULLSCREEN 221 f.write("VICIISDLFullscreenMode=0\n") 222 f.write("AutostartWarp=0\n") 223 f.write("\n") 224 225 if self.helper.has_floppy_drive(): 226 f.write("Drive8Type=1541\n") 227 else: 228 # f.write("FileSystemDevice8=0\n") 229 f.write("Drive8Type=None\n") 230 f.write("\n") 231 232 # Virtual device traps? 233 # f.write("VirtualDevices=1\n") 234 235 # print("set_media_options") 236 # media_list = self.create_media_list() 237 # print("sort media list") 238 # # FIXME: SORT ON name (lowercase) ONLY, not whole path, because 239 # # some files may have moved to another dir (temp dir) 240 # media_list = sorted(media_list) 241 # print(media_list) 242 # if media_list[0].lower()[-4:] == ".crt": 243 # f.write("CartridgeFile=\"{path}\"\n".format(path=media_list[0])) 244 # f.write("CartridgeType={type}\n".format(type=0)) 245 # f.write("CartridgeMode={mode}\n".format(mode=0)) 246 # f.write("CartridgeReset={reset}\n".format(reset=1)) 247 # # #self.args.extend(["-autostart", media_list[0]]) 248 # else: 249 # #f.write('AutostartPrgMode=2\n') # disk image 250 # #f.write('AutostartPrgDiskImage="{path}"\n'.format( 251 # # path=media_list[0])) 252 # self.add_arg("-autostart", media_list[0]) 253 254 # # FIXME: Floppies? 255 # print(media_list) 256 # if media_list[0].lower()[-4:] == ".d64": 257 # f = self.context.temp.file('fliplist') 258 # f.write("# Vice fliplist file\n\n") 259 # f.write("UNIT 8\n") 260 # print("FLIP LIST:") 261 # # Make sure first disk is added to the end of the fliplist 262 # for floppy in (media_list[1:] + [media_list[:1]]): 263 # print("%s\n" % (floppy,)) 264 # print 265 # f.write("%s\n" % (floppy,)) 266 # f.close() 267 # self.add_arg("-flipname", self.context.temp.file("fliplist")) 268 # 269 # media_dir = os.path.dirname(media_list[0]) 270 # print(media_dir) 271 # f.write("InitialDefaultDir=\"{dir}\"\n".format(dir=media_dir)) 272 # f.write("InitialTapeDir=\"{dir}\"\n".format(dir=media_dir)) 273 # f.write("InitialCartDir=\"{dir}\"\n".format(dir=media_dir)) 274 # f.write("InitialDiskDir=\"{dir}\"\n".format(dir=media_dir)) 275 # f.write("InitialAutostartDir=\"{dir}\"".format(dir=media_dir)) 276 277 # def vice_prepare_floppies(self): 278 # floppies = [] 279 # #media_dir = os.path.dirname(self.context.game.file) 280 # #base_match = self.extract_match_name(os.path.basename( 281 # # self.context.game.file)) 282 # #for name in os.listdir(media_dir): 283 # # dummy, ext = os.path.splitext(name) 284 # # if ext.lower() not in ['.st']: 285 # # continue 286 # # match = self.extract_match_name(name) 287 # # if base_match == match: 288 # # floppies.append(os.path.join(media_dir, name)) 289 # # #floppies.append(name) 290 # if self.config["floppy_drive_0"]: 291 # floppies.append(self.config["floppy_drive_0"]) 292 # return floppies 293 294 def configure_audio(self, f): 295 # Seems to be some issues with sound in VICE (buffers filling up, 296 # slowing down the emulation?). Using defaults for now (uses big 297 # buffers by default, only seems to delay the problem). 298 # return 299 audio_driver = self.options[Option.VICE_AUDIO_DRIVER] 300 audio_driver = "sdl" 301 f.write("SoundBufferSize={0}\n".format(50)) 302 if audio_driver: 303 print("[VICE] Using audio driver", repr(audio_driver)) 304 f.write("SoundDeviceName={0}\n".format(audio_driver)) 305 # else: 306 # audio_driver = self.config.get("audio_driver", "") 307 # if audio_driver == "sdl": 308 # f.write("SoundDeviceName={0}\n".format(audio_driver)) 309 310 if self.use_audio_frequency(): 311 f.write("SoundSampleRate={0}\n".format(self.use_audio_frequency())) 312 # default buffer size for vice is 100ms, that's far too much... 313 # EDIT: Lowering the sound buffer size seems to cause FPS problems 314 # Maybe due to buffer filling up and emu running slower (??) 315 # f.write("SoundBufferSize={0}\n".format(50)) 316 if self.options[Option.FLOPPY_DRIVE_VOLUME] == 0: 317 f.write("DriveSoundEmulation=0\n") 318 else: 319 # f.write("DriveSoundEmulationVolume=1200\n") 320 f.write("DriveSoundEmulation=1\n") 321 f.write("\n") 322 323 def configure_input(self, f): 324 # FIXME: Enable when ready 325 # f.write("KeymapIndex=1\n") # Use positional keys 326 327 for i, port in enumerate(self.ports): 328 vice_port = [2, 1, 3, 4][i] 329 # vice_port = i + 1 330 if port.device is None: 331 vice_port_type = 0 332 elif port.device.type == "joystick": 333 vice_port_type = VICE_JOYSTICK 334 elif port.device.type == "keyboard": 335 vice_port_type = VICE_KEY_SET_A + i 336 # assert False 337 else: 338 vice_port_type = 0 339 f.write("JoyDevice{0}={1}\n".format(vice_port, vice_port_type)) 340 print( 341 "[INPUT] Port", 342 port.type_option, 343 "VicePort", 344 vice_port, 345 port.type, 346 port.device, 347 ) 348 349 if port.device is None: 350 continue 351 if port.device.type != "keyboard": 352 continue 353 input_mapping = { 354 "1": "KeySet{0}Fire".format(i + 1), 355 "UP": "KeySet{0}North".format(i + 1), 356 "DOWN": "KeySet{0}South".format(i + 1), 357 "LEFT": "KeySet{0}West".format(i + 1), 358 "RIGHT": "KeySet{0}East".format(i + 1), 359 } 360 mapper = ViceInputMapper(port, input_mapping) 361 for key, value in mapper.items(): 362 f.write("{0}={1}\n".format(key, value)) 363 f.write("\n") 364 365 def configure_video(self, f): 366 # Enable the "Pepto" palette (http://www.pepto.de/projects/colorvic/) 367 # f.write("VICIIPaletteFile=\"vice\"\n") 368 palette_file = self.options[Option.C64_PALETTE] 369 if not palette_file: 370 # palette_file = "pepto-ntsc-sony" 371 # palette_file = "vice" 372 # palette_file = "community-colors" 373 palette_file = "0" 374 if palette_file != "0": 375 f.write("VICIIExternalPalette=1\n") 376 f.write('VICIIPaletteFile="{}"\n'.format(palette_file)) 377 378 f.write("VICIIAudioLeak=1\n") 379 380 if self.effect() == self.CRT_EFFECT: 381 f.write("VICIIFilter=1\n") 382 elif self.effect() == self.DOUBLE_EFFECT: 383 f.write("VICIIFilter=0\n") 384 elif self.effect() == self.HQ2X_EFFECT: 385 # HQ2X is not supported 386 f.write("VICIIFilter=0\n") 387 elif self.effect() == self.SCALE2X_EFFECT: 388 f.write("VICIIFilter=2\n") 389 else: 390 f.write("VICIIFilter=0\n") 391 f.write("VICIIDoubleSize=0\n") 392 393 screen_w, screen_h = self.screen_size() 394 395 if self.use_fullscreen(): 396 f.write("VICIIFullscreen=1\n") 397 398 f.write("SDLWindowWidth={w}\n".format(w=960)) 399 f.write("SDLWindowHeight={h}\n".format(h=540)) 400 f.write("SDLCustomWidth={w}\n".format(w=screen_w)) 401 f.write("SDLCustomHeight={h}\n".format(h=screen_h)) 402 403 if self.scaling() == self.MAX_SCALING: 404 f.write("VICIIHwScale=1\n") 405 elif self.scaling() == self.INTEGER_SCALING: 406 # FIXME: Does not support this yet, disabling scaling 407 f.write("VICIIHwScale=0\n") 408 else: 409 f.write("VICIIHwScale=0\n") 410 411 if self.stretching() == self.STRETCH_FILL_SCREEN: 412 f.write("SDLGLAspectMode=0\n") 413 elif self.stretching() == self.STRETCH_ASPECT: 414 f.write("SDLGLAspectMode=2\n") 415 else: 416 f.write("SDLGLAspectMode=1\n") 417 418 # if self.border() == self.LARGE_BORDER: 419 # f.write("VICIIBorderMode=1\n") 420 # elif self.border() == self.SMALL_BORDER: 421 # # Value 4 is an FSGS extension in Vice-FS. 422 # f.write("VICIIBorderMode=4\n") 423 # else: 424 # f.write("VICIIBorderMode=3\n") 425 426 # Experimental "540 border mode" 427 f.write("VICIIBorderMode=5\n") 428 429 # # Disable scanlines in CRT mode 430 # f.write("VICIIPALScanLineShade=1000\n") 431 # Disable scanlines in CRT mode 432 f.write("VICIIPALScanLineShade=850\n") 433 434 # Can this be used to set fullscreen desktop, etc? 435 # f.write("VICIISDLFullscreenMode = 1\n") 436 437 f.write("\n") 438 439 def create_joymap_file(self, f): 440 # VICE joystick mapping file 441 # 442 # A joystick map is read in as patch to the current map. 443 # 444 # File format: 445 # - comment lines start with '#' 446 # - keyword lines start with '!keyword' 447 # - normal line has 'joynum inputtype inputindex action' 448 # 449 # Keywords and their lines are: 450 # '!CLEAR' clear all mappings 451 # 452 # inputtype: 453 # 0 axis 454 # 1 button 455 # 2 hat 456 # 3 ball 457 # 458 # Note that each axis has 2 inputindex entries and each hat has 4. 459 # 460 # action [action_parameters]: 461 # 0 none 462 # 1 port pin joystick (pin: 1/2/4/8/16 = u/d/l/r/fire) 463 # 2 row col keyboard 464 # 3 map 465 # 4 UI activate 466 # 5 path&to&item UI function 467 # 468 f.write("!CLEAR\n") 469 for i, port in enumerate(self.ports): 470 if port.device is None: 471 continue 472 if port.device.type != "joystick": 473 continue 474 if i == 0: 475 input_mapping = { 476 "1": "1 1 16 ", 477 "UP": "1 1 1", 478 "DOWN": "1 1 2", 479 "LEFT": "1 1 4", 480 "RIGHT": "1 1 8", 481 "MENU": "4", 482 } 483 elif i == 1: 484 input_mapping = { 485 "1": "1 0 16 ", 486 "UP": "1 0 1", 487 "DOWN": "1 0 2", 488 "LEFT": "1 0 4", 489 "RIGHT": "1 0 8", 490 "MENU": "4", 491 } 492 else: 493 raise Exception("Invalid port") 494 mapper = ViceInputMapper(port, input_mapping) 495 for key, value in mapper.items(): 496 f.write("{0} {1}\n".format(value, key)) 497 498 def create_hotkey_file(self, f): 499 # noinspection SpellCheckingInspection 500 hotkeys = [ 501 (sdl2.SDLK_i, "Statusbar"), 502 # (112, "Pause"), # Mod+Pause 503 # (113, "Quit emulator"), # Mod+Q 504 # ALT+S 505 # (115, "Snapshot&Save snapshot image"), 506 # (115, "Screenshot&Save PNG screenshot"), # Mod+S 507 ( 508 sdl2.SDLK_s, 509 "Save media file&Create screenshot&Save PNG screenshot", 510 ), 511 (sdl2.SDLK_RETURN, "Video settings&Size settings&Fullscreen"), 512 # (119, "Speed settings&Warp mode"), # Mod+W 513 ] 514 if macosx: 515 mod = 8 # Cmd key 516 else: 517 mod = 2 # Left alt key 518 # f.write("!CLEAR\n") 519 for key, action in hotkeys: 520 f.write( 521 "{0} {1}\n".format(sdl2.SDL_NUM_SCANCODES * mod + key, action) 522 ) 523 524 525class ViceInputMapper(InputMapper): 526 def axis(self, axis, positive): 527 offset = 0 if positive else 1 528 return "{0} 0 {1}".format(self.device.index, axis * 2 + offset) 529 530 def hat(self, hat, direction): 531 offset = {"left": 2, "right": 3, "up": 0, "down": 1}[direction] 532 return "{0} 2 {1}".format(self.device.index, hat * 4 + offset) 533 534 def button(self, button): 535 return "{0} 1 {1}".format(self.device.index, button) 536 537 def key(self, key): 538 return "{0}".format(key.sdl_code) 539 540 541class Commodore64Helper: 542 def __init__(self, options): 543 self.options = options 544 545 def accuracy(self): 546 try: 547 accuracy = int(self.options.get(Option.ACCURACY, "1")) 548 except ValueError: 549 accuracy = 1 550 return accuracy 551 552 def has_floppy_drive(self): 553 if self.model() == C64_MODEL_C64C_1541_II: 554 return "1541-II" 555 return None 556 557 def model(self): 558 if self.options[Option.C64_MODEL] == "c64": 559 return C64_MODEL_C64 560 elif self.options[Option.C64_MODEL] == "c64c": 561 return C64_MODEL_C64C 562 elif self.options[Option.C64_MODEL] == "c64c/1541-ii": 563 return C64_MODEL_C64C_1541_II 564 return C64_MODEL_C64C 565