Free Hero Mesh

picture.c at [60b09ee8e4]
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 picture.c artifact ff76ebbf80 part of check-in 60b09ee8e4


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

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

#define _BSD_SOURCE
#include "SDL.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sqlite3.h"
#include "smallxrm.h"
#include "pcfont.h"
#include "quarks.h"
#include "heromesh.h"
#include "cursorshapes.h"
#include "keyicons.xbm"

#define PPR 128 // pictures per row
#define PPRM 127 // pictures per row bit mask (one less than pictures per row)
#define PPRS 7 // pictures per row bit shift amount (log2 of pictures per row)

SDL_Surface*screen;
Uint16 picture_size;
int left_margin;
Uint32 codepage;

static SDL_Surface*picts;
static Uint8*curpic;
static const unsigned char*fontdata;
static const Uint8 bytewidth[32];

static const char default_palette[]=
  "C020FF "
  "000000 222222 333333 444444 555555 666666 777777 888888 999999 AAAAAA BBBBBB CCCCCC DDDDDD EEEEEE FFFFFF "
  "281400 412300 5F3200 842100 A05000 C35F14 E1731E FF8232 FF9141 FFA050 FFAF5F FFBE73 FFD282 FFE191 FFF0A0 "
  "321E1E 412220 5F2830 823040 A03A4C BE4658 E15464 FF6670 FF7F7B FF8E7F FF9F7F FFAF7F FFBF7F FFCF7F FFDF7F "
  "280D0D 401515 602020 802A2A A03535 C04040 E04A4A FF5555 FF6764 FF6F64 FF7584 FF849D FF94B7 FF9FD1 FFAEEA "
  "901400 A02000 B03000 C04000 D05000 E06000 F07000 FF8000 FF9000 FFA000 FFB000 FFC000 FFD000 FFE000 FFF000 "
  "280000 400000 600000 800000 A00000 C00000 E00000 FF0000 FF2828 FF4040 FF6060 FF8080 FFA0A0 FFC0C0 FFE0E0 "
  "280028 400040 600060 800080 A000A0 C000C0 E000E0 FF00FF FF28FF FF40FF FF60FF FF80FF FFA0FF FFC0FF FFE0FF "
  "281428 402040 603060 804080 A050A0 C060C0 E070E0 FF7CFF FF8CFF FF9CFF FFACFF FFBCFF FFCCFF FFDCFF FFECFF "
  "280050 350566 420A7C 4F0F92 5C14A8 6919BE 761ED4 8323EA 9028FF A040FF B060FF C080FF D0A0FF E0C0FF F0E0FF "
  "000028 000040 000060 000080 0000A0 0000C0 0000E0 0000FF 0A28FF 284AFF 466AFF 678AFF 87AAFF A7CAFF C7EBFF "
  "0F1E1E 142323 193232 1E4141 285050 325F5F 377373 418282 469191 50A0A0 5AAFAF 5FC3C3 69D2D2 73E1E1 78F0F0 "
  "002828 004040 006060 008080 00A0A0 00C0C0 00E0E0 00FFFF 28FFFF 40FFFF 60FFFF 80FFFF A0FFFF C0FFFF E0FFFF "
  "002800 004000 006000 008000 00A000 00C000 00E000 00FF00 28FF28 40FF40 60FF60 80FF80 A0FFA0 C0FFC0 E0FFE0 "
  "002110 234123 325F32 418241 50A050 5FC35F 73E173 85FF7A 91FF6E A0FF5F B4FF50 C3FF41 D2FF32 E1FF23 F0FF0F "
  "282800 404000 606000 808000 A0A000 C0C000 E0E000 FFFF00 FFFF28 FFFF40 FFFF60 FFFF80 FFFFA0 FFFFC0 FFFFE0 "
  //
  "442100 00FF55 0055FF FF5500 55FF00 FF0055 5500FF CA8B25 F078F0 F0F078 FF7F00 DD6D01 7AFF00 111111 "
  //
  "000000 0000AA 00AA00 00AAAA AA0000 AA00AA AAAA00 AAAAAA "
  "555555 5555FF 55FF55 55FFFF FF5555 FF55FF FFFF55 FFFFFF "
;

void init_palette(void) {
  double gamma;
  int usegamma=1;
  int i,j;
  SDL_Color pal[256];
  FILE*fp=0;
  const char*v;
  optionquery[1]=Q_gamma;
  gamma=strtod(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"0",0);
  if(gamma<=0.0 || gamma==1.0) usegamma=0;
  optionquery[1]=Q_palette;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(v && *v) {
    fp=fopen(v,"r");
    if(!fp) fatal("Unable to load palette file '%s'\n%m",v);
  }
  for(i=0;i<256;i++) {
    if(fp) {
      if(fscanf(fp,"%2hhX%2hhX%2hhX ",&pal[i].r,&pal[i].g,&pal[i].b)!=3) fatal("Invalid palette file\n");
    } else {
      sscanf(default_palette+i*7,"%2hhX%2hhX%2hhX ",&pal[i].r,&pal[i].g,&pal[i].b);
    }
    if(usegamma) {
      j=(int)(255.0*pow(pal[i].r/255.0,gamma)+0.2); pal[i].r=j<0?0:j>255?255:j;
      j=(int)(255.0*pow(pal[i].g/255.0,gamma)+0.2); pal[i].g=j<0?0:j>255?255:j;
      j=(int)(255.0*pow(pal[i].b/255.0,gamma)+0.2); pal[i].b=j<0?0:j>255?255:j;
    }
  }
  if(fp) fclose(fp);
  SDL_SetColors(screen,pal,0,256);
  SDL_SetColors(picts,pal,0,256);
}

void draw_picture(int x,int y,Uint16 img) {
  // To be called only when screen is unlocked!
  SDL_Rect src={(img&PPRM)*picture_size,(img>>PPRS)*picture_size,picture_size,picture_size};
  SDL_Rect dst={x,y,picture_size,picture_size};
  SDL_BlitSurface(picts,&src,screen,&dst);
}

void draw_cell(int x,int y) {
  // To be called only when screen is unlocked!
  Uint32 o;
  Class*c;
  int i;
  SDL_Rect dst={(x-1)*picture_size+left_margin,(y-1)*picture_size,picture_size,picture_size};
  if(x<1 || x>64 || y<1 || y>64) return;
  SDL_FillRect(screen,&dst,back_color);
  o=playfield[y*64+x-65];
  while(o!=VOIDLINK) {
    if(main_options['e'] || !(objects[o]->oflags&OF_INVISIBLE)) {
      c=classes[objects[o]->class];
      if(objects[o]->anim && (objects[o]->anim->status&ANISTAT_VISUAL)) i=objects[o]->anim->vimage; else i=objects[o]->image;
      if(i<c->nimages) draw_picture((x-1)*picture_size+left_margin,(y-1)*picture_size,c->images[i]&0x7FFF);
    }
    o=objects[o]->up;
  }
}

void draw_selection_rectangle(void) {
  Uint16 pitch=screen->pitch;
  Uint8*p=screen->pixels+left_margin+(editrect.x0-1)*picture_size+pitch*(editrect.y0-1)*picture_size;
  int xr=(editrect.x1+1-editrect.x0)*picture_size-1;
  int yr=(editrect.y1+1-editrect.y0)*picture_size-1;
  int i;
  if(p+xr+yr*pitch>=((Uint8*)screen->pixels)+screen->h*pitch+screen->w) return;
  memset(p,0xF7,xr);
  memset(p+yr*pitch,0xF7,xr);
  for(i=1;i<yr;i++) {
    p[i*pitch]=p[i*pitch+xr]=0xF7;
    p[i*pitch+1]=p[i*pitch+xr-1]=0xF0;
  }
  memset(p+pitch+1,0xF0,xr-2);
  memset(p+(yr-1)*pitch+1,0xF0,xr-2);
  p[0]=p[xr]=p[yr*pitch]=p[yr*pitch+xr]=0xF8;
}

void draw_text(int x,int y,const unsigned char*t,int bg,int fg) {
  // To be called only when screen is locked!
  int len=strlen(t);
  Uint8*pix=screen->pixels;
  Uint8*p;
  Uint16 pitch=screen->pitch;
  int xx,yy;
  const unsigned char*f;
  if(x+8*len>screen->w) len=(screen->w-x)>>3;
  if(len<=0 || y+8>screen->h) return;
  pix+=y*pitch+x;
  while(*t) {
    f=fontdata+(*t<<3);
    p=pix;
    for(yy=0;yy<8;yy++) {
      for(xx=0;xx<8;xx++) p[xx]=(*f<<xx)&128?fg:bg;
      p+=pitch;
      ++f;
    }
    t++;
    if(!--len) return;
    pix+=8;
  }
}

void draw_text_v(int x,int y,const unsigned char*t,int bg,int fg) {
  // Draw text using sizes other than 8x8 and possible multibyte encodings.
  // To be called only when screen is locked!
  //TODO
}

int measure_text_v(const unsigned char*t,int len) {
  // Returns number of character cells of text (t).
  // If len is positive then it is the maximum number of bytes to measure.
  int r=0;
  int c;
  while((len<0 || len--) && (c=*t++)) {
    r++;
    if(c&0x80) r-=(bytewidth[(c&0x7C)>>2]>>((c&3)<<1))&3;
  }
  return r>0?r:0;
}

int draw_text_line(int x,int y,unsigned char*t,int cur,Uint8**cp) {
  // To be called only when screen is locked!
  int len=strlen(t);
  const unsigned char*s=t;
  Uint8*pix=screen->pixels;
  Uint8*p;
  Uint16 pitch=screen->pitch;
  int bg,fg,xx,yy;
  char isimg=0;
  char e=0;
  const unsigned char*f;
  if(!*t) return 0;
  len=(screen->w-x)>>3;
  if(len<=0 || y+8>screen->h) return len<1?1:len;
  pix+=y*pitch+x;
  for(;;) {
    if(!cur) *cp=t;
    if(*t==10) {
      f=fontdata+(17<<3);
      bg=0xF0,fg=0xF1;
      e=1;
    } else if(!*t) {
      f=fontdata+(254<<3);
      bg=0xF0,fg=0xF1;
      e=1;
    } else if(*t<31) {
      f=fontdata+(" 01234567?NLC#IBQ?????????????D?"[*t]<<3);
      if(*t==14 || *t==30) isimg=1;
      bg=0xF3,fg=0xF0;
    } else {
      if(*t==31 && t[1]) t++;
      f=fontdata+(*t<<3);
      bg=0xF0,fg=isimg?0xF2:0xF7;
    }
    if(!cur--) bg^=15,fg^=15;
    p=pix;
    for(yy=0;yy<8;yy++) {
      for(xx=0;xx<8;xx++) p[xx]=(*f<<xx)&128?fg:bg;
      p+=pitch;
      ++f;
    }
    if(*t=='\\') isimg=0;
    if(!--len || e) return t-s;
    t++;
    pix+=8;
  }
}

int draw_text_line_v(int x,int y,unsigned char*t,int cur,Uint8**cp) {
  //TODO
}

int measure_text_line_v(unsigned char*t,int len,Uint8**cp) {
  //TODO
}

void draw_key(int x,int y,int k,int bg,int fg) {
  // To be called only when screen is locked!
  Uint8*p=screen->pixels;
  Uint16 pitch=screen->pitch;
  int xx,yy;
  const unsigned char*f;
  if(x<0 || y<0 || x+16>screen->w || y+16>screen->h) return;
  p+=y*pitch+x;
  f=keyicons_bits+(k<<5);
  for(yy=0;yy<16;yy++) {
    for(xx=0;xx<8;xx++) p[xx]=(*f>>xx)&1?fg:bg;
    ++f;
    for(xx=0;xx<8;xx++) p[xx+8]=(*f>>xx)&1?fg:bg;
    p+=pitch;
    ++f;
  }
}

const char*screen_prompt(const char*txt) {
  static char*t=0;
  int n=0;
  SDL_Rect r={0,0,screen->w,16};
  int m=r.w>>3;
  SDL_Event ev;
  if(!t) {
    t=malloc(m+2);
    if(!t) fatal("Allocation failed\n");
  }
  *t=0;
  SDL_FillRect(screen,&r,0xF1);
  r.y=16;
  r.h=1;
  SDL_FillRect(screen,&r,0xF8);
  SDL_LockSurface(screen);
  draw_text(0,0,txt,0xF1,0xFE);
  draw_text(0,8,"\xB1",0xF1,0xFB);
  SDL_UnlockSurface(screen);
  set_cursor(XC_iron_cross);
  SDL_Flip(screen);
  r.y=8;
  r.h=8;
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_QUIT:
        SDL_PushEvent(&ev);
        return 0;
      case SDL_KEYDOWN:
        SDL_FillRect(screen,&r,0xF1);
        switch(ev.key.keysym.sym) {
          case SDLK_RETURN: case SDLK_KP_ENTER:
            r.y=0;
            r.h=17;
            SDL_FillRect(screen,&r,0xF0);
            t[n]=0;
            return t;
          case SDLK_BACKSPACE: case SDLK_DELETE:
            if(n) t[n--]=0;
            break;
          case SDLK_CLEAR:
            t[n=0]=0;
            break;
          case SDLK_INSERT:
            if(ev.key.keysym.mod&KMOD_SHIFT) {
              const char*s;
              FILE*fp;
              int c;
              paste:
              optionquery[1]=Q_pasteCommand;
              if((s=xrm_get_resource(resourcedb,optionquery,optionquery,2)) && (fp=popen(s,"r"))) {
                for(;;) {
                  c=fgetc(fp);
                  if(c=='\t') c=' ';
                  if(c>=32 && n<m) t[n++]=c;
                  if(c=='\n' || c<0 || n>=m) break;
                }
                t[n]=0;
                pclose(fp);
              }
            }
            break;
          default:
            if(ev.key.keysym.sym==SDLK_u && (ev.key.keysym.mod&KMOD_CTRL)) {
              t[n=0]=0;
            } else if(ev.key.keysym.sym==SDLK_c && (ev.key.keysym.mod&KMOD_CTRL)) {
              r.y=0;
              r.h=17;
              SDL_FillRect(screen,&r,0xF0);
              return 0;
            } else if(n<m && ev.key.keysym.unicode<127 && ev.key.keysym.unicode>=32 && !(ev.key.keysym.mod&KMOD_CTRL)) {
              t[n++]=ev.key.keysym.unicode;
              t[n]=0;
            }
        }
        t[n]=0;
        SDL_FillRect(screen,&r,0xF1);
        SDL_LockSurface(screen);
        draw_text(0,8,t,0xF1,0xFF);
        draw_text(n<<3,8,"\xB1",0xF1,0xFB);
        SDL_UnlockSurface(screen);
        SDL_Flip(screen);
        break;
      case SDL_MOUSEBUTTONDOWN:
        if(ev.button.button==2) goto paste;
        break;
    }
  }
  return 0;
}

int screen_message(const char*txt) {
  int n=0;
  SDL_Rect r={0,0,0,16};
  SDL_Event ev;
  if(!screen) {
    fprintf(stderr," * %s\n",txt);
    return 0;
  }
  r.w=screen->w;
  SDL_FillRect(screen,&r,0xF4);
  r.y=16;
  r.h=1;
  SDL_FillRect(screen,&r,0xF8);
  SDL_LockSurface(screen);
  draw_text(0,0,txt,0xF4,0xFE);
  draw_text(0,8,"<Push ENTER to continue>",0xF4,0xF7);
  SDL_UnlockSurface(screen);
  set_cursor(XC_iron_cross);
  SDL_Flip(screen);
  r.y=8;
  r.h=8;
  while(SDL_WaitEvent(&ev)) {
    switch(ev.type) {
      case SDL_QUIT:
        SDL_PushEvent(&ev);
        return -1;
      case SDL_KEYDOWN:
        switch(ev.key.keysym.sym) {
          case SDLK_RETURN: case SDLK_KP_ENTER:
            r.y=0;
            r.h=17;
            SDL_FillRect(screen,&r,0xF0);
            return 0;
        }
        break;
    }
  }
  return -1;
}

static Uint16 decide_picture_size(int nwantsize,const Uint8*wantsize,const Uint16*havesize,int n) {
  int i,j;
  if(!nwantsize) fatal("Unable to determine what picture size is wanted\n");
  for(i=0;i<nwantsize;i++) if(havesize[j=wantsize[i]]==n) return j;
  for(i=*wantsize;i;--i) if(havesize[i]) return i;
  fatal("Unable to determine what picture size is wanted\n");
}

static void load_one_picture_sub(FILE*fp,int size,int meth) {
  Uint8*p=curpic;
  int c,n,t,x,y;
  if(meth==15) {
    fread(p,size,size,fp);
    return;
  }
  n=t=0;
  y=size*size;
  while(y--) {
    if(!n) {
      n=fgetc(fp);
      if(n<85) {
        // Homogeneous run
        n++;
        x=fgetc(fp);
        if(t==1 && x==c) n*=85; else n++;
        c=x;
        t=1;
      } else if(n<170) {
        // Heterogeneous run
        n-=84;
        t=2;
      } else {
        // Copy-above run
        n-=169;
        if(t==3) n*=85;
        t=3;
      }
    }
    n--;
    if(t==2) c=fgetc(fp);
    if(t==3) c=p-curpic>=size?p[-size]:0;
    *p++=c;
  }
}

static void load_one_picture(FILE*fp,Uint16 img,int alt) {
  int h,i,j,k,pitch,which,meth,size,psize,zoom;
  Uint8 buf[32];
  Uint8*pix;
  *buf=fgetc(fp);
  j=*buf&15;
  fread(buf+1,1,j+(j>>1),fp);
  k=0;
  zoom=1;
  for(i=1;i<=j;i++) if(buf[i]==picture_size) ++k;
  if(k) {
    psize=picture_size;
  } else {
    for(zoom=2;zoom<=picture_size;zoom++) if(!(picture_size%zoom)) {
      psize=picture_size/zoom;
      for(i=1;i<=j;i++) if(buf[i]==psize) ++k;
      if(k) break;
    }
  }
  alt%=k;
  for(i=1;i<=j;i++) if(buf[i]==psize && !alt--) break;
  which=i;
  i=1;
  while(which--) load_one_picture_sub(fp,size=buf[i],meth=(i==1?*buf>>4:buf[(*buf&15)+1+((i-2)>>1)]>>(i&1?4:0))&15),i++;
  if(meth==5 || meth==6) meth^=3;
  if(meth==15) meth=0;
  SDL_LockSurface(picts);
  pitch=picts->pitch;
  pix=picts->pixels+((img&PPRM)+pitch*(img>>PPRS))*picture_size;
  for(i=0;i<size;i++) {
    for(h=0;h<zoom;h++) {
      for(j=0;j<size;j++) {
        if(meth&1) j=size-j-1;
        if(meth&2) i=size-i-1;
        for(k=0;k<zoom;k++) *pix++=curpic[meth&4?j*size+i:i*size+j];
        if(meth&1) j=size-j-1;
        if(meth&2) i=size-i-1;
      }
      pix+=pitch-picture_size;
    }
  }
  SDL_UnlockSurface(picts);
}

static void pic_shift_right(SDL_Rect*r,int sh) {
  Uint8*p;
  int y;
  char buf[256];
  sh%=r->w;
  if(!sh) return;
  SDL_LockSurface(picts);
  p=picts->pixels+r->y*picts->pitch+r->x;
  for(y=0;y<r->h;y++) {
    memcpy(buf,p,r->w);
    memcpy(p,buf+r->w-sh,sh);
    memcpy(p+sh,buf,r->w-sh);
    p+=picts->pitch;
  }
  SDL_UnlockSurface(picts);
}

static void pic_shift_down(SDL_Rect*r,int sh) {
  Uint8*p;
  int y;
  char b1[256];
  char b2[256];
  sh%=r->h;
  SDL_LockSurface(picts);
  // This implementation is probably inefficient.
  while(sh--) {
    p=picts->pixels+r->y*picts->pitch+r->x;
    memcpy(b1,p+picts->pitch*(r->h-1),r->w);
    for(y=0;y<r->h;y++) {
      memcpy(b2,p,r->w);
      memcpy(p,b1,r->w);
      p+=picts->pitch;
      memcpy(b1,b2,r->w);
    }
  }
  SDL_UnlockSurface(picts);
}

static void pic_orientation(SDL_Rect*r,Uint8 m) {
  Uint8*d=malloc(r->w*r->h);
  Uint8*p;
  Uint8*q;
  int i,j;
  if(!d) fatal("Allocation failed\n");
  SDL_LockSurface(picts);
  p=picts->pixels+r->y*picts->pitch+r->x;
  q=d;
  for(i=0;i<r->h;i++) {
    for(j=0;j<r->w;j++) {
      if(m&1) j=r->w-j-1;
      if(m&2) i=r->h-i-1;
      *q++=p[m&4?j*picts->pitch+i:i*picts->pitch+j];
      if(m&1) j=r->w-j-1;
      if(m&2) i=r->h-i-1;
    }
  }
  q=d;
  for(i=0;i<r->h;i++) {
    memcpy(p,q,r->w);
    p+=picts->pitch;
    q+=r->w;
  }
  SDL_UnlockSurface(picts);
  free(d);
}

static void load_dependent_picture(FILE*fp,Sint32 sz,Uint16 img,int alt) {
  SDL_Rect src={0,0,picture_size,picture_size};
  SDL_Rect dst={(img&PPRM)*picture_size,(img>>PPRS)*picture_size,picture_size,picture_size};
  sqlite3_stmt*st;
  int c,i,x,y;
  char nam[128];
  Uint8 buf[512];
  Uint8*p;
  if(sqlite3_prepare_v2(userdb,"SELECT `ID` FROM `PICTURES` WHERE `NAME` = ?1 AND NOT `DEPENDENT`;",-1,&st,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  i=0;
  while(sz) {
    c=fgetc(fp);
    if(c<32) break;
    if(i<127) nam[i++]=c;
    sz--;
  }
  if(sz) ungetc(c,fp);
  if(i) {
    sqlite3_bind_text(st,1,nam,i,0);
    i=sqlite3_step(st);
    if(i==SQLITE_DONE) {
      fprintf(stderr,"Cannot find base picture for a dependent picture; ignoring\n");
      sqlite3_finalize(st);
      return;
    } else if(i!=SQLITE_ROW) {
      fatal("SQL error (%d): %s\n",i,sqlite3_errmsg(userdb));
    }
    i=sqlite3_column_int(st,0);
    sqlite3_reset(st);
    src.x=(i&PPRM)*picture_size;
    src.y=(i>>PPRS)*picture_size;
    SDL_SetColorKey(picts,0,0);
    SDL_BlitSurface(picts,&src,picts,&dst);
  } else {
    SDL_FillRect(picts,&dst,0);
  }
  while(sz-->0) switch(c=fgetc(fp)) {
    case 0 ... 7: // Flip/rotate
      pic_orientation(&dst,c);
      break;
    case 8: // *->* *->* *->*
      c=fgetc(fp);
      sz-=c+1;
      fread(buf,1,c&255,fp);
      SDL_LockSurface(picts);
      p=picts->pixels+((img&PPRM)+picts->pitch*(img>>PPRS))*picture_size;
      for(y=0;y<picture_size;y++) {
        for(x=0;x<picture_size;x++) {
          for(i=0;i<c;i+=2) {
            if(p[x]==buf[i]) {
              p[x]=buf[i+1];
              break;
            }
          }
        }
        p+=picts->pitch;
      }
      SDL_UnlockSurface(picts);
      break;
    case 9: // *->*->*->*->*
      c=fgetc(fp);
      sz-=c+1;
      fread(buf,1,c&255,fp);
      SDL_LockSurface(picts);
      p=picts->pixels+((img&PPRM)+picts->pitch*(img>>PPRS))*picture_size;
      for(y=0;y<picture_size;y++) {
        for(x=0;x<picture_size;x++) {
          for(i=0;i<c-1;i++) {
            if(p[x]==buf[i]) {
              p[x]=buf[i+1];
              break;
            }
          }
        }
        p+=picts->pitch;
      }
      SDL_UnlockSurface(picts);
      break;
    case 10: // *<->* *<->* *<->*
      c=fgetc(fp);
      sz-=c+1;
      fread(buf,1,c&255,fp);
      SDL_LockSurface(picts);
      p=picts->pixels+((img&PPRM)+picts->pitch*(img>>PPRS))*picture_size;
      for(y=0;y<picture_size;y++) {
        for(x=0;x<picture_size;x++) {
          for(i=0;i<c;i+=2) {
            if(p[x]==buf[i]) {
              p[x]=buf[i+1];
              break;
            } else if(p[x]==buf[i+1]) {
              p[x]=buf[i];
              break;
            }
          }
        }
        p+=picts->pitch;
      }
      SDL_UnlockSurface(picts);
      break;
    case 11: // Overlay
      SDL_SetColorKey(picts,SDL_SRCCOLORKEY,0);
      i=0;
      while(sz>0) {
        c=fgetc(fp);
        if(c<32) break;
        if(i<127) nam[i++]=c;
        sz--;
      }
      if(sz) ungetc(c,fp);
      sqlite3_bind_text(st,1,nam,i,0);
      i=sqlite3_step(st);
      if(i==SQLITE_DONE) {
        fprintf(stderr,"Cannot find overlay for a dependent picture; ignoring\n");
        break;
      } else if(i!=SQLITE_ROW) {
        fatal("SQL error (%d): %s\n",i,sqlite3_errmsg(userdb));
      }
      i=sqlite3_column_int(st,0);
      sqlite3_reset(st);
      src.x=(i&PPRM)*picture_size;
      src.y=(i>>PPRS)*picture_size;
      SDL_BlitSurface(picts,&src,picts,&dst);
      break;
    case 12 ... 15: // Shift (up/down/right/left)
      while(sz>0) {
        x=fgetc(fp);
        y=fgetc(fp);
        sz-=2;
        if(y && !(picture_size%y)) {
          if(c==12) pic_shift_down(&dst,picture_size-((x&127)*picture_size)/y);
          if(c==13) pic_shift_down(&dst,((x&127)*picture_size)/y);
          if(c==14) pic_shift_right(&dst,((x&127)*picture_size)/y);
          if(c==15) pic_shift_right(&dst,picture_size-((x&127)*picture_size)/y);
          break;
        }
        if(x&128) break;
      }
      while(sz>0 && x<128) {
        x=fgetc(fp);
        fgetc(fp);
        sz-=2;
      }
      break;
    case 16: // Hue/shade
      fread(buf,1,2,fp);
      c=(Sint8)buf[1];
      sz-=2;
      SDL_LockSurface(picts);
      p=picts->pixels+((img&PPRM)+picts->pitch*(img>>PPRS))*picture_size;
      for(y=0;y<picture_size;y++) {
        for(x=0;x<picture_size;x++) {
          if(p[x] && p[x]<=225) {
            i=(p[x]-1)%15+c;
            p[x]=15*(((p[x]-1)/15+buf[0])%15)+1;
            if(i<0) p[x]=1; else if(i>14) p[x]=15; else p[x]+=i;
          }
        }
        p+=picts->pitch;
      }
      SDL_UnlockSurface(picts);
      break;
    default:
      fprintf(stderr,"Unrecognized command in dependent picture (%d)\n",c);
      goto done;
  }
  if(sz<-1) fprintf(stderr,"Lump size of dependent picture is too short\n");
  done: sqlite3_finalize(st);
}

static int find_multidependent(sqlite3_stmt*st,FILE*fp,int len,int npic) {
  sqlite3_stmt*s1;
  int at=4;
  long rew=ftell(fp);
  Uint8*mem=malloc(len+1);
  int i,j,zt;
  if(!mem) fatal("Allocation failed\n");
  fread(mem,1,len,fp);
  mem[len]=0;
  fseek(fp,rew+len,SEEK_SET);
  if(sqlite3_exec(userdb,"CREATE TEMPORARY TABLE IF NOT EXISTS `_MUL`(`X`,`Y`,`N`,`Z`);",0,0,0))
   fatal("SQL error: %s\n",sqlite3_errmsg(userdb));
  if(sqlite3_prepare_v2(userdb,"INSERT INTO `_MUL`(`X`,`Y`,`N`,`Z`) VALUES(?1,?2,SUBSTR('._-',?5,1)||?3,?4);",-1,&s1,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  sqlite3_bind_int(s1,2,0);
  sqlite3_bind_text(s1,3,"",0,0);
  sqlite3_bind_int(s1,5,4);
  for(i=0;i<4;i++) if((mem[i]&128) || !mem[i]) {
    sqlite3_reset(s1);
    sqlite3_bind_int(s1,1,i);
    sqlite3_bind_blob(s1,4,"",i?0:1,0);
    while(sqlite3_step(s1)==SQLITE_ROW);
  }
  sqlite3_reset(s1);
  sqlite3_bind_int(s1,1,0);
  for(i=0;i<(*mem&63);i++) {
    if(at>=len) fatal("Malformed multidependent picture lump\n");
    sqlite3_reset(s1);
    sqlite3_bind_int(s1,2,i+1);
    j=strlen(mem+at);
    sqlite3_bind_text(s1,3,mem+at,j,0);
    sqlite3_bind_blob(s1,4,mem+at,j+1,0);
    at+=j+1;
    while(sqlite3_step(s1)==SQLITE_ROW);
  }
  zt=at;
  for(j=1;j<3;j++) for(i=0;i<(mem[j]&63);i++) {
    if(zt+2>=len) fatal("Malformed multidependent picture lump\n");
    zt+=((mem[zt+1]>>3)&7)+2;
  }
  for(j=1;j<3;j++) for(i=0;i<(mem[j]&63);i++) {
    sqlite3_reset(s1);
    sqlite3_bind_int(s1,1,j);
    sqlite3_bind_int(s1,2,i+1);
    sqlite3_bind_text(s1,3,mem+at+2,(mem[at+1]>>3)&7,0);
    if(zt+mem[at]+((mem[at+1]&7)<<8)>len) fatal("Malformed multidependent picture lump\n");
    sqlite3_bind_blob(s1,4,mem+zt,mem[at]|((mem[at+1]&7)<<8),0);
    sqlite3_bind_int(s1,5,4-(mem[at+1]>>6));
    zt+=mem[at]|((mem[at+1]&7)<<8);
    at+=((mem[at+1]>>3)&7)+2;
    while(sqlite3_step(s1)==SQLITE_ROW);
  }
  sqlite3_finalize(s1);
  free(mem);
  if(sqlite3_prepare_v2(userdb,"SELECT A.N||B.N||C.N||D.N,BCAT(A.Z,B.Z,C.Z,D.Z) FROM _MUL A,_MUL B,_MUL C,_MUL D WHERE A.X=0 AND B.X=1 AND C.X=2 AND D.X=3 AND B.Y+C.Y+D.Y<>0;",-1,&s1,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  while((j=sqlite3_step(s1))==SQLITE_ROW) {
    if(npic++==32768) fatal("Too many pictures\n");
    sqlite3_reset(st);
    sqlite3_bind_int(st,1,npic);
    sqlite3_bind_value(st,2,sqlite3_column_value(s1,0));
    sqlite3_bind_value(st,5,sqlite3_column_value(s1,1));
    while(sqlite3_step(st)==SQLITE_ROW);
  }
  if(j!=SQLITE_DONE) fatal("SQL error (%d): %s\n",j,sqlite3_errmsg(userdb));
  sqlite3_finalize(s1);
  if(sqlite3_exec(userdb,"DELETE FROM `_MUL`;",0,0,0))
   fatal("SQL error: %s\n",sqlite3_errmsg(userdb));
  return npic;
}

static void load_multidependent(int n,const char*data,int len) {
  FILE*fp=fmemopen((char*)data,len,"r");
  if(!fp) fatal("Allocation failed\n");
  load_dependent_picture(fp,len,n,0);
  fclose(fp);
}

void load_pictures(void) {
  sqlite3_stmt*st=0;
  FILE*fp;
  Uint8 wantsize[32];
  Uint8 nwantsize=0;
  Uint8 altImage;
  Uint8 havesize1[256];
  Uint16 havesize[256];
  char*nam=sqlite3_mprintf("%s.xclass",basefilename);
  const char*v;
  int i,j,n;
  if(!nam) fatal("Allocation failed\n");
  printStatus("Loading pictures...\n");
  fp=main_options['z']?composite_slice(".xclass",1):fopen(nam,"r");
  if(!fp) fatal("Failed to open xclass file (%m)\n");
  sqlite3_free(nam);
  optionquery[1]=Q_altImage;
  altImage=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"0",0,10);
  optionquery[1]=Q_imageSize;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(v) while(nwantsize<32) {
    i=j=0;
    sscanf(v," %d %n",&i,&j);
    if(!j) break;
    if(i<2 || i>255) fatal("Invalid picture size %d\n",i);
    wantsize[nwantsize++]=i;
    v+=j;
  }
  if(n=sqlite3_exec(userdb,"BEGIN;",0,0,0)) fatal("SQL error (%d): %s\n",n,sqlite3_errmsg(userdb));
  if(sqlite3_prepare_v2(userdb,"INSERT INTO `PICTURES`(`ID`,`NAME`,`OFFSET`,`DEPENDENT`,`MISC`) VALUES(?1,?2,?3,?4,?5);",-1,&st,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  nam=malloc(256);
  if(!nam) fatal("Allocation failed\n");
  n=0;
  memset(havesize,0,256*sizeof(Uint16));
  while(!feof(fp)) {
    i=0;
    while(j=fgetc(fp)) {
      if(j==EOF) goto nomore1;
      if(i<255) nam[i++]=j;
    }
    nam[i]=0;
    if(i>4 && (!memcmp(".IMG",nam+i-4,4) || !memcmp(".DEP",nam+i-4,4))) {
      if(nam[i-3]=='I') j=1; else j=0;
      if(n++==32768) fatal("Too many pictures\n");
      sqlite3_reset(st);
      sqlite3_bind_int(st,1,n);
      sqlite3_bind_text(st,2,nam,i-4,SQLITE_TRANSIENT);
      sqlite3_bind_int64(st,3,ftell(fp)+4);
      sqlite3_bind_int(st,4,j^1);
      sqlite3_bind_null(st,5);
      while((i=sqlite3_step(st))==SQLITE_ROW);
      if(i!=SQLITE_DONE) fatal("SQL error (%d): %s\n",i,sqlite3_errmsg(userdb));
    } else if(i>4 && !memcmp(".MUL",nam+i-4,4)) {
      j=2;
    } else {
      j=0;
    }
    i=fgetc(fp)<<16;
    i|=fgetc(fp)<<24;
    i|=fgetc(fp)<<0;
    i|=fgetc(fp)<<8;
    if(j==1 && i>1) {
      memset(havesize1,0,256);
      i-=j=fgetc(fp)&15;
      while(j--) havesize1[fgetc(fp)&255]=1;
      fseek(fp,i-1,SEEK_CUR);
      for(i=1;i<256;i++) if(havesize1[i]) for(j=i+i;j<256;j+=i) havesize1[j]=1;
      for(j=1;j<256;j++) havesize[j]+=havesize1[j];
    } else if(j==2 && i>4) {
      sqlite3_reset(st);
      sqlite3_bind_int64(st,3,ftell(fp));
      sqlite3_bind_int(st,4,1);
      n=find_multidependent(st,fp,i,n);
    } else {
      fseek(fp,i,SEEK_CUR);
    }
  }
nomore1:
  if(!n) fatal("Cannot find any pictures in this puzzle set\n");
  free(nam);
  sqlite3_finalize(st);
  rewind(fp);
  for(i=255;i;--i) if(havesize[i]) {
    curpic=malloc(i*i);
    break;
  }
  if(!curpic) fatal("Allocation failed\n");
  picture_size=decide_picture_size(nwantsize,wantsize,havesize,n);
  if(main_options['x']) goto done;
  if(sqlite3_prepare_v2(userdb,"SELECT `ID`, `OFFSET` FROM `PICTURES` WHERE NOT `DEPENDENT`;",-1,&st,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  optionquery[1]=Q_screenFlags;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  i=v&&strchr(v,'h');
  picts=SDL_CreateRGBSurface((i?SDL_HWSURFACE:SDL_SWSURFACE)|SDL_SRCCOLORKEY,picture_size<<PPRS,picture_size*((n+PPR)>>PPRS),8,0,0,0,0);
  if(!picts) fatal("Error allocating surface for pictures: %s\n",SDL_GetError());
  init_palette();
  for(i=0;i<n;i++) {
    if((j=sqlite3_step(st))!=SQLITE_ROW) {
      if(j==SQLITE_DONE) break;
      fatal("SQL error (%d): %s\n",j,sqlite3_errmsg(userdb));
    }
    fseek(fp,sqlite3_column_int64(st,1),SEEK_SET);
    load_one_picture(fp,sqlite3_column_int(st,0),altImage);
  }
  sqlite3_finalize(st);
  if(sqlite3_prepare_v2(userdb,"SELECT `ID`, `OFFSET`, `MISC` FROM `PICTURES` WHERE `DEPENDENT`;",-1,&st,0))
   fatal("Unable to prepare SQL statement while loading pictures: %s\n",sqlite3_errmsg(userdb));
  for(i=0;i<n;i++) {
    if((j=sqlite3_step(st))!=SQLITE_ROW) {
      if(j==SQLITE_DONE) break;
      fatal("SQL error (%d): %s\n",j,sqlite3_errmsg(userdb));
    }
    if(sqlite3_column_type(st,2)==SQLITE_NULL) {
      fseek(fp,sqlite3_column_int64(st,1)-4,SEEK_SET);
      j=fgetc(fp)<<16;
      j|=fgetc(fp)<<24;
      j|=fgetc(fp);
      j|=fgetc(fp)<<8;
      load_dependent_picture(fp,j,sqlite3_column_int(st,0),0);
    } else {
      v=sqlite3_column_blob(st,2);
      j=sqlite3_column_bytes(st,2);
      load_multidependent(sqlite3_column_int(st,0),v,j);
    }
  }
  sqlite3_finalize(st);
  fclose(fp);
  SDL_SetColorKey(picts,SDL_SRCCOLORKEY|SDL_RLEACCEL,0);
done:
  if(n=sqlite3_exec(userdb,"COMMIT;",0,0,0)) fatal("SQL error (%d): %s\n",n,sqlite3_errmsg(userdb));
  printStatus("Done\n");
}

void init_screen(void) {
  const char*v;
  int w,h,i;
  if(main_options['x']) return;
  if(!fontdata) fontdata=pcfont;
  optionquery[1]=Q_screenWidth;
  w=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"800",0,10);
  optionquery[1]=Q_screenHeight;
  h=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"600",0,10);
  optionquery[1]=Q_screenFlags;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
  if(SDL_Init(SDL_INIT_VIDEO|(strchr(v,'z')?SDL_INIT_NOPARACHUTE:0)|SDL_INIT_TIMER)) fatal("Error initializing SDL: %s\n",SDL_GetError());
#ifdef CONFIG_EXTRA_SCREEN_INIT
  do{ CONFIG_EXTRA_SCREEN_INIT }while(0);
#endif
  atexit(SDL_Quit);
  i=0;
  while(*v) switch(*v++) {
    case 'd': i|=SDL_DOUBLEBUF; break;
    case 'f': i|=SDL_FULLSCREEN; break;
    case 'h': i|=SDL_HWSURFACE; break;
    case 'n': i|=SDL_NOFRAME; break;
    case 'p': i|=SDL_HWPALETTE; break;
    case 'r': i|=SDL_RESIZABLE; break;
    case 'y': i|=SDL_ASYNCBLIT; break;
  }
  if(!(i&SDL_HWSURFACE)) i|=SDL_SWSURFACE;
  screen=SDL_SetVideoMode(w,h,8,i);
  if(!screen) fatal("Failed to initialize screen mode: %s\n",SDL_GetError());
  optionquery[1]=Q_keyRepeat;
  if(v=xrm_get_resource(resourcedb,optionquery,optionquery,2)) {
    w=strtol(v,(void*)&v,10);
    h=strtol(v,0,10);
    SDL_EnableKeyRepeat(w,h);
  } else {
    SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY,SDL_DEFAULT_REPEAT_INTERVAL);
  }
  SDL_EnableUNICODE(1);
  optionquery[1]=Q_margin;
  left_margin=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"65",0,10);
}

void set_code_page(Uint32 n) {
  int c,i,j,s;
  const char*v;
  unsigned char*d;
  Uint8 b[32];
  FILE*fp;
  if(!n) return;
  if(n==367) n=437;
  if(fontdata && fontdata!=pcfont) fatal("Multiple code page specifications\n");
  optionquery[1]=Q_codepage;
  v=xrm_get_resource(resourcedb,optionquery,optionquery,2);
  if(!v || !*v) {
    if(n==437) return;
    fatal("Cannot load code page %d; code page file is not configured\n",n);
  }
  fp=fopen(v,"r");
  if(!fp) {
    perror(0);
    fatal("Cannot open code page file\n");
  }
  fontdata=d=malloc(0x800);
  if(!d) fatal("Allocation failed\n");
  memcpy(d,pcfont,0x800);
  name:
  s=i=0;
  for(;;) {
    c=fgetc(fp);
    if(c<0) {
      if(n!=437) fatal("Cannot find code page %d\n",n);
      goto done;
    }
    if(!c) break;
    if(!s) {
      if(c<'0' || c>'9') s=1;
      else if(c=='0' && !i) s=1;
      else i=10*i+c-'0';
      if(i>0x7FFFFF) s=1;
    }
  }
  if(s || i!=n) goto skip;
  i=fgetc(fp)<<16;
  i|=fgetc(fp)<<24;
  i|=fgetc(fp)<<0;
  i|=fgetc(fp)<<8;
  if(i==0x800) {
    fread(d,8,256,fp);
    goto done;
  } else if(i==0x400) {
    fread(d+0x400,8,128,fp);
  } else if(i<32 || i>0x800) {
    fatal("Unrecognized format of code page %d\n",n);
  } else {
    fread(b,32,1,fp);
    for(s=0;s<256;s++) if(!(b[s>>3]&(1<<(s&7)))) {
      if(c=fgetc(fp)) {
        i=fgetc(fp);
        if(i&~255) fatal("Error reading data for character %d in code page %d\n",s,n);
        if(i!=s) memcpy(d+s*8,d+i*8,8);
      }
      for(j=0;j<8;j++) if(!(c&(1<<j))) d[j+s*8]=fgetc(fp);
    }
  }
  memset(d,0,8);
  goto done;
  skip:
  i=fgetc(fp)<<16;
  i|=fgetc(fp)<<24;
  i|=fgetc(fp)<<0;
  i|=fgetc(fp)<<8;
  fseek(fp,i,SEEK_CUR);
  goto name;
  done:
  codepage=n;
  fclose(fp);
}

// Widgets

static void draw_scrollbar(int*cur,int page,int max,int x0,int y0,int x1,int y1) {
  Uint8*pix=screen->pixels;
  Uint16 pitch=screen->pitch;
  int x,y,m0,m1;
  double f;
  f=(y1-y0)/(double)(max+page);
  if(*cur<0) *cur=0; else if(*cur>max) *cur=max;
  m0=*cur*f+y0;
  m1=(*cur+page)*f+y0;
  pix+=y0*pitch;
  SDL_LockSurface(screen);
  for(y=y0;y<y1;y++) {
    for(x=x0;x<x1;x++) pix[x]=y<m0?0xFF:y>m1?0xFF:(y^x)&1?0xF0:0xFF;
    pix[x1]=0xFF;
    pix+=pitch;
  }
  SDL_UnlockSurface(screen);
}

int scrollbar(int*cur,int page,int max,SDL_Event*ev,SDL_Rect*re) {
  int x0=re?re->x:0;
  int y0=re?re->y:0;
  int x1=re?re->x+12:12;
  int y1=re?re->y+re->h:screen->h;
  int y;
  double f;
  max-=page;
  if(max<0) max=0;
  switch(ev?ev->type:SDL_VIDEOEXPOSE) {
    case SDL_MOUSEMOTION:
      if(ev->motion.x<x0 || ev->motion.x>x1 || ev->motion.y<y0 || ev->motion.y>=y1) return 0;
      if(ev->motion.state&SDL_BUTTON(2)) {
        y=ev->motion.y;
        goto move;
      } else if(ev->motion.state&SDL_BUTTON(1)) {
        set_cursor(XC_sb_up_arrow);
      } else if(ev->motion.state&SDL_BUTTON(3)) {
        set_cursor(XC_sb_down_arrow);
      } else {
        set_cursor(XC_sb_v_double_arrow);
      }
      return 1;
    case SDL_MOUSEBUTTONDOWN:
      if(ev->button.x<x0 || ev->button.x>x1 || ev->button.y<y0 || ev->button.y>=y1) return 0;
      if(ev->button.button==2) {
        y=ev->button.y;
        goto move;
      } else if(ev->button.button==1) {
        set_cursor(XC_sb_up_arrow);
      } else if(ev->button.button==3) {
        set_cursor(XC_sb_down_arrow);
      }
      return 1;
    case SDL_MOUSEBUTTONUP:
      if(ev->button.x<x0 || ev->button.x>x1 || ev->button.y<y0 || ev->button.y>=y1) return 0;
      f=(y1-y0)/(double)page;
      y=(ev->button.y-y0+0.5)/f;
      if(ev->button.button==1) {
        *cur+=y;
      } else if(ev->button.button==3) {
        *cur-=y;
      }
      draw_scrollbar(cur,page,max,x0,y0,x1,y1);
      SDL_Flip(screen);
      set_cursor(XC_sb_v_double_arrow);
      return 1;
    case SDL_VIDEOEXPOSE:
      draw_scrollbar(cur,page,max,x0,y0,x1,y1);
      return 0;
    default:
      return 0;
  }
  move:
  f=(y1-y0)/(double)(max+page);
  *cur=(y-y0+0.5)/f;
  draw_scrollbar(cur,page,max,x0,y0,x1,y1);
  SDL_Flip(screen);
  set_cursor(XC_sb_right_arrow);
  return 1;
}

// Popup text

/*
  \0 to \7 (1 to 8) - Colours
  \b (15) - Horizontal line
  \c (12) - Centre align
  \i (14) - Image of class
  \l (11) - Left align
  \n (10) - Line break
  \q (16) - Quiz button
  \x (31) - Hex escape
*/

typedef struct {
  Uint8 a,h;
  Uint16 w;
} PopLine;

static void pop_bar(int x,int y,PopLine*li,int w) {
  Uint8*p=screen->pixels;
  Uint16 pitch=screen->pitch;
  if(x+w>=screen->w || y+8>screen->h) return;
  p+=(y+3)*pitch+x;
  while(w--) *p++=w&1?7:5;
}

static void pop_char(int x,int y,PopLine*li,Uint8 c,Uint8 v) {
  Uint8*p=screen->pixels;
  Uint16 pitch=screen->pitch;
  int xx,yy;
  const unsigned char*f=fontdata+(v<<3);
  if(li->h>8) y+=(li->h-8)/2;
  if(y+8>=screen->h || x+8>=screen->w) return;
  p+=y*pitch+x;
  for(yy=0;yy<8;yy++) {
    for(xx=0;xx<8;xx++) p[xx]=(*f<<xx)&128?c:0xF7;
    p+=pitch;
    ++f;
  }
}

static void pop_image(int x,int y,PopLine*li,const unsigned char*t) {
  const unsigned char*p=strchr(t,':');
  int i,n;
  if(!p) return;
  n=strtol(p+1,0,10);
  if(li->h>picture_size) y+=(li->h-picture_size)/2;
  if(y+picture_size>=screen->h || x+picture_size>=screen->w) return;
  SDL_UnlockSurface(screen);
  if(p==t+1 && *t==7) {
    // This case is used for %i substitutions
    draw_picture(x,y,n);
  } else if(p) {
    for(i=1;i<0x4000;i++) if(classes[i]) {
      if(strlen(classes[i]->name)==p-t && !memcmp(t,classes[i]->name,p-t)) {
        if(n>=0 && n<classes[i]->nimages) draw_picture(x,y,classes[i]->images[n]&0x7FFF);
        break;
      }
    }
  }
  SDL_LockSurface(screen);
}

static void pop_quiz(int x,int y,PopLine*li,Uint8 c,Uint8 v) {
  SDL_Rect r;
  Uint8*p=screen->pixels;
  Uint16 pitch=screen->pitch;
  int xx,yy;
  const unsigned char*f=fontdata+(v<<3);
  if(li->h>16) y+=(li->h-16)/2;
  r.x=x;
  r.y=y;
  r.w=r.h=16;
  SDL_FillRect(screen,&r,0x07);
  if(y+16>=screen->h || x+24>=screen->w) return;
  p+=(y+4)*pitch+x+4;
  for(yy=0;yy<8;yy++) {
    for(xx=0;xx<8;xx++) p[xx]=(*f<<xx)&128?c:0x07;
    p+=pitch;
    ++f;
  }
}

void draw_popup(const unsigned char*txt) {
  static colo[8]={1,144,188,173,83,98,218,15};
  static PopLine li[64];
  SDL_Rect r;
  int bx,by,x,y,c;
  int ln=0; // line number
  int lh=8; // height of current line
  int th=0; // total height
  int lw=0; // width of current line
  int tw=0; // total width
  const unsigned char*p=txt;
  li[0].w=li[0].h=li[0].a=0;
  // Figure out size
  while(ln<64 && *p) switch(*p++) {
    case 1 ... 8:
      // needing doing nothing yet
      break;
    case 10:
      th+=lh;
      li[ln].h=lh;
      li[ln].w=lw;
      if(tw<lw) tw=lw;
      if(*p && *p!=15) lh=8; else lh=0;
      lw=0;
      ln++;
      if(ln<64) li[ln].a=li[ln-1].a;
      break;
    case 11: case 12:
      li[ln].a=p[-1]-11;
      break;
    case 14:
      if(lh<picture_size) lh=picture_size;
      lw+=picture_size;
      p=strchr(p,'\\')?:"";
      if(*p) p++;
      break;
    case 15:
      th+=lh+8;
      li[ln].h=lh;
      li[ln].w=lw;
      if(tw<lw) tw=lw;
      lh=8;
      lw=0;
      ln++;
      if(ln<64) li[ln].a=li[ln-1].a;
      break;
    case 16:
      lw+=24;
      if(lh<16) lh=16;
      if(*p) p++;
      break;
    case 30:
      p=strchr(p,'\\')?:"";
      if(*p) p++;
      break;
    case 31:
      lw+=8;
      if(*p) p++;
      break;
    default:
      lw+=8;
  }
  done:
  if(ln<64) li[ln].h=lh,li[ln].w=lw;
  p=txt;
  th+=lh;
  if(tw<lw) tw=lw;
  if(tw>screen->w-32) tw=screen->w-32;
  if(th>screen->h-32) th=screen->h-32;
  bx=(screen->w-tw-8)/2;
  by=(screen->h-th-8)/2;
  // Draw box
  r.x=bx; r.y=by; r.w=tw+16; r.h=th+16;
  SDL_FillRect(screen,&r,0xF7);
  r.w=tw+16; r.h=1; SDL_FillRect(screen,&r,0x0F);
  r.w=1; r.h=th+16; SDL_FillRect(screen,&r,0x0F);
  r.x++; r.y++;
  r.w=tw+14; r.h=1; SDL_FillRect(screen,&r,0x0D);
  r.w=1; r.h=th+14; SDL_FillRect(screen,&r,0x0D);
  r.x++; r.y++;
  r.w=tw+12; r.h=1; SDL_FillRect(screen,&r,0x0B);
  r.w=1; r.h=th+12; SDL_FillRect(screen,&r,0x0B);
  r.x=bx+tw+15; r.y=by;
  r.w=1; r.h=th+16; SDL_FillRect(screen,&r,0x02);
  r.x--; r.y++; r.h-=2; SDL_FillRect(screen,&r,0x05);
  r.x--; r.y++; r.h-=2; SDL_FillRect(screen,&r,0x08);
  r.x=bx; r.y=by+th+15;
  r.w=tw+16; r.h=1; SDL_FillRect(screen,&r,0x02);
  r.x++; r.y--; r.w-=2; SDL_FillRect(screen,&r,0x05);
  r.x++; r.y--; r.w-=2; SDL_FillRect(screen,&r,0x08);
  // Draw text
  bx+=8; by+=8; tw-=8; th-=8;
  x=bx; y=by;
  c=colo[0];
  ln=0;
  SDL_LockSurface(screen);
  while(*p && ln<64 && y+li[ln].h<screen->h) switch(*p++) {
    case 1 ... 8:
      c=colo[p[-1]-1];
      break;
    case 10:
      y+=li[ln++].h;
      if(ln<64) x=li[ln].a?bx+(tw-li[ln].w)/2:bx;
      break;
    case 11:
      // do nothing
      break;
    case 12:
      if(x==bx) x+=(tw-li[ln].w)/2;
      break;
    case 14:
      pop_image(x,y,li+ln,p);
      x+=picture_size;
      p=strchr(p,'\\')?:"";
      if(*p) p++;
      break;
    case 15:
      y+=li[ln++].h;
      pop_bar(bx-8,y,li+ln,tw+22);
      y+=8;
      if(ln<64) x=li[ln].a?bx+(tw-li[ln].w)/2:bx;
      break;
    case 16:
      pop_quiz(x,y,li+ln,c,*p);
      if(*p) p++;
      x+=24;
      break;
    case 30:
      p=strchr(p,'\\')?:"";
      if(*p) p++;
      break;
    case 31:
      pop_char(x,y,li+ln,c,*p);
      if(*p) p++;
      x+=8;
      break;
    default:
      pop_char(x,y,li+ln,c,p[-1]);
      x+=8;
      break;
  }
  SDL_UnlockSurface(screen);
}

int modal_draw_popup(const unsigned char*txt) {
  SDL_Event ev;
  set_cursor(XC_iron_cross);
  redraw:
  draw_popup(txt);
  SDL_Flip(screen);
  while(SDL_WaitEvent(&ev)) switch(ev.type) {
    case SDL_QUIT:
      SDL_PushEvent(&ev);
      return -1;
    case SDL_KEYDOWN:
      switch(ev.key.keysym.sym) {
        case SDLK_RETURN: case SDLK_KP_ENTER: case SDLK_ESCAPE:
          return 0;
      }
      break;
    case SDL_MOUSEBUTTONDOWN:
      return 0;
    case SDL_VIDEOEXPOSE:
      goto redraw;
  }
  return -1;
}