Free Hero Mesh

edit.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 edit.c artifact 50c4e83094 part of check-in ef8fe744ff


#if 0
gcc ${CFLAGS:--s -O2} -c -Wno-multichar edit.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 "sqlite3.h"
#include "smallxrm.h"
#include "heromesh.h"
#include "quarks.h"
#include "cursorshapes.h"
#include "hash.h"

EditorRect editrect;

typedef struct {
  Uint16 class;
  Uint8 img,dir;
  Value misc1,misc2,misc3;
} MRU;

#define MRUCOUNT 32
static MRU mru[MRUCOUNT];
static int curmru;
static char*solution_data;
static size_t solution_length;

static inline void discard_solution(void) {
  if(solution_data) {
    free(solution_data);
    solution_data=0;
    solution_length=0;
  }
}

static void rewrite_class_def(void) {
  Uint32 i,n;
  Uint8 cu[0x4000/8];
  Uint8 mu[0x4000/8];
  Object*o;
  long size=0;
  unsigned char*data=read_lump(FIL_LEVEL,LUMP_CLASS_DEF,&size);
  sqlite3_str*s;
  memset(cu,0,0x4000/8);
  memset(mu,0,0x4000/8);
  if(data && size) {
    for(i=0;i<size-3;) {
      n=data[i]|(data[i+1]<<8);
      if(!n) break;
      i+=2;
      while(i<size && data[i++]);
      cu[n/8]|=1<<(n&7);
    }
    i+=2;
    for(;i<size-3;) {
      n=data[i]|(data[i+1]<<8);
      n-=256;
      i+=2;
      while(i<size && data[i++]);
      if(n>=0) mu[n/8]|=1<<(n&7);
    }
  }
  free(data);
  for(i=0;i<nobjects;i++) if(o=objects[i]) {
    cu[o->class/8]|=1<<(o->class&7);
#define DoMisc(a) \
  if(o->a.t==TY_CLASS) { \
    cu[o->a.u/8]|=1<<(o->a.u&7); \
  } else if(o->a.t==TY_MESSAGE && o->a.u>=256) { \
    mu[(o->a.u-256)/8]|=1<<(o->a.u&7); \
  }
    DoMisc(misc1)
    DoMisc(misc2)
    DoMisc(misc3)
#undef DoMisc
  }
  // Now write out the new data
  s=sqlite3_str_new(0);
  for(i=0;i<0x4000;i++) if(cu[i/8]&(1<<(i&7))) {
    sqlite3_str_appendchar(s,1,i&255);
    sqlite3_str_appendchar(s,1,i>>8);
    sqlite3_str_appendall(s,classes[i]->name);
    sqlite3_str_appendchar(s,1,0);
  }
  sqlite3_str_appendchar(s,2,0);
  for(i=0;i<0x4000;i++) if(mu[i/8]&(1<<(i&7))) {
    sqlite3_str_appendchar(s,1,i&255);
    sqlite3_str_appendchar(s,1,(i+256)>>8);
    sqlite3_str_appendall(s,messages[i]);
    sqlite3_str_appendchar(s,1,0);
  }
  // End of data
  size=sqlite3_str_length(s);
  data=sqlite3_str_finish(s);
  if(!size || !data) fatal("Error in string builder\n");
  write_lump(FIL_LEVEL,LUMP_CLASS_DEF,size,data);
  sqlite3_free(data);
}

static inline void version_change(void) {
  long sz=0;
  long n;
  unsigned char*buf=read_lump(FIL_SOLUTION,level_id,&sz);
  if(!buf) goto us;
  if(sz>2 && (buf[0]|(buf[1]<<8))==level_version) ++level_version;
  free(buf);
  us:
  // Set user state to unsolved, if applicable
  buf=read_userstate(FIL_LEVEL,level_id,&sz);
  if(!buf) return;
  if(sz>2 && !*buf) {
    for(n=1;n<sz-2;) {
      if(buf[n]==0x41) {
        memmove(buf+n,buf+n+3,sz-n-3);
        write_userstate(FIL_LEVEL,level_id,sz-3,buf);
        break;
      } else if(buf[n]<0x40) {
        n+=strnlen(buf+n,sz-n)+1;
      } else {
        n+=(buf[n]<0x80?3:buf[n]<0xC0?5:9);
      }
    }
  }
  free(buf);
}

static void save_obj(sqlite3_str*s,const Object*o,const Object**m,Uint8 x,Uint8 y) {
  static Uint8 r=0;
  static Uint8 rx,ry;
  const Object*p=0;
  Uint8 b;
  Uint16 c;
  if(!o) goto nrle;
  b=o->dir&7;
  if(o->x==x+1) b|=0x40; else if(o->x!=x) b|=0x20;
  if(o->y!=y) b|=0x10;
  p=m[b&0x70?0:1];
  if((b&0x70)!=0x40 || !r || !p) goto nrle;
  if(o->class!=p->class || o->image!=p->image || !ValueEq(o->misc1,p->misc1) || !ValueEq(o->misc2,p->misc2) || !ValueEq(o->misc3,p->misc3)) goto nrle;
  if(0x0F&~r) {
    r++;
  } else {
    sqlite3_str_appendchar(s,1,r);
    if(r&0x20) sqlite3_str_appendchar(s,1,rx);
    if(r&0x10) sqlite3_str_appendchar(s,1,ry);
    r=0xC0;
  }
  return;
  nrle:
  if(r) {
    sqlite3_str_appendchar(s,1,r);
    if(r&0x20) sqlite3_str_appendchar(s,1,rx);
    if(r&0x10) sqlite3_str_appendchar(s,1,ry);
  }
  r=0;
  if(!o) return;
  if(o->misc1.t|o->misc1.u|o->misc2.t|o->misc2.u|o->misc3.t|o->misc3.u) b|=0x08;
  if(p && o->class==p->class && o->image==p->image && ValueEq(o->misc1,p->misc1) && ValueEq(o->misc2,p->misc2) && ValueEq(o->misc3,p->misc3)) {
    // Use RLE
    r=0x80|b&0xF0;
    rx=o->x;
    ry=o->y;
    return;
  }
  m[b&0x70?0:1]=o;
  sqlite3_str_appendchar(s,1,b);
  if(b&0x20) sqlite3_str_appendchar(s,1,o->x);
  if(b&0x10) sqlite3_str_appendchar(s,1,o->y);
  c=o->class;
  for(x=0;x<=o->image && x<classes[c]->nimages;x++) if(classes[c]->images[x]&0x8000) break;
  if(x==o->image) c|=0x8000;
  sqlite3_str_appendchar(s,1,c&0xFF);
  sqlite3_str_appendchar(s,1,c>>8);
  if(c<0x8000) sqlite3_str_appendchar(s,1,o->image);
  if(b&0x08) {
    b=o->misc1.t|(o->misc2.t<<2)|(o->misc3.t<<4);
    if(o->misc1.t|o->misc1.u) {
      if(o->misc3.t|o->misc3.u) b|=0xC0;
      else if(o->misc2.t|o->misc2.u) b|=0x80;
      else b|=0x40;
    }
    sqlite3_str_appendchar(s,1,b);
    if(b&0xC0) {
      sqlite3_str_appendchar(s,1,o->misc1.u&0xFF);
      sqlite3_str_appendchar(s,1,o->misc1.u>>8);
    }
    if((b&0xC0)!=0x40) {
      sqlite3_str_appendchar(s,1,o->misc2.u&0xFF);
      sqlite3_str_appendchar(s,1,o->misc2.u>>8);
    }
    if(!((b&0xC0)%0xC0)) {
      sqlite3_str_appendchar(s,1,o->misc3.u&0xFF);
      sqlite3_str_appendchar(s,1,o->misc3.u>>8);
    }
  }
}

static void update_level_index(void) {
  Uint8*data;
  int i;
  if(level_ord>level_nindex) {
    level_index=realloc(level_index,++level_nindex*sizeof(Uint16));
    if(!level_index) fatal("Allocation failed\n");
    level_index[level_ord-1]=level_id;
  }
  data=malloc(level_nindex<<1);
  if(!data) fatal("Allocation failed\n");
  for(i=0;i<level_nindex;i++) data[i+i]=level_index[i]&255,data[i+i+1]=level_index[i]>>8;
  write_lump(FIL_LEVEL,LUMP_LEVEL_IDX,2*level_nindex,data);
  free(data);
}

static void save_level(void) {
  /*
   Format of objects:
     * bit flags (or 0xFF for end):
       bit7 = MRU (omit everything but position)
       bit6 = Next position
       bit5 = New X position
       bit4 = New Y position
       bit3 = Has MiscVars (RLE in case of MRU)
       bit2-bit0 = LastDir (RLE in case of MRU)
     * new X if applicable
     * new Y if applicable
     * class (one-based; add 0x8000 for default image) (two bytes)
     * image (one byte)
     * data types (if has MiscVars):
       bit7-bit6 = How many (0=has Misc2 and Misc3, not Misc1)
       bit5-bit4 = Misc3 type
       bit3-bit2 = Misc2 type
       bit1-bit0 = Misc1 type
     * misc data (variable size)
   Store/use MRU slot 0 if any bits of 0x70 set in flag byte; slot 1 otherwise
  */
  Uint32*p=playfield;
  Uint8 x=0;
  Uint8 y=1;
  const Object*m[2]={0,0};
  sqlite3_str*str=sqlite3_str_new(0);
  Uint32 n;
  long sz;
  char*data;
  int i;
  // Header
  if(level_changed) version_change();
  level_changed=0;
  sqlite3_str_appendchar(str,1,level_version&255);
  sqlite3_str_appendchar(str,1,level_version>>8);
  sqlite3_str_appendchar(str,1,level_code&255);
  sqlite3_str_appendchar(str,1,level_code>>8);
  sqlite3_str_appendchar(str,1,pfwidth-1);
  sqlite3_str_appendchar(str,1,pfheight-1);
  if(level_title) sqlite3_str_appendall(str,level_title);
  sqlite3_str_appendchar(str,1,0);
  // Objects
  again:
  for(i=0;i<64*64;i++) {
    n=p[i];
    while(n!=VOIDLINK) {
      save_obj(str,objects[n],m,x,y);
      x=objects[n]->x;
      y=objects[n]->y;
      n=objects[n]->up;
    }
  }
  save_obj(str,0,m,x,y);
  if(p==playfield) {
    p=bizplayfield;
    for(i=0;i<64*64;i++) if(p[i]!=VOIDLINK) {
      sqlite3_str_appendchar(str,1,0xFE);
      x=0;
      y=1;
      goto again;
    }
  }
  sqlite3_str_appendchar(str,1,0xFF);
  // Level strings
  for(i=0;i<nlevelstrings;i++) {
    sqlite3_str_appendall(str,levelstrings[i]);
    sqlite3_str_appendchar(str,1,0);
  }
  // Done
  sz=sqlite3_str_length(str);
  if(i=sqlite3_str_errcode(str)) fatal("SQL string error (%d)\n",i);
  data=sqlite3_str_finish(str);
  if(!data) fatal("Allocation failed\n");
  sqlite3_exec(userdb,"BEGIN;",0,0,0);
  // This next line might fail if the LEVELS table has not been loaded yet.
  // Attempting to drop it before it is loaded is slower than it should be, somehow.
  // (I also suspect there might be a bug in SQLite, although I am unsure.)
  // Therefore, it first tries to truncate it; if it hasn't been loaded, it fails.
  sqlite3_exec(userdb,"DELETE FROM LEVELS; DROP TABLE TEMP.LEVELS;",0,0,0);
  write_lump(FIL_LEVEL,level_id,sz,data);
  sqlite3_free(data);
  if(level_ord==level_nindex+1) update_level_index();
  rewrite_class_def();
  if(solution_data) {
    solution_data[0]=level_version&255;
    solution_data[1]=level_version>>8;
    write_lump(FIL_SOLUTION,level_id,solution_length,solution_data);
    discard_solution();
  }
  sqlite3_exec(userdb,"COMMIT;",0,0,0);
}

static void redraw_editor(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,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,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);
  }
  snprintf(buf,8,"%2dx%2d",pfwidth,pfheight);
  draw_text(8,32,buf,0xF0,0xFD);
  draw_text(24,32,"x",0xF0,level_changed?0xF4:0xF5);
  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,0xF3);
  for(x=0;x<MRUCOUNT;x++) {
    y=picture_size*x+56;
    if(y+picture_size>screen->h) break;
    if(x==curmru) draw_text(0,y,">",0xF0,0xFE);
    if(mru[x].misc1.u|mru[x].misc1.t|mru[x].misc2.u|mru[x].misc2.t|mru[x].misc3.u|mru[x].misc3.t) draw_text(picture_size+16,y,"*",0xF0,0xFB);
  }
  if(editrect.x0 && editrect.x1) draw_selection_rectangle();
  SDL_UnlockSurface(screen);
  r.w=r.h=picture_size;
  r.x=8;
  for(x=0;x<MRUCOUNT;x++) {
    y=picture_size*x+56;
    if(y+picture_size>screen->h) break;
    if(mru[x].class) {
      r.y=y;
      SDL_FillRect(screen,&r,0x00);
      draw_picture(8,y,classes[mru[x].class]->images[mru[x].img]&0x7FFF);
    }
  }
  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) x=0;
  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,0xF3);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
}

static void set_caption(void) {
  const char*r;
  char*s;
  sqlite3_str*m;
  int c;
  optionquery[1]=Q_editTitle;
  r=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"Free Hero Mesh - ~ - Edit";
  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 void add_mru(int cl,int img) {
  int i;
  if(!cl) return;
  for(i=0;i<MRUCOUNT-1;i++) if(mru[i].class==cl && mru[i].img==img) break;
  memmove(mru+1,mru,i*sizeof(MRU));
  mru[0].class=cl;
  mru[0].img=img;
  mru[0].dir=0;
  mru[0].misc1=mru[0].misc2=mru[0].misc3=NVALUE(0);
  curmru=0;
}

static void class_image_select(void) {
  SDL_Event ev;
  SDL_Rect r;
  int clscroll=0;
  int imgscroll=0;
  int clcount=0;
  int imgcount=0;
  int cl=0;
  int img=0;
  char re=0;
  Uint16 cllist[0x4000];
  Uint8 imglist[256];
  char name[256];
  int namei=0;
  char buf[4];
  int i,j;
  sqlite3_stmt*st;
  if(sqlite3_prepare_v2(userdb,"SELECT `ID` FROM `CLASSES` WHERE `GROUP` IS NULL AND CLASS_DATA(`ID`,7) NOT NULL ORDER BY `NAME` COLLATE NOCASE;",-1,&st,0))
   fatal("Unexpected SQL error: %s\n",sqlite3_errmsg(userdb));
  while(sqlite3_step(st)==SQLITE_ROW) cllist[clcount++]=sqlite3_column_int(st,0);
  sqlite3_finalize(st);
  redraw:
  r.y=r.x=0;
  r.w=picture_size+40;
  r.h=screen->h;
  SDL_FillRect(screen,&r,0xF0);
  r.x=picture_size+40;
  r.w=screen->w-r.x;
  SDL_FillRect(screen,&r,0xF1);
  scrollbar(&clscroll,screen->h/8,clcount,0,&r);
  r.x+=15;
  r.h=8;
  SDL_LockSurface(screen);
  for(i=0;i<screen->h/8;i++) {
    if(i+clscroll>=clcount) break;
    j=cllist[i+clscroll];
    if(j==cl) SDL_FillRect(screen,&r,0xF3),re=0;
    draw_text(r.x,r.y,classes[j]->name,j==cl?0xF3:0xF1,classes[j]->cflags&CF_PLAYER?0xFE:0xFF);
    if(j==cl && namei) {
      memcpy(name,classes[j]->name,namei);
      name[namei]=0;
      draw_text(r.x,r.y,name,0xF4,classes[j]->cflags&CF_PLAYER?0xFE:0xFF);
    }
    r.y+=8;
  }
  SDL_UnlockSurface(screen);
  if(re && cl) {
    for(i=0;i<clcount;i++) if(cllist[i]==cl) {
      clscroll=i+(clscroll>i?0:1-screen->h/8);
      break;
    }
    re=0;
    goto redraw;
  }
  scrollbar(&imgscroll,screen->h/picture_size,imgcount,0,0);
  r.x=14;
  r.y=0;
  r.w=r.h=picture_size;
  for(i=0;i<screen->h/picture_size;i++) {
    if(i+imgscroll>=imgcount) break;
    j=imglist[i+imgscroll];
    SDL_FillRect(screen,&r,0);
    draw_picture(14,r.y,classes[cl]->images[j]&0x7FFF);
    r.y+=picture_size;
  }
  r.y=0;
  SDL_LockSurface(screen);
  for(i=0;i<screen->h/picture_size;i++) {
    if(i+imgscroll>=imgcount) break;
    j=imglist[i+imgscroll];
    sprintf(buf,"%3d",j);
    draw_text(picture_size+15,r.y,buf,j==img?0xF2:0xF0,j==img?0xFF:0xF7);
    r.y+=picture_size;
  }
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    if(ev.type!=SDL_VIDEOEXPOSE) {
      if(scrollbar(&imgscroll,screen->h/picture_size,imgcount,&ev,0)) goto redraw;
      r.h=screen->h;
      r.x=picture_size+40;
      r.y=0;
      if(scrollbar(&clscroll,screen->h/8,clcount,&ev,&r)) goto redraw;
    }
    switch(ev.type) {
      case SDL_MOUSEMOTION:
        set_cursor(XC_draft_small);
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.x>picture_size+40) {
          i=clscroll+ev.button.y/8;
          if(i<0 || i>=clcount) break;
          namei=0;
          cl=cllist[i];
          setclass:
          imgscroll=imgcount=0;
          for(i=0;i<classes[cl]->nimages;i++) if(classes[cl]->images[i]&0x8000) imglist[imgcount++]=i;
          img=*imglist;
          re=1;
        } else {
          i=imgscroll+ev.button.y/picture_size;
          if(i<0 || i>=imgcount) break;
          img=imglist[i];
        }
        if(ev.button.button==3 || ev.button.button==2) add_mru(cl,img);
        if(ev.button.button==2) return;
        goto redraw;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_HOME: case SDLK_KP7: clscroll=0; goto redraw;
          case SDLK_END: case SDLK_KP1: clscroll=clcount-screen->h/8; goto redraw;
          case SDLK_PAGEDOWN: case SDLK_KP3: clscroll+=screen->h/8; goto redraw;
          case SDLK_PAGEUP: case SDLK_KP9: clscroll-=screen->h/8; goto redraw;
          case SDLK_KP_PLUS: clscroll++; goto redraw;
          case SDLK_KP_MINUS: clscroll--; goto redraw;
          case SDLK_ESCAPE: return;
          case SDLK_CLEAR: case SDLK_DELETE: case SDLK_KP_PERIOD: namei=0; goto redraw;
          case SDLK_BACKSPACE: if(namei) --namei; goto redraw;
          case SDLK_TAB:
            if(!cl) break;
            strncpy(name,classes[cl]->name,254);
            for(j=0;j<clcount;j++) if(cl==cllist[j]) break;
            // Increase namei according to all directly following letters that are matching
            while(namei<253 && classes[cl]->name[namei]) {
              for(i=j+1;i<clcount;i++) {
                if(sqlite3_strnicmp(name,classes[cllist[i]]->name,namei)) {
                  // The first part of the name doesn't match; all others did, so accept the next letter.
                  break;
                } else {
                  if(sqlite3_strnicmp(classes[cl]->name+namei,classes[cllist[i]]->name+namei,1)) {
                    // The first part of the name matches but the next letter doesn't.
                    goto redraw;
                  } else {
                    // The first part and next letter are both matching; see if the next one also matches.
                    continue;
                  }
                }
              }
              ++namei;
            }
            goto redraw;
          case SDLK_LEFT: case SDLK_KP4:
            for(i=0;i<imgcount-1;i++) if(img==imglist[i+1]) {
              img=imglist[i];
              if(imgscroll>i) imgscroll=i;
              goto redraw;
            }
            break;
          case SDLK_RIGHT: case SDLK_KP6:
            for(i=0;i<imgcount-1;i++) if(img==imglist[i]) {
              img=imglist[i+1];
              if(imgscroll<i+2-screen->h/picture_size) imgscroll=i+2-screen->h/picture_size;
              goto redraw;
            }
            break;
          case SDLK_UP: case SDLK_KP8:
            namei=0;
            for(i=1;i<clcount;i++) if(cl==cllist[i]) {
              cl=cllist[i-1];
              goto setclass;
            }
            break;
          case SDLK_DOWN: case SDLK_KP2:
            namei=0;
            if(!cl) {
              cl=*cllist;
              goto setclass;
            }
            for(i=0;i<clcount-1;i++) if(cl==cllist[i]) {
              cl=cllist[i+1];
              goto setclass;
            }
            break;
          case SDLK_RETURN: case SDLK_KP_ENTER:
            add_mru(cl,img);
            return;
          case SDLK_F1: case SDLK_KP_DIVIDE:
            if(cl && classes[cl] && classes[cl]->gamehelp) modal_draw_popup(classes[cl]->gamehelp);
            goto redraw;
          case SDLK_F2: case SDLK_KP_MULTIPLY:
            if(cl && classes[cl] && classes[cl]->edithelp) modal_draw_popup(classes[cl]->edithelp);
            goto redraw;
          default:
            j=ev.key.keysym.unicode;
            if(j=='$' || j==21) {
              namei=0;
              goto redraw;
            }
            if(j==27) return;
            if(j>32 && j<127 && namei<254) {
              name[namei++]=j;
              for(i=0;i<clcount;i++) {
                if(!sqlite3_strnicmp(name,classes[cllist[i]]->name,namei)) {
                  ev.button.button=1;
                  cl=cllist[i];
                  goto setclass;
                }
              }
              --namei;
            }
        }
        break;
      case SDL_VIDEOEXPOSE:
        goto redraw;
      case SDL_QUIT:
        exit(0);
        break;
    }
  }
}

static int string_list(void);

static Value ask_value(const char*s) {
  int i;
  s=screen_prompt(s);
  if(!s || !*s) return UVALUE(0,4);
  if(*s=='-' || *s=='+' || (*s>='0' && *s<='9')) {
    if(*s=='0' && s[1]=='x') return NVALUE(strtol(s+2,0,16)&0xFFFF);
    if(*s=='0' && s[1]=='o') return NVALUE(strtol(s+2,0,8)&0xFFFF);
    return NVALUE(strtol(s,0,10)&0xFFFF);
  } else if(*s=='$') {
    for(i=1;i<0x4000;i++) {
      if(classes[i] && !(classes[i]->cflags&CF_NOCLASS2) && !strcmp(s+1,classes[i]->name)) return CVALUE(i);
    }
    screen_message("Undefined class");
  } else if(*s=='#') {
    for(i=0;i<0x4000;i++) {
      if(messages[i] && !strcmp(s+1,messages[i])) return MVALUE(i+256);
    }
    screen_message("Undefined message");
  } else if(*s>='A' && *s<='Z') {
    for(i=0;i<N_MESSAGES;i++) {
      if(!strcmp(s,standard_message_names[i])) return MVALUE(i);
    }
    screen_message("Undefined message");
  } else if(*s=='\'') {
    for(i=0;i<256;i++) {
      if(heromesh_key_names[i] && !strcmp(s+1,heromesh_key_names[i])) return NVALUE(i);
    }
    screen_message("Invalid key");
  } else {
    screen_message("Syntax error");
  }
  return UVALUE(0,4);
}

static void mru_edit(MRU*m) {
  SDL_Event ev;
  SDL_Rect r;
  char buf[256];
  int i,j;
  Value v;
  if(!m->class) return;
  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);
  draw_text(0,0,"Class:",0xF0,0xF7);
  draw_text(64,0,"$",0xF0,0xFB);
  draw_text(72,0,classes[m->class]->name,0xF0,0xFB);
  draw_text(0,8,"Image:",0xF0,0xF7);
  snprintf(buf,255,"%d",m->img);
  draw_text(64,8,buf,0xF0,0xFE);
  draw_text(0,24,"Dir:",0xF0,0xF7);
  snprintf(buf,255,"%2.2s","E NEN NWW SWS SE"+(m->dir&7)*2);
  draw_text(64,24,buf,0xF0,0xFF);
  for(i=32;i<56;i+=8) {
    switch(i) {
      case 32: v=m->misc1; draw_text(0,32,"Misc1:",0xF0,0xF7); break;
      case 40: v=m->misc2; draw_text(0,40,"Misc2:",0xF0,0xF7); break;
      case 48: v=m->misc3; draw_text(0,48,"Misc3:",0xF0,0xF7); break;
    }
    switch(v.t) {
      case TY_NUMBER:
        snprintf(buf,255,"%u",(int)v.u);
        draw_text(64,i,buf,0xF0,0xFE);
        break;
      case TY_CLASS:
        draw_text(64,i,"$",0xF0,0xFB);
        draw_text(72,i,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(64,i,buf,0xF0,0xFD);
        break;
      case TY_LEVELSTRING:
        snprintf(buf,255,"String %u",(int)v.u);
        draw_text(200,i,buf,0xF0,0xF9);
        break;
    }
  }
  draw_text(0,64,"Str:",0xF0,0xF7);
  snprintf(buf,255,"%d",nlevelstrings);
  draw_text(64,64,buf,0xF0,0xF7);
  draw_text(0,80,"<1-3> Misc   <4-6> Misc=String   <NumPad> Dir",0xF0,0xF3);
  draw_text(0,88,"<F1> Help   <F2> EditorHelp   <RET> Exit",0xF0,0xF3);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) switch(ev.type) {
    case SDL_KEYDOWN:
      switch(ev.key.keysym.sym) {
        case SDLK_RETURN: case SDLK_KP_ENTER: case SDLK_ESCAPE: return;
        case SDLK_1: v=ask_value("Misc1:"); if(v.t<4) m->misc1=v; break;
        case SDLK_2: v=ask_value("Misc2:"); if(v.t<4) m->misc2=v; break;
        case SDLK_3: v=ask_value("Misc3:"); if(v.t<4) m->misc3=v; break;
        case SDLK_4: case SDLK_q: j=string_list(); if(j!=-1) m->misc1=UVALUE(j,TY_LEVELSTRING); break;
        case SDLK_5: case SDLK_w: j=string_list(); if(j!=-1) m->misc2=UVALUE(j,TY_LEVELSTRING); break;
        case SDLK_6: case SDLK_e: j=string_list(); if(j!=-1) m->misc3=UVALUE(j,TY_LEVELSTRING); break;
        case SDLK_KP1: m->dir=5; break;
        case SDLK_KP2: m->dir=6; break;
        case SDLK_KP3: m->dir=7; break;
        case SDLK_KP4: m->dir=4; break;
        case SDLK_KP6: m->dir=0; break;
        case SDLK_KP7: m->dir=3; break;
        case SDLK_KP8: m->dir=2; break;
        case SDLK_KP9: m->dir=1; break;
        case SDLK_KP_PLUS: m->dir=(m->dir+1)&7; break;
        case SDLK_KP_MINUS: m->dir=(m->dir-1)&7; break;
        case SDLK_F1: case SDLK_g: if(classes[m->class]->gamehelp) modal_draw_popup(classes[m->class]->gamehelp); break;
        case SDLK_F2: case SDLK_h: if(classes[m->class]->edithelp) modal_draw_popup(classes[m->class]->edithelp); break;
      }
      goto redraw;
    case SDL_VIDEOEXPOSE:
      goto redraw;
    case SDL_QUIT:
      exit(0);
      break;
  }
}

static void mru_edit_obj(Uint32 obj) {
  Object*o;
  MRU m;
  if(obj>=nobjects) return;
  o=objects[obj];
  if(!o) return;
  m.class=o->class;
  m.img=o->image;
  m.misc1=o->misc1;
  m.misc2=o->misc2;
  m.misc3=o->misc3;
  m.dir=o->dir;
  mru_edit(&m);
  o->image=m.img;
  o->misc1=m.misc1;
  o->misc2=m.misc2;
  o->misc3=m.misc3;
  o->dir=m.dir;
}

static void add_object_at(int x,int y,MRU*m,int d) {
  Uint32 n,u;
  Class*c;
  if(x<1 || x>pfwidth || y<1 || y>pfheight || !m || !m->class) return;
  c=classes[m->class];
  if(!c) return;
  n=playfield[y*64+x-65];
  while(n!=VOIDLINK) {
    if(d && objects[n]->class==m->class) return;
    if(c->collisionLayers&classes[objects[n]->class]->collisionLayers) {
      u=objects[n]->up;
      objtrash(n);
      n=u;
    } else {
      n=objects[n]->up;
    }
  }
  generation_number_inc=0;
  n=objalloc(m->class);
  if(n==VOIDLINK) return;
  level_changed=1;
  objects[n]->oflags&=~OF_BIZARRO;
  objects[n]->x=x;
  objects[n]->y=y;
  objects[n]->image=m->img;
  objects[n]->dir=m->dir;
  objects[n]->misc1=m->misc1;
  objects[n]->misc2=m->misc2;
  objects[n]->misc3=m->misc3;
  pflink(n);
}

static void fprint_esc(FILE*fp,Uint8 c,const char*t) {
  char isimg=0;
  if(!c) return;
  fputc(c,fp);
  while(*t) switch(c=*t++) {
    case 1 ... 8: fprintf(fp,"\\%c",c+'0'-1); break;
    case 10: fprintf(fp,"\\n"); break;
    case 11: fprintf(fp,"\\l"); break;
    case 12: fprintf(fp,"\\c"); break;
    case 14: fprintf(fp,"\\i"); isimg=1; break;
    case 15: fprintf(fp,"\\b"); break;
    case 16: fprintf(fp,"\\q"); break;
    case 30: fprintf(fp,"\\d"); isimg=1; break;
    case 31:
      if(!*t) break;
      fprintf(fp,"\\x%02X",*t++);
      break;
    case 32 ... 127:
      if(c=='\\') {
        if(isimg) isimg=0; else fputc(c,fp);
      }
      fputc(c,fp);
      break;
    default:
      fprintf(fp,"\\x%02X",c);
  }
  fputc('\n',fp);
}

static void fprint_misc(FILE*fp,Value v) {
  switch(v.t) {
    case TY_NUMBER: fprintf(fp," %u",(int)v.u); break;
    case TY_CLASS: fprintf(fp," $%s",classes[v.u]->name); break;
    case TY_MESSAGE: fprintf(fp," %s%s",v.u<256?"":"#",v.u<256?standard_message_names[v.u]:messages[v.u-256]);
    case TY_LEVELSTRING: fprintf(fp," %%%u",(int)v.u); break;
    default: fprintf(fp," ???"); break;
  }
}

static void export_level_stream(FILE*fp) {
  Uint32*p=playfield;
  int i;
  Uint32 n;
  Object*o;
  if(!main_options['S']) fprint_esc(fp,'@',level_title);
  fprintf(fp,"C %d\nD %d %d\n",level_code,pfwidth,pfheight);
  again:
  for(i=0;i<64*64;i++) {
    n=p[i];
    while(n!=VOIDLINK) {
      o=objects[n];
      fprintf(fp,"%d %d $%s %d",o->x,o->y,classes[o->class]->name,o->image);
      fprint_misc(fp,o->misc1);
      fprint_misc(fp,o->misc2);
      fprint_misc(fp,o->misc3);
      fprintf(fp," %d\n",o->dir);
      n=o->up;
    }
  }
  if(p==playfield) {
    p=bizplayfield;
    for(i=0;i<64*64;i++) if(p[i]!=VOIDLINK) {
      fprintf(fp,"W\n");
      goto again;
    }
  } else {
    fprintf(fp,"W\n");
  }
  for(i=0;i<nlevelstrings;i++) fprint_esc(fp,'%',levelstrings[i]);
}

static void export_level(const char*cmd) {
  FILE*fp;
  if(!cmd || !*cmd) return;
  fp=popen(cmd,"w");
  if(!fp) {
    screen_message("Cannot open pipe");
    return;
  }
  fprintf(fp,"; Free Hero Mesh exported level ID=%d ORD=%d\n",level_id,level_ord);
  export_level_stream(fp);
  pclose(fp);
}

static char*import_numbers(char*p,int*x,int*y) {
  if(!p) return 0;
  while(*p==' ' || *p=='\t') p++;
  if(*p<'0' || *p>'9') return 0;
  *x=strtol(p,&p,10);
  if(*p && *p!=' ' && *p!='\t') return 0;
  while(*p==' ' || *p=='\t') p++;
  if(!y) return p;
  if(*p<'0' || *p>'9') return 0;
  *y=strtol(p,&p,10);
  if(*p && *p!=' ' && *p!='\t') return 0;
  while(*p==' ' || *p=='\t') p++;
  return p;
}

static char*import_value(char*p,Value*v) {
  int i,n;
  if(!p) return 0;
  while(*p==' ' || *p=='\t') p++;
  n=strcspn(p," \t");
  if(*p>='0' && *p<='9') {
    v->t=TY_NUMBER;
    v->u=strtol(p,&p,10)&0xFFFF;
  } else if(*p=='$') {
    v->t=TY_CLASS;
    p++; n--;
    for(i=1;i<0x4000;i++) {
      if(classes[i] && !(classes[i]->cflags&CF_NOCLASS2) && strlen(classes[i]->name)==n && !memcmp(p,classes[i]->name,n)) {
        v->u=i;
        p+=n;
        goto ok;
      }
    }
    return 0;
  } else if(*p=='%') {
    v->t=TY_LEVELSTRING;
    v->u=strtol(p+1,&p,10)&0xFFFF;
  } else if(*p=='#') {
    v->t=TY_MESSAGE;
    p++; n--;
    for(i=0;i<0x4000;i++) {
      if(messages[i] && strlen(messages[i])==n && !memcmp(p,messages[i],n)) {
        v->u=i+256;
        p+=n;
        goto ok;
      }
    }
    return 0;
  } else if(*p>='A' && *p<='Z') {
    v->t=TY_MESSAGE;
    for(i=0;i<N_MESSAGES;i++) {
      if(strlen(standard_message_names[i])==n && !memcmp(p,standard_message_names[i],n)) {
        v->u=i;
        p+=n;
        goto ok;
      }
    }
    return 0;
  } else {
    return 0;
  }
  ok: if(*p==' ' || *p=='\t') return p+1; else return 0;
}

static char*import_string(char*p) {
  char isimg=0;
  char*s=malloc(strlen(p)+3);
  char*q=s;
  if(!s) fatal("Allocation failed\n");
  while(*p) {
    if(*p=='\\') {
      p++;
      if(isimg) {
        isimg=0;
        *q++='\\';
      } else {
        switch(*p) {
          case '0' ... '7': *q++=*p+1-'0'; break;
          case 'b': *q++=15; break;
          case 'c': *q++=12; break;
          case 'd': *q++=30; isimg=1; break;
          case 'i': *q++=14; isimg=1; break;
          case 'l': *q++=11; break;
          case 'n': *q++=10; break;
          case 'q': *q++=16; break;
          case 'x':
            *q++=31;
            *q=0;
            if(p[1]>='0' && p[1]<='9') *q=(p[1]-'0')<<4;
            else if(p[1]>='A' && p[1]<='F') *q=(p[1]+10-'A')<<4;
            else if(p[1]>='a' && p[1]<='f') *q=(p[1]+10-'a')<<4;
            else goto bad;
            if(p[2]>='0' && p[2]<='9') *q|=(p[2]-'0');
            else if(p[2]>='A' && p[2]<='F') *q|=(p[2]+10-'A');
            else if(p[2]>='a' && p[2]<='f') *q|=(p[2]+10-'a');
            else goto bad;
            p+=2; q++; break;
          default: *q++=*p;
        }
        p++;
      }
    } else {
      *q++=*p++;
    }
  }
  if(0) {
    bad:
    screen_message("Bad \\x escape");
  }
  if(isimg) *q++='\\';
  *q=0;
  return s;
}

static void import_level(const char*cmd) {
  char d=0;
  int x,y;
  Object*o;
  FILE*fp;
  size_t size=0;
  char*buf=0;
  char*p;
  FILE*sol=0;
  Value v;
  if(!cmd || !*cmd) return;
  fp=main_options['i']?stdin:popen(cmd,"r");
  if(!fp) {
    screen_message("Cannot open pipe");
    return;
  }
  discard_solution();
  level_changed=1;
  while(getline(&buf,&size,fp)>0) {
    p=buf;
    p[strcspn(p,"\r\n\f")]=0;
    p+=strspn(p,"\t ");
    switch(*p) {
      case '0' ... '9':
        if(!d) {
          missd:
          screen_message("Missing D line");
          if(main_options['i']) exit(1);
          goto done;
        }
        p=import_numbers(p,&x,&y);
        if(!p) goto bad;
        if(x<1 || x>pfwidth || y<1 || y>pfheight) {
          range:
          fprintf(stderr,"Imported level coordinates out of range: %d %d\n",x,y);
          screen_message("Coordinates out of range");
          if(main_options['i']) exit(1);
          goto done;
        }
        p=import_value(p,&v);
        if(!p || v.t!=TY_CLASS) goto bad;
        v.u=objalloc(v.u);
        if(v.u==VOIDLINK) goto bad;
        o=objects[v.u];
        o->x=x;
        o->y=y;
        p=import_numbers(p,&x,0);
        o->image=x;
        p=import_value(p,&o->misc1);
        p=import_value(p,&o->misc2);
        p=import_value(p,&o->misc3);
        p=import_numbers(p,&x,0);
        o->dir=x;
        if((x&~7) || !p) {
          objtrash(v.u);
          goto bad;
        }
        pflink(v.u);
        break;
      case 'C':
        p=import_numbers(p+1,&x,0);
        if(!p || *p) goto bad;
        level_code=x;
        break;
      case 'D':
        p=import_numbers(p+1,&x,&y);
        if(!p || *p) goto bad;
        if(x<1 || x>64 || y<1 || y>64) goto range;
        if(d) {
          screen_message("Duplicate D line");
          goto done;
        }
        d=1;
        annihilate();
        pfwidth=x;
        pfheight=y;
        break;
      case 'F':
        if(!d) goto missd;
        v.t=0;
        p=import_value(p+1,&v);
        if(v.t!=TY_CLASS) goto bad;
        for(y=1;y<=pfheight;y++) for(x=1;x<=pfwidth;x++) {
          v.t=objalloc(v.u);
          if(v.t==VOIDLINK) goto bad;
          objects[v.t]->x=x;
          objects[v.t]->y=y;
          pflink(v.t);
        }
        break;
      case 'V':
        p=import_numbers(p+1,&x,0);
        if(!p || *p) goto bad;
        level_version=x;
        level_changed=0;
        break;
      case 'W':
        if(!d) goto missd;
        swap_world();
        break;
      case '@':
        free(level_title);
        level_title=import_string(p+1);
        break;
      case '%':
        if(nlevelstrings>0x2000) {
          screen_message("Too many level strings");
        } else {
          levelstrings=realloc(levelstrings,(nlevelstrings+1)*sizeof(unsigned char*));
          if(!levelstrings) fatal("Allocation failed\n");
          levelstrings[nlevelstrings++]=import_string(p+1);
        }
        break;
      case '\'':
        if(!sol) {
          sol=open_memstream(&solution_data,&solution_length);
          if(!sol) fatal("Allocation failed\n");
          fwrite("\0\0",1,3,sol); // append level version (will be overwritten later) and flags
        }
        p=strchr(buf,' ');
        if(p) {
          *p=0;
          p=import_numbers(p+1,&y,0);
          if(!p || *p) goto bad;
        } else {
          y=1;
        }
        for(x=8;x<256;x++) {
          if(heromesh_key_names[x] && !strcmp(buf+1,heromesh_key_names[x])) {
            while(y-->0) encode_move(sol,x);
            break;
          }
        }
        if(x==256) goto bad;
        break;
      case '=':
        if(main_options['i']) goto done;
        // fall through
      default:
        if(*p && *p!=';') {
          bad:
          fprintf(stderr,"Invalid record in imported data:  %s\n",buf);
          if(main_options['i']) exit(1);
          screen_message("Invalid record");
          goto done;
        }
    }
  }
  done:
  free(buf);
  if(!main_options['i']) pclose(fp);
  generation_number_inc=0;
  if(sol) {
    fclose(sol);
    if(!solution_data) fatal("Allocation failed");
  }
}

static void new_level(void) {
  sqlite3_stmt*st;
  int i;
  if(i=sqlite3_prepare_v2(userdb,"SELECT COALESCE(MAX(`LEVEL`),-1)+1 FROM `USERCACHEDATA` WHERE `FILE` = LEVEL_CACHEID();",-1,&st,0)) {
    screen_message(sqlite3_errmsg(userdb));
    return;
  }
  i=sqlite3_step(st);
  if(i!=SQLITE_ROW) {
    sqlite3_finalize(st);
    screen_message(sqlite3_errmsg(userdb));
    return;
  }
  i=sqlite3_column_int(st,0);
  sqlite3_finalize(st);
  if(i>0xFFFE) return;
  discard_solution();
  annihilate();
  level_id=i;
  free(level_title);
  level_title=strdup("New Level");
  if(!level_title) fatal("Allocation failed\n");
  level_changed=level_version=level_code=0;
  level_ord=level_nindex+1;
}

static int cursor_end(const Uint8*s) {
  int i=0;
  for(;;) {
    if(*s==10 || !*s) return i;
    if(*s==31 && s[1]) s++;
    i++;
    s++;
  }
}

static inline Uint8 pick_character(void) {
  SDL_Rect r;
  SDL_Event ev;
  Uint8 buf[17];
  int i,j;
  static Uint8 p=0;
  redraw:
  r.x=r.y=4;
  r.w=r.h=0x9C;
  SDL_FillRect(screen,&r,0xF8);
  SDL_LockSurface(screen);
  draw_text(24,8,"0123456789ABCDEF",0xF8,0xF3);
  buf[16]=0;
  for(i=0;i<16;i++) {
    snprintf(buf,2,"%X",i);
    draw_text(8,(i+3)<<3,buf,0xF8,0xF3);
    for(j=0;j<16;j++) buf[j]=(i<<4)|j?:255;
    draw_text(24,(i+3)<<3,buf,0xF8,0xFF);
  }
  buf[0]=p?:255;
  buf[1]=0;
  i=p>>4;
  j=p&15;
  draw_text((j+3)<<3,(i+3)<<3,buf,0xF2,0xFE);
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_QUIT:
        SDL_PushEvent(&ev);
        return;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_RETURN: case SDLK_SPACE: case SDLK_KP_ENTER: return p?:255;
          case SDLK_KP2: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_DOWN: p+=16; break;
          case SDLK_KP4: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_LEFT: p--; break;
          case SDLK_KP6: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_RIGHT: p++; break;
          case SDLK_KP8: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_UP: p-=16; break;
          default: norm:
            i=ev.key.keysym.unicode;
            if(i>='0' && i<='9') p=(p<<4)|(i-'0');
            if(i>='A' && i<='F') p=(p<<4)|(i+10-'A');
            if(i>='a' && i<='f') p=(p<<4)|(i+10-'a');
            if(i==13 || i==10) return p?:255;
        }
        goto redraw;
      case SDL_VIDEOEXPOSE:
        goto redraw;
    }
  }
}

static void edit_string(unsigned char**ps) {
  SDL_Rect rect;
  SDL_Event ev;
  char buf[19];
  Uint8*s=malloc(0x3000);
  Uint8*cp;
  Uint16 li[64];
  int c,e,i,j,n,r,sz;
  char o=0;
  char d=0;
  if(!s) fatal("Allocation failed\n");
  if(*ps) strncpy(s,*ps,0x2FFD); else *s=0;
  s[0x2FFD]=0;
  *li=0;
  c=r=0;
  redraw:
  e=0;
  redraw2:
  set_cursor(XC_xterm);
  rect.x=rect.y=0;
  rect.w=screen->w;
  rect.h=screen->h;
  SDL_FillRect(screen,&rect,0xF0);
  rect.h=24;
  SDL_FillRect(screen,&rect,0xF1);
  SDL_LockSurface(screen);
  if(ps==&level_title) {
    draw_text(0,16," Title: ",0xF9,0xFF);
  } else {
    snprintf(buf,19," String %d: ",(int)(ps-levelstrings));
    draw_text(0,16,buf,0xF9,0xFF);
  }
  draw_text(160,16,o?"OVR":"INS",0xF1,0xFE);
  draw_text(0,0,"<Esc> Cancel  <F1> Preview  <F2> Save  <\x18\x19\x1A\x1B> MoveCursor  <^P> InsertChar  <^Y> DelLine",0xF1,0xFB);
  draw_text(0,8,"ALT+  <0-7> Color  <B> Bar  <C> Center  <D> Data  <I> Image  <L> Left  <Q> Quiz",0xF1,0xFB);
  draw_text(0,24,"\x10",0xF0,0xF1);
  cp=0;
  for(i=j=n=0;s[i] && n<63;) {
    draw_text(0,(n+3)<<3,"\x10",0xF0,0xF1);
    i+=draw_text_line(8,(n+3)<<3,s+i,r==n?c:-1,&cp);
    if(j=(s[i]==10)) i++;
    sz=li[++n]=i;
  }
  if(!i) {
    sz=li[n=1]=r=c=0;
    cp=s;
    draw_text(8,24,"\xFE",0xFF,0xFE);
  } else if(j && n<63) {
    draw_text(0,(n+3)<<3,"\x10",0xF0,0xF1);
    draw_text(8,(n+3)<<3,"\xFE",r==n?0xFF:0xF0,r==n?0xFE:0xF1);
    if(r==n) cp=s+i,c=0;
    li[++n]=i;
  }
  if(r && r>=n) r--,e=0;
  if(n<63) li[n+1]=0xFFFF;
  SDL_UnlockSurface(screen);
  if(!cp) {
    if(e) fatal("Confusion in edit_string\n");
    c=cursor_end(s+li[r]);
    e=1;
    goto redraw2;
  }
  if(d) {
    d=0;
    goto del;
  }
  snprintf(buf,19,"%d,%d / %d [%d]",r+1,c,n,sz);
  draw_text(200,16,buf,0xF1,0xFA);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_QUIT:
        SDL_PushEvent(&ev);
        return;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_ESCAPE: return;
          case SDLK_F1: modal_draw_popup(s); break;
          case SDLK_F2:
            free(*ps);
            *ps=s;
            return;
          case SDLK_KP0: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_INSERT: ins: o^=1; break;
          case SDLK_KP1: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_END: end: c=cursor_end(s+li[r]); break;
          case SDLK_KP2: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_DOWN: down: if(r<63 && li[r+1]!=0xFFFF) ++r; break;
          case SDLK_KP4: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_LEFT: left: if(c) --c; break;
          case SDLK_KP6: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_RIGHT: right: ++c; break;
          case SDLK_KP7: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_HOME: home: c=0; break;
          case SDLK_KP8: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_UP: up: if(r) --r; break;
          case SDLK_KP_PERIOD: if(ev.key.keysym.mod&KMOD_NUM) goto norm; //fallthrough
          case SDLK_DELETE: del:
            if(!*cp) break;
            i=(*cp==31?2:1);
            memmove(cp,cp+i,(s+sz)-(cp+i-1));
            break;
          case SDLK_BACKSPACE: back:
            if(cp==s || !c) break;
            c--;
            d=1;
            break;
          default: norm:
            i=ev.key.keysym.unicode;
            if(ev.key.keysym.mod&(KMOD_ALT|KMOD_META)) {
              switch(ev.key.keysym.sym) {
                case SDLK_0: i=1; goto addch;
                case SDLK_1: i=2; goto addch;
                case SDLK_2: i=3; goto addch;
                case SDLK_3: i=4; goto addch;
                case SDLK_4: i=5; goto addch;
                case SDLK_5: i=6; goto addch;
                case SDLK_6: i=7; goto addch;
                case SDLK_7: i=8; goto addch;
                case SDLK_b: i=15; goto addch;
                case SDLK_c: i=12; goto addch;
                case SDLK_d: i=30; goto addch;
                case SDLK_i: i=14; goto addch;
                case SDLK_l: i=11; goto addch;
                case SDLK_n: i=10; goto addch;
                case SDLK_q: i=16; goto addch;
              }
            }
            if(i<32) {
              switch(i+64) {
                case 'A': goto home;
                case 'D': goto right;
                case 'E': goto up;
                case 'F': goto end;
                case 'G': goto del;
                case 'H': goto back;
                case 'M': i=10; goto addch;
                case 'P': goto specialchar;
                case 'S': goto left;
                case 'W': r=c=0; break;
                case 'X': goto down;
                case 'V': goto ins;
                case 'Y': goto erase;
                case 'Z': r=n?n-1:0; c=0; break;
              }
            } else if(i<127) {
              addch:
              if(i==10 && n>62) break;
              if(sz>=0x2FFC) break;
              if(o && *cp==31) memmove(cp,cp+1,(s+sz)-cp);
              if(!*cp) cp[1]=0;
              if(!o) memmove(cp+1,cp,(s+sz+1)-cp);
              *cp=i;
              if(i==10) r++,c=0; else c++;
            }
            break;
        }
        goto redraw;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.button==1 && ev.button.y>=24) {
          if(c=ev.button.x>>3) c--;
          i=(ev.button.y-24)>>3;
          r=0;
          while(r<63 && r<i && li[r+1]!=0xFFFF) r++;
        }
        goto redraw;
      case SDL_VIDEOEXPOSE:
        goto redraw;
    }
  }
  erase:
  c=0;
  if(li[r+1]==0xFFFF) s[li[r]]=0; else memmove(s+li[r],s+li[r+1],sz+1-li[r+1]);
  if(r && r==n-1) --r;
  goto redraw;
  specialchar:
  if(sz>=0x2FFA) goto redraw;
  i=pick_character();
  if(i>=32) goto addch;
  if(o && *cp==31) memmove(cp,cp+1,(s+sz)-cp);
  if(!*cp) cp[1]=0;
  s[sz+2]=0;
  memmove(cp+1,cp,(s+sz+1)-cp);
  if(!o) memmove(cp+1,cp,(s+sz+1)-cp);
  *cp=31;
  cp[1]=i;
  c++;
  goto redraw;
}

static inline void replace_level_string_uses(int x) {
  Uint32 n;
  Object*o;
  for(n=0;n<nobjects;n++) if(o=objects[n]) {
    if(o->misc1.t==TY_LEVELSTRING && o->misc1.u>=x) --o->misc1.u;
    if(o->misc2.t==TY_LEVELSTRING && o->misc2.u>=x) --o->misc2.u;
    if(o->misc3.t==TY_LEVELSTRING && o->misc3.u>=x) --o->misc3.u;
  }
}

static inline int count_level_string_uses(int x) {
  int c=0;
  Uint32 n;
  Object*o;
  for(n=0;n<nobjects;n++) if(o=objects[n]) {
    if(o->misc1.t==TY_LEVELSTRING && o->misc1.u==x) c++;
    if(o->misc2.t==TY_LEVELSTRING && o->misc2.u==x) c++;
    if(o->misc3.t==TY_LEVELSTRING && o->misc3.u==x) c++;
  }
  return c;
}

static int string_list(void) {
  char buf[18];
  SDL_Event ev;
  SDL_Rect r;
  int n=0;
  int scroll=0;
  int i,j;
  redraw:
  r.x=r.y=0;
  r.w=screen->w;
  r.h=screen->h;
  SDL_FillRect(screen,&r,0xF0);
  r.h=8;
  SDL_FillRect(screen,&r,0xF1);
  r.y=8;
  r.h=screen->h-8;
  scrollbar(&scroll,screen->h/8-1,nlevelstrings,0,&r);
  SDL_LockSurface(screen);
  draw_text(0,0,"Level Strings:   <INS> Add  <DEL> Delete  <SP> Edit  <RET> Pick  <F1> Preview  <F2> Uses  <ESC> Cancel",0xF1,0xFF);
  for(i=0;i<screen->h/8-1;i++) {
    j=i+scroll;
    if(j>=nlevelstrings) break;
    snprintf(buf,8,"%c%5d%c",n==j?'<':' ',j,n==j?'>':' ');
    draw_text(16,(i+1)<<3,buf,0xF0,0xFB);
    draw_text(80,(i+1)<<3,levelstrings[j],0xF0,0xF7);
  }
  SDL_UnlockSurface(screen);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    if(ev.type!=SDL_VIDEOEXPOSE && scrollbar(&scroll,screen->h/8-1,nlevelstrings,&ev,&r)) goto redraw;
    switch(ev.type) {
      case SDL_MOUSEMOTION:
        set_cursor(XC_draft_small);
        break;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_0: case SDLK_KP0: n*=10; break;
          case SDLK_1: case SDLK_KP1: n=10*n+1; break;
          case SDLK_2: case SDLK_KP2: n=10*n+2; break;
          case SDLK_3: case SDLK_KP3: n=10*n+3; break;
          case SDLK_4: case SDLK_KP4: n=10*n+4; break;
          case SDLK_5: case SDLK_KP5: n=10*n+5; break;
          case SDLK_6: case SDLK_KP6: n=10*n+6; break;
          case SDLK_7: case SDLK_KP7: n=10*n+7; break;
          case SDLK_8: case SDLK_KP8: n=10*n+8; break;
          case SDLK_9: case SDLK_KP9: n=10*n+9; break;
          case SDLK_BACKSPACE: n/=10; break;
          case SDLK_ESCAPE: return -1;
          case SDLK_RETURN: case SDLK_KP_ENTER: if(n<nlevelstrings) return n; break;
          case SDLK_SPACE: if(n<nlevelstrings) edit_string(levelstrings+n); break;
          case SDLK_INSERT:
            if(nlevelstrings<0x2000) {
              levelstrings=realloc(levelstrings,(nlevelstrings+1)*sizeof(unsigned char*));
              if(!levelstrings) fatal("Allocation failed\n");
              levelstrings[n=nlevelstrings++]=strdup("");
              if(!levelstrings[n]) fatal("Allocation failed\n");
              edit_string(levelstrings+n);
            }
            break;
          case SDLK_DELETE:
            if(n>=nlevelstrings) break;
            if(n==nlevelstrings-1) {
              free(levelstrings[--nlevelstrings]);
            } else {
              replace_level_string_uses(n+1);
              free(levelstrings[n]);
              --nlevelstrings;
              for(i=n;i<nlevelstrings;i++) levelstrings[i]=levelstrings[i+1];
            }
            break;
          case SDLK_F1: if(nlevelstrings) modal_draw_popup(levelstrings[n]); set_cursor(XC_draft_small); break;
          case SDLK_F2:
            if(nlevelstrings) {
              snprintf(buf,17,"%d uses",count_level_string_uses(n));
              modal_draw_popup(buf);
            }
            break;
          case SDLK_UP: case SDLK_KP_MINUS: n--; break;
          case SDLK_DOWN: case SDLK_KP_PLUS: n++; break;
        }
        if(n<=0) n=0; else if(n>=nlevelstrings) n=nlevelstrings-1;
        goto redraw;
      case SDL_MOUSEBUTTONDOWN:
        i=ev.button.y/8-scroll-1;
        if(i<0 || i>=nlevelstrings) break;
        n=i;
        if(ev.button.button==3 && n<nlevelstrings) return n;
        goto redraw;
      case SDL_VIDEOEXPOSE:
        goto redraw;
      case SDL_QUIT:
        exit(0);
        return -1;
    }
  }
}

typedef struct {
  char*title;
  Uint16 id,kind,mark;
} IndexItem;

static void level_index_editor(void) {
  SDL_Rect r;
  SDL_Event ev;
  char had_divisions=0;
  FILE*out;
  char*buf=0;
  char b[32];
  size_t bufsize=0;
  IndexItem*items;
  IndexItem*items2;
  IndexItem citem;
  int nitems;
  int nmark=0;
  sqlite3_stmt*st=0;
  int i,j,x,y;
  int sel=0;
  int scroll=0;
  char rescroll=1;
  const char*q;
  if(sqlite3_prepare_v2(userdb,"SELECT COUNT() FROM `DIVISIONS`;",-1,&st,0) || sqlite3_step(st)!=SQLITE_ROW) {
    screen_message(sqlite3_errmsg(userdb));
    sqlite3_finalize(st);
    return;
  }
  had_divisions=sqlite3_column_int(st,0)?1:0;
  nitems=level_nindex+sqlite3_column_int(st,0);
  sqlite3_finalize(st);
  if(sqlite3_prepare_v2(userdb,"SELECT 0,`FIRST`,0,`HEADING`,_ROWID_ FROM `DIVISIONS` UNION ALL SELECT 1,`ORD`,`ID`,`TITLE`,0 FROM `LEVELS` ORDER BY 2,1,5;",-1,&st,0)) {
    screen_message(sqlite3_errmsg(userdb));
    return;
  }
  items=calloc(nitems,sizeof(IndexItem));
  if(!items) fatal("Allocation failed\n");
  for(i=0;i<nitems;i++) {
    j=sqlite3_step(st);
    if(j!=SQLITE_ROW) {
      if(j!=SQLITE_DONE) screen_message(sqlite3_errmsg(userdb));
      nitems=i;
      break;
    }
    items[i].kind=sqlite3_column_int(st,0);
    items[i].id=sqlite3_column_int(st,2);
    items[i].title=strdup(sqlite3_column_text(st,3)?:(const unsigned char*)"<?>");
    if(!items[i].title) fatal("Allocation failed\n");
  }
  sqlite3_finalize(st);
  // Main editor loop (GUI)
  set_cursor(XC_arrow);
  redraw:
  if(sel<0) sel=0;
  if(sel>nitems) sel=nitems;
  if(rescroll) {
    if(scroll<0) scroll=0;
    if(sel<scroll) scroll=sel;
    if(sel>=scroll+screen->h/8-2) scroll=sel+3-screen->h/8;
    rescroll=0;
  }
  SDL_FillRect(screen,0,0x02);
  r.x=r.y=0;
  r.w=screen->w;
  r.h=16;
  SDL_FillRect(screen,&r,0xF7);
  SDL_LockSurface(screen);
  draw_text(0,0,"<\x18\x19> Cursor  <SHIFT+\x18\x19> Exchange  <INS> Add Division  <DEL> Delete Division  <ESC> Save",0xF7,0xF0);
  draw_text(0,8,"<F1/E> Edit Title  <F2/M> Mark  <F3/P> Paste  <F4/T> Show Title  <F5/U> Unmark All  <F6/F> Find  <F7> Current",0xF7,0xF0);
  for(y=16,i=scroll;y<screen->h-7 && i<=nitems;y+=8,i++) {
    if(i==sel) {
      r.x=16; r.y=y;
      r.w=screen->w-16; r.h=8;
      SDL_FillRect(screen,&r,0xF8);
      draw_text(16,y,"\x10",x=0xF8,nmark?0xFD:0xFE);
    } else {
      draw_text(16,y,"\xB3",x=0x02,0xF8);
    }
    draw_text(6*8,y,"\xB3",x,x^0xFA);
    draw_text(12*8,y,"\xB3",x,x^0xFA);
    if(i==nitems) {
      draw_text(4*8,y,"\x04 END \x04",x,0xFC);
    } else if(items[i].kind) {
      snprintf(b,8,"%5u",items[i].id);
      draw_text(7*8,y,b,x,0xFA);
      draw_text(13*8,y,items[i].title,x,0xFF);
    } else {
      draw_text(7*8,y,"\x07\x07\x07\x07\x07",x,0xF9);
      draw_text(13*8,y,items[i].title,x,0xFE);
    }
    if(i<nitems && items[i].mark) {
      snprintf(b,8,"%3u",items[i].mark);
      draw_text(3*8,y,b,0xF1,0xFB);
    }
  }
  SDL_UnlockSurface(screen);
  r.x=r.w=0; r.y=16; r.h=screen->h-16;
  scrollbar(&scroll,screen->h/8-2,nitems+1,0,&r);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) {
    if(ev.type!=SDL_VIDEOEXPOSE && scrollbar(&scroll,screen->h/8-2,nitems+1,&ev,&r)) goto redraw;
    switch(ev.type) {
      case SDL_MOUSEMOTION:
        set_cursor(XC_arrow);
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.x>=15) sel=(ev.button.y-16)/8+scroll;
        if(ev.button.button==2) goto paste;
        if(ev.button.button==3) goto mark;
        goto redraw;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_UP: case SDLK_k: case SDLK_KP8:
            if(!sel) break;
            sel--;
            if(ev.key.keysym.mod&(KMOD_ALT|KMOD_META)) {
              while(sel>0 && items[sel].kind) sel--;
            } else if((ev.key.keysym.mod&KMOD_SHIFT) && sel+1<nitems) {
              citem=items[sel+1];
              items[sel+1]=items[sel];
              items[sel]=citem;
            }
            rescroll=1; goto redraw;
          case SDLK_DOWN: case SDLK_j: case SDLK_KP2:
            if(sel>=nitems) break;
            sel++;
            if(ev.key.keysym.mod&(KMOD_ALT|KMOD_META)) {
              while(sel<nitems && items[sel].kind) sel++;
            } else if((ev.key.keysym.mod&KMOD_SHIFT) && sel!=nitems) {
              citem=items[sel-1];
              items[sel-1]=items[sel];
              items[sel]=citem;
            }
            rescroll=1; goto redraw;
          case SDLK_PAGEUP: case SDLK_KP9:
            sel-=screen->h/8-2;
            scroll-=screen->h/8-2;
            rescroll=1; goto redraw;
          case SDLK_PAGEDOWN: case SDLK_KP3:
            sel+=screen->h/8-2;
            scroll+=screen->h/8-2;
            rescroll=1; goto redraw;
          case SDLK_HOME: case SDLK_KP7: scroll=sel=0; goto redraw;
          case SDLK_END: case SDLK_KP1: sel=nitems; rescroll=1; goto redraw;
          case SDLK_INSERT: case SDLK_KP0:
            if(nitems>=65535) {
              screen_message("Too many items");
              goto redraw;
            }
            q=screen_prompt("Add division?");
            if(!q || !*q) goto redraw;
            items=realloc(items,(nitems+1)*sizeof(IndexItem));
            if(!items) fatal("Allocation failed\n");
            memmove(items+sel+1,items+sel,(nitems-sel)*sizeof(IndexItem));
            items[sel].kind=items[sel].mark=0;
            items[sel].title=strdup(q);
            if(!items[sel].title) fatal("Allocation failed\n");
            ++nitems;
            rescroll=1; goto redraw;
          case SDLK_DELETE: case SDLK_KP_PERIOD:
            if(sel==nitems || items[sel].kind) break;
            if(items[sel].mark) {
              --nmark;
              for(i=0;i<nitems;i++) if(items[i].mark>items[sel].mark) --items[i].mark;
            }
            free(items[sel].title);
            memmove(items+sel,items+sel+1,(nitems-sel-1)*sizeof(IndexItem));
            --nitems;
            goto redraw;
          case SDLK_F1: case SDLK_e:
            if(sel==nitems || items[sel].kind) break;
            q=screen_prompt("Edit division?");
            if(!q || !*q) goto redraw;
            free(items[sel].title);
            items[sel].title=strdup(q);
            if(!items[sel].title) fatal("Allocation failed");
            goto redraw;
          case SDLK_F2: case SDLK_m: mark:
            if(sel==nitems || items[sel].mark || nmark==999) goto redraw;
            items[sel].mark=++nmark;
            goto redraw;
          case SDLK_F3: case SDLK_p: paste:
            if(!nmark) goto redraw;
            x=sel;
            if(x<nitems) while(x && items[x].mark && items[x-1].mark) --x;
            items2=items;
            items=malloc(nitems*sizeof(IndexItem));
            if(!items) fatal("Allocation failed\n");
            for(i=j=0;i<x;i++) if(!items2[i].mark) items[j++]=items2[i];
            sel=j; j+=nmark;
            for(i=0;i<nitems;i++) {
              if(y=items2[i].mark) items2[i].mark=0,items[sel+y-1]=items2[i];
              else if(i>=x) items[j++]=items2[i];
            }
            free(items2);
            nmark=0;
            rescroll=1; goto redraw;
          case SDLK_F4: case SDLK_t:
            if(sel==nitems) break;
            modal_draw_popup(items[sel].title);
            goto redraw;
          case SDLK_F5: case SDLK_u:
            for(i=0;i<nitems;i++) items[i].mark=0;
            nmark=0;
            goto redraw;
          case SDLK_F6: case SDLK_f:
            q=screen_prompt("Find?");
            if(!q || !*q) goto redraw;
            if(*q>='0' && *q<='9') {
              j=strtol(q,0,10);
              for(i=0;i<nitems;i++) if(items[i].kind && items[i].id==j) sel=i;
            }
            rescroll=1; goto redraw;
          case SDLK_F7:
            for(i=0;i<nitems;i++) if(items[i].kind && items[i].id==level_id) {
              sel=i;
              rescroll=1; goto redraw;
            }
            break;
          case SDLK_ESCAPE:
            if(ev.key.keysym.mod&KMOD_SHIFT) goto final;
            goto save;
        }
        break;
      case SDL_VIDEOEXPOSE: goto redraw;
      case SDL_QUIT: SDL_PushEvent(&ev); goto save;
    }
  }
  save:
  // Saving the changes, including SQL table and DIVISION.IDX lump
  sqlite3_exec(userdb,"DELETE FROM `DIVISIONS`;",0,0,0);
  if(sqlite3_prepare_v2(userdb,"INSERT INTO `DIVISIONS`(`HEADING`,`FIRST`) VALUES(?1,?2);",-1,&st,0)) {
    screen_message(sqlite3_errmsg(userdb));
    goto final;
  }
  out=open_memstream(&buf,&bufsize);
  if(!out) fatal("Allocation failed");
  for(i=j=0;i<nitems;i++) {
    if(items[i].kind) {
      if(j<level_nindex) level_index[j++]=items[i].id;
    } else {
      sqlite3_bind_text(st,1,items[i].title,-1,0);
      sqlite3_bind_int(st,2,j+1);
      while(sqlite3_step(st)==SQLITE_ROW);
      sqlite3_reset(st);
      fputc((j+1)&255,out); fputc((j+1)>>8,out);
      fwrite(items[i].title,1,strlen(items[i].title)+1,out);
    }
  }
  sqlite3_finalize(st);
  if(!ftell(out)) {
    if(!had_divisions) goto nodiv;
    fputc(0,out);
  }
  fclose(out);
  if(!buf) fatal("Allocation failed");
  write_lump(FIL_LEVEL,LUMP_DIVISION_IDX,bufsize,buf);
  nodiv:
  free(buf);
  // LEVEL.IDX lump
  update_level_index();
  // Done
  final:
  for(i=0;i<nitems;i++) free(items[i].title);
  free(items);
}

static int editor_command(int prev,int cmd,int number,int argc,sqlite3_stmt*args,void*aux) {
  int x,y;
  switch(cmd) {
    case '^a': // Add object (no duplicates)
      if(prev) return prev;
      add_object_at(number&63?:64,number/64?:64,mru+curmru,1);
      return 0;
    case '^c': // Select class/image
      class_image_select();
      return 0;
    case '^e': // Edit Misc/Dir of MRU slot
      mru_edit(mru+curmru);
      return 0;
    case '^s': // String list/edit
      string_list();
      return 0;
    case '^u': // Add object (allow duplicates)
      if(prev) return prev;
      add_object_at(number&63?:64,number/64?:64,mru+curmru,0);
      return 0;
    case '^w': // Swap world
      swap_world();
      level_changed=1;
      return 0;
    case '^I': // Level index editor
      sqlite3_exec(userdb,"BEGIN;",0,0,0);
      level_index_editor();
      sqlite3_exec(userdb,"DELETE FROM LEVELS; DROP TABLE TEMP.LEVELS;",0,0,0);
      sqlite3_exec(userdb,"COMMIT;",0,0,0);
      for(x=0;x<level_nindex;x++) if(level_id==level_index[x]) level_ord=x+1;
      return 0;
    case '^N': // New level
      if(level_nindex>0xFFFE) return 0;
      new_level();
      return 1;
    case '^P': // Play
      discard_solution();
      return -2;
    case '^Q': // Quit
      return -1;
    case '^S': // Save level
      save_level();
      return 1;
    case '^T': // Level title
      edit_string(&level_title);
      return 0;
    case '^Y': // Sound test
      sound_test();
      return prev;
    case '^Z': // Cancel rectangle
      editrect.x0=editrect.y0=editrect.x1=editrect.y1=0;
      return prev;
    case '^<': // First corner
      if((number&63?:64)>pfwidth || (number/64?:64)>pfheight) return prev;
      editrect.x0=number&63?:64;
      editrect.y0=number/64?:64;
      goto setrect;
    case '^>': // Second corner
      if((number&63?:64)>pfwidth || (number/64?:64)>pfheight) return prev;
      editrect.x1=number&63?:64;
      editrect.y1=number/64?:64;
      setrect:
      if(!editrect.x1) editrect.x1=editrect.x0;
      if(!editrect.y1) editrect.y1=editrect.y0;
      if(editrect.x0>editrect.x1) x=editrect.x0,editrect.x0=editrect.x1,editrect.x1=x;
      if(editrect.y0>editrect.y1) y=editrect.y0,editrect.y0=editrect.y1,editrect.y1=y;
      return prev;
    case 'am': // Add MRU
      if(argc<7) return prev;
      for(x=1;x<7;x++) if(sqlite3_column_type(args,x)==SQLITE_NULL) return prev;
      x=sqlite3_column_int(args,1)&0x3FFF;
      if(!x) return prev;
      y=sqlite3_column_int(args,2)&0xFF;
      add_mru(x,y);
      mru->misc1=UVALUE(sqlite3_column_int64(args,3),sqlite3_column_int64(args,3)>>32);
      mru->misc2=UVALUE(sqlite3_column_int64(args,4),sqlite3_column_int64(args,4)>>32);
      mru->misc3=UVALUE(sqlite3_column_int64(args,5),sqlite3_column_int64(args,5)>>32);
      mru->dir=sqlite3_column_int(args,6)&7;
      return prev;
    case 'em': // Edit Misc/Dir of object
      if(argc>1 && sqlite3_column_type(args,1)!=SQLITE_NULL) mru_edit_obj(sqlite3_column_int64(args,1));
      return 0;
    case 'ex': // Export level
      if(argc<2) return prev;
      export_level(sqlite3_column_text(args,1));
      return prev;
    case 'go': // Select level
      discard_solution();
      load_level(number);
      return 1;
    case 'im': // Import level
      if(argc<2) return prev;
      import_level(sqlite3_column_text(args,1));
      return 0;
    case 'lc': // Set level code
      level_code=number;
      level_changed=1;
      return 0;
    case 'lt': // Set level title
      if(argc<2 || !sqlite3_column_text(args,1)) break;
      free(level_title);
      level_title=strdup(sqlite3_column_text(args,1));
      if(!level_title) fatal("Allocation failed\n");
      return 0;
    case 'lv': // Set level version
      discard_solution();
      level_version=number;
      level_changed=0;
      return 0;
    case 'mR': // Select MRU relative
      number+=curmru;
      // fall through
    case 'mr': // Select MRU absolute
      if(number>=0 && number<MRUCOUNT) curmru=number;
      return 0;
    case 're': // Resize and clear
      if(argc<3) return 0;
      editrect.x0=editrect.y0=editrect.x1=editrect.y1=0;
      x=sqlite3_column_int(args,1);
      y=sqlite3_column_int(args,2);
      if(x<1 || y<1 || x>64 || y>64) return 0;
      level_changed=1;
      discard_solution();
      annihilate();
      pfwidth=x;
      pfheight=y;
      return 0;
    default:
      return prev;
  }
}

void run_editor(void) {
  SDL_Event ev;
  int i;
  curmru=0;
  set_caption();
  discard_solution();
  load_level(level_id);
  redraw_editor();
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_VIDEOEXPOSE:
        redraw_editor();
        break;
      case SDL_QUIT:
        exit(0);
        break;
      case SDL_MOUSEMOTION:
        set_cursor(ev.motion.x<left_margin?XC_arrow:XC_tcross);
        show_mouse_xy(&ev);
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.x<left_margin) {
          if(ev.button.y<56) break;
          i=(ev.button.y-56)/picture_size;
          if(i>=0 && i<MRUCOUNT) {
            curmru=i;
            if(ev.button.button==3) mru_edit(mru+i);
            redraw_editor();
          }
          break;
        } else {
          i=exec_key_binding(&ev,1,(ev.button.x-left_margin)/picture_size+1,ev.button.y/picture_size+1,editor_command,0);
          goto command;
        }
      case SDL_KEYDOWN:
        i=exec_key_binding(&ev,1,0,0,editor_command,0);
      command:
        if(i==-1) exit(0);
        if(i==-2) {
          main_options['e']=0;
          return;
        }
        redraw_editor();
        break;
    }
  }
}

void write_empty_level_set(FILE*fp) {
  static const unsigned char d[]=
    "CLASS.DEF\0\x00\x00\x02\x00"
    "\x00\x00" // no classes or messages
    "LEVEL.IDX\0\x00\x00\x02\x00"
    "\x00\x00" // only one level
    "0.LVL\0\x00\x00\x0D\x00"
    "\x00\x00" // level version
    "\x00\x00" // level code
    "\x00\x00" // width/height
    "Blank\0" // title
    "\xFF" // objects
  ;
  fwrite(d,1,sizeof(d)-1,fp);
  fflush(fp);
  rewind(fp);
}

void batch_import(void) {
  load_level(level_id);
  if(nobjects) new_level();
  while(!feof(stdin)) {
    if(main_options['v']) fprintf(stderr,"Level %d\n",level_ord);
    import_level("#");
    if(nobjects) {
      save_level();
      if(!feof(stdin)) new_level();
    }
  }
}

static inline void hexout(const unsigned char*p,int n) {
  while(n--) printf("%02x",*p++);
}

void make_level_hashes(void) {
  char*v;
  FILE*in;
  FILE*out;
  unsigned char h1[128];
  unsigned char h2[128];
  long long alg;
  int hn,c,n;
  optionquery[1]=Q_hash;
  alg=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"",0,0)?:HASH_SHA3_256;
  hn=hash_length(alg);
  if(hn<1 || hn>128) fatal("Hash algorithm not valid or too long: %llu (length %d)\n",alg,hn);
  // Class hash
  if(main_options['z']) {
    in=composite_slice(".class",1);
  } else {
    v=sqlite3_mprintf("%s.class",basefilename);
    if(!v) fatal("Allocation failed\n");
    in=fopen(v,"r");
    sqlite3_free(v);
  }
  if(!in) fatal("Cannot open class file\n");
  out=hash_stream(alg,0,h1);
  if(!out) fatal("Allocation failed\n");
  while((c=fgetc(in))!=EOF) {
    if(c=='\t') c=' ';
    if(c!='\r') fputc(c,out);
  }
  fclose(in);
  fclose(out);
  printf("-1 0 %llx ",alg);
  hexout(h1,hn);
  putchar('\n');
  for(n=0;n<level_nindex;n++) {
    if(load_level(level_index[n])) fatal("Level load error\n");
    out=hash_stream(alg,0,h2);
    if(!out) fatal("Allocation failed\n");
    fprintf(out,"\x01%llx %x\n",alg,hn);
    fwrite(h1,1,hn,out);
    export_level_stream(out);
    fclose(out);
    printf("%d 0 %llx ",level_id,alg);
    hexout(h2,hn);
    putchar('\n');
  }
  exit(0);
}