shithub: zelda3

Download patch

ref: c9e2b4ced16118446f6901a4a3026bc06aa90f5e
parent: 46ae80aa51eebd835fb3467c8b89659649d533f8
author: Snesrev <snesrev@protonmail.com>
date: Thu Sep 1 23:22:45 EDT 2022

Add config file.

Also fixed wonkiness in dpad and changed rendering.

Dim's screen on pause (Thanks Nutzzz)

Fixes #42 #40 #12 #43 #29

--- /dev/null
+++ b/config.c
@@ -1,0 +1,280 @@
+#include "config.h"
+#include "types.h"
+#include <stdio.h>
+#include <string.h>
+#include <SDL.h>
+
+
+enum {
+  kKeyMod_ScanCode = 0x200,
+  kKeyMod_Alt = 0x400,
+  kKeyMod_Shift = 0x800,
+  kKeyMod_Ctrl = 0x1000,
+};
+
+#define REMAP_SDL_KEYCODE(key) ((key) & SDLK_SCANCODE_MASK ? kKeyMod_ScanCode : 0) | (key) & (kKeyMod_ScanCode - 1)
+#define _(x) REMAP_SDL_KEYCODE(x)
+#define S(x) REMAP_SDL_KEYCODE(x) | kKeyMod_Shift
+#define C(x) REMAP_SDL_KEYCODE(x) | kKeyMod_Alt
+#define A(x) REMAP_SDL_KEYCODE(x) | kKeyMod_Ctrl
+#define N 0
+static const uint16 kDefaultKbdControls[kKeys_Total] = {
+  // Controls
+  _(SDLK_UP), _(SDLK_DOWN), _(SDLK_LEFT), _(SDLK_RIGHT), _(SDLK_RSHIFT), _(SDLK_RETURN), _(SDLK_x), _(SDLK_z), _(SDLK_s), _(SDLK_a), _(SDLK_d), _(SDLK_c),
+  // LoadState
+  _(SDLK_F1), _(SDLK_F2), _(SDLK_F3), _(SDLK_F4), _(SDLK_F5), _(SDLK_F6), _(SDLK_F7), _(SDLK_F8), _(SDLK_F9), _(SDLK_F10), N, N, N, N, N, N, N, N, N, N,
+  // SaveState
+  S(SDLK_F1), S(SDLK_F2), S(SDLK_F3), S(SDLK_F4), S(SDLK_F5), S(SDLK_F6), S(SDLK_F7), S(SDLK_F8), S(SDLK_F9), S(SDLK_F10), N, N, N, N, N, N, N, N, N, N,
+  // Replay State
+  C(SDLK_F1), C(SDLK_F2), C(SDLK_F3), C(SDLK_F4), C(SDLK_F5), C(SDLK_F6), C(SDLK_F7), C(SDLK_F8), C(SDLK_F9), C(SDLK_F10), N, N, N, N, N, N, N, N, N, N,
+  // Load Ref State
+  _(SDLK_1), _(SDLK_2), _(SDLK_3), _(SDLK_4), _(SDLK_5), _(SDLK_6), _(SDLK_7), _(SDLK_8), _(SDLK_9), _(SDLK_0), _(SDLK_MINUS), _(SDLK_EQUALS), _(SDLK_BACKSPACE), N, N, N, N, N, N, N,
+  // Replay Ref State
+  C(SDLK_1), C(SDLK_2), C(SDLK_3), C(SDLK_4), C(SDLK_5), C(SDLK_6), C(SDLK_7), C(SDLK_8), C(SDLK_9), C(SDLK_0), C(SDLK_MINUS), C(SDLK_EQUALS), C(SDLK_BACKSPACE), N, N, N, N, N, N, N,
+  // CheatLife, CheatKeys, MigrateSnapshot, Fullscreen, Reset, Pause, PauseDimmed, Turbo, ZoomIn, ZoomOut
+  _(SDLK_w), _(SDLK_o), _(SDLK_k), A(SDLK_RETURN), _(SDLK_e), S(SDLK_p), _(SDLK_p), _(SDLK_t), N, N
+};
+#undef _
+#undef A
+#undef C
+#undef S
+#undef N
+
+typedef struct KeyNameId {
+  const char *name;
+  uint16 id, size;
+} KeyNameId;
+
+#define M(n) {#n, kKeys_##n, kKeys_##n##_Last - kKeys_##n + 1}
+#define S(n) {#n, kKeys_##n, 1}
+static const KeyNameId kKeyNameId[] = {
+  M(Controls), M(Load), M(Save), M(Replay), M(LoadRef), M(ReplayRef),
+  S(CheatLife), S(CheatKeys), S(MigrateSnapshot), S(Fullscreen), S(Reset),
+  S(Pause), S(PauseDimmed), S(Turbo), S(ZoomIn), S(ZoomOut),
+};
+#undef S
+#undef M
+typedef struct KeyMapHashEnt {
+  uint16 key, cmd, next;
+} KeyMapHashEnt;
+
+static uint16 keymap_hash_first[255];
+static KeyMapHashEnt *keymap_hash;
+static int keymap_hash_size;
+static bool has_keynameid[countof(kKeyNameId)];
+
+bool KeyMapHash_Add(uint16 key, uint16 cmd) {
+  if ((keymap_hash_size & 0xff) == 0) {
+    if (keymap_hash_size > 10000) 
+      Die("Too many keys");
+    keymap_hash = realloc(keymap_hash, sizeof(KeyMapHashEnt) * (keymap_hash_size + 256));
+  }
+  int i = keymap_hash_size++;
+  KeyMapHashEnt *ent = &keymap_hash[i];
+  ent->key = key;
+  ent->cmd = cmd;
+  ent->next = 0;
+  int j = (uint32)key % 255;
+
+  uint16 *cur = &keymap_hash_first[j];
+  while (*cur) {
+    KeyMapHashEnt *ent = &keymap_hash[*cur - 1];
+    if (ent->key == key)
+      return false;
+    cur = &ent->next;
+  }
+  *cur = i + 1;
+  return true;
+}
+
+static int KeyMapHash_Find(uint16 key) {
+  int i = keymap_hash_first[key % 255];
+  while (i) {
+    KeyMapHashEnt *ent = &keymap_hash[i - 1];
+    if (ent->key == key)
+      return ent->cmd;
+    i = ent->next;
+  }
+  return -1;
+}
+
+int FindCmdForSdlKey(SDL_Keycode code, SDL_Keymod mod) {
+  if (code & ~(SDLK_SCANCODE_MASK | 0x1ff))
+    return -1;
+  int key = mod & KMOD_ALT ? kKeyMod_Alt : 0;
+  key |= mod & KMOD_CTRL ? kKeyMod_Ctrl : 0;
+  key |= mod & KMOD_SHIFT ? kKeyMod_Shift : 0;
+  key |= REMAP_SDL_KEYCODE(code);
+  return KeyMapHash_Find(key);
+}
+
+static char *NextDelim(char **s, int sep) {
+  char *r = *s;
+  if (r) {
+    while (r[0] == ' ' || r[0] == '\t')
+      r++;
+    char *t = strchr(r, sep);
+    *s = t ? (*t++ = 0, t) : NULL;
+  }
+  return r;
+}
+
+static inline int ToLower(int a) {
+  return a + (a >= 'A' && a <= 'Z') * 32;
+}
+
+static bool StringEqualsNoCase(const char *a, const char *b) {
+  for (;;) {
+    int aa = ToLower(*a++), bb = ToLower(*b++);
+    if (aa != bb)
+      return false;
+    if (aa == 0)
+      return true;
+  }
+}
+
+static bool StringStartsWithNoCase(const char *a, const char *b) {
+  for (;;) {
+    int aa = ToLower(*a++), bb = ToLower(*b++);
+    if (bb == 0)
+      return true;
+    if (aa != bb)
+      return false;
+  }
+}
+
+static void ParseKeyArray(char *value, int cmd, int size) {
+  char *s;
+  int i = 0;
+  for (; i < size && (s = NextDelim(&value, ',')) != NULL; i++, cmd++) {
+    if (*s == 0)
+      continue;
+    int key_with_mod = 0;
+    for (;;) {
+      if (StringStartsWithNoCase(s, "Shift+")) {
+        key_with_mod |= kKeyMod_Shift, s += 6;
+      } else if (StringStartsWithNoCase(s, "Ctrl+")) {
+        key_with_mod |= kKeyMod_Ctrl, s += 5;
+      } else if (StringStartsWithNoCase(s, "Alt+")) {
+        key_with_mod |= kKeyMod_Alt, s += 4;
+      } else {
+        break;
+      }
+    }
+    SDL_Keycode key = SDL_GetKeyFromName(s);
+    if (key == SDLK_UNKNOWN) {
+      fprintf(stderr, "Unknown key: '%s'\n", s);
+      continue;
+    }
+    if (!KeyMapHash_Add(key_with_mod | REMAP_SDL_KEYCODE(key), cmd))
+      fprintf(stderr, "Duplicate key: '%s'\n", s);
+  }
+}
+
+
+static void RegisterDefaultKeys() {
+  for (int i = 0; i < countof(kKeyNameId); i++) {
+    if (!has_keynameid[i]) {
+      int size = kKeyNameId[i].size, k = kKeyNameId[i].id;
+      for (int j = 0; j < size; j++, k++)
+        KeyMapHash_Add(kDefaultKbdControls[k], k);
+    }
+  }
+}
+
+
+static int GetIniSection(const char *s) {
+  if (StringEqualsNoCase(s, "[KeyMap]"))
+    return 0;
+  return -1;
+}
+
+static bool HandleIniConfig(int section, const char *key, char *value) {
+  if (section == 0) {
+    for (int i = 0; i < countof(kKeyNameId); i++) {
+      if (StringEqualsNoCase(key, kKeyNameId[i].name)) {
+        has_keynameid[i] = true;
+        ParseKeyArray(value, kKeyNameId[i].id, kKeyNameId[i].size);
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+
+uint8 *ReadFile(const char *name, size_t *length) {
+  FILE *f = fopen(name, "rb");
+  if (f == NULL)
+    return NULL;
+  fseek(f, 0, SEEK_END);
+  size_t size = ftell(f);
+  rewind(f);
+  uint8 *buffer = (uint8 *)malloc(size + 1);
+  if (!buffer) Die("malloc failed");
+  // Always zero terminate so this function can be used also for strings.
+  buffer[size] = 0;
+  if (fread(buffer, 1, size, f) != size)
+    Die("fread failed");
+  fclose(f);
+  if (length) *length = size;
+  return buffer;
+}
+
+void ParseConfigFile() {
+  uint8 *file = ReadFile("zelda3.user.ini", NULL);
+  if (!file) {
+    file = ReadFile("zelda3.ini", NULL);
+    if (!file)
+      return;
+  }
+  fprintf(stderr, "Loading zelda3.ini\n");
+  int section = -2;
+  int lineno = 1;
+  char *p, *next_p = (char*)file;
+  for (; (p = next_p) != NULL; lineno++) {
+    // find end of line
+    char *eol = strchr(p, '\n');
+    next_p = eol ? eol + 1 : NULL;
+    eol = eol ? eol : p + strlen(p);
+    // strip comments
+    char *comment = memchr(p, '#', eol - p);
+    eol = (comment != 0) ? comment : eol;
+    // strip trailing whitespace
+    while (eol > p && (eol[-1] == '\r' || eol[-1] == ' ' || eol[-1] == '\t'))
+      eol--;
+    *eol = 0;
+    if (p == eol)
+      continue; // empty line
+    // strip leading whitespace
+    while (p[0] == ' ' || p[0] == '\t')
+      p++;
+    if (*p == '[') {
+      section = GetIniSection(p);
+      if (section < 0)
+        fprintf(stderr, "zelda3.ini:%d: Invalid .ini section %s\n", lineno, p);
+    } else if (section == -2) {
+      fprintf(stderr, "zelda3.ini:%d: Expecting [section]\n", lineno);
+    } else {
+      char *equals = memchr(p, '=', eol - p);
+      if (equals == NULL) {
+        fprintf(stderr, "zelda3.ini:%d: Expecting 'key=value'\n", lineno);
+      } else {
+        char *kr = equals;
+        while (kr > p && (kr[-1] == ' ' || kr[-1] == '\t'))
+          kr--;
+        *kr = 0;
+        char *v = equals + 1;
+        while (v[0] == ' ' || v[0] == '\t')
+          v++;
+        if (section >= 0 && !HandleIniConfig(section, p, v))
+          fprintf(stderr, "zelda3.ini:%d: Can't parse '%s'\n", lineno, p);
+      }
+    }
+  }
+  free(file);
+}
+
+void AfterConfigParse() {
+  RegisterDefaultKeys();
+}
--- /dev/null
+++ b/config.h
@@ -1,0 +1,35 @@
+#pragma once
+#include "types.h"
+#include <SDL_keycode.h>
+
+enum {
+  kKeys_Controls = 0,
+  kKeys_Controls_Last = kKeys_Controls + 11,
+  kKeys_Load,
+  kKeys_Load_Last = kKeys_Load + 19,
+  kKeys_Save,
+  kKeys_Save_Last = kKeys_Save + 19,
+  kKeys_Replay,
+  kKeys_Replay_Last = kKeys_Replay + 19,
+  kKeys_LoadRef,
+  kKeys_LoadRef_Last = kKeys_LoadRef + 19,
+  kKeys_ReplayRef,
+  kKeys_ReplayRef_Last = kKeys_ReplayRef + 19,
+  kKeys_CheatLife,
+  kKeys_CheatKeys,
+  kKeys_MigrateSnapshot,
+  kKeys_Fullscreen,
+  kKeys_Reset,
+  kKeys_Pause,
+  kKeys_PauseDimmed,
+  kKeys_Turbo,
+  kKeys_ZoomIn,
+  kKeys_ZoomOut,
+  kKeys_Total,
+};
+
+uint8 *ReadFile(const char *name, size_t *length);
+void ParseConfigFile();
+void AfterConfigParse();
+
+int FindCmdForSdlKey(SDL_Keycode code, SDL_Keymod mod);
\ No newline at end of file
--- a/main.c
+++ b/main.c
@@ -18,13 +18,16 @@
 #include "variables.h"
 
 #include "zelda_rtl.h"
+#include "config.h"
 
 extern Ppu *GetPpuForRendering();
 extern Dsp *GetDspForRendering();
-
+extern Snes *g_snes;
 extern uint8 g_emulated_ram[0x20000];
 bool g_run_without_emu = false;
 
+static int g_input1_state;
+
 void PatchRom(uint8 *rom);
 void SetSnes(Snes *snes);
 void RunAudioPlayer();
@@ -33,8 +36,7 @@
 void PatchCommand(char cmd);
 bool RunOneFrame(Snes *snes, int input_state, bool turbo);
 
-static uint8 *ReadFile(char* name, size_t* length);
-static bool LoadRom(char* name, Snes* snes);
+static bool LoadRom(const char *name, Snes *snes);
 static void PlayAudio(Snes *snes, SDL_AudioDeviceID device, int16 *audioBuffer);
 static void RenderScreen(SDL_Window *window, SDL_Renderer *renderer, SDL_Texture *texture, bool fullscreen);
 static void HandleInput(int keyCode, int modCode, bool pressed);
@@ -41,11 +43,24 @@
 static void HandleGamepadInput(int button, bool pressed);
 static void HandleGamepadAxisInput(int gamepad_id, int axis, int value);
 static void OpenOneGamepad(int i);
+
 static inline int IntMin(int a, int b) { return a < b ? a : b; }
 static inline int IntMax(int a, int b) { return a > b ? a : b; }
 
-static int input1_current_state;
+enum {
+  kRenderWidth = 512,
+  kRenderHeight = 480,
+  kDefaultZoom = 2,
+};
 
+
+static uint32 g_win_flags = SDL_WINDOW_RESIZABLE;
+static SDL_Window *g_window;
+static SDL_Renderer *g_renderer;
+static uint8 g_paused, g_turbo = true;
+static int current_zoom = kDefaultZoom;
+static uint8 g_gamepad_buttons;
+
 void NORETURN Die(const char *error) {
   fprintf(stderr, "Error: %s\n", error);
   exit(1);
@@ -54,22 +69,15 @@
 void SetButtonState(int button, bool pressed) {
   // set key in constroller
   if (pressed) {
-    input1_current_state |= 1 << button;
+    g_input1_state |= 1 << button;
   } else {
-    input1_current_state &= ~(1 << button);
+    g_input1_state &= ~(1 << button);
   }
 }
 
-enum {
-  kRenderWidth = 512,
-  kRenderHeight = 480,
-  kDefaultZoom = 2,
-};
 
-static int current_zoom = kDefaultZoom;
-
-void DoZoom(SDL_Window *window, SDL_Renderer *renderer, int zoom_step) {
-  int screen = SDL_GetWindowDisplayIndex(window);
+void DoZoom(int zoom_step) {
+  int screen = SDL_GetWindowDisplayIndex(g_window);
   if (screen < 0) screen = 0;
   int max_zoom = kDefaultZoom * 5;
   SDL_Rect bounds;
@@ -77,7 +85,7 @@
   // note this takes into effect Windows display scaling, i.e., resolution is divided by scale factor
   if (SDL_GetDisplayUsableBounds(screen, &bounds) == 0) {
     // this call may take a while before it is reported by Windows (or not at all in my testing)
-    if (SDL_GetWindowBordersSize(window, &bt, &bl, &bb, &br) != 0) {
+    if (SDL_GetWindowBordersSize(g_window, &bt, &bl, &bb, &br) != 0) {
       // guess based on Windows 10/11 defaults
       bl = br = bb = 1;
       bt = 31;
@@ -91,9 +99,9 @@
   current_zoom = new_zoom;
   int w = new_zoom * (kRenderWidth / kDefaultZoom);
   int h = new_zoom * (kRenderHeight / kDefaultZoom);
-  SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
-  SDL_RenderSetLogicalSize(renderer, w, h);
-  SDL_SetWindowSize(window, w, h);
+  
+  //SDL_RenderSetLogicalSize(g_renderer, w, h);
+  SDL_SetWindowSize(g_window, w, h);
   if (bt >= 0) {
     // Center the window on top of the mouse
     int mx, my;
@@ -100,9 +108,9 @@
     SDL_GetGlobalMouseState(&mx, &my);
     int wx = IntMax(IntMin(mx - w / 2, bounds.x + bounds.w - bl - br - w), bounds.x + bl);
     int wy = IntMax(IntMin(my - h / 2, bounds.y + bounds.h - bt - bb - h), bounds.y + bt);
-    SDL_SetWindowPosition(window, wx, wy);
+    SDL_SetWindowPosition(g_window, wx, wy);
   } else {
-    SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+    SDL_SetWindowPosition(g_window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
   }
 }
 
@@ -114,17 +122,20 @@
 
 #undef main
 int main(int argc, char** argv) {
+  ParseConfigFile();
+  AfterConfigParse();
+
   // set up SDL
   if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
     printf("Failed to init SDL: %s\n", SDL_GetError());
     return 1;
   }
-  uint32 win_flags = SDL_WINDOWPOS_UNDEFINED;
-  SDL_Window* window = SDL_CreateWindow("Zelda3", SDL_WINDOWPOS_UNDEFINED, win_flags, kRenderWidth, kRenderHeight, 0);
+  SDL_Window* window = SDL_CreateWindow("Zelda3", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, kRenderWidth, kRenderHeight, g_win_flags);
   if(window == NULL) {
     printf("Failed to create window: %s\n", SDL_GetError());
     return 1;
   }
+  g_window = window;
   SDL_SetWindowHitTest(window, HitTestCallback, NULL);
   SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
   if(renderer == NULL) {
@@ -131,11 +142,15 @@
     printf("Failed to create renderer: %s\n", SDL_GetError());
     return 1;
   }
+  g_renderer = renderer;
+  SDL_RenderSetLogicalSize(renderer, 512, 480); 
   SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBX8888, SDL_TEXTUREACCESS_STREAMING, kRenderWidth, kRenderHeight);
   if(texture == NULL) {
     printf("Failed to create texture: %s\n", SDL_GetError());
     return 1;
   }
+  SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
+
   SDL_AudioSpec want, have;
   SDL_AudioDeviceID device;
   SDL_memset(&want, 0, sizeof(want));
@@ -178,19 +193,10 @@
   for (int i = 0; i < SDL_NumJoysticks(); i++)
     OpenOneGamepad(i);
 
-  bool hooks = true;
-  // sdl loop
   bool running = true;
   SDL_Event event;
   uint32 lastTick = SDL_GetTicks();
   uint32 curTick = 0;
-  uint32 delta = 0;
-  int numFrames = 0;
-  bool cpuNext = false;
-  bool spcNext = false;
-  int counter = 0;
-  bool paused = false;
-  bool turbo = true;
   uint32 frameCtr = 0;
 
   while(running) {
@@ -209,69 +215,41 @@
         HandleGamepadInput(event.cbutton.button, event.cbutton.state == SDL_PRESSED);
         break;
       case SDL_MOUSEWHEEL:
-        if ((win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == 0 && event.wheel.y != 0 && SDL_GetModState() & KMOD_CTRL)
-          DoZoom(window, renderer, event.wheel.y > 0 ? 1 : -1);
+        if ((g_win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == 0 && event.wheel.y != 0 && SDL_GetModState() & KMOD_CTRL)
+          DoZoom(event.wheel.y > 0 ? 1 : -1);
         break;
       case SDL_MOUSEBUTTONDOWN:
         if (event.button.button == SDL_BUTTON_LEFT && event.button.state == SDL_PRESSED && event.button.clicks == 2) {
-          if ((win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == 0 && SDL_GetModState() & KMOD_SHIFT) {
-            win_flags ^= SDL_WINDOW_BORDERLESS;
-            SDL_SetWindowBordered(window, (win_flags & SDL_WINDOW_BORDERLESS) == 0);
+          if ((g_win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == 0 && SDL_GetModState() & KMOD_SHIFT) {
+            g_win_flags ^= SDL_WINDOW_BORDERLESS;
+            SDL_SetWindowBordered(g_window, (g_win_flags & SDL_WINDOW_BORDERLESS) == 0);
           }
         }
         break;
-      case SDL_KEYDOWN: {
-        bool skip_default = false;
-        switch(event.key.keysym.sym) {
-        case SDLK_e:
-          if (snes) {
-            snes_reset(snes, event.key.keysym.sym == SDLK_e);
-            CopyStateAfterSnapshotRestore(true);
-          }
-          break;
-        case SDLK_p: paused ^= true; break;
-        case SDLK_w:
-          PatchCommand('w');
-          break;
-        case SDLK_o:
-          PatchCommand('o');
-          break;
-        case SDLK_k:
-          PatchCommand('k');
-          break;
-        case SDLK_t:
-          turbo = !turbo;
-          break;
-        case SDLK_RETURN:
-          if (event.key.keysym.mod & KMOD_ALT) {
-            win_flags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
-            SDL_SetWindowFullscreen(window, win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP);
-            skip_default = true;
-          }
-          break;
-        }
-        if (!skip_default)
-          HandleInput(event.key.keysym.sym, event.key.keysym.mod, true);
+      case SDL_KEYDOWN:
+        HandleInput(event.key.keysym.sym, event.key.keysym.mod, true);
         break;
-      }
-      case SDL_KEYUP: {
+      case SDL_KEYUP:
         HandleInput(event.key.keysym.sym, event.key.keysym.mod, false);
         break;
-      }
-      case SDL_QUIT: {
+      case SDL_QUIT:
         running = false;
         break;
       }
     }
-  }
 
-    if (paused) {
+    if (g_paused) {
       SDL_Delay(16);
       continue;
     }
 
-    bool is_turbo = RunOneFrame(snes_run, input1_current_state, (counter++ & 0x7f) != 0 && turbo);
+    // Clear gamepad inputs when joypad directional inputs to avoid wonkiness
+    int inputs = g_input1_state;
+    if (g_input1_state & 0xf0)
+      g_gamepad_buttons = 0;
 
+    bool is_turbo = RunOneFrame(snes_run, g_input1_state | g_gamepad_buttons, (frameCtr++ & 0x7f) != 0 && g_turbo);
+
     if (is_turbo)
       continue;
 
@@ -278,7 +256,7 @@
     ZeldaDrawPpuFrame();
 
     PlayAudio(snes_run, device, audioBuffer);
-    RenderScreen(window, renderer, texture, (win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0);
+    RenderScreen(window, renderer, texture, (g_win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0);
 
     SDL_RenderPresent(renderer); // vsyncs to 60 FPS
     // if vsync isn't working, delay manually
@@ -286,10 +264,10 @@
 
     static const uint8 delays[3] = { 17, 17, 16 }; // 60 fps
 #if 1
-    lastTick += delays[frameCtr++ % 3];
+    lastTick += delays[frameCtr % 3];
 
     if (lastTick > curTick) {
-      delta = lastTick - curTick;
+      uint32 delta = lastTick - curTick;
       if (delta > 500) {
         lastTick = curTick - 500;
         delta = 500;
@@ -337,83 +315,66 @@
     printf("Failed to lock texture: %s\n", SDL_GetError());
     return;
   }
-
   ppu_putPixels(GetPpuForRendering(), (uint8_t*) pixels);
   SDL_UnlockTexture(texture);
-
-  SDL_DisplayMode display_mode;
-  if (fullscreen && SDL_GetWindowDisplayMode(window, &display_mode) == 0) {
-    uint32 w = display_mode.w, h = display_mode.h;
-    if (w * 15 < h * 16)
-      h = w * 15 / 16;  // limit height
-    else
-      w = h * 16 / 15;  // limit width
-    SDL_Rect r = { (display_mode.w - w) / 2, (display_mode.h - h) / 2, w, h };
-    SDL_RenderCopy(renderer, texture, NULL, &r);
-  } else {
-    SDL_RenderCopy(renderer, texture, NULL, NULL);
-  }
+  SDL_RenderCopy(renderer, texture, NULL, NULL);
 }
 
-
-static void HandleInput(int keyCode, int keyMod, bool pressed) {
-  switch(keyCode) {
-    case SDLK_z: SetButtonState(0, pressed); break;
-    case SDLK_a: SetButtonState(1, pressed); break;
-    case SDLK_RSHIFT: SetButtonState(2, pressed); break;
-    case SDLK_RETURN: SetButtonState(3, pressed); break;
-    case SDLK_UP: SetButtonState(4, pressed); break;
-    case SDLK_DOWN: SetButtonState(5, pressed); break;
-    case SDLK_LEFT: SetButtonState(6, pressed); break;
-    case SDLK_RIGHT: SetButtonState(7, pressed); break;
-    case SDLK_x: SetButtonState(8, pressed); break;
-    case SDLK_s: SetButtonState(9, pressed); break;
-    case SDLK_d: SetButtonState(10, pressed); break;
-    case SDLK_c: SetButtonState(11, pressed); break;
-    case SDLK_BACKSPACE:
-    case SDLK_1:
-    case SDLK_2:
-    case SDLK_3:
-    case SDLK_4:
-    case SDLK_5:
-    case SDLK_6:
-    case SDLK_7:
-    case SDLK_8:
-    case SDLK_9:
-    case SDLK_0:
-    case SDLK_MINUS:
-    case SDLK_EQUALS:
-      if (pressed) {
-        SaveLoadSlot(
-          (keyMod & KMOD_CTRL) != 0 ? kSaveLoad_Replay : kSaveLoad_Load,
-          256 + (keyCode == SDLK_0 ? 9 : 
-                 keyCode == SDLK_MINUS ? 10 : 
-                 keyCode == SDLK_EQUALS ? 11 :
-                 keyCode == SDLK_BACKSPACE ? 12 :
-                 keyCode - SDLK_1));
+static void HandleCommand(uint32 j, bool pressed) {
+  if (j <= kKeys_Controls_Last) {
+    static const uint8 kKbdRemap[12] = { 4, 5, 6, 7, 2, 3, 8, 0, 9, 1, 10, 11 };
+    SetButtonState(kKbdRemap[j], pressed);
+    return;
+  }
+  if (!pressed)
+    return;
+  if (j <= kKeys_Load_Last) {
+    SaveLoadSlot(kSaveLoad_Load, j - kKeys_Load);
+  } else if (j <= kKeys_Save_Last) {
+    SaveLoadSlot(kSaveLoad_Save, j - kKeys_Save);
+  } else if (j <= kKeys_Replay_Last) {
+    SaveLoadSlot(kSaveLoad_Replay, j - kKeys_Replay);
+  } else if (j <= kKeys_LoadRef_Last) {
+    SaveLoadSlot(kSaveLoad_Load, 256 + j - kKeys_LoadRef);
+  } else if (j <= kKeys_ReplayRef_Last) {
+    SaveLoadSlot(kSaveLoad_Replay, 256 + j - kKeys_ReplayRef);
+  } else {
+    switch (j) {
+    case kKeys_CheatLife: PatchCommand('w'); break;
+    case kKeys_CheatKeys: PatchCommand('o'); break;
+    case kKeys_MigrateSnapshot: PatchCommand('k'); break;
+    case kKeys_Fullscreen:
+      g_win_flags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
+      SDL_SetWindowFullscreen(g_window, g_win_flags & SDL_WINDOW_FULLSCREEN_DESKTOP);
+      break;
+    case kKeys_Reset:
+      snes_reset(g_snes, true);
+      CopyStateAfterSnapshotRestore(true);
+      break;
+    case kKeys_Pause: g_paused = !g_paused; break;
+    case kKeys_PauseDimmed: 
+      g_paused = !g_paused;
+      if (g_paused) {
+        SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
+        SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 159);
+        SDL_RenderFillRect(g_renderer, NULL);
+        SDL_RenderPresent(g_renderer);
       }
       break;
-
-    case SDLK_F1: 
-    case SDLK_F2: 
-    case SDLK_F3: 
-    case SDLK_F4: 
-    case SDLK_F5: 
-    case SDLK_F6: 
-    case SDLK_F7: 
-    case SDLK_F8: 
-    case SDLK_F9: 
-    case SDLK_F10: 
-      if (pressed) {
-        SaveLoadSlot(
-          (keyMod & KMOD_CTRL) != 0 ? kSaveLoad_Replay : 
-          (keyMod & KMOD_SHIFT) != 0 ? kSaveLoad_Save : kSaveLoad_Load,
-          keyCode - SDLK_F1);
-      }
-      break;
+    case kKeys_Turbo: g_turbo = !g_turbo; break;
+    case kKeys_ZoomIn: DoZoom(1); break;
+    case kKeys_ZoomOut: DoZoom(-1); break;
+    default: assert(0);
+    }
   }
 }
 
+static void HandleInput(int keyCode, int keyMod, bool pressed) {
+  int j = FindCmdForSdlKey(keyCode, keyMod);
+  if (j >= 0)
+    HandleCommand(j, pressed);
+}
+
 static void OpenOneGamepad(int i) {
   if (SDL_IsGameController(i)) {
     SDL_GameController *controller = SDL_GameControllerOpen(i);
@@ -466,14 +427,13 @@
       int angle = (int)(atan2f(last_y, last_x) * (float)(128 / M_PI) + 0.5f);
       buttons = kSegmentToButtons[(uint8)(angle + 16 + 64) >> 5];
     }
-    input1_current_state = input1_current_state & ~0xf0 | buttons;
+    g_gamepad_buttons = buttons;
   }
 }
 
-static bool LoadRom(char* name, Snes* snes) {
+static bool LoadRom(const char *name, Snes *snes) {
   size_t length = 0;
-  uint8_t* file = NULL;
-  file = ReadFile(name, &length);
+  uint8 *file = ReadFile(name, &length);
   if(!file) Die("Failed to read file");
 
   PatchRom(file);
@@ -484,18 +444,3 @@
 }
 
 
-static uint8_t* ReadFile(char* name, size_t* length) {
-  FILE* f = fopen(name, "rb");
-  if(f == NULL) {
-    return NULL;
-  }
-  fseek(f, 0, SEEK_END);
-  int size = ftell(f);
-  rewind(f);
-  uint8_t* buffer = (uint8_t *)malloc(size);
-  if (!buffer) Die("malloc failed");
-  fread(buffer, size, 1, f);
-  fclose(f);
-  *length = size;
-  return buffer;
-}
--- a/types.h
+++ b/types.h
@@ -72,4 +72,6 @@
 #define uvram (*(UploadVram_3*)(&g_ram[0x1000]))
 
 typedef void PlayerHandlerFunc();
-typedef void HandlerFuncK(int k);
\ No newline at end of file
+typedef void HandlerFuncK(int k);
+
+void NORETURN Die(const char *error);
--- /dev/null
+++ b/zelda3.ini
@@ -1,0 +1,27 @@
+[KeyMap]
+# Change what keyboard keys map to the joypad
+# Order: Up, Down, Left, Right, Select, Start, A, B, X, Y, L, R
+
+# This default is suitable for QWERTY keyboards.
+Controls = Up, Down, Left, Right, Right Shift, Return, x, z, s, a, d, c
+
+# This one is suitable for AZERTY keyboards.
+#Controls = Up, Down, Left, Right, Right Shift, Return, x, w, s, q, d, c
+
+CheatLife = w
+CheatKeys = o
+MigrateSnapshot = k
+Fullscreen = Alt+Return
+Reset = Ctrl+e
+Pause = Shift+p
+PauseDimmed = p
+Turbo = t
+ZoomIn = Ctrl+Up
+ZoomOut = Ctrl+Down
+
+Load =      F1,     F2,     F3,     F4,     F5,     F6,     F7,     F8,     F9,     F10
+Save = Shift+F1,Shift+F2,Shift+F3,Shift+F4,Shift+F5,Shift+F6,Shift+F7,Shift+F8,Shift+F9,Shift+F10
+Replay= Ctrl+F1,Ctrl+F2,Ctrl+F3,Ctrl+F4,Ctrl+F5,Ctrl+F6,Ctrl+F7,Ctrl+F8,Ctrl+F9,Ctrl+F10
+
+LoadRef = 1,2,3,4,5,6,7,8,9,0,-,=,Backspace
+ReplayRef = Ctrl+1,Ctrl+2,Ctrl+3,Ctrl+4,Ctrl+5,Ctrl+6,Ctrl+7,Ctrl+8,Ctrl+9,Ctrl+0,Ctrl+-,Ctrl+=,Ctrl+Backspace
--- a/zelda3.vcxproj
+++ b/zelda3.vcxproj
@@ -125,6 +125,7 @@
   <ItemGroup>
     <ClCompile Include="ancilla.c" />
     <ClCompile Include="attract.c" />
+    <ClCompile Include="config.c" />
     <ClCompile Include="dungeon.c" />
     <ClCompile Include="ending.c" />
     <ClCompile Include="hud.c" />
@@ -198,6 +199,7 @@
   <ItemGroup>
     <ClInclude Include="ancilla.h" />
     <ClInclude Include="attract.h" />
+    <ClInclude Include="config.h" />
     <ClInclude Include="dungeon.h" />
     <ClInclude Include="ending.h" />
     <ClInclude Include="hud.h" />
--- a/zelda3.vcxproj.filters
+++ b/zelda3.vcxproj.filters
@@ -115,6 +115,9 @@
     <ClCompile Include="zelda_cpu_infra.c">
       <Filter>Zelda</Filter>
     </ClCompile>
+    <ClCompile Include="config.c">
+      <Filter>Zelda</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="snes\apu.h">
@@ -230,6 +233,9 @@
     </ClInclude>
     <ClInclude Include="snes\dsp_regs.h">
       <Filter>Snes</Filter>
+    </ClInclude>
+    <ClInclude Include="config.h">
+      <Filter>Zelda</Filter>
     </ClInclude>
   </ItemGroup>
   <ItemGroup>
--- a/zelda_rtl.h
+++ b/zelda_rtl.h
@@ -195,7 +195,6 @@
 void ClearOamBuffer();
 void Startup_InitializeMemory();
 void LoadSongBank(const uint8 *p);
-void NORETURN Die(const char *error);
 void ZeldaWriteSram();
 void ZeldaReadSram(Snes *snes);