Free Hero Mesh

game.c at [ef8fe744ff]
Login
This is a mirror of the main repository for Free Hero Mesh. New tickets and changes will not be accepted at this mirror.

File game.c artifact 522c906303 part of check-in ef8fe744ff


#if 0
gcc ${CFLAGS:--s -O2} -c -Wno-unused-result -Wno-multichar -fwrapv game.c `sdl-config --cflags`
exit
#endif

/*
  This program is part of Free Hero Mesh and is public domain.
*/

#include "SDL.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "sqlite3.h"
#include "smallxrm.h"
#include "heromesh.h"
#include "quarks.h"
#include "cursorshapes.h"
#include "names.h"

MoveItem*replay_list;
size_t replay_size;
Uint16 replay_count,replay_pos,replay_mark;
Uint8 solution_replay=255;
char*best_list;
Sint32 best_score=NO_SCORE;

static volatile Uint8 timerflag;
static int exam_scroll;
static MoveItem*inputs;
static size_t inputs_size;
static int inputs_count;
static Uint8 side_mode=255;
static Uint8 should_record_solution;
static Uint8 should_record_private_solution;
static Uint8 replay_speed;
static Uint8 replay_time;
static Uint8 solved;
static Uint8 inserting,saved_inserting;
static sqlite3_stmt*autowin;
static size_t dum_size; // not used by Free Hero Mesh, but needed by some C library functions.

int encode_move(FILE*fp,MoveItem v) {
  // Encodes a single move and writes the encoded move to the file.
  // Returns the number of bytes of the encoded move.
  if(v>=8 && v<256) {
    fputc(v,fp);
    return 1;
  } else if(v>=0x8000 && v<=0x8FFF) {
    fputc(KEY_XY,fp);
    fputc(((v>>6)&63)+1,fp);
    fputc((v&63)+1,fp);
    return 3;
  } else {
    fatal("Unencodable move (%u)\n",(int)v);
  }
}

int encode_move_list(FILE*fp) {
  // Encodes the current replay list into the file; returns the number of bytes.
  // Does not write a null terminator.
  int i;
  int c=0;
  for(i=0;i<replay_count;i++) c+=encode_move(fp,replay_list[i]);
  return c;
}

MoveItem decode_move(FILE*fp) {
  // Decodes a single move from the file, and returns the move.
  // Returns zero if there is no more moves.
  int v=fgetc(fp);
  if(v>=8 && v<256) {
    return v;
  } else if(v==KEY_XY) {
    v=0x8000|((fgetc(fp)-1)<<6);
    return v|(fgetc(fp)-1);
  } else if(v==EOF || !v) {
    return 0;
  } else {
    fatal("Undecodable move (%u)\n",v);
  }
}

int decode_move_list(FILE*fp) {
  // Decodes a move list from the file, and stores it in replay_list and replay_count.
  // Returns the number of moves (replay_count).
  MoveItem v;
  FILE*o;
  free(replay_list);
  replay_list=0;
  replay_size=0;
  replay_count=0;
  o=open_memstream((char**)&replay_list,&replay_size);
  if(!o) fatal("Allocation failed\n");
  while(replay_count<0xFFFE && (v=decode_move(fp))) {
    fwrite(&v,1,sizeof(MoveItem),o);
    replay_count++;
  }
  fclose(o);
  if(replay_count && !replay_list) fatal("Allocation failed\n");
  return replay_count;
}

#define MSIZ (sizeof(MoveItem))
#define memcpyM(a_,b_,c_) memcpy(a_,b_,(c_)*MSIZ);
#define memmoveM(a_,b_,c_) memmove(a_,b_,(c_)*MSIZ);

static void record_solution(void);

static void setup_game(void) {
  const char*v;
  optionquery[1]=Q_showInventory;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
  side_mode=boolxrm(v,1);
  optionquery[1]=Q_replaySpeed;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
  replay_speed=strtol(v,0,10)?:16;
  optionquery[1]=Q_saveSolutions;
  optionquery[2]=Q_private;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,3)?:"";
  should_record_private_solution=boolxrm(v,0);
  if(main_options['r']) {
    should_record_solution=0;
  } else {
    v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
    should_record_solution=boolxrm(v,0);
  }
  solution_replay=0;
  optionquery[1]=Q_autoWin;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(v && *v) sqlite3_prepare_v3(userdb,v,-1,SQLITE_PREPARE_PERSISTENT,&autowin,0);
}

static void redraw_game(void) {
  char buf[32];
  SDL_Rect r;
  int x,y;
  r.x=r.y=0;
  r.h=screen->h;
  r.w=left_margin;
  SDL_FillRect(screen,&r,0xF0);
  r.x=left_margin-1;
  r.w=1;
  SDL_FillRect(screen,&r,0xF7);
  r.x=left_margin;
  r.w=screen->w-r.x;
  SDL_FillRect(screen,&r,back_color);
  for(x=1;x<=pfwidth;x++) for(y=1;y<=pfheight;y++) draw_cell(x,y);
  x=y=0;
  SDL_GetMouseState(&x,&y);
  SDL_LockSurface(screen);
  if(left_margin>=88) {
    snprintf(buf,32,"%5d/%5d",level_ord,level_nindex);
    draw_text(0,0,buf,0xF0,solution_replay?0xFE:solved?0xFA:0xFC);
    snprintf(buf,32,"%5d",level_id);
    draw_text(0,8,"ID",0xF0,0xF7);
    draw_text(48,8,buf,0xF0,0xFF);
    snprintf(buf,32,"%5d",level_version);
    draw_text(0,16,"VER",0xF0,0xF7);
    draw_text(48,16,buf,0xF0,0xFF);
    snprintf(buf,32,"%5d",level_code);
    draw_text(0,24,"CODE",0xF0,0xF7);
    draw_text(48,24,buf,0xF0,0xFF);
  } else {
    snprintf(buf,32,"%5d",level_ord);
    draw_text(16,0,buf,0xF0,solution_replay?0xFE:solved?0xFA:0xFC);
    snprintf(buf,32,"%5d",level_id);
    draw_text(0,8,"I",0xF0,0xF7);
    draw_text(16,8,buf,0xF0,0xFF);
    snprintf(buf,32,"%5d",level_version);
    draw_text(0,16,"V",0xF0,0xF7);
    draw_text(16,16,buf,0xF0,0xFF);
    snprintf(buf,32,"%5d",level_code);
    draw_text(0,24,"C",0xF0,0xF7);
    draw_text(16,24,buf,0xF0,0xFF);
  }
  if(!gameover) {
    snprintf(buf,8,"%2dx%2d",pfwidth,pfheight);
    draw_text(8,32,buf,0xF0,0xFD);
    draw_text(24,32,"x",0xF0,0xF5);
  } else if(gameover<0) {
    draw_text(4,32,"*LOSE*",0xF4,0xFC);
  } else {
    draw_text(4,32,"*WIN*",0xF2,0xFA);
  }
  x=x>=left_margin?(x-left_margin)/picture_size+1:0;
  y=y/picture_size+1;
  if(x>0 && y>0 && x<=pfwidth && y<=pfheight) snprintf(buf,8,"(%2d,%2d)",x,y);
  else strcpy(buf,"       ");
  draw_text(0,40,buf,0xF0,0xF1);
  if(side_mode) {
    // Inventory
    x=20-(left_margin-picture_size)/8;
    if(x>19) x=19;
    if(x<0) x=0;
    for(y=0;y<ninventory;y++) {
      if(y*picture_size+60>=screen->h) break;
      snprintf(buf,22,"%20d",inventory[y].value);
      draw_text(picture_size,y*picture_size+52,buf+x,0xF8,0xFE);
    }
    SDL_UnlockSurface(screen);
    r.x=0; r.y=52; r.w=picture_size; r.h=screen->h-52;
    SDL_FillRect(screen,&r,inv_back_color);
    for(y=0;y<ninventory;y++) {
      if(y*picture_size+60>=screen->h) break;
      if(classes[inventory[y].class]->nimages<inventory[y].image) continue;
      draw_picture(0,y*picture_size+52,classes[inventory[y].class]->images[inventory[y].image]&0x7FFF);
    }
  } else {
    // Move list
    snprintf(buf,8,"%5d",replay_pos);
    draw_text(8,52,buf,0xF0,0xF9);
    snprintf(buf,8,"%5d",replay_count);
    draw_text(8,screen->h-8,buf,0xF0,solution_replay?0xFA:0xFC);
    for(y=44,x=replay_pos-(screen->h-68)/32;;x++) {
      y+=16;
      if(y+24>screen->h) break;
      if(x>=0 && x<replay_count) {
        if(replay_list[x]<256) {
          draw_key(16,y,replay_list[x],0xF8,0xFB);
        } else if((replay_list[x]&0xF000)==0x8000) {
          sprintf(buf,"%02u",((replay_list[x]>>6)&63)+1);
          draw_text(16,y,buf,0xF8,0x47);
          sprintf(buf,"%02u",(replay_list[x]&63)+1);
          draw_text(16,y+8,buf,0xF8,0x45);
        }
      }
      if(x==replay_count) draw_key(16,y,1,0xF0,0xF8);
      if(x==replay_pos) draw_text(0,y,inserting?"I~":"~~",0xF0,0xFE);
      if(x==replay_mark) draw_text(32,y,"~~",0xF0,0xFD);
    }
    SDL_UnlockSurface(screen);
  }
  if(quiz_text) draw_popup(quiz_text);
  SDL_Flip(screen);
  set_cursor(XC_arrow);
}

static void continue_animation(void) {
  Uint32 n=firstobj;
  Object*o;
  Animation*a;
  DeadAnimation*d;
  int i;
  for(i=0;i<8;i++) if(anim_slot[i].length && ++anim_slot[i].vtime==anim_slot[i].speed && (anim_slot[i].vtime=0,++anim_slot[i].frame==anim_slot[i].length)) anim_slot[i].frame=0;
  while(n!=VOIDLINK) {
    o=objects[n];
    if((a=o->anim) && (a->status&ANISTAT_VISUAL)) {
      i=a->vstep;
      if(a->step[i].flag&ANI_SYNC) {
        i=anim_slot[a->step[i].slot].frame+a->step[i].start;
        if(i!=a->vimage) {
          a->vimage=i;
          draw_cell(o->x,o->y);
        }
      } else if(++a->vtime>=a->step[i].speed) {
        a->vtime=0;
        if(a->vimage==a->step[i].end) {
          if(a->step[i].flag&ANI_ONCE) {
            if(a->vstep==a->lstep) {
              a->status&=~ANISTAT_VISUAL;
            } else {
              if(++a->vstep==max_animation) a->vstep=0;
              a->vimage=a->step[a->vstep].start;
            }
          } else if(a->step[i].flag&ANI_OSC) {
            a->step[i].end=a->step[i].start;
            a->step[i].start=a->vimage;
            goto advance;
          } else {
            a->vimage=a->step[i].start;
          }
        } else {
          advance:
          if(a->step[i].end>=a->step[i].start) ++a->vimage; else --a->vimage;
        }
        draw_cell(o->x,o->y);
      }
    }
    n=o->next;
  }
  if(ndeadanim) {
    for(i=0;i<ndeadanim;i++) {
      d=deadanim+i;
      draw_cell(d->x,d->y);
      if(!d->s.flag) continue;
      if(d->delay) {
        --d->delay;
        continue;
      }
      if(d->vimage<classes[d->class]->nimages)
       draw_picture((d->x-1)*picture_size+left_margin,(d->y-1)*picture_size,classes[d->class]->images[d->vimage]&0x7FFF);
      if(++d->vtime>=d->s.speed) {
        if(d->vimage==d->s.end) d->s.flag=0;
        if(d->s.end>=d->s.start) ++d->vimage; else --d->vimage;
        d->vtime=0;
      }
    }
    for(i=0;i<ndeadanim;i++) while(i<ndeadanim && !deadanim[i].s.flag) {
      draw_cell(deadanim[i].x,deadanim[i].y);
      if(i<ndeadanim-1) deadanim[i]=deadanim[ndeadanim-1];
      --ndeadanim;
    }
  }
  SDL_Flip(screen);
}

static void show_mouse_xy(SDL_Event*ev) {
  char buf[32];
  int x,y;
  x=(ev->motion.x-left_margin)/picture_size+1;
  y=ev->motion.y/picture_size+1;
  if(ev->motion.x<left_margin) {
    if(ev->button.y<48) {
      strcpy(buf,"       ");
    } else if(side_mode) {
      // Inventory
      x=(ev->button.y-52)/picture_size;
      if(x<0 || x>=ninventory) strcpy(buf,"       "); else snprintf(buf,8,"%7d",inventory[x].value);
    } else {
      // Move list
      x=replay_pos+(ev->button.y+4)/16-(screen->h-68)/32-4;
      if(x<0 || x>replay_count) strcpy(buf,"       "); else snprintf(buf,8,"%c%6d",x<replay_pos?0xAE:x>replay_pos?0xAF:0xFE,x);
    }
  } else {
    if(x>0 && y>0 && x<=pfwidth && y<=pfheight) snprintf(buf,8,"(%2d,%2d)",x,y);
    else strcpy(buf,"       ");
  }
  SDL_LockSurface(screen);
  draw_text(0,40,buf,0xF0,0xF1);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
}

static void save_replay(void) {
  unsigned char*buf=0;
  size_t sz=0;
  FILE*fp;
  if(solution_replay || !replay_list || !replay_count) return;
  if(gameover==1) solved=1;
  fp=open_memstream((char**)&buf,&sz);
  if(!fp) fatal("Allocation failed\n");
  fputc(0x00,fp);
  fputc(0x01,fp);
  encode_move_list(fp);
  fputc(0x00,fp);
  if(solved) {
    fputc(0x41,fp);
    fputc(level_version,fp);
    fputc(level_version>>8,fp);
    if(best_list) {
      fputc(0x02,fp);
      fwrite(best_list,1,strlen(best_list)+1,fp);
      fputc(0x81,fp);
      fputc(best_score,fp);
      fputc(best_score>>8,fp);
      fputc(best_score>>16,fp);
      fputc(best_score>>24,fp);
    }
  }
  if(replay_mark) {
    fputc(0x42,fp);
    fputc(replay_mark,fp);
    fputc(replay_mark>>8,fp);
  }
  fclose(fp);
  if(!buf) fatal("Allocation failed\n");
  write_userstate(FIL_LEVEL,level_id,sz,buf);
  free(buf);
}

static void load_replay(void) {
  FILE*fp=0;
  unsigned char*buf=0;
  long sz;
  int i,j;
  free(replay_list);
  replay_list=0;
  replay_count=replay_mark=replay_size=0;
  free(best_list);
  best_list=0;
  if(solution_replay) {
    gameover_score=NO_SCORE;
    if(buf=read_lump(FIL_SOLUTION,level_id,&sz)) {
      fp=fmemopen(buf,sz,"r");
      if(!fp) fatal("Allocation failed\n");
      // Solution format: version (16-bits), flag (8-bits), score (32-bits), user name (null-terminated), timestamp (64-bits), move list
      if(sz>3) {
        i=fgetc(fp); i|=fgetc(fp)<<8;
        if(i==level_version) {
          j=fgetc(fp);
          if(j&128) {
            gameover_score=fgetc(fp);
            gameover_score|=fgetc(fp)<<8;
            gameover_score|=fgetc(fp)<<16;
            gameover_score|=fgetc(fp)<<24;
          }
          if(j&1) while(fgetc(fp)>0);
          if(j&2) for(i=0;i<8;i++) fgetc(fp);
          decode_move_list(fp);
        }
      }
    }
  } else if(buf=read_userstate(FIL_LEVEL,level_id,&sz)) {
    fp=fmemopen(buf,sz,"r");
    if(!fp) fatal("Allocation failed\n");
    best_score=NO_SCORE;
    if(sz>2 && *buf) {
      // Old format
      replay_count=(buf[sz-2]<<8)|buf[sz-1];
      if(sz-replay_count>=4) replay_mark=(buf[replay_count]<<8)|buf[replay_count+1]; else replay_mark=0;
      if(sz-replay_count>=6) {
        i=(buf[replay_count+2]<<8)|buf[replay_count+3];
        if(i==level_version) solved=1;
      }
      replay_list=malloc(replay_size=sizeof(MoveItem)*(replay_count+1));
      if(!replay_list) fatal("Allocation failed\n");
      for(i=0;i<replay_count;i++) replay_list[i]=buf[i];
    } else {
      // New format
      fgetc(fp); // skip first null byte
      while((i=fgetc(fp))!=EOF) switch(i) {
        case 0x01: // Replay list
          if(replay_list) goto skip;
          decode_move_list(fp);
          break;
        case 0x02: // Best list
          if(best_list) goto skip;
          dum_size=0;
          getdelim(&best_list,&dum_size,0,fp);
          break;
        case 0x41: // Solved version
          i=fgetc(fp); i|=fgetc(fp)<<8;
          if(i==level_version) solved=1;
          break;
        case 0x42: // Mark
          replay_mark=fgetc(fp);
          replay_mark|=fgetc(fp)<<8;
          break;
        case 0x81: // Best score
          best_score=fgetc(fp);
          best_score|=fgetc(fp)<<8;
          best_score|=fgetc(fp)<<16;
          best_score|=fgetc(fp)<<24;
          break;
        default: skip:
          if(i<0x40) {
            while(fgetc(fp)>0);
          } else if(i<0x80) {
            fgetc(fp); fgetc(fp);
          } else if(i<0xC0) {
            for(i=0;i<4;i++) fgetc(fp);
          } else {
            for(i=0;i<8;i++) fgetc(fp);
          }
      }
      if(best_list && !solved) {
        free(best_list);
        best_list=0;
        best_score=NO_SCORE;
      }
    }
  }
  if(fp) fclose(fp);
  free(buf);
}

static void begin_level(int id) {
  const char*t;
  replay_time=0;
  if(replay_count) save_replay();
  inputs_count=0;
  replay_pos=0;
  solved=0;
  inserting=0;
  t=load_level(id)?:init_level();
  load_replay();
  if(t) {
    gameover=-1;
    screen_message(t);
  } else {
    gameover=0;
  }
  timerflag=0;
}

static inline void exam_value(const char*t,int y,Value v) {
  char buf[256];
  int i;
  y=(y-exam_scroll)*8;
  if(y<0 || y>screen->h-8) return;
  draw_text(0,y,t,0xF0,0xF7);
  switch(v.t) {
    case TY_NUMBER:
      snprintf(buf,255,"%12lu  0x%08lX  %ld",(long)v.u,(long)v.u,(long)v.s);
      draw_text(200,y,buf,0xF0,0xFE);
      break;
    case TY_CLASS:
      draw_text(200,y,"$",0xF0,0xFB);
      draw_text(208,y,classes[v.u]->name,0xF0,0xFB);
      break;
    case TY_MESSAGE:
      snprintf(buf,255,"%s%s",v.u<256?"":"#",v.u<256?standard_message_names[v.u]:messages[v.u-256]);
      draw_text(200,y,buf,0xF0,0xFD);
      break;
    case TY_LEVELSTRING: case TY_STRING:
      draw_text(200,y,"<String>",0xF0,0xF9);
      break;
    case TY_SOUND: case TY_USOUND:
      draw_text(200,y,"<Sound>",0xF0,0xF6);
      break;
    case TY_MARK:
      draw_text(200,y,"<Mark>",0xF0,0xF3);
      break;
    case TY_ARRAY:
      draw_text(200,y,"<Array>",0xF0,0xF9);
      snprintf(buf,255,"0x%08lX",(long)v.u);
      draw_text(264,y,buf,0xF0,0xFE);
      break;
    default:
      snprintf(buf,80,"<%lu:%lu>",(long)v.u,(long)v.t);
      draw_text(200,y,buf,0xF0,0xFA);
      i=strlen(buf)*8+208;
      if(v.u<nobjects && objects[v.u] && objects[v.u]->generation==v.t) {
        snprintf(buf,80,"@ (%d,%d)",objects[v.u]->x,objects[v.u]->y);
        draw_text(i,y,buf,0xF0,0xF2);
      } else {
        draw_text(i,y,"(dead)",0xF0,0xF4);
      }
      break;
  }
}

static inline void exam_flags(int y,Uint16 v) {
  y=(y-exam_scroll)*8;
  if(y<0 || y>screen->h-8) return;
  draw_text(0,y,"Flags:",0xF0,0xF7);
  draw_text(200,y,"--- --- --- --- --- --- --- --- --- --- --- --- --- ---",0xF0,0xF8);
  if(v&OF_INVISIBLE) draw_text(200,y,"Inv",0xF0,0xFF);
  if(v&OF_VISUALONLY) draw_text(232,y,"Vis",0xF0,0xFF);
  if(v&OF_STEALTHY) draw_text(264,y,"Stl",0xF0,0xFF);
  if(v&OF_BUSY) draw_text(296,y,"Bus",0xF0,0xFF);
  if(v&OF_USERSTATE) draw_text(328,y,"Ust",0xF0,0xFF);
  if(v&OF_USERSIGNAL) draw_text(360,y,"Usg",0xF0,0xFF);
  if(v&OF_MOVED) draw_text(392,y,"Mov",0xF0,0xFF);
  if(v&OF_DONE) draw_text(424,y,"Don",0xF0,0xFF);
  if(v&OF_KEYCLEARED) draw_text(456,y,"Key",0xF0,0xFF);
  if(v&OF_DESTROYED) draw_text(488,y,"Des",0xF0,0xFF);
  if(v&OF_BIZARRO) draw_text(520,y,"Biz",0xF0,0xFF);
  if(v&OF_CONNECTION) draw_text(552,y,"Con",0xF0,0xFF);
  if(v&OF_MOVING) draw_text(584,y,"Mvi",0xF0,0xFF);
  if(v&OF_ORDERED) draw_text(616,y,"Ord",0xF0,0xFF);
}

static inline void exam_hardsharp(const char*t,int y,Uint16*v) {
  int i;
  char buf[16];
  y=(y-exam_scroll)*8;
  if(y<0 || y>screen->h-8) return;
  draw_text(0,y,t,0xF0,0xF7);
  for(i=0;i<4;i++) {
    snprintf(buf,8,"%5u",v[i]);
    draw_text(200+i*56,y,buf,0xF0,v[i]?0xFF:0xF8);
  }
}

static void draw_back_line(int y,int c) {
  unsigned char*p=screen->pixels;
  int i;
  p+=screen->pitch*y;
  for(i=0;i<screen->w;i++) if(p[i]==0xF0 || p[i]==0xF1) p[i]=i&1?c:0xF0;
}

static void examine(Uint32 n) {
  sqlite3_stmt*st;
  SDL_Event ev;
  SDL_Rect r;
  Object*o;
  int i,y;
  y=0;
  i=sqlite3_prepare_v2(userdb,"SELECT '%'||`NAME`,`ID`&0xFFFF FROM `VARIABLES` WHERE `ID` BETWEEN ?1 AND (?1|0xFFFF) ORDER BY `ID`",-1,&st,0);
  if(i) fatal("SQL error (%d): %s",i,sqlite3_errmsg(userdb));
  object:
  if(n==VOIDLINK) return;
  o=objects[n];
  if(!o) return;
  sqlite3_bind_int(st,1,o->class<<16);
  exam_scroll=0;
  redraw:
  set_cursor(XC_arrow);
  r.x=r.y=0;
  r.w=screen->w;
  r.h=screen->h;
  SDL_FillRect(screen,&r,0xF0);
  SDL_LockSurface(screen);
  exam_value("Self:",0,OVALUE(n));
  exam_value("Class:",1,CVALUE(o->class));
  exam_value("Image:",2,NVALUE(o->image));
  if(classes[o->class]->cflags&CF_QUIZ) goto quiz;
  exam_value("Dir:",3,NVALUE(o->dir));
  exam_value("Misc1:",4,o->misc1);
  exam_value("Misc2:",5,o->misc2);
  exam_value("Misc3:",6,o->misc3);
  exam_value("Misc4:",7,o->misc4);
  exam_value("Misc5:",8,o->misc5);
  exam_value("Misc6:",9,o->misc6);
  exam_value("Misc7:",10,o->misc7);
  exam_value("Temperature:",11,NVALUE(o->temperature));
  exam_flags(12,o->oflags);
  exam_value("Density:",13,NVALUE(o->density));
  exam_value("Volume:",14,NVALUE(o->volume));
  exam_value("Strength:",15,NVALUE(o->strength));
  exam_value("Weight:",16,NVALUE(o->weight));
  exam_value("Climb:",17,NVALUE(o->climb));
  exam_value("Height:",18,NVALUE(o->height));
  exam_value("Arrivals:",19,NVALUE(o->arrivals));
  exam_value("Departures:",20,NVALUE(o->departures));
  exam_value("Shape:",21,NVALUE(o->shape));
  exam_value("Shovable:",22,NVALUE(o->shovable));
  exam_value("Distance:",23,NVALUE(o->distance));
  exam_value("Inertia:",24,NVALUE(o->inertia));
  exam_hardsharp("Hardness:",25,o->hard);
  exam_hardsharp("Sharpness:",26,o->sharp);
  exam_value("NextR:",27,o->replacement);
  while(sqlite3_step(st)==SQLITE_ROW) {
    i=sqlite3_column_int(st,1);
    exam_value(sqlite3_column_text(st,0),i+29,o->uservars[i]);
  }
  quiz:
  sqlite3_reset(st);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) switch(ev.type) {
    case SDL_KEYDOWN:
      switch(ev.key.keysym.sym) {
        case SDLK_ESCAPE: case SDLK_RETURN: case SDLK_KP_ENTER: sqlite3_finalize(st); return;
        case SDLK_UP: if(exam_scroll) exam_scroll--; break;
        case SDLK_DOWN: exam_scroll++; break;
        case SDLK_HOME: exam_scroll=0; break;
        case SDLK_PAGEUP: exam_scroll-=screen->h/8; if(exam_scroll<0) exam_scroll=0; break;
        case SDLK_PAGEDOWN: exam_scroll+=screen->h/8; break;
        case SDLK_F1: case SDLK_g: if(classes[o->class]->gamehelp) modal_draw_popup(classes[o->class]->gamehelp); break;
        case SDLK_F2: case SDLK_h: if(classes[o->class]->edithelp) modal_draw_popup(classes[o->class]->edithelp); break;
        case SDLK_1: case SDLK_4: if(o->misc1.t==TY_LEVELSTRING) modal_draw_popup(value_string_ptr(o->misc1)); break;
        case SDLK_2: case SDLK_5: if(o->misc2.t==TY_LEVELSTRING) modal_draw_popup(value_string_ptr(o->misc2)); break;
        case SDLK_3: case SDLK_6: if(o->misc3.t==TY_LEVELSTRING) modal_draw_popup(value_string_ptr(o->misc3)); break;
      }
      goto redraw;
    case SDL_MOUSEMOTION:
      if(ev.motion.y!=y && ev.motion.y<screen->h) {
        SDL_LockSurface(screen);
        draw_back_line(y,0xF0);
        draw_back_line(y=ev.motion.y,0xF1);
        SDL_UnlockSurface(screen);
        SDL_Flip(screen);
      }
      break;
    case SDL_VIDEOEXPOSE:
      goto redraw;
    case SDL_QUIT:
      exit(0);
      break;
  }
}

static void global_examine(void) {
  sqlite3_stmt*st;
  SDL_Event ev;
  SDL_Rect r;
  int i,y;
  y=0;
  i=sqlite3_prepare_v2(userdb,"SELECT '@'||`NAME`,`ID` FROM `VARIABLES` WHERE `ID` BETWEEN 0x0000 AND 0xFFFF ORDER BY `ID`",-1,&st,0);
  if(i) fatal("SQL error (%d): %s",i,sqlite3_errmsg(userdb));
  exam_scroll=0;
  redraw:
  set_cursor(XC_arrow);
  r.x=r.y=0;
  r.w=screen->w;
  r.h=screen->h;
  SDL_FillRect(screen,&r,0xF0);
  SDL_LockSurface(screen);
  exam_value("MoveNumber:",0,NVALUE(move_number));
  exam_value("LevelStrings:",1,NVALUE(nlevelstrings));
  exam_value("Generation:",2,NVALUE(generation_number));
  while(sqlite3_step(st)==SQLITE_ROW) {
    i=sqlite3_column_int(st,1);
    exam_value(sqlite3_column_text(st,0),i+4,globals[i]);
  }
  sqlite3_reset(st);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) switch(ev.type) {
    case SDL_KEYDOWN:
      switch(ev.key.keysym.sym) {
        case SDLK_ESCAPE: case SDLK_RETURN: case SDLK_KP_ENTER: sqlite3_finalize(st); return;
        case SDLK_UP: if(exam_scroll) exam_scroll--; break;
        case SDLK_DOWN: exam_scroll++; break;
        case SDLK_HOME: exam_scroll=0; break;
        case SDLK_PAGEUP: exam_scroll-=screen->h/8; if(exam_scroll<0) exam_scroll=0; break;
        case SDLK_PAGEDOWN: exam_scroll+=screen->h/8; break;
      }
      goto redraw;
    case SDL_MOUSEMOTION:
      if(ev.motion.y!=y && ev.motion.y<screen->h) {
        SDL_LockSurface(screen);
        draw_back_line(y,0xF0);
        draw_back_line(y=ev.motion.y,0xF1);
        SDL_UnlockSurface(screen);
        SDL_Flip(screen);
      }
      break;
    case SDL_VIDEOEXPOSE:
      goto redraw;
    case SDL_QUIT:
      exit(0);
      break;
  }
}

static void list_objects_at(int xy,Uint32*pf,const char*s) {
  static const char*const dirs[8]={"E ","NE","N ","NW","W ","SW","S ","SE"};
  SDL_Event ev;
  SDL_Rect r;
  char buf[256];
  int scroll=0;
  int count=0;
  Uint32 n,t;
  Object*o;
  int i,j;
  if(xy<0 || xy>=64*64) return;
  n=pf[xy];
  if(n==VOIDLINK) return;
  while(n!=VOIDLINK) t=n,count++,n=objects[n]->up;
  redraw:
  r.x=r.y=0;
  r.w=screen->w;
  r.h=screen->h;
  SDL_FillRect(screen,&r,0xF1);
  r.y=8;
  r.h-=8;
  scrollbar(&scroll,r.h/8,count,0,&r);
  snprintf(buf,255," %d %sobjects at (%d,%d): ",count,s,(xy&63)+1,(xy/64)+1);
  SDL_LockSurface(screen);
  draw_text(0,0,buf,0xF7,0xF0);
  n=t;
  for(i=0;i<scroll && n!=VOIDLINK;i++) n=objects[n]->down;
  for(i=0;i<screen->h/8 && n!=VOIDLINK;i++) {
    o=objects[n];
    snprintf(buf,255," %8d: %-14.14s %3d %s",n,classes[o->class]->name,o->image,classes[o->class]->cflags&CF_QUIZ?"":dirs[o->dir&7]);
    draw_text(24,r.y,buf,0xF1,o->generation?(classes[o->class]->cflags&CF_PLAYER?0xFE:0xFF):0xF8);
    n=o->down;
    r.y+=8;
  }
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    if(ev.type!=SDL_VIDEOEXPOSE) {
      r.h=screen->h-8;
      r.x=0;
      r.y=8;
      if(scrollbar(&scroll,r.h/8,count,&ev,&r)) goto redraw;
    }
    switch(ev.type) {
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.button!=1 || ev.button.y<8) break;
        j=ev.button.y/8-scroll-1;
        if(j>=count) break;
        n=t;
        for(i=0;i<j;i++) n=objects[n]->down;
        examine(n);
        set_cursor(XC_draft_small);
        goto redraw;
      case SDL_MOUSEMOTION:
        set_cursor(XC_draft_small);
        break;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_ESCAPE: case SDLK_RETURN: case SDLK_KP_ENTER: return;
        }
        goto redraw;
      case SDL_VIDEOEXPOSE:
        goto redraw;
      case SDL_QUIT:
        exit(0);
        break;
    }
  }
}

static void describe_at(int xy) {
  unsigned char*s;
  Uint32 n;
  if(xy<0 || xy>=64*64) return;
  n=playfield[xy];
  if(n==VOIDLINK) return;
  while(n!=VOIDLINK && objects[n]->up!=VOIDLINK) n=objects[n]->up;
  while(n!=VOIDLINK && !classes[objects[n]->class]->gamehelp) n=objects[n]->down;
  if(n==VOIDLINK) return;
  if(classes[objects[n]->class]->gamehelp[0]==16 && !classes[objects[n]->class]->gamehelp[1]) {
    if(objects[n]->misc1.t!=TY_LEVELSTRING || objects[n]->misc1.u>=nlevelstrings) return;
    modal_draw_popup(levelstrings[objects[n]->misc1.u]);
    return;
  }
  s=sqlite3_mprintf("\x0C\x0E%s:%d\\ %s\x0B\x0F%s",classes[objects[n]->class]->name,objects[n]->image,classes[objects[n]->class]->name,classes[objects[n]->class]->gamehelp);
  if(!s) fatal("Allocation failed\n");
  modal_draw_popup(s);
  sqlite3_free(s);
}

static void describe_inventory(int n) {
  unsigned char*s;
  if(n<0 || n>=ninventory) return;
  if(!classes[inventory[n].class]->gamehelp) return;
  if(classes[inventory[n].class]->gamehelp[0]==16) return;
  s=sqlite3_mprintf("\x0C\x0E%s:%d\\ %s\x0B\x0F%s",classes[inventory[n].class]->name,inventory[n].image,classes[inventory[n].class]->name,classes[inventory[n].class]->gamehelp);
  if(!s) fatal("Allocation failed\n");
  modal_draw_popup(s);
  sqlite3_free(s);
}

static void do_import_moves(const char*arg) {
  FILE*fp;
  int i;
  if(!arg || !arg[strspn(arg," \t")]) return;
  fp=popen(arg,"r");
  if(!fp) {
    screen_message("Unable to open pipe for reading");
    return;
  }
  replay_mark=0;
  decode_move_list(fp);
  pclose(fp);
}

static void do_export_moves(const char*arg) {
  FILE*fp;
  int i;
  if(!arg || !arg[strspn(arg," \t")]) return;
  fp=popen(arg,"w");
  if(!fp) {
    screen_message("Unable to open pipe for writing");
    return;
  }
  encode_move_list(fp);
  pclose(fp);
}

static void do_load_moves(sqlite3_stmt*st) {
  FILE*fp;
  int i=sqlite3_column_bytes(st,1);
  fp=fmemopen((char*)sqlite3_column_blob(st,1)?:"",i,"r");
  if(!fp) fatal("Allocation failed\n");
  decode_move_list(fp);
  fclose(fp);
}

static int copy_text_to_plain(unsigned char*out,int maxlen,const unsigned char*in) {
  int at=0;
  if(!in) {
    *out=0;
    return 0;
  }
  while(*in && at<maxlen) switch(*in) {
    case 10: if(at && out[at-1]!=32) out[at++]=32; in++; break;
    case 14: case 30: in=strchrnul(in,'\\'); if(*in) in++; break;
    case 31: in++; if(*in) out[at++]=*in++; break;
    case 32 ... 255: out[at++]=*in++; break;
    default: in++;
  }
  out[at]=0;
  return at;
}

static inline void levels_column(int x,int y,int n,int bg,sqlite3_stmt*st,char*buf) {
  const DisplayColumn*dc=ll_disp+n;
  Uint8 co=dc->color;
  int w=dc->width;
  int nc=dc->data+(dc->flag&4?0:8);
  int t=sqlite3_column_type(st,nc);
  int a=0;
  int i;
  const char*p;
  sqlite3_int64 v;
  if(t==SQLITE_NULL) return;
  if(dc->flag&1) w=255;
  if(t==SQLITE_BLOB || t==SQLITE_TEXT) {
    blob:
    if(p=sqlite3_column_text(st,nc)) i=snprintf(buf,w,"%s",p); else *buf=i=0;
    if(dc->form[0]=='R' && i<w) a=w-i;
    if(dc->flag&2) co=0xFF;
  } else {
    // This implementation does not check that the format is necessarily valid.
    // You should not rely on the use of any undocumented format.
    v=sqlite3_column_int64(st,nc);
    if(dc->flag&2) {
      co=0xFF;
      for(i=0;i<dc->color;i++) if(ll_code[dc->ptr+i]>=0xFF00 || v+128<=(ll_code[dc->ptr+i]>>8)) {
        co=ll_code[dc->ptr+i]&0xFF;
        break;
      }
    }
    switch(dc->form[0]) {
      case 'L': case 'R':
        if(dc->form[1]) {
          if(v<0 || v>w) v=w;
          memset(buf,dc->form[1],v);
          buf[v]=0;
          if(dc->form[0]=='R') a=w-v;
        } else {
          goto blob;
        }
        break;
      case 'd':
        snprintf(buf,w+1,"%*lld",w,(long long)(dc->form[1] && v<0?-v:v));
        if(dc->form[1]=='-') *buf=v<0?'-':' ';
        if(dc->form[1]=='+') *buf=v<0?'-':'+';
        break;
      case 'o':
        snprintf(buf,w+1,dc->form[1]?"%0*llo":"%*llo",w,(unsigned long long)v);
        break;
      case 'u':
        snprintf(buf,w+1,dc->form[1]?"%0*llu":"%*llu",w,(unsigned long long)v);
        break;
      case 'x':
        snprintf(buf,w+1,dc->form[1]?"%0*llx":"%*llx",w,(unsigned long long)v);
        break;
      case 'X':
        snprintf(buf,w+1,dc->form[1]?"%0*llX":"%*llX",w,(unsigned long long)v);
        break;
      default: *buf=0;
    }
  }
  draw_text(a*8+x,y,buf,bg,co);
}

static int list_levels(void) {
  static Sint8 mo=-1;
  static Uint8 columns=0;
  static int scroll=0;
  static Uint16 divmin,divmax; // 1 less than level order number
  sqlite3_stmt*st;
  SDL_Event ev;
  SDL_Rect r;
  const char*v;
  int scrmax=0;
  int sel=level_ord-1;
  int b,i,j,x,y;
  char buf[256];
  char rescroll=2;
  Uint16 ndiv;
  if(mo<0) {
    optionquery[1]=Q_listMode;
    v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
    mo=strtol(v,0,16);
    if(mo<0) mo=0;
  }
  if(!columns) {
    optionquery[1]=Q_listColumns;
    v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
    i=strtol(v,0,10);
    j=(screen->w-16)/070;
    if(i<1) i=j;
    if(i>j) i=j;
    if(i>255) i=255;
    columns=i;
  }
  if(!ll_head) ll_head="\xB3W \xB3H \xB3 TITLE";
  // ID, ORD, CODE, WIDTH, HEIGHT, TITLE, SOLVED, SOLVABLE, ...
  prepare:
  if(sqlite3_prepare_v2(userdb,mo&4
   ?"WITH D(F,L,H,R) AS (SELECT `FIRST`, LEAD(`FIRST`,1,MAX_LEVEL()+1) OVER (ORDER BY `FIRST`,_ROWID_), `HEADING`, _ROWID_ FROM `DIVISIONS`)"
    " SELECT F, L, H, PRINTF('(%d/%d)',(SELECT COUNT() FROM `LEVELS` WHERE `ORD` >= F AND `ORD` < L AND `SOLVED`),L-F) FROM D WHERE F <> L ORDER BY F, R;"
   :"SELECT * FROM `LEVELS` WHERE `ORD` NOT NULL AND `ORD` >= ?1 AND `ORD` < ?2 ORDER BY `ORD`;"
   ,-1,&st,0)) {
    screen_message(sqlite3_errmsg(userdb));
    return 0;
  }
  if(mo&4) {
    ndiv=0;
    while(sqlite3_step(st)==SQLITE_ROW) ndiv++;
    if(!ndiv) {
      mo&=3;
      sqlite3_finalize(st);
      goto prepare;
    }
  }
  set_cursor(XC_arrow);
  redraw:
  if(sel<0) sel=0;
  if(sel>=level_nindex) sel=level_nindex-1;
  if((mo&8) && sel>=divmax-divmin) sel=divmax-divmin-1;
  SDL_FillRect(screen,0,0x02);
  r.x=r.y=0;
  r.w=screen->w;
  r.h=24;
  SDL_FillRect(screen,&r,0xF7);
  SDL_LockSurface(screen);
  draw_text(0,0,"<LMB/\x18\x19\x1A\x1B> Select  <MMB/SP> Title  <RMB/RET> Play  <0-9> Find  <ESC> Cancel",0xF7,0xF0);
  draw_text(0,8,"<F1> Wide/Tall  <F2> ID/Ord  <F3> Divisions  <F4> All",0xF7,0xF0);
  if(mo&8) draw_text(0,16,"\x15",0xF7,0xF0);
  reset:
  sqlite3_reset(st);
  sqlite3_bind_int(st,2,mo&8?divmax:level_nindex+1);
  if(mo&4) {
    scrmax=ndiv;
    for(i=0;i<scroll;i++) if(sqlite3_step(st)!=SQLITE_ROW) sqlite3_reset(st),scroll=i-1;
    for(j=0,y=24;y<screen->h-7;y+=8,j++) {
      i=sqlite3_step(st);
      if(i!=SQLITE_ROW) break;
      if(rescroll==2 && sel>=sqlite3_column_int(st,0) && sel<sqlite3_column_int(st,1)) {
        found:
        sel=j;
        rescroll=1;
        goto reset;
      }
      if(j==sel) {
        divmin=sqlite3_column_int(st,0);
        divmax=sqlite3_column_int(st,1);
        r.x=16; r.y=y;
        r.w=screen->w-16; r.h=8;
        SDL_FillRect(screen,&r,0xF8);
        draw_text(2*8,y,"\x10",b=0xF8,0xFE);
        snprintf(buf,100,"#%d  (%d - %d)",sel+1,divmin,divmax-1);
        draw_text(80,16,buf,0xF7,0xFE);
      } else {
        draw_text(2*8,y,"\xB3",b=0x02,0xF8);
      }
      if(v=sqlite3_column_text(st,2)) {
        x=sqlite3_column_bytes(st,2);
        draw_text(4*8,y,v,b,0xFF);
        if(v=sqlite3_column_text(st,3)) draw_text((x+5)*8,y,v,b,0xFB);
      }
    }
    if(rescroll) {
      if(rescroll==2 && i==SQLITE_ROW) {
        while((i=sqlite3_step(st))==SQLITE_ROW) {
          j++;
          if(sel>=sqlite3_column_int(st,0) && sel<sqlite3_column_int(st,1)) goto found;
        }
      }
      if(sel<scroll) scroll=sel;
      if(sel>=scroll+screen->h/8-3) scroll=sel+4-screen->h/8;
      rescroll=0;
    }
    if(y<screen->h-7 && sel>=j) {
      sel=j-1;
      goto reset;
    }
  } else if(mo&1) {
    scrmax=mo&8?(divmax-divmin):level_nindex;
    draw_text(16+060,16,ll_head,0xF7,0xF1);
    draw_text(16,16,mo&2?"\xB3 ID  ":"\xB3 ORD ",0xF7,0xF1);
    if(rescroll) {
      if(sel<scroll) scroll=sel;
      if(sel>=scroll+screen->h/8-3) scroll=sel+4-screen->h/8;
      rescroll=0;
    }
    sqlite3_bind_int(st,1,scroll+(mo&8?divmin:1));
    for(y=24;y<screen->h-7;y+=8) {
      if(sqlite3_step(st)!=SQLITE_ROW) break;
      i=sqlite3_column_int(st,1);
      if(mo&8) i-=divmin-1;
      if(i-1==sel) {
        r.x=16; r.y=y;
        r.w=screen->w-16; r.h=8;
        SDL_FillRect(screen,&r,0xF8);
        draw_text(2*8,y,"\x10",b=0xF8,0xFE);
      } else {
        draw_text(2*8,y,"\xB3",b=0x02,0xF8);
      }
      if(mo&8) i+=divmin-1;
      snprintf(buf,6,"%5u",mo&2?sqlite3_column_int(st,0):i);
      draw_text(3*8,y,buf,b,sqlite3_column_int(st,6)?0xFA:0xFC);
      draw_text(8*8,y,"\xB3",b,b^0xFA);
      if(ll_ndisp) {
        x=9*8;
        for(i=0;i<ll_ndisp && x<screen->w;i++) {
          levels_column(x,y,i,b,st,buf);
          x+=ll_disp[i].width*8+8;
          if(x<screen->w && !(ll_disp[i].flag&1)) draw_text(x-8,y,"\xB3",b,b^0xFA);
        }
      } else {
        snprintf(buf,6,"%2u %2u",sqlite3_column_int(st,3),sqlite3_column_int(st,4));
        draw_text(9*8,y,buf,b,0xFF);
        draw_text(11*8,y,"\xB3",b,b^0xFA);
        draw_text(14*8,y,"\xB3",b,b^0xFA);
        copy_text_to_plain(buf,255,sqlite3_column_text(st,5));
        draw_text(15*8,y,buf,b,0xFF);
      }
    }
  } else {
    scrmax=((mo&8?divmax-divmin:level_nindex)+columns-1)/columns;
    draw_text(8,16,mo&2?"(ID)":"(Ord)",0xF7,0xF1);
    if(rescroll) {
      if(sel<scroll*columns) scroll=sel/columns;
      if(sel>(scroll+screen->h/8-3)*columns) scroll=(sel+3-screen->h/8)/columns;
      rescroll=0;
    }
    sqlite3_bind_int(st,1,scroll*columns+(mo&8?divmin:1));
    for(y=24;y<screen->h-7;y+=8) for(x=0;x<columns;x++) {
      if(sqlite3_step(st)!=SQLITE_ROW) goto done;
      i=sqlite3_column_int(st,1);
      if(mo&8) i-=divmin-1;
      if(i-1==sel) {
        draw_text(x*070+16,y,"\x10",b=0xF8,0xFE);
        snprintf(buf,128,"[ ID=%u Ord=%u Code=%u Size=%ux%u ]",sqlite3_column_int(st,0),i,sqlite3_column_int(st,2),sqlite3_column_int(st,3),sqlite3_column_int(st,4));
        draw_text(80,16,buf,0xF7,0xFE);
      } else {
        draw_text(x*070+16,y," ",b=0x02,0x02);
      }
      if(mo&8) i+=divmin-1;
      snprintf(buf,7,"%5u ",mo&2?sqlite3_column_int(st,0):i);
      draw_text(x*070+24,y,buf,b,sqlite3_column_int(st,6)?0xFA:0xFC);
      if(!sqlite3_column_int(st,7)) draw_text(x*070+64,y,"\a",b,b==0xF8?0xF1:0xF9);
    }
  }
  done:
  SDL_UnlockSurface(screen);
  r.x=r.w=0; r.y=24; r.h=screen->h-24;
  scrollbar(&scroll,screen->h/8-3,scrmax,0,&r);
  SDL_Flip(screen);
  sqlite3_reset(st);
  while(SDL_WaitEvent(&ev)) {
    if(ev.type!=SDL_VIDEOEXPOSE && scrollbar(&scroll,screen->h/8-3,scrmax,&ev,&r)) goto redraw;
    switch(ev.type) {
      case SDL_MOUSEMOTION:
        set_cursor(XC_arrow);
        break;
      case SDL_KEYDOWN:
        if((ev.key.keysym.mod&KMOD_NUM) && ev.key.keysym.sym>=SDLK_KP0 && ev.key.keysym.sym<=SDLK_KP9) goto digit;
        switch(ev.key.keysym.sym) {
          case SDLK_ESCAPE: i=0; goto final;
          //TODO: Change the scroll to approximately the middle, instead of zero
          case SDLK_F1: scroll=0; mo^=1; rescroll=1; goto redraw;
          case SDLK_F2: mo^=2; goto redraw;
          case SDLK_F3:
            mo&=~8;
            if(mo&4) break;
            mo|=4;
            rescroll=2;
            sqlite3_finalize(st);
            goto prepare;
          case SDLK_F4:
            if(mo&8) {
              sel+=divmin-1;
              divmin=0;
            }
            if(mo&4) {
              sqlite3_finalize(st);
              mo&=3;
              sel=level_ord-1;
              goto prepare;
            }
            mo&=3;
            goto redraw;
          case SDLK_SPACE: title:
            if(mo&4) break;
            sqlite3_reset(st);
            sqlite3_bind_int(st,1,sel+1);
            if(sqlite3_step(st)==SQLITE_ROW) {
              v=sqlite3_column_text(st,5); // not Unicode, but add the null terminator
              if(v) modal_draw_popup(v);
            }
            goto redraw;
          case SDLK_0 ... SDLK_9: digit:
            if(mo&4) break;
            SDL_PushEvent(&ev);
            if(v=screen_prompt("Find?")) {
              if(!*v) goto redraw;
              i=strtol(v,0,10);
              if(mo&2) {
                for(j=0;j<level_nindex;j++) if(level_index[j]==i) sel=j;
              } else {
                if(i>0 && i<=level_nindex) sel=i-1;
              }
              rescroll=1;
            }
            goto redraw;
          case SDLK_DOWN: case SDLK_KP2: case SDLK_j:
            sel+=mo&5?1:columns;
            rescroll=1; goto redraw;
          case SDLK_UP: case SDLK_KP8: case SDLK_k:
            sel-=mo&5?1:columns;
            rescroll=1; goto redraw;
          case SDLK_LEFT: case SDLK_KP4: case SDLK_h:
            sel--;
            rescroll=1; goto redraw;
          case SDLK_RIGHT: case SDLK_KP6: case SDLK_l:
            sel++;
            rescroll=1; goto redraw;
          case SDLK_PAGEUP: case SDLK_KP9:
            sel-=(screen->h/8-3)*(mo&5?1:columns);
            scroll-=(screen->h/8-3)*(mo&5?1:columns);
            rescroll=1; goto redraw;
          case SDLK_PAGEDOWN: case SDLK_KP3:
            sel+=(screen->h/8-3)*(mo&5?1:columns);
            scroll+=(screen->h/8-3)*(mo&5?1:columns);
            rescroll=1; goto redraw;
          case SDLK_HOME: case SDLK_KP7: sel=scroll=0; rescroll=1; goto redraw;
          case SDLK_END:
            sel=mo&8?divmax-divmin-1:level_nindex-1;
            rescroll=1; goto redraw;
          case SDLK_RETURN: case SDLK_KP_ENTER: play:
            if(mo&4) {
              mo=(mo|8)&~4;
              sel=0;
              sqlite3_finalize(st);
              goto prepare;
            }
            i=1; goto final;
        }
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.x>=16 && ev.button.y>=24) {
          if(mo&1) i=scroll+ev.button.y/8-3;
          else i=(scroll+ev.button.y/8-3)*columns+(ev.button.x-16)/070;
          if(i>=0 && i<level_nindex) sel=i;
          if(ev.button.button==2) goto title;
          if(ev.button.button==3) goto play;
          goto redraw;
        }
        break;
      case SDL_VIDEOEXPOSE: goto redraw;
      case SDL_QUIT: exit(0); break;
    }
  }
  i=-1;
  final:
  sqlite3_finalize(st);
  if(i==1) begin_level(mo&8?-sel-divmin:~sel);
  return i;
}

static int game_command(int prev,int cmd,int number,int argc,sqlite3_stmt*args,void*aux) {
  switch(cmd) {
    case '\' ': play: // Play a move
      if(replay_time) {
        replay_time=0;
        return -3;
      }
      if(solution_replay) {
        screen_message("You cannot play your own moves during the solution replay");
        return -3;
      }
      if(inputs_count*MSIZ>=inputs_size) {
        inputs=realloc(inputs,inputs_size+=32*MSIZ);
        if(!inputs) fatal("Allocation failed\n");
      }
      inputs[inputs_count++]=number;
      return 0;
    case '+ ': replay: // Replay
      saved_inserting=inserting; inserting=0;
      replay_time=0;
      if(number>replay_count-replay_pos) number=replay_count-replay_pos;
      if(number<=0) return prev;
      if((inputs_count+number)*MSIZ>=inputs_size) {
        inputs=realloc(inputs,inputs_size+=(number+1)*MSIZ);
        if(!inputs) fatal("Allocation failed\n");
      }
      memcpyM(inputs+inputs_count,replay_list+replay_pos,number);
      inputs_count+=number;
      return 0;
    case '- ': // Rewind
      saved_inserting=inserting;
      number=replay_pos-number;
      if(number<0) number=0;
      //fallthru
    case '= ': restart: // Restart
      begin_level(level_id);
      if(!number) return 1;
      if(number>replay_count) number=replay_count;
      if(number*MSIZ>=inputs_size) {
        inputs=realloc(inputs,inputs_size=(number+1)*MSIZ);
        if(!inputs) fatal("Allocation failed\n");
      }
      memcpyM(inputs,replay_list,inputs_count=number);
      no_dead_anim=1;
      return 1;
    case '^<': // Rewind to mark
      number=replay_mark;
      goto restart;
    case '^>': // Replay to mark
      inputs_count=0;
      number=replay_mark-replay_pos;
      goto replay;
    case '^-': // Delete move
      inputs_count=0;
      if(solution_replay) {
        screen_message("You cannot delete moves during the solution replay");
        return -3;
      }
      if(replay_pos==replay_count) return 0;
      memmoveM(replay_list+replay_pos,replay_list+replay_pos+1,replay_count-replay_pos-1);
      replay_count--;
      if(replay_mark>replay_pos) replay_mark--;
      return 0;
    case '^+': // Insert moves
      if(solution_replay) return 0;
      inputs_count=0;
      inserting^=1;
      return 0;
    case '^E': // Edit
      return main_options['r']?1:-2;
    case '^I': // Toggle inventory display
      side_mode^=1;
      return prev;
    case '^L': // List levels
      return list_levels();
    case '^M': // Mark replay position
      replay_mark=replay_pos+inputs_count;
      return prev;
    case '^Q': // Quit
      return -1;
    case '^S': // Save solution
      if(gameover==1) record_solution();
      return 1;
    case '^T': // Show title
      modal_draw_popup(level_title);
      return prev;
    case '^Y': // Sound test
      sound_test();
      return prev;
    case '^d': // Describe object
      describe_at(number-65);
      return prev;
    case '^g': // Display global variables
      global_examine();
      return prev;
    case '^n': // List objects (bizarro)
      list_objects_at(number-65,bizplayfield,"bizarro ");
      return prev;
    case '^o': // List objects
      list_objects_at(number-65,playfield,"");
      return prev;
    case '^p': // Slow replay
      replay_time=replay_time?0:1;
      return 0;
    case '^s': // Toggle solution replay
      inserting=0;
      if(replay_count) save_replay();
      solution_replay^=1;
      if(replay_count) replay_count=0,begin_level(level_id); else load_replay();
      return 1;
    case '^x': // Cancel dead animation
      ndeadanim=0;
      return prev;
    case 'go': // Select level
      begin_level(number);
      return 1;
    case 'lo': // Locate me
      locate_me(number&63?:64,number/64?:64);
      return prev;
    case 'mi': // Move list import
      if(argc<2 || solution_replay) break;
      if(replay_pos) begin_level(level_id);
      do_import_moves(sqlite3_column_text(args,1));
      return 1;
    case 'ml': // Move list load
      if(argc<2 || solution_replay) break;
      if(replay_pos) begin_level(level_id);
      do_load_moves(args);
      return 1;
    case 'mx': // Move list export
      if(argc<2) break;
      do_export_moves(sqlite3_column_text(args,1));
      return 0;
    case 'rs': // Replay speed
      number+=replay_speed;
      if(number<1) number=1; else if(number>255) number=255;
      replay_speed=number;
      return prev;
    case 'xy': // Coordinate input
      if(argc<3 || !has_xy_input) break;
      argc=sqlite3_column_int(args,1);
      number=sqlite3_column_int(args,2);
      if(argc<1 || argc>pfwidth || number<1 || number>pfheight) return 0;
      number=(number-1)|((argc-1)<<6)|0x8000;
      goto play;
    default:
      return prev;
  }
}

static void do_autowin(void) {
  const char*name;
  int i,j,k;
  int prev=0;
  sqlite3_reset(autowin);
  if(sqlite3_bind_parameter_count(autowin)) {
    for(i=sqlite3_bind_parameter_count(autowin);i;--i) if(name=sqlite3_bind_parameter_name(autowin,i)) {
      if(*name=='$') {
        if(!sqlite3_stricmp(name+1,"LEVEL")) {
          sqlite3_bind_int(autowin,i,level_ord);
        } else if(!sqlite3_stricmp(name+1,"LEVEL_ID")) {
          sqlite3_bind_int(autowin,i,level_id);
        }
      }
    }
  }
  while((i=sqlite3_step(autowin))==SQLITE_ROW) {
    if(i=sqlite3_data_count(autowin)) {
      j=(i>1&&sqlite3_column_type(autowin,1)!=SQLITE_NULL)?sqlite3_column_int(autowin,1):0;
      if((name=sqlite3_column_text(autowin,0)) && *name) {
        if(name[0]==':') {
          switch(name[1]) {
            case '!': if(i>1) i=system(sqlite3_column_text(autowin,1)?:(const unsigned char*)"# "); break;
            case ';': goto done;
            case '?': if(i>1) puts(sqlite3_column_text(autowin,1)?:(const unsigned char*)"(null)"); break;
            case 'm': if(i>1) screen_message(sqlite3_column_text(autowin,1)?:(const unsigned char*)"(null)"); break;
          }
        } else {
          k=name[0]*'\1\0'+name[1]*'\0\1';
          while(i && sqlite3_column_type(autowin,i-1)==SQLITE_NULL) i--;
          prev=game_command(prev,k,j,i,autowin,0);
          if(prev<0) goto done;
        }
      }
    }
  }
  if(i!=SQLITE_DONE) screen_message("SQL error");
  done:
  sqlite3_reset(autowin);
}

static void set_caption(void) {
  const char*r;
  char*s;
  sqlite3_str*m;
  int c;
  optionquery[1]=Q_gameTitle;
  r=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"Free Hero Mesh - ~ - Game";
  m=sqlite3_str_new(0);
  c=strcspn(r,"~");
  sqlite3_str_append(m,r,c);
  if(r[c]=='~') {
    sqlite3_str_appendall(m,basefilename);
    sqlite3_str_appendall(m,r+c+1);
  }
  s=sqlite3_str_finish(m);
  if(s) SDL_WM_SetCaption(s,s); else SDL_WM_SetCaption("Free Hero Mesh","Free Hero Mesh");
  sqlite3_free(s);
}

static Uint32 timer_callback(Uint32 n) {
  if(!timerflag) {
    static SDL_Event ev={SDL_USEREVENT};
    SDL_PushEvent(&ev);
  }
  timerflag=1;
  return n;
}

static inline void input_move(MoveItem k) {
  const char*t=execute_turn(k);
  if(replay_pos>0xFFFE && !gameover) t="Too many moves played";
  if(t) {
    screen_message(t);
    gameover=-1;
    return;
  }
  if(!key_ignored) {
    if(inserting) {
      if(replay_pos>=0xFFFE || replay_pos==replay_count) {
        inserting=0;
      } else {
        if(replay_count>0xFFFE) replay_count=0xFFFE;
        if(replay_size<0x10000*MSIZ) {
          replay_list=realloc(replay_list,replay_size=0x10000*MSIZ);
          if(!replay_list) fatal("Allocation failed\n");
        }
        memmoveM(replay_list+replay_pos+1,replay_list+replay_pos,replay_count-replay_pos);
        replay_count++;
      }
    }
    if(replay_pos*MSIZ>=replay_size) {
      replay_list=realloc(replay_list,replay_size+=0x200*MSIZ);
      if(!replay_list) fatal("Allocation failed\n");
    }
    replay_list[replay_pos++]=k;
    if(replay_pos>replay_count) replay_count=replay_pos;
  }
}

static void record_solution(void) {
  const char*v;
  const char*com;
  FILE*fp;
  Uint8 flag;
  long n;
  unsigned char*buf=0;
  size_t sz=0;
  if(solution_replay) return;
  if(buf=read_lump(FIL_SOLUTION,level_id,&n)) {
    if(n<3 || (buf[0]|(buf[1]<<8))!=level_version || (buf[2]&~0x83)) goto dontkeep;
    n-=3;
    if((buf[2]&128) && n>4) {
      Sint32 sco=buf[3]|(buf[4]<<8)|(buf[5]<<16)|(buf[6]<<24);
      if(gameover_score!=NO_SCORE && sco<=gameover_score) goto dontkeep;
    } else {
      if(buf[2]&1) n-=strnlen(buf+3,n);
      if(buf[2]&2) n-=8;
      if(n<=0 || n>replay_pos) goto dontkeep;
    }
    free(buf);
    return;
    dontkeep:
    free(buf);
    buf=0;
  }
  optionquery[1]=Q_solutionComment;
  com=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(com && !*com) com=0;
  optionquery[1]=Q_solutionTimestamp;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
  flag=0;
  if(com) flag|=1;
  if(boolxrm(v,0)) flag|=2;
  if(gameover_score!=NO_SCORE) flag|=128;
  fp=open_memstream((char**)&buf,&sz);
  if(!fp) fatal("Allocation failed\n");
  fputc(level_version,fp);
  fputc(level_version>>8,fp);
  fputc(flag,fp);
  if(flag&128) {
    fputc(gameover_score,fp);
    fputc(gameover_score>>8,fp);
    fputc(gameover_score>>16,fp);
    fputc(gameover_score>>24,fp);
  }
  if(flag&1) fwrite(com,1,strlen(com+1),fp);
  if(flag&2) {
    time_t t=time(0);
    fputc(t>>000,fp); fputc(t>>010,fp); fputc(t>>020,fp); fputc(t>>030,fp);
#ifdef CONFIG_USING_32BIT_TIMESTAMPS
    fputc(0,fp); fputc(0,fp); fputc(0,fp); fputc(0,fp);
#else
    fputc(t>>040,fp); fputc(t>>050,fp); fputc(t>>060,fp); fputc(t>>070,fp);
#endif
  }
  n=replay_count;
  replay_count=replay_pos;
  encode_move_list(fp);
  replay_count=n;
  fclose(fp);
  if(!buf) fatal("Allocation failed\n");
  write_lump(FIL_SOLUTION,level_id,sz,buf);
  free(buf);
  sqlite3_exec(userdb,"UPDATE `LEVELS` SET `SOLVABLE` = 1 WHERE `ID` = LEVEL_ID();",0,0,0);
}

static void record_private_solution(void) {
  FILE*fp;
  char*buf=0;
  int n;
  if(solution_replay) return;
  if(gameover_score==NO_SCORE) gameover_score=replay_pos;
  if(best_list && best_score!=NO_SCORE && gameover_score>best_score) return;
  dum_size=0;
  fp=open_memstream(&buf,&dum_size);
  if(!fp) fatal("Allocation failed\n");
  n=replay_count;
  replay_count=replay_pos;
  encode_move_list(fp);
  replay_count=n;
  fclose(fp);
  if(buf) {
    free(best_list);
    best_list=buf;
    best_score=gameover_score;
  }
}

void run_game(void) {
  int i;
  SDL_Event ev;
  set_caption();
  replay_count=0;
  replay_time=0;
  if(side_mode==255) setup_game();
  begin_level(level_id);
  redraw_game();
  timerflag=0;
  SDL_SetTimer(10,timer_callback);
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_VIDEOEXPOSE:
        redraw_game();
        break;
      case SDL_QUIT:
        goto quit;
        break;
      case SDL_MOUSEMOTION:
        show_mouse_xy(&ev);
        break;
      case SDL_USEREVENT:
        if(!gameover && !quiz_text) continue_animation();
        timerflag=0;
        if(replay_time && !--replay_time && !game_command(1,'+ ',1,0,0,0)) {
          replay_time=replay_speed;
          goto replay;
        }
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.x<left_margin) {
          if(ev.button.y<48) break;
          if(side_mode) {
            // Inventory
            describe_inventory((ev.button.y-52)/picture_size);
            timerflag=0;
            redraw_game();
          } else {
            // Move list
            i=(ev.button.y+4)/16-(screen->h-68)/32-4;
            if(i<0) game_command(0,'- ',-i,0,0,0); else if(i>0) game_command(0,'+ ',i,0,0,0);
            goto replay;
          }
          break;
        } else {
          i=exec_key_binding(&ev,0,(ev.button.x-left_margin)/picture_size+1,ev.button.y/picture_size+1,game_command,0);
          goto command;
        }
      case SDL_KEYDOWN:
        i=exec_key_binding(&ev,0,0,0,game_command,0);
      command:
        if(i==-1) goto quit;
        if(i==-2) {
          main_options['e']=1;
          SDL_SetTimer(0,0);
          save_replay();
          return;
        }
      replay:
        if(inputs_count) {
          for(i=0;i<inputs_count && !gameover;i++) if(inputs[i]) input_move(inputs[i]);
          inputs_count=0;
          if(saved_inserting) inserting=1,saved_inserting=0;
          no_dead_anim=0;
          if(gameover==1) {
            if(should_record_solution) record_solution();
            if(should_record_private_solution) record_private_solution();
            if(!solution_replay && !solved) sqlite3_exec(userdb,"UPDATE `LEVELS` SET `SOLVED` = 1 WHERE `ID` = LEVEL_ID();",0,0,0);
            if(autowin) do_autowin();
          }
        }
        redraw_game();
        timerflag=0; // ensure we have not missed a timer event
        break;
    }
  }
  quit:
  SDL_SetTimer(0,0);
  save_replay();
  exit(0);
}

void run_auto_test(void) {
  Uint8 rc=0;
  int lvl,pro,i,n;
  const char*t;
  no_dead_anim=1;
  setbuf(stdout,0);
  solution_replay=1;
  optionquery[1]=Q_progress;
  t=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  pro=t?strtol(t,0,10):0;
  if(main_options['t']) pro=0;
  if(main_options['+']) {
    lvl=1;
    goto start2;
  }
  optionquery[1]=Q_level;
  t=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(n=lvl=t?strtol(t,0,10):0) goto start;
  for(n=1;n<=level_nindex;n++) {
    if(lvl) break;
    start:
    if(pro<0) sqlite3_sleep(-pro);
    if(main_options['t']) printf("*** Level %d\n",n); else printf("Level %d",n);
    if(t=load_level(-n)) {
      printf(": Error during loading: %s\n",t);
      rc=1; continue;
    }
    start2:
    load_replay();
    if(!replay_count) {
      printf(": Solution is absent, invalid, or for wrong version of this level\n");
      rc=1; continue;
    }
    if(t=init_level()) {
      printf(": Error during initialization: %s\n",t);
      rc=1; continue;
    }
    if(gameover==-1) {
      printf(": Lose during initialization\n");
      rc=1; continue;
    }
    for(i=0;i<replay_count;i++) {
      if(gameover) {
        printf(": Premature termination on move %d\n",i);
        rc=1; goto cont;
      }
      if(pro>0 && !(i%pro)) putchar('.');
      if(t=execute_turn(replay_list[i])) {
        printf(": Error on move %d: %s\n",i+1,t);
        rc=1; goto cont;
      }
      if(gameover==-1) {
        printf(": Game loss on move %d\n",i+1);
        rc=1; goto cont;
      }
    }
    if(gameover<=0) {
      printf(": Game not terminated after %d moves\n",replay_count);
      rc=1; continue;
    }
    printf(": OK\n");
    cont: ;
  }
  exit(rc);
}

void export_private_solutions(void) {
  int rc=0;
  const char*t;
  int i0,i1,i2,i,j,n;
  unsigned char*data;
  long sz;
  for(n=1;n<=level_nindex;n++) {
    level_id=level_index[n-1];
    data=read_userstate(FIL_LEVEL,level_id,&sz);
    if(!data) continue;
    if(!sz || *data) {
      free(data);
      continue;
    }
    i0=i1=i2=0;
    for(i=1;i<sz;) {
      if(data[i]==0x02 && i+2<=sz) i0=i+1;
      else if(data[i]==0x41 && i+3<=sz) i1=i+1;
      else if(data[i]==0x81 && i+5<=sz) i2=i+1;
      if(data[i]<0x40) i+=strnlen(data+i,sz-i)+1;
      else if(data[i]<0x80) i+=3;
      else if(data[i]<0xC0) i+=5;
      else i+=9;
    }
    if(i0 && i1) {
      i=strnlen(data+i0,sz-i0);
      printf("%d.SOL",level_id);
      putchar(0); // null terminator of lump name
      j=i+(i2?7:3);
      putchar(j>>16); putchar(j>>24); putchar(j); putchar(j>>8); // lump data size
      putchar(data[i1]); putchar(data[i1+1]); // level version
      putchar(i2?0x80:0x00); // flag
      if(i2) fwrite(data+i2,1,4,stdout); // score
      fwrite(data+i0,1,i,stdout); // move list
    }
    free(data);
  }
  exit(rc);
}

void locate_me(int x,int y) {
  Uint8 c=7;
  SDL_Rect r,rh,rv;
  SDL_Event ev;
  if(!screen) return;
  redraw_game();
  r.x=(x-1)*picture_size+left_margin;
  r.y=(y-1)*picture_size;
  r.w=r.h=picture_size;
  rh.x=0;
  rh.y=r.y+picture_size/2;
  rh.w=screen->w;
  rh.h=1;
  rv.x=r.x+picture_size/2;
  rv.y=0;
  rv.w=1;
  rv.h=screen->h;
  show:
  timerflag=0;
  SDL_FillRect(screen,&rh,c+45);
  SDL_FillRect(screen,&rv,c+67);
  SDL_FillRect(screen,&r,c);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) switch(ev.type) {
    case SDL_USEREVENT:
      if(c>240) return;
      c+=15;
      goto show;
    case SDL_KEYDOWN: case SDL_QUIT: case SDL_MOUSEBUTTONDOWN:
      SDL_PushEvent(&ev);
      return;
  }
}