// Hyperbolic Rogue -- multiplayer features
// Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details

/** \file multi.cpp
 *  \brief multiplayer features, also input configuration
 */

#include "hyper.h"
namespace hr {

EX namespace multi {

  #if HDR
  static const int MAXJOY = 8;
  static const int MAXBUTTON = 64;
  static const int MAXAXE = 16;
  static const int MAXHAT = 4;
  
  struct config {
    char keyaction[512];
    char joyaction[MAXJOY][MAXBUTTON];
    char axeaction[MAXJOY][MAXAXE];
    char hataction[MAXJOY][MAXHAT][4];
    int  deadzoneval[MAXJOY][MAXAXE];
    };
  #endif
  
  EX config scfg;  
  EX charstyle scs[MAXPLAYER];

  EX bool split_screen;
  EX bool pvp_mode;
  EX bool friendly_fire = true;
  EX bool self_hits;
  EX bool two_focus;

  EX int players = 1;
  EX cellwalker player[MAXPLAYER];
  EX vector<int> revive_queue; // queue for revival
  
  EX cell *origpos[MAXPLAYER], *origtarget[MAXPLAYER];

  EX bool flipped[MAXPLAYER];
  
  // treasure collection, kill, and death statistics
  EX int treasures[MAXPLAYER], kills[MAXPLAYER], deaths[MAXPLAYER], pkills[MAXPLAYER], suicides[MAXPLAYER];
  
  EX bool alwaysuse = false;

  EX void recall() {
    for(int i=0; i<numplayers(); i++) {
        int idir = (3 * i) % cwt.at->type;
        cell *c2 = cwt.at->move(idir);
        makeEmpty(c2);
        if(!passable(c2, NULL, P_ISPLAYER)) c2 = cwt.at;
        multi::player[i].at = c2;
        multi::player[i].spin = 0;
        
        multi::flipped[i] = true;
        multi::whereto[i].d = MD_UNDECIDED;
        }
    }
  
  EX shiftmatrix whereis[MAXPLAYER];
  EX shiftmatrix crosscenter[MAXPLAYER];
  EX double ccdist[MAXPLAYER];
  EX cell *ccat[MAXPLAYER];
  
  bool combo[MAXPLAYER];

  EX int cpid; // player id -- an extra parameter for player-related functions
  EX int cpid_edit; // cpid currently being edited
  
  EX movedir whereto[MAXPLAYER]; // player's target cell  

  EX double mdx[MAXPLAYER], mdy[MAXPLAYER]; // movement vector for the next move
  
  static const int CMDS = 15;
  static const int CMDS_PAN = 11;

  vector<string> playercmds_shmup = {
    "forward", "backward", "turn left", "turn right",
    "move up", "move right", "move down", "move left", 
    "throw a knife", "face the pointer", "throw at the pointer", 
    "drop Dead Orb", "center the map on me", "Orb power (target: mouse)",
    "Orb power (target: facing)"
    };

  vector<string> playercmds_shmup3 = {
    "rotate up", "rotate down", "rotate left", "rotate right",
    "move forward", "strafe right", "move backward", "strafe left", 
    "throw a knife", "face the pointer", "throw at the pointer", 
    "drop Dead Orb", "center the map on me", "Orb power (target: mouse)",
    "Orb power (target: facing)"
    };
  
  vector<string> playercmds_turn = {
    "move up-right", "move up-left", "move down-right", "move down-left", 
    "move up", "move right", "move down", "move left", 
    "stay in place (left + right)", "cancel move", "leave the game", 
    "drop Dead Orb (up + down)", "center the map on me", "",
    ""
    };
  
  vector<string> pancmds = {
    "pan up", "pan right", "pan down", "pan left",
    "rotate left", "rotate right", "home",
    "world overview", "review your quest", "inventory", "main menu"
    };

  vector<string> pancmds3 = {
    "look up", "look right", "look down", "look left",
    "rotate left", "rotate right", "home",
    "world overview", "review your quest", "inventory", "main menu",
    "scroll forward", "scroll backward"
    };

#if HDR
#define SHMUPAXES_BASE 4
#define SHMUPAXES ((SHMUPAXES_BASE) + 4 * (MAXPLAYER))
#define SHMUPAXES_CUR ((SHMUPAXES_BASE) + 4 * playercfg)
#endif

EX const char* axemodes[SHMUPAXES] = {
  "do nothing", 
  "rotate view",
  "panning X",
  "panning Y",
  "player 1 X", 
  "player 1 Y", 
  "player 1 go", 
  "player 1 spin", 
  "player 2 X", 
  "player 2 Y", 
  "player 2 go", 
  "player 2 spin",
  "player 3 X", 
  "player 3 Y", 
  "player 3 go", 
  "player 3 spin",
  "player 4 X", 
  "player 4 Y", 
  "player 4 go", 
  "player 4 spin",
  "player 5 X", 
  "player 5 Y", 
  "player 5 go", 
  "player 5 spin",
  "player 6 X", 
  "player 6 Y", 
  "player 6 go", 
  "player 6 spin",
  "player 7 X", 
  "player 7 Y", 
  "player 7 go", 
  "player 7 spin"
  };

EX const char* axemodes3[4] = {
  "do nothing",
  "camera forward",
  "camera rotate X",
  "camera rotate Y"
  };

EX int centerplayer = -1;

char* axeconfigs[24]; int numaxeconfigs;
int* dzconfigs[24];

string listkeys(int id) {
#if CAP_SDL
  string lk = "";
  for(int i=0; i<512; i++)
    if(scfg.keyaction[i] == id)
      #if CAP_SDL2
      lk = lk + " " + SDL_GetScancodeName(SDL_Scancode(i));
      #else
      lk = lk + " " + SDL_GetKeyName(SDLKey(i));
      #endif
#if CAP_SDLJOY
  for(int i=0; i<numsticks; i++) for(int k=0; k<SDL_JoystickNumButtons(sticks[i]) && k<MAXBUTTON; k++)
    if(scfg.joyaction[i][k] == id) {
      lk = lk + " " + cts('A'+i)+"-B"+its(k);
      }
  for(int i=0; i<numsticks; i++) for(int k=0; k<SDL_JoystickNumHats(sticks[i]) && k<MAXHAT; k++)
    for(int d=0; d<4; d++)
      if(scfg.hataction[i][k][d] == id) {
        lk = lk + " " + cts('A'+i)+"-"+"URDL"[d];
        }
#endif
  return lk;
#else
  return "";
#endif
  }

#define SCJOY 16

string dsc(int id) {
  string buf = XLAT(" (%1 $$$, %2 kills, %3 deaths)",
    its(multi::treasures[id]),
    its(multi::kills[id]),
    its(multi::deaths[id])
    );
  if(friendly_fire)
    buf += XLAT(" (%1 pkills)", its(multi::pkills[id]));
  if(self_hits)
    buf += XLAT(" (%1 self)", its(multi::suicides[id]));
  return buf;
  }

EX void resetScores() {
  for(int i=0; i<MAXPLAYER; i++)
    multi::treasures[i] = multi::kills[i] = multi::deaths[i] = multi::pkills[i] = multi::suicides[i] = 0;
  }
 
bool configdead;

void handleConfig(int sym, int uni);

EX string player_count_name(int p) {
  return 
    p == 2 ? XLAT("two players") : 
    p == 3 ? XLAT("three players") : 
    p == 4 ? XLAT("four players") : 
    p == 5 ? XLAT("five players") : 
    p == 6 ? XLAT("six players") : 
    p == 7 ? XLAT("seven players") : 
    XLAT("one player");
  }

struct key_configurer {

  int sc;
  vector<string>& shmupcmdtable;
  string caption;
  int setwhat;

  key_configurer(int sc, vector<string>& sct, const string& caption) : sc(sc), shmupcmdtable(sct), caption(caption), setwhat(0) {
    }

  void operator() () {
      
    dialog::init(caption);
  
    getcstat = ' ';
    
    for(int i=0; i<isize(shmupcmdtable); i++) if(shmupcmdtable[i][0])
      dialog::addSelItem(XLAT(shmupcmdtable[i]), listkeys(16*sc+i),
        setwhat ? (setwhat>1 && i == (setwhat&15) ? '?' : 0) : 'a'+i);
      else dialog::addBreak(100);
  
    if(setwhat == 1)
      dialog::addItem(XLAT("press a key to unassign"), 0);
    else if(setwhat)
      dialog::addItem(XLAT("press a key for '%1'", XLAT(shmupcmdtable[setwhat&15])), 0);
    else
      dialog::addItem(XLAT("unassign a key"), 'z');
    
    dialog::display();
  
    keyhandler = [this] (int sym, int uni) {
      if(!setwhat) dialog::handleNavigation(sym, uni);
      if(sym) {
        if(setwhat) {
          scfg.keyaction[sym] = setwhat;
          setwhat = 0;
          }
        else if(uni >= 'a' && uni < 'a' + isize(shmupcmdtable) && shmupcmdtable[uni-'a'][0])
          setwhat = 16*sc+uni - 'a';
        else if(uni == 'z')
          setwhat = 1;
        else if(doexiton(sym, uni))
          popScreen();
        }
      };

#if CAP_SDLJOY    
    joyhandler = [this] (SDL_Event& ev) { 
      if(ev.type == SDL_JOYBUTTONDOWN && setwhat) {
        int joyid = ev.jbutton.which;
        int button = ev.jbutton.button;
        if(joyid < 8 && button < 32)
           scfg.joyaction[joyid][button] = setwhat;
        setwhat = 0;
        return true;
        }
  
      else if(ev.type == SDL_JOYHATMOTION && setwhat) {
        int joyid = ev.jhat.which;
        int hat = ev.jhat.hat;
        int dir = 4;
        if(ev.jhat.value == SDL_HAT_UP) dir = 0;
        if(ev.jhat.value == SDL_HAT_RIGHT) dir = 1;
        if(ev.jhat.value == SDL_HAT_DOWN) dir = 2;
        if(ev.jhat.value == SDL_HAT_LEFT) dir = 3;
        printf("%d %d %d\n", joyid, hat, dir);
        if(joyid < 8 && hat < 4 && dir < 4) {
          scfg.hataction[joyid][hat][dir] = setwhat;
          setwhat = 0;
          return true;
          }
        }
      return false;
      };
#endif
    }
  };

EX reaction_t get_key_configurer(int sc, vector<string>& sct, string caption) { 
  return key_configurer(sc, sct, caption); 
  }

EX reaction_t get_key_configurer(int sc, vector<string>& sct) { 
  return key_configurer(sc, sct, sc == 1 ? XLAT("configure player 1") :
    sc == 2 ? XLAT("configure player 2") :
    sc == 3 ? XLAT("configure panning") :
    sc == 4 ? XLAT("configure player 3") :
    sc == 5 ? XLAT("configure player 4") :
    sc == 6 ? XLAT("configure player 5") :
    sc == 7 ? XLAT("configure player 6") :
    sc == 8 ? XLAT("configure player 7") : ""
    ); 
  }

#if CAP_SDLJOY
struct joy_configurer {

  bool shmupcfg, racecfg;
  int playercfg;
  joy_configurer(int playercfg) : playercfg(playercfg) {}

  void operator() () {
    dialog::init();
    getcstat = ' ';
    numaxeconfigs = 0;
    for(int j=0; j<numsticks; j++) {
      for(int ax=0; ax<SDL_JoystickNumAxes(sticks[j]) && ax < MAXAXE; ax++) if(numaxeconfigs<24) {
        int y = SDL_JoystickGetAxis(sticks[j], ax);
        string buf = " ";
        if(configdead)
          buf += its(y);
        else {
          while(y >  10000) buf += "+", y -= 10000;
          while(y < -10000) buf += "-", y += 10000;
          if(y>0) buf += "+";
          if(y<0) buf += "-";
          }
        axeconfigs[numaxeconfigs] = &(scfg.axeaction[j][ax]);
        dzconfigs[numaxeconfigs] = &(scfg.deadzoneval[j][ax]);
        char aa = *axeconfigs[numaxeconfigs];
        string what = configdead ? its(scfg.deadzoneval[j][ax]) : 
          (GDIM == 3 && (aa%SHMUPAXES < 4)) ? XLAT(axemodes3[aa%SHMUPAXES]) :
          XLAT(axemodes[aa%SHMUPAXES]);
        dialog::addSelItem(XLAT("Joystick %1, axis %2", cts('A'+j), its(ax)) + buf, 
          what, 'a'+numaxeconfigs);
        numaxeconfigs++;
        }
      }
    
    dialog::addBoolItem(XLAT("Configure dead zones"), (configdead), 'z');
    dialog::display();

    keyhandler = [this] (int sym, int uni) { 
      dialog::handleNavigation(sym, uni);
      if(sym) {
        char xuni = uni | 96;
        if(xuni >= 'a' && xuni < 'a' + numaxeconfigs) {
          if(configdead) 
            dialog::editNumber( (*dzconfigs[xuni - 'a']), 0, 65536, 100, 0, XLAT("Configure dead zones"), "");
          else {
            int v = (*axeconfigs[xuni - 'a']);
            v += (shiftmul>0?1:-1);
            v += SHMUPAXES_CUR;
            v %= SHMUPAXES_CUR;
            (*axeconfigs[xuni - 'a']) = v;
            }
          }
        else if(xuni == 'z')
          configdead = !configdead;
        else if(doexiton(sym, uni))
          popScreen();
        }
      };
    }
  };
#endif

EX const char *axmodes[7] = {"OFF", "auto", "light", "heavy", "arrows", "WASD keys", "VI keys"};

struct shmup_configurer {

  void operator()() {
  #if CAP_SDL
    cmode = sm::SHMUPCONFIG | sm::SIDE | sm::DARKEN;
    gamescreen();
    dialog::init(XLAT("keyboard & joysticks"));
    
    bool haveconfig = shmup::on || players > 1 || multi::alwaysuse;
  
    if(haveconfig)
      dialog::addItem(XLAT("configure player 1"), '1');
    else
      dialog::addBreak(100);
    if(players > 1)
      dialog::addItem(XLAT("configure player 2"), '2');
    else if(players == 1 && !shmup::on)
      dialog::addSelItem(XLAT("input"), multi::alwaysuse ? XLAT("config") : XLAT("default"), 'a');
    else
      dialog::addBreak(100);
    if(players > 2)
      dialog::addItem(XLAT("configure player 3"), '3');
  #if CAP_SDLJOY
    else if(!haveconfig)
      dialog::addItem(XLAT("old style joystick configuration"), 'b');
  #endif
    else dialog::addBreak(100);
    if(players > 3)
      dialog::addItem(XLAT("configure player 4"), '4');
    else if(!shmup::on && !multi::alwaysuse) {
      dialog::addBoolItem(XLAT("smooth scrolling"), smooth_scrolling, 'c');
      }
    else if(alwaysuse)
      dialog::addInfo(XLAT("note: configured input is designed for"));
    else dialog::addBreak(100);
      
    if(players > 4)
      dialog::addItem(XLAT("configure player 5"), '5');
    else if(!shmup::on && !multi::alwaysuse) {
      if(GDIM == 2) {
        dialog::addSelItem(XLAT("help for keyboard users"), XLAT(axmodes[vid.axes]), 'h');
        dialog::add_action([] {vid.axes += 70 + (shiftmul > 0 ? 1 : -1); vid.axes %= 7; } );
        }
      else dialog::addBreak(100);
      }
    else if(alwaysuse)
      dialog::addInfo(XLAT("multiplayer and shmup mode; some features"));
    else dialog::addBreak(100);
  
    if(players > 5)
      dialog::addItem(XLAT("configure player 6"), '6');
    else if(alwaysuse)
      dialog::addInfo(XLAT("work worse if you use it."));
    else dialog::addBreak(100);
  
    if(players > 6)
      dialog::addItem(XLAT("configure player 7"), '7');
    else dialog::addBreak(100);
      
    if(shmup::on || multi::alwaysuse || players > 1)
      dialog::addItem(XLAT("configure panning and general keys"), 'p');
    else dialog::addBreak(100);
  
  #if CAP_SDLJOY
    if(numsticks > 0) {
      if(shmup::on || multi::alwaysuse || players > 1) 
        dialog::addItem(XLAT("configure joystick axes"), 'j');
      else dialog::addBreak(100);
      }
  #endif
  
    dialog::addBreak(50);
  
    dialog::addHelp();
  
    dialog::addBack();
    dialog::display();
    
    keyhandler = [this] (int sym, int uni) { return handleConfig(sym, uni); };
  #endif
    }

  void handleConfig(int sym, int uni) {
    auto& cmdlist = shmup::on ? (WDIM == 3 ? playercmds_shmup3 : playercmds_shmup) : playercmds_turn;
    dialog::handleNavigation(sym, uni);
    
    if(0) ;
    #if CAP_SDL
    else if(uni == '1') pushScreen(get_key_configurer(1, cmdlist));
    else if(uni == '2') pushScreen(get_key_configurer(2, cmdlist));
    else if(uni == 'p') pushScreen(get_key_configurer(3, GDIM == 3 ? pancmds3 : pancmds));
    else if(uni == '3') pushScreen(get_key_configurer(4, cmdlist));
    else if(uni == '4') pushScreen(get_key_configurer(5, cmdlist));
    else if(uni == '5') pushScreen(get_key_configurer(6, cmdlist));
    else if(uni == '6') pushScreen(get_key_configurer(7, cmdlist));
    else if(uni == '7') pushScreen(get_key_configurer(8, cmdlist));
  #if CAP_SDLJOY
    else if(uni == 'j') pushScreen(joy_configurer(players));
  #endif
    else if(uni == 'a') multi::alwaysuse = !multi::alwaysuse;
  #if CAP_SDLJOY
    else if(uni == 'b') pushScreen(showJoyConfig);
  #endif
    else if(uni == 'c') smooth_scrolling = !smooth_scrolling;
    #endif
    else if(doexiton(sym, uni)) popScreen();
    }
  };

EX void configure() {
  pushScreen(shmup_configurer());
  }

EX void showConfigureMultiplayer() {
  cmode = sm::SIDE | sm::MAYDARK;
  gamescreen();
  dialog::init("multiplayer");
  
  for(int i=1; i <= MAXPLAYER; i++) {
    string s = player_count_name(i);
    if(i <= players) s += dsc(i-1);
    dialog::addBoolItem(s, i == multi::players, '0' + i);
    if(!dual::state) dialog::add_action([i] {
      dialog::do_if_confirmed([i] {
        stop_game();
        players = i;
        start_game();
        });
      });
    }

  add_edit(self_hits);
  if(multi::players > 1) {
    dialog::addItem(XLAT("reset per-player statistics"), 'r');
    dialog::add_action([] {
      for(int i=0; i<MAXPLAYER; i++) 
        kills[i] = deaths[i] = treasures[i] = 0;
      });

    dialog::addSelItem(XLAT("keyboard & joysticks"), "", 'k');
    dialog::add_action(multi::configure);
    add_edit(split_screen);
    if(shmup::on && !racing::on) {
      add_edit(pvp_mode);
      add_edit(friendly_fire);
      if(pvp_mode)
        dialog::addInfo(XLAT("PvP grants infinite lives -- achievements disabled"));
      else if(friendly_fire)
        dialog::addInfo(XLAT("friendly fire off -- achievements disabled"));
      else if(split_screen)
        dialog::addInfo(XLAT("achievements disabled in split screen"));
      else
        dialog::addBreak(100);
      }
    else {
      dialog::addInfo(XLAT("PvP available only in shmup"));
      dialog::addBreak(400);
      }
    if(multi::players == 2 && !split_screen)
      add_edit(two_focus);
    else
      dialog::addBreak(100);
    }
  else dialog::addBreak(600);
  
  dialog::addBack();
  dialog::display();
  }

#if HDR
#define NUMACT 128

enum pcmds {
  pcForward, pcBackward, pcTurnLeft, pcTurnRight,
  pcMoveUp, pcMoveRight, pcMoveDown, pcMoveLeft,
  pcFire, pcFace, pcFaceFire,
  pcDrop, pcCenter, pcOrbPower, pcOrbKey
  };
#endif
  
EX int actionspressed[NUMACT], axespressed[SHMUPAXES], lactionpressed[NUMACT];

void pressaction(int id) {
  if(id >= 0 && id < NUMACT)
    actionspressed[id]++;
  }

EX bool notremapped(int sym) {
  int k = scfg.keyaction[sym];
  if(k == 0) return true;
  k /= 16;
  if(k > 3) k--; else if(k==3) k = 0;
  return k > multi::players;
  }

EX void initConfig() {
  
  char* t = scfg.keyaction;
  
  #if CAP_SDL2

  t[SDL_SCANCODE_W] = 16 + 4;
  t[SDL_SCANCODE_D] = 16 + 5;
  t[SDL_SCANCODE_S] = 16 + 6;
  t[SDL_SCANCODE_A] = 16 + 7;

  t[SDL_SCANCODE_KP_8] = 16 + 4;
  t[SDL_SCANCODE_KP_6] = 16 + 5;
  t[SDL_SCANCODE_KP_2] = 16 + 6;
  t[SDL_SCANCODE_KP_4] = 16 + 7;

  t[SDL_SCANCODE_F] = 16 + pcFire;
  t[SDL_SCANCODE_G] = 16 + pcFace;
  t[SDL_SCANCODE_H] = 16 + pcFaceFire;
  t[SDL_SCANCODE_R] = 16 + pcDrop;
  t[SDL_SCANCODE_T] = 16 + pcOrbPower;
  t[SDL_SCANCODE_Y] = 16 + pcCenter;

  t[SDL_SCANCODE_I] = 32 + 4;
  t[SDL_SCANCODE_L] = 32 + 5;
  t[SDL_SCANCODE_K] = 32 + 6;
  t[SDL_SCANCODE_J] = 32 + 7;
  t[SDL_SCANCODE_SEMICOLON] = 32 + 8;
  t[SDL_SCANCODE_APOSTROPHE] = 32 + 9;
  t[SDL_SCANCODE_P] = 32 + 10;
  t[SDL_SCANCODE_LEFTBRACKET] = 32 + pcCenter;

  t[SDL_SCANCODE_UP] = 48 ;
  t[SDL_SCANCODE_RIGHT] = 48 + 1;
  t[SDL_SCANCODE_DOWN] = 48 + 2;
  t[SDL_SCANCODE_LEFT] = 48 + 3;
  t[SDL_SCANCODE_PAGEUP] = 48 + 4;
  t[SDL_SCANCODE_PAGEDOWN] = 48 + 5;
  t[SDL_SCANCODE_HOME] = 48 + 6;
  
  #else  
  t[(int)'w'] = 16 + 4;
  t[(int)'d'] = 16 + 5;
  t[(int)'s'] = 16 + 6;
  t[(int)'a'] = 16 + 7;

#if !ISMOBILE
  t[SDLK_KP8] = 16 + 4;
  t[SDLK_KP6] = 16 + 5;
  t[SDLK_KP2] = 16 + 6;
  t[SDLK_KP4] = 16 + 7;
#endif

  t[(int)'f'] = 16 + pcFire;
  t[(int)'g'] = 16 + pcFace;
  t[(int)'h'] = 16 + pcFaceFire;
  t[(int)'r'] = 16 + pcDrop;
  t[(int)'t'] = 16 + pcOrbPower;
  t[(int)'y'] = 16 + pcCenter;

  t[(int)'i'] = 32 + 4;
  t[(int)'l'] = 32 + 5;
  t[(int)'k'] = 32 + 6;
  t[(int)'j'] = 32 + 7;
  t[(int)';'] = 32 + 8;
  t[(int)'\''] = 32 + 9;
  t[(int)'p'] = 32 + 10;
  t[(int)'['] = 32 + pcCenter;

#if !ISMOBILE
  t[SDLK_UP] = 48 ;
  t[SDLK_RIGHT] = 48 + 1;
  t[SDLK_DOWN] = 48 + 2;
  t[SDLK_LEFT] = 48 + 3;
  t[SDLK_PAGEUP] = 48 + 4;
  t[SDLK_PAGEDOWN] = 48 + 5;
  t[SDLK_HOME] = 48 + 6;
#endif
  #endif

  scfg.joyaction[0][0] = 16 + pcFire;
  scfg.joyaction[0][1] = 16 + pcOrbPower;
  scfg.joyaction[0][2] = 16 + pcDrop;
  scfg.joyaction[0][3] = 16 + pcCenter;
  scfg.joyaction[0][4] = 16 + pcFace;
  scfg.joyaction[0][5] = 16 + pcFaceFire;

  scfg.joyaction[1][0] = 32 + pcFire;
  scfg.joyaction[1][1] = 32 + pcOrbPower;
  scfg.joyaction[1][2] = 32 + pcDrop;
  scfg.joyaction[1][3] = 32 + pcCenter;
  scfg.joyaction[1][4] = 32 + pcFace;
  scfg.joyaction[1][5] = 32 + pcFaceFire;

  scfg.axeaction[0][0] = 4;
  scfg.axeaction[0][1] = 5;
  scfg.axeaction[0][3] = 2;
  scfg.axeaction[0][4] = 3;

  scfg.axeaction[1][0] = 8;
  scfg.axeaction[1][1] = 9;
  
  // ULRD
  scfg.hataction[0][0][0] = 16 + 0;
  scfg.hataction[0][0][1] = 16 + 3;
  scfg.hataction[0][0][2] = 16 + 1;
  scfg.hataction[0][0][3] = 16 + 2;
  scfg.hataction[0][1][0] = 16 + 4;
  scfg.hataction[0][1][1] = 16 + 7;
  scfg.hataction[0][1][2] = 16 + 5;
  scfg.hataction[0][1][3] = 16 + 6;

  scfg.hataction[1][0][0] = 32 + 0;
  scfg.hataction[1][0][1] = 32 + 3;
  scfg.hataction[1][0][2] = 32 + 1;
  scfg.hataction[1][0][3] = 32 + 2;
  scfg.hataction[1][1][0] = 32 + 4;
  scfg.hataction[1][1][1] = 32 + 7;
  scfg.hataction[1][1][2] = 32 + 5;
  scfg.hataction[1][1][3] = 32 + 6;

  int charidtable[MAXPLAYER] = {0, 1, 4, 6, 2, 3, 0};
    
  for(int i=0; i<MAXPLAYER; i++) {
    initcs(multi::scs[i]); 
    multi::scs[i].charid = charidtable[i];
    }
  
  multi::scs[0].uicolor = 0xC00000FF;
  multi::scs[1].uicolor = 0x00C000FF;
  multi::scs[2].uicolor = 0x0000C0FF;
  multi::scs[3].uicolor = 0xC0C000FF;
  multi::scs[4].uicolor = 0xC000C0FF;
  multi::scs[5].uicolor = 0x00C0C0FF;
  multi::scs[6].uicolor = 0xC0C0C0FF;
  
  #if CAP_CONFIG
  addsaver(multi::players, "mode-number of players");
  param_b(multi::split_screen, "splitscreen", false)
    ->editable("split screen mode", 's');
  param_b(multi::pvp_mode, "pvp_mode", false)
    ->editable("player vs player", 'v');
  param_b(multi::friendly_fire, "friendly_fire", true)
    ->editable("friendly fire", 'f');
  param_b(multi::self_hits, "self_hits", false)
    ->editable("self hits", 'h');
  param_b(multi::two_focus, "two_focus", false)
    ->editable("auto-adjust dual-focus projections", 'f');
  addsaver(alwaysuse, "use configured keys");  
  // unfortunately we cannot use key names here because SDL is not yet initialized
  for(int i=0; i<512; i++)
    addsaver(scfg.keyaction[i], string("key:")+its(i));
  for(int i=0; i<MAXJOY; i++) {
    string pre = "joystick "+cts('A'+i);
    for(int j=0; j<MAXBUTTON; j++) 
      addsaver(scfg.joyaction[i][j], pre+"-B"+its(j));
    for(int j=0; j<MAXAXE; j++) {
      addsaver(scfg.axeaction[i][j], pre+" axis "+its(j));
      addsaver(scfg.deadzoneval[i][j], pre+" deadzone "+its(j));
      }
    for(int j=0; j<MAXHAT; j++) for(int k=0; k<4; k++) {
      addsaver(scfg.hataction[i][j][k], pre+" hat "+its(j)+" "+"URDL"[k]);
      }
    }
  for(int i=0; i<7; i++) addsaver(multi::scs[i], "player"+its(i));
  #endif
  }

EX void get_actions() {
  #if !ISMOBILE
  const Uint8 *keystate = SDL12_GetKeyState(NULL);

  for(int i=0; i<NUMACT; i++) 
    lactionpressed[i] = actionspressed[i],
    actionspressed[i] = 0;

  for(int i=0; i<SHMUPAXES; i++) axespressed[i] = 0;
  
  for(int i=0; i<KEYSTATES; i++) if(keystate[i]) 
    pressaction(scfg.keyaction[i]);

#if CAP_SDLJOY  
  for(int j=0; j<numsticks; j++) {

    for(int b=0; b<SDL_JoystickNumButtons(sticks[j]) && b<MAXBUTTON; b++)
      if(SDL_JoystickGetButton(sticks[j], b))
        pressaction(scfg.joyaction[j][b]);

    for(int b=0; b<SDL_JoystickNumHats(sticks[j]) && b<MAXHAT; b++) {
      int stat = SDL_JoystickGetHat(sticks[j], b);
      if(stat & SDL_HAT_UP) pressaction(scfg.hataction[j][b][0]);
      if(stat & SDL_HAT_RIGHT) pressaction(scfg.hataction[j][b][1]);
      if(stat & SDL_HAT_DOWN) pressaction(scfg.hataction[j][b][2]);
      if(stat & SDL_HAT_LEFT) pressaction(scfg.hataction[j][b][3]);
      }
    
    for(int b=0; b<SDL_JoystickNumAxes(sticks[j]) && b<MAXAXE; b++) {
      int value = SDL_JoystickGetAxis(sticks[j], b);
      int dz = scfg.deadzoneval[j][b];
      if(value > dz) value -= dz; else if(value < -dz) value += dz;
      else value = 0;
      axespressed[scfg.axeaction[j][b] % SHMUPAXES] += value;
      }
    }
#endif
#endif
  }

EX void handleInput(int delta) {
#if CAP_SDL
  double d = delta / 500.;

  get_actions();

  const Uint8 *keystate = SDL12_GetKeyState(NULL);
  if(keystate[SDLK_LCTRL] || keystate[SDLK_RCTRL]) d /= 5;
  
  double panx = 
    actionspressed[49] - actionspressed[51] + axespressed[2] / 32000.0;
  double pany = 
    actionspressed[50] - actionspressed[48] + axespressed[3] / 32000.0;
    
  double panspin = actionspressed[52] - actionspressed[53];
  
  double panmove = actionspressed[59] - actionspressed[60];
  
  if(GDIM == 3)
    panmove += axespressed[1] / 32000.0;
  else
    panspin += axespressed[1] / 32000.0;
  
  if(actionspressed[54]) { centerplayer = -1, playermoved = true; centerpc(100); }

  if(actionspressed[55] && !lactionpressed[55]) 
    get_o_key().second();
  
  if(actionspressed[56] && !lactionpressed[56]) 
    showMissionScreen();
  
#if CAP_INV
  if(actionspressed[57] && !lactionpressed[57] && inv::on) 
    pushScreen(inv::show);
#endif
  
  if(actionspressed[58] && !lactionpressed[58]) 
    pushScreen(showGameMenu);
    
  panx *= d;
  pany *= d;
  panspin *= d;
  panmove *= d;
  
  #if CAP_MOUSEGRAB
  if(lctrlclick) {
    panx += mouseaim_x / 2;
    pany += mouseaim_y / 2;
    mouseaim_x = mouseaim_y = 0;
    }
  #endif

  if(panx || pany || panspin || (GDIM == 3 && panmove)) {
    if(GDIM == 2) {
      View = xpush(-panx) * ypush(-pany) * spin(panspin) * View;
      playermoved = false;
      }
    else {
      View = cspin(0, 2, -panx) * cspin(1, 2, -pany) * spin(panspin) * cpush(2, panmove) * View;
      if(panmove) playermoved = false;
      }
    }
#endif
  }

  EX int tableid[7] = {1, 2, 4, 5, 6, 7, 8};

  EX void leaveGame(int i) {
    multi::player[i].at = NULL;
    multi::deaths[i]++;
    revive_queue.push_back(i);
    checklastmove();
    }

  EX bool playerActive(int p) {
    if(multi::players == 1 || shmup::on) return true;
    return player[p].at;
    }
  
  EX int activePlayers() {
    int q = 0;
    for(int i=0; i<players; i++) if(playerActive(i)) q++;
    return q;
    }
  
  EX cell *multiPlayerTarget(int i) {
    cellwalker cwti = multi::player[i];
    if(!cwti.at) return NULL;
    int dir = multi::whereto[i].d;
    if(dir == MD_UNDECIDED) return NULL;
    if(dir == MD_USE_ORB) return multi::whereto[i].tgt;
    if(dir >= 0) 
      cwti = cwti + dir + wstep;
    return cwti.at;
    }
  
  EX void checklastmove() {
    for(int i: player_indices()) {
      multi::cpid = i;
      cwt = multi::player[i]; break;
      }
    if(multi::activePlayers() == 1) {
      multi::checkonly = true;
      checkmove();
      multi::checkonly = false;
      }
    }

  bool needinput = true;
  
  EX void handleMulti(int delta) {
    multi::handleInput(delta);
    
    shiftmatrix bcwtV = cwtV;
    cellwalker bcwt = cwt;
    
    bool alldecided = !needinput;
    
    if(multi::players == 1) {
      multi::cpid = 0;
      multi::whereis[0] = cwtV;
      multi::player[0] = cwt;
      }
    
    for(int i: player_indices()) {
    
      using namespace multi;
      
  // todo refactor
  
      cpid = i;
      
      int b = 16*tableid[cpid];
      for(int ik=0; ik<8; ik++) if(actionspressed[b+ik]) playermoved = true;
      for(int ik=0; ik<16; ik++) if(actionspressed[b+ik] && !lactionpressed[b+ik]) 
        multi::combo[i] = false;
          
      bool anypressed = false;
      
      int jb = 4*tableid[cpid];
      for(int ik=0; ik<4; ik++) 
        if(axespressed[jb+ik]) 
          anypressed = true, playermoved = true, multi::combo[i] = false;
      
      double mdx = 
        (actionspressed[b+0] + actionspressed[b+2] - actionspressed[b+1] - actionspressed[b+3]) * .7 +
        actionspressed[b+pcMoveRight] - actionspressed[b+pcMoveLeft] + axespressed[jb]/30000.;
      double mdy = 
        (actionspressed[b+3] + actionspressed[b+2] - actionspressed[b+1] - actionspressed[b+0]) * .7 +
        actionspressed[b+pcMoveDown] - actionspressed[b+pcMoveUp] + axespressed[jb+1]/30000.;
      
      if((actionspressed[b+pcMoveRight] && actionspressed[b+pcMoveLeft]) ||
        (actionspressed[b+pcMoveUp] && actionspressed[b+pcMoveDown]))
          multi::mdx[i] = multi::mdy[i] = 0;
        
      multi::mdx[i] = multi::mdx[i] * (1 - delta / 1000.) + mdx * delta / 2000.;
      multi::mdy[i] = multi::mdy[i] * (1 - delta / 1000.) + mdy * delta / 2000.;
  
      if(WDIM == 2) {
        if(mdx != 0 || mdy != 0) if(!multi::combo[i]) {
          cwtV = multi::whereis[i]; cwt = multi::player[i];
          flipplayer = multi::flipped[i];
          multi::whereto[i] = vectodir(hpxy(multi::mdx[i], multi::mdy[i]));
          }
        }
      
      if(multi::actionspressed[b+pcFire] || 
        (multi::actionspressed[b+pcMoveLeft] && multi::actionspressed[b+pcMoveRight]))
        multi::combo[i] = true, multi::whereto[i].d = MD_WAIT;
  
      if(multi::actionspressed[b+pcFace])
        multi::whereto[i].d = MD_UNDECIDED;
      
      cwt.at = multi::player[i].at;      
      if(multi::ccat[i] && !multi::combo[i] && targetRangedOrb(multi::ccat[i], roMultiCheck)) {
        multi::whereto[i].d = MD_USE_ORB;
        multi::whereto[i].tgt = multi::ccat[i];
        }

      if(multi::actionspressed[b+pcFaceFire] && activePlayers() > 1) {
        addMessage(XLAT("Left the game."));
        multi::leaveGame(i);
        }
  
      if(actionspressed[b+pcDrop] || 
        (multi::actionspressed[b+pcMoveUp] && multi::actionspressed[b+pcMoveDown]))
        multi::combo[i] = true, multi::whereto[i].d = MD_DROP;
  
      if(actionspressed[b+pcCenter]) {
        centerplayer = cpid; centerpc(100); playermoved = true; 
        }
  
      if(multi::whereto[i].d == MD_UNDECIDED) alldecided = false;
      
      for(int ik=0; ik<16; ik++) if(actionspressed[b+ik]) anypressed = true;

      if(anypressed) alldecided = false, needinput = false;
      else multi::mdx[i] = multi::mdy[i] = 0;
      }
      
    cwtV = bcwtV;
    cwt = bcwt;
    
    if(alldecided) {
      flashMessages();
      // check for crashes
      needinput = true;

      for(int i: player_indices()) {
        origpos[i] = player[i].at;
        origtarget[i] = multiPlayerTarget(i);
        }
  
      for(int i: player_indices())
      for(int j: player_indices()) if(i != j) {
        if(origtarget[i] == origtarget[j]) {
          addMessage("Two players cannot move/attack the same location!");
          return;
          }
/*      if(multiPlayerTarget(i) == multi::player[j].at) {
          addMessage("Cannot move into the current location of another player!");
          return;
          }
        if(celldistance(multiPlayerTarget(i), multiPlayerTarget(j)) > 8) {
          addMessage("Players cannot get that far away!");
          return;
          } */
        }
  
      if(multi::players == 1) {
        if(movepcto(multi::whereto[0]))
          multi::whereto[0].d = MD_UNDECIDED;
        return;
        }
      
      multi::cpid = 0;
      if(multimove()) {      
        multi::aftermove = false;
        if(shmup::delayed_safety) {
          activateSafety(shmup::delayed_safety_land);
          shmup::delayed_safety = false;
          checklastmove();
          }
        else {          
          monstersTurn();
          checklastmove();
          }
        }
      }
    }
  
  EX void mousemovement(cell *c) {
    if(!c) return;
    int countplayers = 0;
    int countplayers_undecided = 0;
    for(int i=0; i<multi::players; i++)
      if(multi::playerActive(i) && (playerpos(i) == c || isNeighbor(c, playerpos(i)))) {
        countplayers++;
        if(multi::whereto[i].d == MD_UNDECIDED) countplayers_undecided++;
        }
  
    for(int i=0; i<multi::players; i++)
      if(multi::playerActive(i) && (playerpos(i) == c || isNeighbor(c, playerpos(i)))) {
        int& cdir = multi::whereto[i].d;
        int scdir = cdir;
        bool isUndecided = cdir == MD_UNDECIDED;
        if(countplayers_undecided > 0 && ! isUndecided) continue;
        if(playerpos(i) == c)
          multi::whereto[i].d = MD_WAIT;
        else {
          for(int d=0; d<playerpos(i)->type; d++) {
            cdir = d;
            if(multi::multiPlayerTarget(i) == c) break;
            cdir = scdir;
            cwt = multi::player[i];
            calcMousedest();
            auto& sd = multi::whereto[i].subdir;
            sd = mousedest.subdir;
            if(sd == 0) sd = 1;
            }
          }
        }
    
    needinput = 
      ((countplayers == 2 && !countplayers_undecided) || countplayers_undecided >= 2);
    }
  
  EX }

}