// Hyperbolic Rogue -- configuration

// Copyright (C) 2017-2018 Zeno Rogue, see 'hyper.cpp' for details

namespace hr {

ld bounded_mine_percentage = 0.1;
int bounded_mine_quantity, bounded_mine_max;

const char *conffile = "hyperrogue.ini";

array<ld, gGUARD> sightranges;

videopar vid;

#define DEFAULT_WALLMODE (ISMOBILE ? 3 : 5)
#define DEFAULT_MONMODE  (ISMOBILE ? 2 : 4)

#if ISANDROID
#define ANDROID_SETTINGS settingsChanged = true;
#else
#define ANDROID_SETTINGS ;
#endif

extern color_t floorcolors[landtypes];

charstyle& getcs(int id) {
  if(multi::players>1 && id >= 0 && id < multi::players)
    return multi::scs[id];
  else
    return vid.cs;
  }

struct charstyle_old {
  int charid;
  color_t skincolor, haircolor, dresscolor, swordcolor, dresscolor2, uicolor;
  bool lefthanded;
  };

void hread(hstream& hs, charstyle& cs) {
  charstyle_old cso;
  hread_raw(hs, cso);
  cs.charid = cso.charid;
  cs.skincolor = cso.skincolor;
  cs.haircolor = cso.haircolor;
  cs.dresscolor = cso.dresscolor;
  cs.swordcolor = cs.eyecolor = cso.swordcolor;
  if(cs.charid < 4) cs.eyecolor = 0;
  cs.dresscolor2 = cso.dresscolor2;
  cs.uicolor = cso.uicolor;
  cs.lefthanded = cso.lefthanded;
  }

void hwrite(hstream& hs, const charstyle& cs) {
  charstyle_old cso;
  cso.charid = cs.charid;
  cso.skincolor = cs.skincolor;
  cso.haircolor = cs.haircolor;
  cso.dresscolor = cs.dresscolor;
  cso.swordcolor = cs.charid >= 4 ? cs.eyecolor : cs.swordcolor;
  cso.dresscolor2 = cs.dresscolor2;
  cso.uicolor = cs.uicolor;
  cso.lefthanded = cs.lefthanded;
  hwrite_raw(hs, cso);
  }

// void hread(hstream& hs, charstyle& cs) { hread_raw(hs, cs); }
// void hwrite(hstream& hs, const charstyle& cs) { hwrite_raw(hs, cs); }

string csnameid(int id) {
  if(id == 0) return XLAT("male");
  if(id == 1) return XLAT("female");
  if(id == 2) return XLAT("Prince");
  if(id == 3) return XLAT("Princess");
  if(id == 4 || id == 5) return XLAT("cat");
  if(id == 6 || id == 7) return XLAT("dog");
  if(id == 8 || id == 9) return XLATN("Familiar");
  return XLAT("none");
  }

string csname(charstyle& cs) {
  return csnameid(cs.charid);
  }

int playergender() {
  return (getcs().charid >= 0 && (getcs().charid&1)) ? GEN_F : GEN_M; 
  }
int princessgender() {
  int g = playergender();
  if(vid.samegender) return g;
  return g == GEN_M ? GEN_F : GEN_M;
  }

int default_language;

int lang() { 
  if(vid.language >= 0)
    return vid.language; 
  return default_language;
  }

bool autojoy = true;

#if CAP_CONFIG
saverlist savers;
#endif

#if !CAP_CONFIG
template<class T, class U, class V> void addsaver(T& i, U name, V dft) {
  i = dft;
  }

template<class T, class U> void addsaver(T& i, U name) {}
template<class T, class U> void addsaverenum(T& i, U name) {}
template<class T, class U> void addsaverenum(T& i, U name, T dft) {}
#endif

void addsaver(charstyle& cs, string s) {
  addsaver(cs.charid, s + ".charid");
  addsaver(cs.skincolor, s + ".skincolor");
  addsaver(cs.haircolor, s + ".haircolor");
  addsaver(cs.dresscolor, s + ".dresscolor");
  addsaver(cs.swordcolor, s + ".swordcolor");
  addsaver(cs.dresscolor2, s + ".dresscolor2");
  addsaver(cs.uicolor, s + ".uicolor");
  addsaver(cs.lefthanded, s + ".lefthanded");
  }
  
// R:239, G:208, B:207 

unsigned int skincolors[]  = { 7, 0xD0D0D0FF, 0xEFD0C9FF, 0xC77A58FF, 0xA58869FF, 0x602010FF, 0xFFDCB1FF, 0xEDE4C8FF };
unsigned int haircolors[]  = { 8, 0x686868FF, 0x8C684AFF, 0xF2E1AEFF, 0xB55239FF, 0xFFFFFFFF, 0x804000FF, 0x502810FF, 0x301800FF };
unsigned int dresscolors[] = { 6, 0xC00000FF, 0x00C000FF, 0x0000C0FF, 0xC0C000FF, 0xC0C0C0FF, 0x202020FF };
unsigned int dresscolors2[] = { 7, 0x8080FFC0, 0x80FF80C0, 0xFF8080C0, 0xFFFF80C0, 0xFF80FFC0, 0x80FFFFC0, 0xFFFFFF80 };
unsigned int swordcolors[] = { 6, 0xC0C0C0FF, 0xFFFFFFFF, 0xFFC0C0FF, 0xC0C0FFFF, 0x808080FF, 0x202020FF };
unsigned int eyecolors[] = { 4, 0x00C000FF, 0x0000C0FF, 0xC00000FF, 0xC0C000FF, 0x804010FF, 0x00C000FF };

void initcs(charstyle &cs) {
  cs.charid     = 0;
  cs.skincolor  = 0xD0D0D0FF;
  cs.haircolor  = 0x686868FF;
  cs.dresscolor = 0xC00000FF;
  cs.swordcolor = 0xD0D0D0FF;
  cs.dresscolor2= 0x8080FFC0;
  cs.uicolor    = 0xFF0000FF;
  cs.eyecolor   = 0x603000FF;
  cs.lefthanded = false;
  }

void savecolortable(colortable& ct, string name) {
  for(int i=0; i<isize(ct); i++)
    addsaver(ct[i], "color:" + name + ":" + its(i));
  }

void initConfig() {
  
  // basic config
  addsaver(vid.flashtime, "flashtime", 8);
  addsaver(vid.msgleft, "message style", 2);
  addsaver(vid.msglimit, "message limit", 5);
  addsaver(vid.timeformat, "message log time format", 0);
  addsaver(fontscale, "fontscale", 100);

  addsaver(vid.mobilecompasssize, "mobile compass size", 0); // ISMOBILE || ISPANDORA ? 30 : 0);
  addsaver(vid.radarsize, "radarsize size", 120);
  addsaver(vid.axes, "movement help", 1);
  addsaver(vid.shifttarget, "shift-targetting", 2);
  addsaver(vid.steamscore, "scores to Steam", 1);
  initcs(vid.cs); addsaver(vid.cs, "single");
  addsaver(vid.samegender, "princess choice", false);
  addsaver(vid.language, "language", -1);  
  addsaver(vid.drawmousecircle, "mouse circle", ISMOBILE || ISPANDORA);
  addsaver(vid.revcontrol, "reverse control", false);
  addsaver(musicvolume, "music volume");
  addsaver(effvolume, "sound effect volume");
  addsaverenum(glyphsortorder, "glyph sort order");
  
  // basic graphics
  
  addsaver(vid.usingGL, "usingGL", true);
  addsaver(vid.antialias, "antialias", AA_NOGL | AA_FONT | (ISWEB ? AA_MULTI : AA_LINES) | AA_LINEWIDTH | AA_VERSION);
  addsaver(vid.linewidth, "linewidth", 1);
  addsaver(linepatterns::width, "pattern-linewidth", 1);
  addsaver(vid.scale, "scale", 1);
  addsaver(vid.xposition, "xposition", 0);
  addsaver(vid.yposition, "yposition", 0);
  addsaver(vid.alpha, "projection", 1);
  addsaver(vid.sspeed, "scrollingspeed", 0);
  addsaver(vid.mspeed, "movement speed", 1);
  addsaver(vid.full, "fullscreen", false);
  addsaver(vid.aurastr, "aura strength", ISMOBILE ? 0 : 128);
  addsaver(vid.aurasmoothen, "aura smoothen", 5);
  addsaver(vid.graphglyph, "graphical items/kills", 1);
  addsaver(vid.particles, "extra effects", 1);
  addsaver(vid.framelimit, "frame limit", 75);
  addsaver(vid.xres, "xres");
  addsaver(vid.yres, "yres");
  addsaver(vid.fsize, "font size");
  addsaver(vid.darkhepta, "mark heptagons", false);
  
  // special graphics

  addsaver(vid.ballangle, "ball angle", 20);
  addsaver(vid.yshift, "Y shift", 0);
  addsaver(vid.use_wall_radar, "wallradar", true);
  addsaver(vid.fixed_facing, "fixed facing", 0);
  addsaver(vid.fixed_yz, "fixed YZ", true);
  addsaver(vid.camera_angle, "camera angle", 0);
  addsaver(vid.ballproj, "ballproj", 1);
  addsaver(vid.monmode, "monster display mode", DEFAULT_MONMODE);
  addsaver(vid.wallmode, "wall display mode", DEFAULT_WALLMODE);

  addsaver(geom3::depth, "3D depth");
  addsaver(geom3::camera, "3D camera level");
  addsaver(geom3::wall_height, "3D wall height");
  addsaver(geom3::rock_wall_ratio, "3D rock-wall ratio");
  addsaver(geom3::human_wall_ratio, "3D human-wall ratio");
  addsaver(geom3::lake_top, "3D lake top");
  addsaver(geom3::lake_bottom, "3D lake bottom");
  addsaver(geom3::tc_depth, "3D TC depth");
  addsaver(geom3::tc_camera, "3D TC camera");
  addsaver(geom3::tc_alpha, "3D TC alpha");
  addsaver(geom3::highdetail, "3D highdetail");
  addsaver(geom3::middetail, "3D middetail");
  addsaver(geom3::gp_autoscale_heights, "3D Goldberg autoscaling");
  
  addsaver(memory_saving_mode, "memory_saving_mode", (ISMOBILE || ISPANDORA || ISWEB) ? 1 : 0);

  addsaver(rug::renderonce, "rug-renderonce");
  addsaver(rug::rendernogl, "rug-rendernogl");
  addsaver(rug::texturesize, "rug-texturesize");
#if CAP_RUG
  addsaver(rug::model_distance, "rug-model-distance");
#endif

  addsaverenum(pmodel, "used model");
  addsaver(polygonal::SI, "polygon sides");
  addsaver(polygonal::STAR, "polygon star factor");
  addsaver(polygonal::deg, "polygonal degree");
  addsaver(conformal::autobandhistory, "include history"); // check!
  addsaver(conformal::lvspeed, "lineview speed");
  addsaver(conformal::extra_line_steps, "lineview extension");
  
  addsaver(polygonal::maxcoef, "polynomial degree");
  for(int i=0; i<polygonal::MSI; i++) {
    addsaver(polygonal::coefr[i], "polynomial "+its(i)+".real");
    addsaver(polygonal::coefi[i], "polynomial "+its(i)+".imag");
    }
  
  addsaver(conformal::bandhalf, "band width");
  addsaver(conformal::bandsegment, "band segment");
  addsaver(conformal::rotation, "conformal rotation");
  addsaver(conformal::rotation_xz, "conformal rotation_xz");
  addsaver(conformal::do_rotate, "conformal rotation mode", 1);
  addsaver(conformal::model_orientation, "model orientation", 0);
  addsaver(conformal::model_orientation_yz, "model orientation-yz", 0);
  addsaver(conformal::top_z, "topz", 5);
  addsaver(conformal::model_transition, "model transition", 1);
  addsaver(conformal::halfplane_scale, "halfplane scale", 1);
  addsaver(conformal::autoband, "automatic band");
  addsaver(conformal::autobandhistory, "automatic band history");
  addsaver(conformal::dospiral, "do spiral");
  addsaver(conformal::clip_min, "clip-min", -1);
  addsaver(conformal::clip_max, "clip-max", +1);
  
  addsaver(vid.backeffects, "background particle effects", (ISMOBILE || ISPANDORA) ? false : true);
  // control
  
  addsaver(vid.joyvalue, "vid.joyvalue", 4800);
  addsaver(vid.joyvalue2, "vid.joyvalue2", 5600);
  addsaver(vid.joypanthreshold, "vid.joypanthreshold", 2500);
  addsaver(vid.joypanspeed, "vid.joypanspeed", ISPANDORA ? 0.0001 : 0);
  addsaver(autojoy, "autojoy");
    
  vid.killreduction = 0;
  
  addsaver(vid.skipstart, "skip the start menu", false);
  addsaver(vid.quickmouse, "quick mouse", !ISPANDORA);
  
  // colors
  
  addsaver(backcolor, "color:background");
  addsaver(forecolor, "color:foreground");
  addsaver(bordcolor, "color:borders");
  addsaver(ringcolor, "color:ring");
  addsaver(modelcolor, "color:model");
  addsaver(periodcolor, "color:period");
  addsaver(stdgridcolor, "color:stdgrid"); 
  addsaver(dialog::dialogcolor, "color:dialog");
  for(auto& p: colortables)
    savecolortable(p.second, s0+"canvas"+p.first);
  savecolortable(distcolors, "distance");
  savecolortable(minecolors, "mines");
  #if CAP_COMPLEX2
  savecolortable(brownian::colors, "color:brown");
  #endif
  
  for(int i=0; i<motypes; i++)
    addsaver(minf[i].color, "color:monster:" + its(i));
  for(int i=0; i<ittypes; i++)
    addsaver(iinf[i].color, "color:item:" + its(i));
  for(int i=0; i<landtypes; i++)
    addsaver(floorcolors[i], "color:land:" + its(i));
  for(int i=0; i<walltypes; i++)
    addsaver(winf[i].color, "color:wall:" + its(i));

  // modes
    
  addsaverenum(geometry, "mode-geometry");
  addsaver(shmup::on, "mode-shmup", false);
  addsaver(hardcore, "mode-hardcore", false);
  addsaver(chaosmode, "mode-chaos");
  addsaver(inv::on, "mode-Orb Strategy");
  addsaverenum(variation, "mode-variation", eVariation::bitruncated);
  addsaver(peace::on, "mode-peace");
  addsaver(peace::otherpuzzles, "mode-peace-submode");
  addsaverenum(specialland, "land for special modes");
  
  addsaver(viewdists, "expansion mode");
  addsaver(backbrightness, "brightness behind sphere");

  addsaver(vid.ipd, "interpupilar-distance", 0.05);
  addsaver(vid.lr_eyewidth, "eyewidth-lr", 0.5);
  addsaver(vid.anaglyph_eyewidth, "eyewidth-anaglyph", 0.1);
  addsaver(vid.fov, "field-of-vision", 90);
  addsaver(vid.desaturate, "desaturate", 0);
  addsaverenum(vid.stereo_mode, "stereo-mode");
  addsaver(vid.euclid_to_sphere, "euclid to sphere projection", 1.5);
  addsaver(vid.twopoint_param, "twopoint parameter", 1);
  addsaver(vid.stretch, "stretch", 1);
  addsaver(vid.binary_width, "binary-tiling-width", 1);

  addsaver(geom3::creature_scale, "3d-creaturescale", 1);
  addsaver(geom3::height_width, "3d-heightwidth", 1.5);

  #if CAP_GP
  addsaver(gp::param.first, "goldberg-x", gp::param.first);
  addsaver(gp::param.second, "goldberg-y", gp::param.second);
  #endif
  
  addsaver(nohud, "no-hud", false);
  addsaver(nofps, "no-fps", false);
  
  #if CAP_IRR
  addsaver(irr::density, "irregular-density", 2);
  addsaver(irr::cellcount, "irregular-cellcount", 150);
  addsaver(irr::quality, "irregular-quality", .2);
  addsaver(irr::place_attempts, "irregular-place", 10);
  addsaver(irr::rearrange_max_attempts, "irregular-rearrange-max", 50);
  addsaver(irr::rearrange_less, "irregular-rearrangeless", 10);
  #endif
  
  addsaver(vid.linequality, "line quality", 0);
  
  #if CAP_FILES && CAP_SHOT && CAP_ANIMATIONS
  addsaver(anims::animfile, "animation file format");
  #endif
  #if CAP_ANIMATIONS
  addsaver(anims::period, "animation period");
  addsaver(anims::noframes, "animation frames");
  addsaver(anims::cycle_length, "animation cycle length");
  addsaver(anims::parabolic_length, "animation parabolic length");
  addsaver(anims::rug_angle, "animation rug angle");
  addsaver(anims::circle_radius, "animation circle radius");
  addsaver(anims::circle_spins, "animation circle spins");
  #endif
  
  #if CAP_CRYSTAL
  addsaver(crystal::compass_probability, "compass-probability");
  addsaver(crystal::view_coordinates, "crystal-coordinates");
  #endif
  
  #if CAP_SHOT
  addsaver(shot::shotx, "shotx");
  addsaver(shot::shoty, "shoty");
  addsaver(shot::make_svg, "shotsvg");
  addsaver(shot::transparent, "shottransparent");
  addsaver(shot::gamma, "shotgamma");
  addsaver(shot::caption, "shotcaption");
  addsaver(shot::fade, "shotfade");
  #endif

#if CAP_TEXTURE  
  addsaver(texture::texture_aura, "texture-aura", false);
#endif

  addsaver(vid.use_smart_range, "smart-range", 0);
  addsaver(vid.smart_range_detail, "smart-range-detail", 8);
  addsaver(vid.smart_range_detail_3, "smart-range-detail", 30);
  addsaver(vid.cells_drawn_limit, "limit on cells drawn", 10000);
  addsaver(vid.cells_generated_limit, "limit on cells generated", 25);
  
  addsaver(vid.skiprope, "mobius", 0);
  
  addsaver(conformal::formula, "formula");
  addsaverenum(conformal::basic_model, "basic model");
  addsaver(conformal::use_atan, "use_atan");
  
  for(int i=0; i<isize(ginf); i++) {
    if(ginf[i].flags & qELLIPTIC)
      sightranges[i] = M_PI;
    else if(ginf[i].cclass == gcSphere)
      sightranges[i] = 2 * M_PI;
    else if(ginf[i].cclass == gcEuclid)
      sightranges[i] = 10;
    else
      sightranges[i] = 5;
    sightranges[gArchimedean] = 10;
    if(i < gBinary3) addsaver(sightranges[i], "sight-g" + its(i));
    }
  
  ld bonus = 0;
  ld emul = 1;

  addsaver(sightranges[gBinary3], "sight-binary3", 3.1 + bonus);
  addsaver(sightranges[gCubeTiling], "sight-cubes", 10);
  addsaver(sightranges[gCell120], "sight-120cell", 2 * M_PI);
  addsaver(sightranges[gECell120], "sight-120cell-elliptic", M_PI);
  addsaver(sightranges[gRhombic3], "sight-rhombic", 10.5 * emul);
  addsaver(sightranges[gBitrunc3], "sight-bitrunc", 12 * emul);
  addsaver(sightranges[gSpace534], "sight-534", 4 + bonus);
  addsaver(sightranges[gSpace435], "sight-435", 3.8 + bonus);

  addsaver(sightranges[gCell5], "sight-5cell", 2 * M_PI);
  addsaver(sightranges[gCell8], "sight-8cell", 2 * M_PI);
  addsaver(sightranges[gECell8], "sight-8cell-elliptic", M_PI);
  addsaver(sightranges[gCell16], "sight-16cell", 2 * M_PI);
  addsaver(sightranges[gECell16], "sight-16cell-elliptic", M_PI);
  addsaver(sightranges[gCell24], "sight-24cell", 2 * M_PI);
  addsaver(sightranges[gECell24], "sight-24cell-elliptic", M_PI);
  addsaver(sightranges[gCell600], "sight-600cell", 2 * M_PI);
  addsaver(sightranges[gECell600], "sight-600cell-elliptic", M_PI);
  addsaver(sightranges[gHoroTris], "sight-horotris", 2.9 + bonus);
  addsaver(sightranges[gHoroRec], "sight-hororec", 2.2 + bonus);
  addsaver(sightranges[gHoroHex], "sight-horohex", 2.75 + bonus);
  
  addsaver(sightranges[gField435], "sight-field435", 4 + bonus);
  addsaver(sightranges[gField534], "sight-field534", 3.8 + bonus);
  addsaver(vid.sloppy_3d, "sloppy3d", true);
  addsaver(vid.texture_step, "wall-quality", 1);
  
  addsaver(smooth_scrolling, "smooth-scrolling", false);
  addsaver(mouseaim_sensitivity, "mouseaim_sensitivity", 0.01);

  addsaver(vid.consider_shader_projection, "shader-projection", true);
  
#if CAP_RACING
  addsaver(racing::race_advance, "race_advance");
  addsaver(racing::race_angle, "race_angle");
  addsaver(racing::ghosts_to_show, "race_ghosts_to_show");
  addsaver(racing::ghosts_to_save, "race_ghosts_to_save");
  addsaver(racing::guiding, "race_guiding");
  addsaver(racing::player_relative, "race_player_relative");
  addsaver(racing::standard_centering, "race_standard_centering");
#endif

  addsaver(bounded_mine_percentage, "bounded_mine_percentage");

#if CAP_SHMUP  
  shmup::initConfig();
#endif

#if CAP_CONFIG
  for(auto s: savers) s->reset();
#endif
  }

bool inSpecialMode() {
  return chaosmode || !BITRUNCATED || peace::on || 
  #if CAP_TOUR
    tour::on ||
  #endif
    yendor::on || tactic::on || randomPatternsMode ||
    geometry != gNormal || pmodel != mdDisk || vid.alpha != 1 || vid.scale != 1 || 
    rug::rugged || vid.monmode != DEFAULT_MONMODE ||
    vid.wallmode != DEFAULT_WALLMODE;
  }

bool have_current_settings() {
  int modecount = 0;
  if(inv::on) modecount++;
  if(shmup::on) modecount += 10;
#if CAP_TOUR
  if(tour::on) modecount += 10;
#endif
  if(chaosmode) modecount += 10;
  if(!BITRUNCATED) modecount += 10;
  if(peace::on) modecount += 10;
  if(yendor::on) modecount += 10;
  if(tactic::on) modecount += 10;
  if(randomPatternsMode) modecount += 10;
  if(geometry != gNormal) modecount += 10;

  if(modecount > 1)
    return true;
  
  return false;
  }

bool have_current_graph_settings() {
  if(vid.xposition || vid.yposition || vid.alpha != 1 || vid.scale != 1)
    return true;
  if(pmodel != mdDisk || vid.monmode != DEFAULT_MONMODE || vid.wallmode != DEFAULT_WALLMODE)
    return true;
  if(firstland != laIce || multi::players != 1 || rug::rugged)
    return true;
  
  return false;
  }

void reset_graph_settings() {
  pmodel = mdDisk; vid.alpha = 1; vid.scale = 1;
  vid.xposition = vid.yposition = 0;
  #if CAP_RUG
  if(rug::rugged) rug::close();
  #endif

  vid.monmode = DEFAULT_MONMODE;
  vid.wallmode = DEFAULT_WALLMODE;
  }

void resetModes(char leave) {
  popAllGames();
  if(shmup::on != (leave == rg::shmup)) stop_game_and_switch_mode(rg::shmup);
  if(inv::on != (leave == rg::inv)) stop_game_and_switch_mode(rg::inv);
  if(chaosmode != (leave == rg::chaos)) stop_game_and_switch_mode(rg::chaos);

  if(peace::on != (leave == rg::peace)) stop_game_and_switch_mode(rg::peace);
#if CAP_TOUR
  if(tour::on != (leave == rg::tour)) stop_game_and_switch_mode(rg::tour);
#endif
  if(yendor::on != (leave == rg::yendor)) stop_game_and_switch_mode(rg::yendor);
  if(tactic::on != (leave == rg::tactic)) stop_game_and_switch_mode(rg::tactic);
  if(randomPatternsMode != (leave == rg::randpattern)) stop_game_and_switch_mode(rg::randpattern);
  if(multi::players != 1) {
    stop_game_and_switch_mode(); multi::players = 1;
    }
  if(firstland != laIce || specialland != laIce) {
    stop_game();
    firstland = laIce; specialland = laIce; stop_game_and_switch_mode();
    }

  set_geometry(gNormal);
  set_variation(eVariation::bitruncated);
  
  start_game();
  }

#if CAP_CONFIG  
void resetConfig() {
  dynamicval<int> rx(vid.xres, 0);
  dynamicval<int> ry(vid.yres, 0);
  dynamicval<int> rf(vid.fsize, 0);
  dynamicval<bool> rfs(vid.full, false);
  for(auto s: savers) 
    if(s->name.substr(0,5) != "mode-")
      s->reset();
  }
#endif

#if CAP_CONFIG
void saveConfig() {
  DEBB(DF_INIT, ("save config\n"));
  FILE *f = fopen(conffile, "wt");
  if(!f) {
    addMessage(s0 + "Could not open the config file: " + conffile);
    return;
    }
  
  {
  int pt_depth = 0, pt_camera = 0, pt_alpha = 0;
  using namespace geom3;
  if(tc_depth > tc_camera) pt_depth++;
  if(tc_depth < tc_camera) pt_camera++;
  if(tc_depth > tc_alpha ) pt_depth++;
  if(tc_depth < tc_alpha ) pt_alpha ++;
  if(tc_alpha > tc_camera) pt_alpha++;
  if(tc_alpha < tc_camera) pt_camera++;
  tc_alpha = pt_alpha;
  tc_camera = pt_camera;
  tc_depth = pt_depth;
  }
  
  for(auto s: savers) if(s->dosave())
    fprintf(f, "%s=%s\n", s->name.c_str(), s->save().c_str());
  
  fclose(f);
#if ISMOBILE==0
  addMessage(s0 + "Configuration saved to: " + conffile);
#else
  addMessage(s0 + "Configuration saved");
#endif
  }

void readf(FILE *f, ld& x) {
  double fl = x; 
  hr::ignore(fscanf(f, "%lf", &fl));
  x = fl;
  }

map<string, shared_ptr<supersaver> > allconfigs;

void parseline(const string& str) {
  if(str[0] == '#') return;
  for(int i=0; i<isize(str); i++) if(str[i] == '=') {
    string cname = str.substr(0, i);
    if(!allconfigs.count(cname)) {
      printf("Warning: unknown config variable: %s\n", str.c_str());
      return;
      }
    auto sav = allconfigs[cname];
    sav->load(str.substr(i+1));
    return;
    }
  printf("Warning: config line without equality sign: %s\n", str.c_str());
  }

void loadNewConfig(FILE *f) {
  for(auto& c: savers) allconfigs[c->name] = c;
  string rd;
  while(true) {
    int c = fgetc(f);
    if(c == -1) break;
    if(c == 10 || c == 13) {
      if(rd != "") parseline(rd);
      rd = "";
      }
    else rd += c;
    }
  allconfigs.clear();
  }

void loadConfig() {
 
  DEBB(DF_INIT, ("load config"));
  vid.xres = 9999; vid.yres = 9999; vid.framelimit = 300;
  FILE *f = fopen(conffile, "rt");
  if(f) {
    int err;
    int fs;
    err=fscanf(f, "%d%d%d%d", &vid.xres, &vid.yres, &fs, &vid.fsize);
    if(!err) 
      loadNewConfig(f);
    else {
      vid.full = fs;
      #if CAP_LEGACY
      loadOldConfig(f);
      #endif
      }
  
    fclose(f);
    DEBB(DF_INIT, ("Loaded configuration: %s\n", conffile));
    }

  polygonal::solve();
  precalc();
  }
#endif

void add_cells_drawn(char c = 'C') {
  dialog::addSelItem(XLAT("cells drawn"), its(cells_drawn), c);
  dialog::add_action([] () { 
    popScreen();
    dialog::editNumber(vid.cells_drawn_limit, 100, 1000000, log(10), 10000, XLAT("limit on cells drawn"), 
      XLAT("This limit exists to protect the engine from freezing when too many cells would be drawn according to the current options.")
      );
    dialog::scaleLog();
    });
  if(WDIM == 3 || vid.use_smart_range == 2) {
    dialog::addSelItem(XLAT("limit generation per frame"), its(vid.cells_generated_limit), 'L');
    dialog::add_action([] () { 
      dialog::editNumber(vid.cells_generated_limit, 1, 1000, log(10), 25, XLAT("limit generation per frame"), 
        XLAT("In the 3D mode, lowering this value may help if the game lags while exploring new areas.")
        );
      });
    }
  }

void edit_sightrange() {
  if(vid.use_smart_range) {
    ld& det = WDIM == 2 ? vid.smart_range_detail : vid.smart_range_detail_3;
    dialog::editNumber(det, 1, 50, 1, WDIM == 2 ? 8 : 30, XLAT("minimum visible cell in pixels"), "");
    }
  else if(WDIM == 3) {
    dialog::editNumber(sightranges[geometry], 0, 2 * M_PI, 0.5, M_PI, XLAT("3D sight range"),
      XLAT(
        "Sight range for 3D geometries is specified in the absolute units. This value also affects the fog effect.\n\n"
        "In spherical geometries, the sight range of 2? will let you see things behind you as if they were in front of you, "
        "and the sight range of ? (or more) will let you see things on the antipodal point just as if they were close to you.\n\n"
        "In hyperbolic geometries, the number of cells to render depends exponentially on the sight range. More cells to drawn "
        "reduces the performance.\n\n"
        "Sight range affects the gameplay, and monsters act iff they are visible. Monster generation takes this into account."
        )
      );
    }
  else {
    dialog::editNumber(sightrange_bonus, -5, allowIncreasedSight() ? 3 : 0, 1, 0, XLAT("sight range"), 
      XLAT("Roughly 42% cells are on the edge of your sight range. Reducing "
      "the sight range makes HyperRogue work faster, but also makes "
      "the game effectively harder."));
    dialog::reaction = doOvergenerate;
    dialog::bound_low(1-getDistLimit());
    dialog::bound_up(allowIncreasedSight() ? gp::dist_2() * 5 : 0);
    }
  dialog::extra_options = [] () {
    dialog::addBoolItem(XLAT("draw range based on distance"), vid.use_smart_range == 0, 'D');
    dialog::add_action([] () { vid.use_smart_range = 0; popScreen(); edit_sightrange(); });
    if(WDIM == 2 && allowIncreasedSight()) {
      dialog::addBoolItem(XLAT("draw based on size in the projection (no generation)"), vid.use_smart_range == 1, 'N');
      dialog::add_action([] () { vid.use_smart_range = 1; popScreen(); edit_sightrange(); });
      }
    if(allowChangeRange() && allowIncreasedSight()) {
      dialog::addBoolItem(XLAT("draw based on size in the projection (generation)"), vid.use_smart_range == 2, 'G');
      dialog::add_action([] () { vid.use_smart_range = 2; popScreen(); edit_sightrange(); });
      }
    if(vid.use_smart_range == 0 && allowChangeRange() && WDIM == 2) {
      dialog::addSelItem(XLAT("generation range bonus"), its(genrange_bonus), 'O');
      dialog::add_action([] () { genrange_bonus = sightrange_bonus; doOvergenerate(); });
      dialog::addSelItem(XLAT("game range bonus"), its(gamerange_bonus), 'S');
      dialog::add_action([] () { gamerange_bonus = sightrange_bonus; doOvergenerate(); });
      }      
    if(!allowChangeRange() || !allowIncreasedSight()) {
      dialog::addItem(XLAT("enable the cheat mode for additional options"), 'X');
      dialog::add_action(enable_cheat);
      }
    if(WDIM == 3 && !vid.use_smart_range) {
      dialog::addBoolItem_action(XLAT("sloppy range checking"), vid.sloppy_3d, 'S');
      }
    if(DIM == 3 && !vid.use_smart_range) {
      dialog::addSelItem(XLAT("limit generation"), fts(extra_generation_distance), 'E');
      dialog::add_action([] {
        dialog::editNumber(extra_generation_distance, 0, 999, 0.5, 999, XLAT("limit generation"), 
          "Cells over this distance will not be generated, but they will be drawn if they are already generated and in the sight range."
          );
        });
      }
    add_cells_drawn('C');
    if(GDIM == 3 && WDIM == 2 && pmodel == mdPerspective) {
      dialog::addSelItem(XLAT("fog effect"), fts(sightranges[geometry]), 'R');
      dialog::add_action([] {
        dialog::editNumber(sightranges[geometry], 0, 2 * M_PI, 0.5, M_PI, XLAT("fog effect"), "");
        });
      };
    };
  }

void menuitem_sightrange(char c) {
  if(vid.use_smart_range)
    dialog::addSelItem(XLAT("minimum visible cell in pixels"), fts(WDIM == 3 ? vid.smart_range_detail_3 : vid.smart_range_detail), c);
  else if(WDIM == 3)
    dialog::addSelItem(XLAT("3D sight range"), fts(sightranges[geometry]), c);
  else
    dialog::addSelItem(XLAT("sight range"), its(sightrange_bonus), c);
  dialog::add_action(edit_sightrange);
  }
  
void showGraphConfig() {
  cmode = vid.xres > vid.yres * 1.4 ? sm::SIDE : sm::MAYDARK;
  gamescreen(0);

  dialog::init(XLAT("graphics configuration"));

  #if CAP_GLORNOT
  dialog::addBoolItem(XLAT("openGL mode"), vid.usingGL, 'o');
  #endif

  if(!vid.usingGL)
    dialog::addBoolItem(XLAT("anti-aliasing"), vid.antialias & AA_NOGL, 'O');

  if(vid.usingGL)
    dialog::addSelItem(XLAT("anti-aliasing"), 
      (vid.antialias & AA_POLY) ? "polygons" :
      (vid.antialias & AA_LINES) ? "lines" :
      (vid.antialias & AA_MULTI) ? "multisampling" :
      "NO", 'O');

  if(vid.usingGL) {
    dialog::addSelItem(XLAT("line width"), fts(vid.linewidth), 'w');
//    dialog::addBoolItem(XLAT("finer lines at the boundary"), vid.antialias & AA_LINEWIDTH, 'b');
    }
  else
    dialog::addBreak(100);
  
  dialog::addSelItem(XLAT("line quality"), its(vid.linequality), 'L');

  #if CAP_FRAMELIMIT
  dialog::addSelItem(XLAT("framerate limit"), its(vid.framelimit), 'l');
  if(getcstat == 'l') 
    mouseovers = XLAT("Reduce the framerate limit to conserve CPU energy");
  #endif

#if !ISIOS && !ISWEB
  dialog::addBoolItem(XLAT("fullscreen mode"), (vid.full), 'f');
#endif

  dialog::addSelItem(XLAT("scrolling speed"), fts(vid.sspeed), 'a');
    
  dialog::addSelItem(XLAT("movement animation speed"), fts(vid.mspeed), 'm');

  dialog::addBoolItem(XLAT("extra graphical effects"), (vid.particles), 'u');
  dialog::addBoolItem(XLAT("background particle effects"), (vid.backeffects), 'p');

  dialog::addBreak(50);
  dialog::addBack();
  dialog::display();

  keyhandler = [] (int sym, int uni) {
    dialog::handleNavigation(sym, uni);
  
    if(uni == 'O') uni = 'o', shiftmul = -1;
    
    char xuni = uni | 96;
  
    if((uni >= 32 && uni < 64) || uni == 'L' || uni == 'C') xuni = uni;
    
    if(xuni == 'u') vid.particles = !vid.particles;

    else if(xuni == 'a') dialog::editNumber(vid.sspeed, -5, 5, 1, 0, 
      XLAT("scrolling speed"),
      XLAT("+5 = center instantly, -5 = do not center the map")
      + "\n\n" +
      XLAT("press Space or Home to center on the PC"));
  
    else if(xuni == 'm') dialog::editNumber(vid.mspeed, -5, 5, 1, 0, 
      XLAT("movement animation speed"),
      XLAT("+5 = move instantly"));
  
    else if(xuni == 'f') switchFullscreen();
  
  #if CAP_GLORNOT
    else if(xuni == 'o' && shiftmul > 0) switchGL();
  #endif
  
    else if(xuni == 'o' && shiftmul < 0) {
      if(!vid.usingGL)
        vid.antialias ^= AA_NOGL | AA_FONT;
      else if(vid.antialias & AA_MULTI)
        vid.antialias ^= AA_MULTI;
      else if(vid.antialias & AA_POLY)
        vid.antialias ^= AA_POLY | AA_LINES | AA_MULTI;
      else if(vid.antialias & AA_LINES) 
        vid.antialias |= AA_POLY;
      else 
        vid.antialias |= AA_LINES;
#if CAP_SDL
      setvideomode();
#endif
      }
    
    // if(xuni == 'b') vid.antialias ^= AA_LINEWIDTH;
   
    else if(xuni == 'w' && vid.usingGL) {
      dialog::editNumber(vid.linewidth, 0, 10, 0.1, 1, XLAT("line width"), "");
      dialog::extra_options = [] () {
        dialog::addBoolItem("finer lines at the boundary", vid.antialias & AA_LINEWIDTH, 'O');
        dialog::add_action([] () { vid.antialias ^= AA_LINEWIDTH; });
        };
      }
    
    else if(xuni == 'L') {
      dialog::editNumber(vid.linequality, -3, 5, 1, 1, XLAT("line quality"), 
        XLAT("Higher numbers make the curved lines smoother, but reduce the performance."));
      dialog::reaction = delayed_geo_reset;
      }
  
  #if CAP_FRAMELIMIT    
    else if(xuni == 'l') {
      dialog::editNumber(vid.framelimit, 5, 300, 10, 300, XLAT("framerate limit"), "");
      dialog::bound_low(5);
      }
  #endif
      
    else if(xuni =='p') 
      vid.backeffects = !vid.backeffects;
      
    else if(doexiton(sym, uni)) popScreen();
    };
  }
  
void switchFullscreen() {
  vid.full = !vid.full;
#if ISANDROID
  addMessage(XLAT("Reenter HyperRogue to apply this setting"));
  ANDROID_SETTINGS
#endif
#if CAP_SDL
  if(true) {
    vid.xres = vid.full ? vid.xscr : 9999;
    vid.yres = vid.full ? vid.yscr : 9999;
    extern bool setfsize;
    setfsize = true;
    }
  setvideomode();
#endif
  }

void switchGL() {
  vid.usingGL = !vid.usingGL;
  if(vid.usingGL) addMessage(XLAT("openGL mode enabled"));
  if(!vid.usingGL) addMessage(XLAT("openGL mode disabled"));
  ANDROID_SETTINGS;
#if CAP_SDL
  setvideomode();
  if(vid.usingGL) {
    glhr::be_textured(); glhr::be_nontextured();
    }
#endif
  }

void resetConfigMenu();

void edit_whatever(char type, int index) {
  if(type == 'f') {
    dialog::editNumber(whatever[index], -10, 10, 1, 0, XLAT("whatever"), 
      "f:" + its(index));
    }
  else {
    dialog::editNumber(whateveri[index], -10, 10, 1, 0, XLAT("whatever"), 
      "i:" + its(index));
    }
  dialog::reaction = delayed_geo_reset;
  dialog::extra_options = [type, index] {
    dialog::addItem(XLAT("integer"), 'X');
    dialog::add_action( [index] { popScreen(); edit_whatever('i', index); });
    dialog::addItem(XLAT("float"), 'Y');
    dialog::add_action( [index] { popScreen(); edit_whatever('f', index); });
    for(int x=0; x<8; x++) {
      dialog::addSelItem(its(x), type == 'i' ? its(whateveri[x]) : fts(whatever[x]), 'A' + x);
      dialog::add_action([type,x] { popScreen(); edit_whatever(type, x); });
      }
    };
  }

void configureOther() {
  gamescreen(3);

  dialog::init(XLAT("other settings"));

#if ISSTEAM
  dialog::addBoolItem(XLAT("send scores to Steam leaderboards"), (vid.steamscore&1), 'x');
  dialog::add_action([] {vid.steamscore = vid.steamscore^1; });
#endif

  dialog::addBoolItem_action(XLAT("skip the start menu"), vid.skipstart, 'm');

  dialog::addBoolItem_action(XLAT("forget faraway cells"), memory_saving_mode, 'y');
  
  #if CAP_AUDIO
  if(CAP_AUDIO) {
    dialog::addSelItem(XLAT("background music volume"), its(musicvolume), 'b');
    dialog::add_action([] {
      dialog::editNumber(musicvolume, 0, 128, 10, 60, XLAT("background music volume"), "");
      dialog::reaction = [] () {
        #if CAP_SDLAUDIO
        Mix_VolumeMusic(musicvolume);
        #endif
        #if ISANDROID
        settingsChanged = true;
        #endif
        };
      dialog::bound_low(0);
      dialog::bound_up(MIX_MAX_VOLUME);
      });

    dialog::addSelItem(XLAT("sound effects volume"), its(effvolume), 'e');
    dialog::add_action([] {
      dialog::editNumber(effvolume, 0, 128, 10, 60, XLAT("sound effects volume"), "");
      dialog::reaction = [] () {
        #if ISANDROID
        settingsChanged = true;
        #endif
        };
      dialog::bound_low(0);
      dialog::bound_up(MIX_MAX_VOLUME);
      });
    }
  #endif

  menuitem_sightrange('r');

#ifdef WHATEVER
  dialog::addSelItem(XLAT("whatever"), fts(whatever[0]), 'j');
  dialog::add_action([] { edit_whatever('f', 0); });
#endif
  
  dialog::addBreak(50);
  dialog::addBack();
  
  dialog::display();
  }

void configureInterface() {
  gamescreen(3);
  dialog::init(XLAT("interface"));

  if(CAP_TRANS) {
    dialog::addSelItem(XLAT("language"), XLAT("EN"), 'l');
    dialog::add_action_push(selectLanguageScreen);
    }

  dialog::addSelItem(XLAT("player character"), numplayers() > 1 ? "" : csname(vid.cs), 'g');
  dialog::add_action_push(showCustomizeChar);
  if(getcstat == 'g') mouseovers = XLAT("Affects looks and grammar");

  dialog::addSelItem(XLAT("message flash time"), its(vid.flashtime), 't');
  dialog::add_action([] {
    dialog::editNumber(vid.flashtime, 0, 64, 1, 8, XLAT("message flash time"),
      XLAT("How long should the messages stay on the screen."));
    dialog::bound_low(0);
    });

  dialog::addSelItem(XLAT("limit messages shown"), its(vid.msglimit), 'z');
  dialog::add_action([] {
    dialog::editNumber(vid.msglimit, 0, 64, 1, 5, XLAT("limit messages shown"),
      XLAT("Maximum number of messages on screen."));
    dialog::bound_low(0);
    });
  
  const char* msgstyles[3] = {"centered", "left-aligned", "line-broken"};
  
  dialog::addSelItem(XLAT("message style"), XLAT(msgstyles[vid.msgleft]), 'a');
  dialog::add_action([] {
    vid.msgleft = (1+vid.msgleft) % 3;
    });

  dialog::addSelItem(XLAT("font scale"), its(fontscale), 'b');
  dialog::add_action([] {
    dialog::editNumber(fontscale, 25, 400, 10, 100, XLAT("font scale"), "");
    const int minfontscale = ISMOBILE ? 50 : 25;
    dialog::reaction = [] () { setfsize = true; do_setfsize(); };
    dialog::bound_low(minfontscale);
    });  

  const char *glyphsortnames[6] = {
    "first on top", "first on bottom", 
    "last on top", "last on bottom",
    "by land", "by number"
    };
  dialog::addSelItem(XLAT("inventory/kill sorting"), XLAT(glyphsortnames[glyphsortorder]), 'k');
  dialog::add_action([] {
    glyphsortorder = eGlyphsortorder((glyphsortorder+6+(shiftmul>0?1:-1)) % gsoMAX);
    });

  const char *glyphmodenames[3] = {"letters", "auto", "images"};
  dialog::addSelItem(XLAT("inventory/kill mode"), XLAT(glyphmodenames[vid.graphglyph]), 'd');
  dialog::add_action([] {
    vid.graphglyph = (1+vid.graphglyph)%3;
    });    
   
  dialog::addBreak(50);
  dialog::addBack();
  
  dialog::display();
  }

#if CAP_SDLJOY
void showJoyConfig() {
  gamescreen(4);

  dialog::init(XLAT("joystick configuration"));
  
  dialog::addSelItem(XLAT("first joystick position (movement)"), its(joyx)+","+its(joyy), 0);
  dialog::addSelItem(XLAT("second joystick position (panning)"), its(panjoyx)+","+its(panjoyy), 0);
  
  dialog::addSelItem(XLAT("joystick mode"), XLAT(autojoy ? "automatic" : "manual"), 'p');
  if(getcstat == 'p') {
    if(autojoy) 
      mouseovers = XLAT("joystick mode: automatic (release the joystick to move)");
    if(!autojoy) 
      mouseovers = XLAT("joystick mode: manual (press a button to move)");
    }
    
  dialog::addSelItem(XLAT("first joystick: movement threshold"), its(vid.joyvalue), 'a');
  dialog::addSelItem(XLAT("first joystick: execute movement threshold"), its(vid.joyvalue2), 'b');
  dialog::addSelItem(XLAT("second joystick: pan threshold"), its(vid.joypanthreshold), 'c');
  dialog::addSelItem(XLAT("second joystick: panning speed"), fts(vid.joypanspeed * 1000), 'd');

  dialog::addBreak(50);
  dialog::addBack();
  dialog::display();
  
  keyhandler = [] (int sym, int uni) {
    dialog::handleNavigation(sym, uni);
    if(uni == 'p') autojoy = !autojoy;
    else if(uni == 'a') {
      dialog::editNumber(vid.joyvalue, 0, 32768, 100, 4800, XLAT("first joystick: movement threshold"), "");
      dialog::bound_low(0);
      }
    else if(uni == 'b') {
      dialog::editNumber(vid.joyvalue2, 0, 32768, 100, 5600, XLAT("first joystick: execute movement threshold"), "");
      dialog::bound_low(0);
      }
    else if(uni == 'c') {
      dialog::editNumber(vid.joypanthreshold, 0, 32768, 100, 2500, XLAT("second joystick: pan threshold"), "");
      dialog::bound_low(0);
      }
    else if(uni == 'd')
      dialog::editNumber(vid.joypanspeed, 0, 1e-2, 1e-5, 1e-4, XLAT("second joystick: panning speed"), "");
  
    else if(doexiton(sym, uni)) popScreen();
    };
  }
#endif

void projectionDialog() {
  geom3::tc_alpha = ticks;
  dialog::editNumber(vid.alpha, -5, 5, .1, 1,
    XLAT("projection"),
    XLAT("HyperRogue uses the Minkowski hyperboloid model internally. "
    "Klein and Poincaré models can be obtained by perspective, "
    "and the Gans model is obtained by orthogonal projection. "
//  "This parameter specifies the distance from the hyperboloid center "
//  "to the eye. "
    "See also the conformal mode (in the special modes menu) "
    "for more models."));
  dialog::extra_options = [] () {
    dialog::addBreak(100);
    dialog::addHelp(XLAT(
      "If we are viewing an equidistant g absolute units below a plane, "
      "from a point c absolute units above the plane, this corresponds "
      "to viewing a Minkowski hyperboloid from a point "
      "tanh(g)/tanh(c) units below the center. This in turn corresponds to "
      "the Poincaré model for g=c, and Klein-Beltrami model for g=0."));
    dialog::addSelItem(sphere ? "stereographic" : "Poincaré model", "1", 'P');
    dialog::add_action([] () { *dialog::ne.editwhat = 1; vid.scale = 1; dialog::ne.s = "1"; });
    dialog::addSelItem(sphere ? "gnomonic" : "Klein model", "0", 'K');
    dialog::add_action([] () { *dialog::ne.editwhat = 0; vid.scale = 1; dialog::ne.s = "0"; });
    if(hyperbolic) {
      dialog::addSelItem("inverted Poincaré model", "-1", 'I');
      dialog::add_action([] () { *dialog::ne.editwhat = -1; vid.scale = 1; dialog::ne.s = "-1"; });
      }
    dialog::addItem(sphere ? "orthographic" : "Gans model", 'O');
    dialog::add_action([] () { vid.alpha = vid.scale = 999; dialog::ne.s = dialog::disp(vid.alpha); });
    dialog::addItem(sphere ? "towards orthographic" : "towards Gans model", 'T');
    dialog::add_action([] () { double d = 1.1; vid.alpha *= d; vid.scale *= d; dialog::ne.s = dialog::disp(vid.alpha); });
    };
  }

void explain_detail() {
  dialog::addHelp(XLAT(
    "Objects at distance less than %1 absolute units "
    "from the center will be displayed with high "
    "detail, and at distance at least %2 with low detail.",
    fts(geom3::highdetail), fts(geom3::middetail)
    ));
  }

void add_edit_fov(char key = 'f') {
  dialog::addSelItem(XLAT("field of view"), fts(vid.fov) + "°", key);
  dialog::add_action([] {
    dialog::editNumber(vid.fov, 1, 170, 1, 45, "field of view", 
      XLAT(
        "Horizontal field of view, in angles. "
        "This affects the Hypersian Rug mode (even when stereo is OFF) "
        "and non-disk models.")
        );
    dialog::bound_low(1e-8);
    dialog::bound_up(179);
    });
  }

void showStereo() {
  cmode = sm::SIDE | sm::MAYDARK;
  gamescreen(0);
  using namespace geom3;
  dialog::init(XLAT("stereo vision config"));

  string modenames[4] = { "OFF", "anaglyph", "side-by-side", "ODS" };
  
  dialog::addSelItem(XLAT("stereo mode"), XLAT(modenames[vid.stereo_mode]), 'm');
  dialog::addSelItem(XLAT("pupillary distance"), fts(vid.ipd), 'e');
  
  switch(vid.stereo_mode) {
    case sAnaglyph:
      dialog::addSelItem(XLAT("distance between images"), fts(vid.anaglyph_eyewidth), 'd');
      break;
    case sLR:
      dialog::addSelItem(XLAT("distance between images"), fts(vid.lr_eyewidth), 'd');
      break;
    default:
      dialog::addBreak(100);
      break;
    }
  
  dialog::addSelItem(XLAT("desaturate colors"), its(vid.desaturate)+"%", 'c');
  dialog::add_action([] {
    dialog::editNumber(vid.desaturate, 0, 100, 10, 0, XLAT("desaturate colors"),
      XLAT("Make the game colors less saturated. This is useful in the anaglyph mode.")
      );    
    });
  
  add_edit_fov('f');

  dialog::addBack();
  dialog::display();

  keyhandler = [] (int sym, int uni) {
    using namespace geom3;
    dialog::handleNavigation(sym, uni);
    
    string help3 = XLAT(
      "This allows you to view the world of HyperRogue in three dimensions. "
      "Best used with the Hypersian Rug mode. When used in the disk model, "
      "this lets you look at the Minkowski hyperboloid (which means the "
      "depth of terrain features is actually reversed). It also works with non-disk models, "
      "from the conformal menu."
       ) + " " + XLAT(
       "Currently, red-cyan anaglyph glasses and mobile VR googles are supported."
        ) + "\n\n";

    if(uni == 'm')
      { vid.stereo_mode = eStereo((1 + vid.stereo_mode) % (CAP_ODS ? 4 : 3)); return; }
    
    else if(uni == 'e') 
      dialog::editNumber(vid.ipd, -10, 10, 0.01, 0, XLAT("pupillary distance"),
        help3 + 
        XLAT("The distance between your eyes in the represented 3D object. This is given in absolute units.")
        );
      
    else if(uni == 'd' && vid.stereo_mode == sAnaglyph)
      dialog::editNumber(vid.anaglyph_eyewidth, -1, 1, 0.01, 0, XLAT("distance between images"),
        help3 +
        XLAT("The distance between your eyes. 1 is the width of the screen."));

    else if(uni == 'd' && vid.stereo_mode == sLR)
      dialog::editNumber(vid.lr_eyewidth, -1, 1, 0.01, 0, XLAT("distance between images"),
        help3 +
        XLAT("The distance between your eyes. 1 is the width of the screen."));
      
    else if(doexiton(sym, uni)) popScreen();
    };
  }

void config_camera_rotation() {
  dialog::editNumber(vid.ballangle, 0, 90, 5, 0, XLAT("camera rotation in 3D models"), 
    "Rotate the camera in 3D models (ball model, hyperboloid, and hemisphere). "
    "Note that hyperboloid and hemisphere models are also available in the "
    "Hypersian Rug surfaces menu, but they are rendered differently there -- "
    "by making a flat picture first, then mapping it to a surface. "
    "This makes the output better in some ways, but 3D effects are lost. "
    "Hypersian Rug model also allows more camera freedom."
    );
  }

void add_edit_wall_quality(char c) {
  dialog::addSelItem(XLAT("wall quality"), its(vid.texture_step), c);
  dialog::add_action([] {
    dialog::editNumber(vid.texture_step, 1, 16, 1, 1, XLAT("wall quality"), 
      XLAT(
      "Controls the number of triangles used for wall surfaces. "
      "Higher numbers reduce the performance. "
      "This has a strong effect when the walls are curved indeed "
      "(honeycombs based on horospheres, and projections other than native perspective), "
      "but otherwise, usually it can be set to 1 without significant adverse effects other "
      "than slightly incorrect texturing."
      )
      );
    dialog::bound_low(1);
    dialog::bound_up(128);
    dialog::reaction = [] {
      #if MAXMDIM >= 4
      if(floor_textures) {
        delete floor_textures;
        floor_textures = NULL;
        }
      #endif
      need_reset_geometry = true;          
      };
    });
  }

void show3D() {
  cmode = sm::SIDE | sm::MAYDARK;
  gamescreen(0);
  using namespace geom3;
  dialog::init(XLAT("3D configuration"));

#if MAXMDIM >= 4
  if(WDIM == 2) {
    dialog::addBoolItem(XLAT("use the full 3D models"), geom3::always3, 'U');
    dialog::add_action(geom3::switch_always3);
    }
#endif
  if(vid.use_smart_range == 0 && DIM == 2) {
    dialog::addSelItem(XLAT("High detail range"), fts(highdetail), 'n');
    dialog::addSelItem(XLAT("Mid detail range"), fts(middetail), 'm');
    dialog::addBreak(50);
    }
  
  if(WDIM == 2) {
    dialog::addSelItem(XLAT(GDIM == 2 ? "Camera level above the plane" : "Z shift"), fts(camera), 'c');
    dialog::addSelItem(XLAT("Ground level below the plane"), fts(depth), 'g');
  
    dialog::addSelItem(XLAT("Projection at the ground level"), fts(vid.alpha), 'a');
    dialog::addBreak(50);
    dialog::addSelItem(XLAT("Height of walls"), fts(wall_height), 'w');
    
    dialog::addSelItem(XLAT("Rock-III to wall ratio"), fts(rock_wall_ratio), 'r');
    dialog::addSelItem(XLAT("Human to wall ratio"), fts(human_wall_ratio), 'h');
    dialog::addSelItem(XLAT("Level of water surface"), fts(lake_top), 'l');
    dialog::addSelItem(XLAT("Level of water bottom"), fts(lake_bottom), 'k');  
    }
  else {
    dialog::addSelItem(XLAT("Creature scale"), fts(creature_scale), 'c');
    dialog::addSelItem(XLAT("Height to width"), fts(height_width), 'h');
    menuitem_sightrange('s');
    }

  dialog::addBreak(50);
  dialog::addSelItem(XLAT(DIM == 3 && WDIM == 2 ? "Y shift" : "third person perspective"), fts(vid.yshift), 'y');
  if(DIM == 3) {
    dialog::addSelItem(XLAT("mouse aiming sensitivity"), fts(mouseaim_sensitivity), 'a');
    dialog::add_action([] () { 
      dialog::editNumber(mouseaim_sensitivity, -1, 1, 0.002, 0.01, XLAT("mouse aiming sensitivity"), "set to 0 to disable");
      });
    }
  dialog::addSelItem(XLAT("camera rotation"), fts(vid.camera_angle), 's');
  if(DIM == 2) {
    dialog::addSelItem(XLAT("fixed facing"), vid.fixed_facing ? fts(vid.fixed_facing_dir) : XLAT("OFF"), 'f');
    dialog::add_action([] () { vid.fixed_facing = !vid.fixed_facing; 
      if(vid.fixed_facing) {
        dialog::editNumber(vid.fixed_facing_dir, 0, 360, 15, 90, "", "");
        dialog::dialogflags |= sm::CENTER;
        }
      });
    }

  if(WDIM == 2 && GDIM == 3)
    dialog::addBoolItem_action("fixed Y/Z rotation", vid.fixed_yz, 'Z');

  if(true) {
    dialog::addBreak(50);
    dialog::addSelItem(XLAT("projection"), current_proj_name(), 'M');
    }
  #if MAXMDIM >= 4
  if(DIM == 3) add_edit_fov('f');
  if(DIM == 3) {
    dialog::addSelItem(XLAT("radar size"), fts(vid.radarsize), 'r');
    dialog::add_action([] () {
      dialog::editNumber(vid.radarsize, 0, 360, 15, 90, "", "set to 0 to disable");
      });
    }
  if(DIM == 3) add_edit_wall_quality('W');
  #endif
  
  dialog::addBreak(50);
  #if CAP_RUG
  if(rug::rugged) {
    dialog::addBoolItem_action(XLAT("3D monsters/walls on the surface"), rug::spatial_rug, 'S');
    }
  #endif
  if(GDIM == 2) {
    dialog::addBoolItem(XLAT("configure TPP automatically"), pmodel == mdDisk && vid.camera_angle, 'T');
    dialog::add_action(geom3::switch_tpp);
    }
  
#if MAXMDIM >=4
  if(WDIM == 2) {
    dialog::addBoolItem(XLAT("configure FPP automatically"), DIM == 3, 'F');
    dialog::add_action(geom3::switch_fpp);
    }
#endif

  if(0);
  #if CAP_RUG
  else if(rug::rugged && !rug::spatial_rug)
    dialog::addBreak(100);
  #endif
  else if(GDIM == 2 && non_spatial_model())
    dialog::addInfo(XLAT("no 3D effects available in this projection"), 0xC00000);
  else if(GDIM == 2 && !spatial_graphics)
    dialog::addInfo(XLAT("set 3D monsters or walls in basic config first"));
  else if(invalid != "")
    dialog::addInfo(XLAT("error: "+invalid), 0xC00000);
  else
    dialog::addInfo(XLAT("parameters set correctly"));
  dialog::addBreak(50);
  dialog::addItem(XLAT("stereo vision config"), 'e');
  dialog::addBack();
  dialog::display();
  
  keyhandler = [] (int sym, int uni) {
    using namespace geom3;
    dialog::handleNavigation(sym, uni);
    
    if(uni == 'n' && DIM == 2) {
      dialog::editNumber(geom3::highdetail, 0, 5, .5, 7, XLAT("High detail range"), "");
      dialog::extra_options = explain_detail;
      dialog::reaction = [] () {
        if(highdetail > middetail) middetail = highdetail;
        };
      }
    else if(uni == 'm' && DIM == 2) {
      dialog::editNumber(geom3::middetail, 0, 5, .5, 7, XLAT("Mid detail range"), "");
      dialog::extra_options = explain_detail;
      dialog::reaction = [] () {
        if(highdetail > middetail) highdetail = middetail;
        };
      }
    else if(uni == 'c' && WDIM == 2) 
      tc_camera = ticks,
      dialog::editNumber(geom3::camera, 0, 5, .1, 1, XLAT(GDIM == 2 ? "Camera level above the plane" : "Z shift"), ""),
      dialog::reaction = [] { if(GDIM == 2) need_reset_geometry = true; },
      dialog::extra_options = [] {
        dialog::addHelp(GDIM == 2 ? XLAT(
          "Camera is placed %1 absolute units above a plane P in a three-dimensional "
          "world. Ground level is actually an equidistant surface, %2 absolute units "
          "below the plane P. The plane P (as well as the ground level or any "
          "other equidistant surface below it) is viewed at an angle of %3 "
          "(the tangent of the angle between the point in "
          "the center of your vision and a faraway location is 1/cosh(c) = %4).",
          fts(camera),
          fts(depth),
          fts(atan(1/cosh(camera))*2/degree),
          fts(1/cosh(camera))) : XLAT("Look from behind."));
        if(DIM == 3 && pmodel == mdPerspective) dialog::extra_options = [] () {
          dialog::addBoolItem_action(XLAT("reduce if walls on the way"), vid.use_wall_radar, 'R');
          };
        };
    else if(uni == 'g' && WDIM == 2) 
      tc_depth = ticks,
      dialog::editNumber(geom3::depth, 0, 5, .1, 1, XLAT("Ground level below the plane"), ""),
      dialog::reaction = delayed_geo_reset,
      dialog::extra_options = [] {
        dialog::addHelp(XLAT(
          "Ground level is actually an equidistant surface, "
          "%1 absolute units below the plane P. "
          "Theoretically, this value affects the world -- "
          "for example, eagles could fly %2 times faster by "
          "flying above the ground level, on the plane P -- "
          "but the actual game mechanics are not affected. "
          "(Distances reported by the vector graphics editor "
          "are not about points on the ground level, but "
          "about the matching points on the plane P -- "
          "divide them by the factor above to get actual "
          "distances.)"
          
          ,
          fts(depth), fts(cosh(depth))));
          // mention absolute units
          };
    else if(uni == 'a' && WDIM == 2) 
      projectionDialog();
    else if(uni == 'w' && WDIM == 2) {
      dialog::editNumber(geom3::wall_height, 0, 1, .1, .3, XLAT("Height of walls"), "");
      dialog::reaction = delayed_geo_reset;
      dialog::extra_options = [] () {
        dialog::addHelp(XLAT(
          "The height of walls, in absolute units. For the current values of g and c, "
          "wall height of %1 absolute units corresponds to projection value of %2.",
          fts(actual_wall_height()), fts(factor_to_projection(geom3::WALL))));
        dialog::addBoolItem(XLAT("auto-adjust in Goldberg grids"), geom3::gp_autoscale_heights, 'O');
        dialog::add_action([] () {
          geom3::gp_autoscale_heights = !geom3::gp_autoscale_heights;
          buildpolys();
          #if CAP_GL
          resetGL();
          #endif
          });
        };
      }
    else if(uni == 'l' && WDIM == 2) 
      dialog::editNumber(geom3::lake_top, 0, 1, .1, .25, XLAT("Level of water surface"), ""),
      dialog::reaction = delayed_geo_reset;
    else if(uni == 'k' && WDIM == 2) 
      dialog::editNumber(geom3::lake_bottom, 0, 1, .1, .9, XLAT("Level of water bottom"), ""),
      dialog::reaction = delayed_geo_reset;
    else if(uni == 'r' && WDIM == 2) 
      dialog::editNumber(geom3::rock_wall_ratio, 0, 1, .1, .9, XLAT("Rock-III to wall ratio"), ""),
      dialog::extra_options = [] { dialog::addHelp(XLAT(
        "The ratio of Rock III to walls is %1, so Rock III are %2 absolute units high. "
        "Length of paths on the Rock III level is %3 of the corresponding length on the "
        "ground level.",
        fts(rock_wall_ratio), fts(wall_height * rock_wall_ratio),
        fts(cosh(depth - wall_height * rock_wall_ratio) / cosh(depth))));
        },
      dialog::reaction = delayed_geo_reset;
    else if(uni == 'h' && WDIM == 2)
      dialog::editNumber(geom3::human_wall_ratio, 0, 1, .1, .7, XLAT("Human to wall ratio"), ""),
      dialog::reaction = delayed_geo_reset,
      dialog::extra_options = [] { dialog::addHelp(XLAT(
        "Humans are %1 "
        "absolute units high. Your head travels %2 times the distance travelled by your "
        "feet.",
        fts(wall_height * human_wall_ratio),
        fts(cosh(depth - wall_height * human_wall_ratio) / cosh(depth)))
        );
        };
    else if(uni == 'h' && WDIM == 3)
      dialog::editNumber(geom3::height_width, 0, 1, .1, .7, XLAT("Height to width"), ""),
      dialog::reaction = delayed_geo_reset;
    else if(uni == 'c' && WDIM == 3)
      dialog::editNumber(geom3::creature_scale, 0, 1, .1, .7, XLAT("Creature scale"), ""),
      dialog::reaction = delayed_geo_reset;

    else if(uni == 'e')
      pushScreen(showStereo);
    
    else if(uni == 'y') {
      dialog::editNumber(vid.yshift, 0, 1, .1, 0, XLAT("Y shift"), 
        XLAT("Don't center on the player character.")
        );
      if(WDIM == 3 && pmodel == mdPerspective) dialog::extra_options = [] () {
        dialog::addBoolItem_action(XLAT("reduce if walls on the way"), vid.use_wall_radar, 'R');
        };
      }
    else if(uni == 's') 
      dialog::editNumber(vid.camera_angle, -180, 180, 5, 0, XLAT("camera rotation"), 
        XLAT("Rotate the camera. Can be used to obtain a first person perspective, "
        "or third person perspective when combined with Y shift.")
        );
    else if(uni == 'b') 
      config_camera_rotation();
    else if(uni == 'M') 
      pushScreen(conformal::model_menu);  
    else if(doexiton(sym, uni)) 
      popScreen();
    };
  }

void switchcolor(unsigned int& c, unsigned int* cs) {
  dialog::openColorDialog(c, cs);
  }

double cc_footphase;
int lmousex, lmousey;

void showCustomizeChar() {

  cc_footphase += hypot(mousex - lmousex, mousey - lmousey);
  lmousex = mousex; lmousey = mousey;

  gamescreen(4);
  dialog::init(XLAT("Customize character"));
  
  if(shmup::on || multi::players) shmup::cpid = shmup::cpid_edit % shmup::players;
  charstyle& cs = getcs();
  
  dialog::addSelItem(XLAT("character"), csname(cs), 'g');
  dialog::addColorItem(XLAT("skin color"), cs.skincolor, 's');
  dialog::addColorItem(XLAT("eye color"), cs.eyecolor, 'e');
  dialog::addColorItem(XLAT("weapon color"), cs.swordcolor, 'w');
  dialog::addColorItem(XLAT("hair color"), cs.haircolor, 'h');
  
  if(cs.charid >= 1) dialog::addColorItem(XLAT("dress color"), cs.dresscolor, 'd');
  else dialog::addBreak(100);
  if(cs.charid == 3) dialog::addColorItem(XLAT("dress color II"), cs.dresscolor2, 'f');
  else dialog::addBreak(100);
  
  dialog::addColorItem(XLAT("movement color"), cs.uicolor, 'u');

  if(!shmup::on && multi::players == 1) dialog::addSelItem(XLAT("save whom"), XLAT1(minf[moPrincess].name), 'p');
  
  if(numplayers() > 1) dialog::addSelItem(XLAT("player"), its(shmup::cpid+1), 'a');

  dialog::addBoolItem(XLAT("left-handed"), cs.lefthanded, 'l');
  
  dialog::addBreak(50);
  dialog::addBack();
  dialog::display();
  
  int firsty = dialog::items[0].position / 2;
  int scale = firsty - 2 * vid.fsize;
  
  dynamicval<eModel> pm(pmodel, DIM == 3 ? mdFlatten : mdDisk);
  dynamicval<ld> va(vid.alpha, 1);
  dynamicval<ld> vs(vid.scale, 1);
  dynamicval<ld> vc(vid.camera_angle, 0);

  initquickqueue();
  transmatrix V = atscreenpos(vid.xres/2, firsty, scale);
  
  double alpha = atan2(mousex - vid.xres/2, mousey - firsty) - M_PI/2;
  drawMonsterType(moPlayer, NULL, V * spin(alpha), 0, cc_footphase / scale);
  quickqueue();
  
  keyhandler = [] (int sym, int uni) {
    dialog::handleNavigation(sym, uni);
  
    if(shmup::on || multi::players) shmup::cpid = shmup::cpid_edit % shmup::players;
    charstyle& cs = getcs();
    bool cat = cs.charid >= 4;
    if(uni == 'a') { shmup::cpid_edit++; shmup::cpid_edit %= 60; }
    else if(uni == 'g') {
      cs.charid++;
      if(cs.charid == 2 && !princess::everSaved && !autocheat) cs.charid = 4;
      cs.charid %= 10;
      }
    else if(uni == 'p') vid.samegender = !vid.samegender;
    else if(uni == 's') switchcolor(cs.skincolor, cat ? haircolors : skincolors);
    else if(uni == 'h') switchcolor(cs.haircolor, haircolors);
    else if(uni == 'w') switchcolor(cs.swordcolor, swordcolors);
    else if(uni == 'd') switchcolor(cs.dresscolor, cat ? haircolors : dresscolors);
    else if(uni == 'f') switchcolor(cs.dresscolor2, dresscolors2);
    else if(uni == 'u') switchcolor(cs.uicolor, eyecolors);
    else if(uni == 'e') switchcolor(cs.eyecolor, eyecolors);
    else if(uni == 'l') cs.lefthanded = !cs.lefthanded;
    else if(doexiton(sym, uni)) popScreen();
    };
  }

void refresh_canvas() {
  manual_celllister cl;
  cl.add(cwt.at);
    
  int at = 0;
  while(at < isize(cl.lst)) {
    cell *c2 = cl.lst[at];
    c2->landparam = patterns::generateCanvas(c2);
    at++;
    
    forCellEx(c3, c2) cl.add(c3);
    }
  }

void edit_color_table(colortable& ct, const reaction_t& r = reaction_t()) {
  cmode = sm::SIDE;
  gamescreen(0);
  dialog::init(XLAT("colors & aura"));
  
  for(int i=0; i<isize(ct); i++) {
    dialog::addColorItem(its(i), ct[i] << 8, 'a'+i);
    dialog::add_action([i, &ct, r] () { dialog::openColorDialog(ct[i]); dialog::reaction = r; dialog::colorAlpha = false; dialog::dialogflags |= sm::SIDE; });
    }

  dialog::addBack();
  dialog::display();
  }

void show_color_dialog() {
  cmode = sm::SIDE | sm::DIALOG_STRICT_X;
  getcstat = '-';
  gamescreen(0);
  dialog::init(XLAT("colors & aura"));

  dialog::addColorItem(XLAT("background"), backcolor << 8, 'b');
  dialog::add_action([] () { dialog::openColorDialog(backcolor); dialog::colorAlpha = false; dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("foreground"), forecolor << 8, 'f');
  dialog::add_action([] () { dialog::openColorDialog(forecolor); dialog::colorAlpha = false; dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("borders"), bordcolor << 8, 'o');
  dialog::add_action([] () { dialog::openColorDialog(bordcolor); dialog::colorAlpha = false; dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("projection boundary"), ringcolor, 'r');
  dialog::add_action([] () { dialog::openColorDialog(ringcolor); dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("projection background"), modelcolor, 'c');
  dialog::add_action([] () { dialog::openColorDialog(modelcolor); dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("standard grid color"), stdgridcolor, 'g');
  dialog::add_action([] () { vid.grid = true; dialog::openColorDialog(stdgridcolor); dialog::dialogflags |= sm::SIDE; });

  dialog::addSelItem(XLAT("brightness behind the sphere"), fts(backbrightness), 'i');
  dialog::add_action([] () { dialog::editNumber(backbrightness, 0, 1, .01, 0.25, XLAT("brightness behind the sphere"), 
    XLAT("In the orthogonal projection, objects on the other side of the sphere are drawn darker.")); dialog::bound_low(0); });

  dialog::addColorItem(XLAT("projection period"), periodcolor, 'p');
  dialog::add_action([] () { dialog::openColorDialog(periodcolor); dialog::dialogflags |= sm::SIDE; });

  dialog::addColorItem(XLAT("dialogs"), dialog::dialogcolor << 8, 'd');
  dialog::add_action([] () { dialog::openColorDialog(dialog::dialogcolor); dialog::colorAlpha = false; dialog::dialogflags |= sm::SIDE; });

  dialog::addBreak(50);
  if(specialland == laCanvas && colortables.count(patterns::whichCanvas)) {
    dialog::addItem(XLAT("pattern colors"), 'P');
    dialog::add_action_push([] { edit_color_table(colortables[patterns::whichCanvas], refresh_canvas); });
    }
 
  if(cwt.at->land == laMinefield) {
    dialog::addItem(XLAT("minefield colors"), 'm');
    dialog::add_action_push([] { edit_color_table(minecolors); });
    }
  
  if(viewdists) {
    dialog::addItem(XLAT("distance colors"), 'd');
    dialog::add_action_push([] () {edit_color_table(distcolors); });
    }
  
  #if CAP_CRYSTAL
  if(geometry == gCrystal && cheater) {
    dialog::addItem(XLAT("crystal coordinate colors"), 'C');
    dialog::add_action([] () { crystal::view_coordinates = true; pushScreen([] () { edit_color_table(crystal::coordcolors); });});
    }
  #endif

  dialog::addInfo(XLAT("colors of some game objects can be edited by clicking them."));
  
  dialog::addBreak(50);

  dialog::addSelItem(XLAT("aura brightness"), its(vid.aurastr), 'a');
  dialog::add_action([] () { dialog::editNumber(vid.aurastr, 0, 256, 10, 128, XLAT("aura brightness"), ""); dialog::bound_low(0); });

  dialog::addSelItem(XLAT("aura smoothening factor"), its(vid.aurasmoothen), 's');
  dialog::add_action([] () { dialog::editNumber(vid.aurasmoothen, 1, 180, 1, 5, XLAT("aura smoothening factor"), ""); dialog::bound_low(1); });  

  dialog::addBreak(50);
  dialog::addBack();
  dialog::display();

  keyhandler = [] (int sym, int uni) {
    if(uni == '-') {
      cell *c = mouseover;
      if(!c) return;
      else if(c == cwt.at) {
        pushScreen(showCustomizeChar);
        return;
        }
      else if(c->monst) 
        dialog::openColorDialog(minf[c->monst].color);
      else if(c->item) 
        dialog::openColorDialog(iinf[c->item].color);
      else if(c->wall) 
        dialog::openColorDialog(winf[c->wall == waMineMine ? waMineUnknown : c->wall].color);
      #if CAP_COMPLEX2
      else if(c->land == laBrownian) 
        dialog::openColorDialog(brownian::get_color_edit(c->landparam));
      #endif
      else 
        dialog::openColorDialog(floorcolors[c->land]);
      dialog::colorAlpha = false;
      dialog::dialogflags |= sm::SIDE;
      return;
      }
    else dialog::handleNavigation(sym, uni);
    if(doexiton(sym, uni)) popScreen();
    };
  }

#if CAP_CONFIG
void resetConfigMenu() {
  dialog::init(XLAT("reset all configuration"));
  dialog::addInfo("Are you sure?");
  dialog::addItem("yes, and delete the config file", 'd');
  dialog::addItem("yes", 'y');
  dialog::addItem("cancel", 'n');
  dialog::addItem("reset the special game modes", 'r');
  dialog::display();
  keyhandler = [] (int sym, int uni) {
    dialog::handleNavigation(sym, uni);

    if(uni == 'd') { 
      resetConfig();
      unlink(conffile);
      popScreen();
      }
    else if(uni == 'y') {
      printf("reseting config\n");
      resetConfig();
      printf("config reset\n");
      popScreen();
      }
    else if(uni == 'r') 
      resetModes();
    else if(uni == 'n' || doexiton(sym, uni)) 
      popScreen();
    
    };
  }
#endif

#if CAP_TRANS
void selectLanguageScreen() {
  gamescreen(4);
  dialog::init("select language"); // intentionally not translated

  int v = vid.language;  
  dynamicval<int> d(vid.language, -1);
  
  for(int i=0; i<NUMLAN-1 || i == v; i++) {
    vid.language = i;
    dialog::addSelItem(XLAT("EN"), its(100 * transcompleteness[i] / transcompleteness[0]) + "%", 'a'+i);
    }
  
  dialog::addBreak(50);
  vid.language = -1;
  dialog::addBoolItem(XLAT("default") + ": " + XLAT("EN"), v == -1, '0');
  dialog::addBack();

  dialog::addBreak(50);

  vid.language = v;
  if(lang() >= 1)
    dialog::addHelp(XLAT("add credits for your translation here"));
  else
    dialog::addHelp(XLAT("original language"));

  if(lang() != 0) {
    string tw = "";
    string s = XLAT("TRANSLATIONWARNING");
    if(s != "" && s != "TRANSLATIONWARNING") tw += s;
    s = XLAT("TRANSLATIONWARNING2");
    if(s != "" && s != "TRANSLATIONWARNING2") { if(tw != "") tw += " "; tw += s; }
    if(tw != "") {
      dialog::addHelp(tw);
      dialog::lastItem().color = 0xFF0000;
      }
    }

  dialog::display();
  
  keyhandler = []   (int sym, int uni) {
    dialog::handleNavigation(sym, uni);
    
    if(uni == '0') {
      vid.language = -1;
      ANDROID_SETTINGS;
      }

    else if(uni >= 'a' && uni < 'a'+NUMLAN) {
      vid.language = uni - 'a';
      ANDROID_SETTINGS;
      }
    
    else if(doexiton(sym, uni))
      popScreen();
    };
  }
#endif

void configureMouse() {
  gamescreen(1);
  dialog::init(XLAT("mouse & touchscreen"));

  dialog::addBoolItem_action(XLAT("reverse pointer control"), (vid.revcontrol), 'r');
  
  dialog::addBoolItem_action(XLAT("draw circle around the target"), (vid.drawmousecircle), 'd');
  
#if ISMOBILE
  dialog::addBoolItem(XLAT("targetting ranged Orbs long-click only"), (vid.shifttarget&2), 'i');
#else
  dialog::addBoolItem(XLAT("targetting ranged Orbs Shift+click only"), (vid.shifttarget&1), 'i');
#endif
  dialog::add_action([] {vid.shifttarget = vid.shifttarget^3; });    

  #if !ISMOBILE
  dialog::addBoolItem_action(XLAT("quick mouse"), vid.quickmouse, 'M');
  #endif

  dialog::addSelItem(XLAT("move by clicking on compass"), its(vid.mobilecompasssize), 'C');
  dialog::add_action([] {
    dialog::editNumber(vid.mobilecompasssize, 0, 100, 10, 20, XLAT("compass size"), XLAT("0 to disable"));
    // we need to check the moves
    dialog::reaction = checkmove;
    dialog::bound_low(0);
    });

  #if CAP_ORIENTATION
  dialog::addSelItem(XLAT("scrolling by device rotation"), ors::choices[ors::mode], '1');  
  dialog::add_action_push(ors::show);
  #endif

  dialog::display();
  }

void showSettings() {
  gamescreen(1);
  dialog::init(XLAT("settings"));

  dialog::addItem(XLAT("interface"), 'i');
  dialog::add_action_push(configureInterface);

  dialog::addItem(XLAT("general graphics"), 'g');
  dialog::add_action_push(showGraphConfig);

  dialog::addItem(XLAT("3D configuration"), '9');
  dialog::add_action_push(show3D);

  dialog::addItem(XLAT("quick options"), 'q');
  dialog::add_action_push(showGraphQuickKeys);

  dialog::addItem(XLAT("models & projections"), 'p');
  dialog::add_action_push(conformal::model_menu);

  dialog::addItem(XLAT("colors & aura"), 'c');
  dialog::add_action_push(show_color_dialog);

#if CAP_SHMUP
  if(CAP_SHMUP && !ISMOBILE) {
    dialog::addSelItem(XLAT("keyboard & joysticks"), "", 'k');
    dialog::add_action(shmup::configure);
    }
#endif

  dialog::addSelItem(XLAT("mouse & touchscreen"), "", 'm');
  dialog::add_action_push(configureMouse);

  dialog::addItem(XLAT("other settings"), 'o');
  dialog::add_action_push(configureOther);
  
  dialog::addBreak(100);

#if CAP_CONFIG
  dialog::addItem(XLAT("save the current config"), 's');
  dialog::add_action(saveConfig);

  dialog::addItem(XLAT("reset all configuration"), 'R');
  dialog::add_action_push(resetConfigMenu);
#endif  
  
  if(getcstat == 's') mouseovers = XLAT("Config file: %1", conffile);
  
  dialog::addBack();
  dialog::display();
  }

#if CAP_COMMANDLINE

int read_color_args() {
  using namespace arg;

  if(argis("-back")) {
    PHASEFROM(2); shift(); backcolor = arghex();
    }
  else if(argis("-fillmodel")) {
    PHASEFROM(2); shift(); modelcolor = arghex();
    }
  else if(argis("-ring")) {
    PHASEFROM(2); shift(); ringcolor = arghex();
    }
  else if(argis("-stdgrid")) {
    PHASEFROM(2); shift(); stdgridcolor = arghex();
    }
  else if(argis("-period")) {
    PHASEFROM(2); shift(); periodcolor = arghex();
    }
  else if(argis("-borders")) {
    PHASEFROM(2); shift(); bordcolor = arghex();
    }
  else if(argis("-fore")) {
    PHASEFROM(2); shift(); forecolor = arghex();
    }
  else if(argis("-dialog")) {
    PHASEFROM(2); shift(); dialog::dialogcolor = arghex();
    }
  else if(argis("-d:color"))
    launch_dialog(show_color_dialog);
  else return 1;
  return 0;
  }

int read_config_args() {
  using namespace arg;

  if(argis("-c")) { PHASE(1); shift(); conffile = argcs(); }
// change the configuration from the command line
  else if(argis("-aa")) { PHASEFROM(2); shift(); vid.antialias = argi(); }
  else if(argis("-lw")) { PHASEFROM(2); shift_arg_formula(vid.linewidth); }
  else if(argis("-wm")) { PHASEFROM(2); shift(); vid.wallmode = argi(); }
  else if(argis("-mm")) { PHASEFROM(2); shift(); vid.monmode = argi(); }

  else if(argis("-noshadow")) { noshadow = true; }
  else if(argis("-bright")) { bright = true; }
  else if(argis("-gridon")) { vid.grid = true; }

// non-configurable options
  else if(argis("-vsync_off")) {
    vsync_off = true;
    if(curphase == 3) setvideomode();
    }
  else if(argis("-aura")) {
    PHASEFROM(2);
    shift(); vid.aurastr = argi();
    shift(); vid.aurasmoothen = argi();
    }
  else if(argis("-nofps")) {
    PHASEFROM(2);
    nofps = true;
    }
  else if(argis("-nohud")) {
    PHASEFROM(2);
    nohud = true;
    }
  else if(argis("-nomenu")) {
    PHASEFROM(2);
    nomenukey = true;
    }
#if MAXMDIM >= 4
  else if(argis("-switch-fpp")) {
    PHASEFROM(2);
    geom3::switch_fpp();
    }
#endif
  else if(argis("-switch-tpp")) {
    PHASEFROM(2);
    geom3::switch_tpp();
    }
#if MAXMDIM >= 4
  else if(argis("-switch-3d")) {
    PHASEFROM(2);
    geom3::switch_always3();
    }
#endif
  else if(argis("-nohelp")) {
    PHASEFROM(2);
    nohelp = true;
    }
  else if(argis("-dont_face_pc")) {
    PHASEFROM(2);
    dont_face_pc = true;
    }

#if CAP_TRANS
  else if(argis("-lang")) { 
    PHASEFROM(2); shift(); vid.language = argi();
    }
#endif
  else if(argis("-vlq")) { 
    PHASEFROM(2); shift(); vid.linequality = argi();
    }
  else if(argis("-r")) { 
    PHASEFROM(2);
    shift(); 
    int clWidth=0, clHeight=0, clFont=0;
    sscanf(argcs(), "%dx%dx%d", &clWidth, &clHeight, &clFont);
    if(clWidth) vid.xres = clWidth;
    if(clHeight) vid.yres = clHeight;
    if(clFont) vid.fsize = clFont;
    }    
  else if(argis("-msm")) {
    PHASEFROM(2); memory_saving_mode = true;
    }
  else if(argis("-yca")) {
    PHASEFROM(2); 
    shift_arg_formula(vid.yshift);
    shift_arg_formula(vid.camera_angle);
    }
  else if(argis("-xy")) {
    PHASEFROM(2); 
    shift_arg_formula(vid.xposition);
    shift_arg_formula(vid.yposition);
    }
  else if(argis("-fixdir")) {
    PHASEFROM(2); 
    vid.fixed_facing = true;
    shift_arg_formula(vid.fixed_facing_dir);
    }
  else if(argis("-fixdiroff")) {
    PHASEFROM(2); 
    vid.fixed_facing = false;
    }
  else if(argis("-msmoff")) {
    PHASEFROM(2); memory_saving_mode = false;
    }
  else if(argis("-msens")) {
    PHASEFROM(2); shift_arg_formula(mouseaim_sensitivity);
    }
  TOGGLE('o', vid.usingGL, switchGL())
  TOGGLE('f', vid.full, switchFullscreen())
  else if(argis("-d:sight")) {
    PHASEFROM(2); launch_dialog(); edit_sightrange();
    }
  else if(argis("-d:char")) {
    PHASEFROM(2); launch_dialog(showCustomizeChar);
    }
  else if(argis("-d:3")) {
    PHASEFROM(2); launch_dialog(show3D);
    }
  else if(argis("-d:stereo")) {
    PHASEFROM(2); launch_dialog(showStereo);
    }
  else if(argis("-d:iface")) {
    PHASEFROM(2); launch_dialog(configureInterface);
    }
  else if(argis("-d:graph")) {
    PHASEFROM(2); launch_dialog(showGraphConfig);
    }
  else return 1;
  return 0;
  }

// mode changes:

int read_gamemode_args() {
  using namespace arg;

  if(argis("-P")) { 
    PHASE(2); shift(); 
    stop_game_and_switch_mode(rg::nothing);
    multi::players = argi();
    }
  TOGGLE('C', chaosmode, stop_game_and_switch_mode(rg::chaos))
  TOGGLE('S', shmup::on, stop_game_and_switch_mode(rg::shmup))
  TOGGLE('H', hardcore, switchHardcore())
  TOGGLE('R', randomPatternsMode, stop_game_and_switch_mode(rg::randpattern))
  TOGGLE('i', inv::on, stop_game_and_switch_mode(rg::inv))
  
  else return 1;
  return 0;
  }

auto ah_config = addHook(hooks_args, 0, read_config_args) + addHook(hooks_args, 0, read_gamemode_args) + addHook(hooks_args, 0, read_color_args);
#endif

unordered_map<string, ld&> params = {
  {"linewidth", vid.linewidth},
  {"patternlinewidth", linepatterns::width},
  {"scale", vid.scale},
  {"xposition", vid.xposition},
  {"yposition", vid.yposition},
  {"projection", vid.alpha},
  {"sspeed", vid.sspeed},
  {"mspeed", vid.mspeed},
  {"ballangle", vid.ballangle},
  {"yshift", vid.yshift},
  {"cameraangle", vid.camera_angle},
  {"depth", geom3::depth},
  {"camera", geom3::camera},
  {"wall_height", geom3::wall_height},
  {"highdetail", geom3::highdetail},
  {"middetail", geom3::middetail},
  {"rock_wall_ratio", geom3::rock_wall_ratio},
  {"human_wall_ratio", geom3::human_wall_ratio},
  {"lake_top", geom3::lake_top},
  {"lake_bottom", geom3::lake_bottom},
  #if CAP_RUG
  {"rug_model_distance", rug::model_distance},
  #endif
  {"star", polygonal::STAR},
  {"lvspeed", conformal::lvspeed},
  {"rotation", conformal::rotation},
  {"mori", conformal::model_orientation},
  {"mori_yz", conformal::model_orientation_yz},
  {"clipmin", conformal::clip_min},
  {"clipmax", conformal::clip_max},
  {"topz", conformal::top_z},
  {"mtrans", conformal::model_transition},
  {"hp", conformal::halfplane_scale},
  {"back", backbrightness},
  {"ipd", vid.ipd},
  {"lr", vid.lr_eyewidth},
  {"anaglyph", vid.anaglyph_eyewidth},
  {"fov", vid.fov},
  {"ets", vid.euclid_to_sphere},
  {"stretch", vid.stretch},
  {"twopoint", vid.twopoint_param},
  {"bwidth", vid.binary_width},
  #if CAP_ANIMATIONS
  {"aperiod", anims::period},
  {"acycle", anims::cycle_length},
  {"aparabolic", anims::parabolic_length},
  {"arugangle", anims::rug_angle},
  {"acradius", anims::circle_radius},
  {"acspins", anims::circle_spins},
  {"a", anims::a},
  {"b", anims::b},
  #endif
  {"mobius", vid.skiprope},
  {"sang", conformal::spiral_angle},
  {"spiralx", conformal::spiral_x},
  {"spiraly", conformal::spiral_y},
  #if CAP_CRYSTAL
  {"cprob", crystal::compass_probability},
  #endif
  #if CAP_SHOT
  {"gamma", shot::gamma},
  {"fade", shot::fade},
  #endif
  };

}
