Free Hero Mesh

Check-in [4919d2c22e]
Login
This is a mirror of the main repository for Free Hero Mesh. New tickets and changes will not be accepted at this mirror.
Overview
Comment:Implement composite puzzle sets
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 4919d2c22ecd2dcd8c72c65d2007de336acfcbdc
User & Date: user on 2021-10-02 01:55:15
Other Links: manifest | tags
Context
2021-10-05
00:26
Change the internal composite_slice function check-in: 2312d4d6df user: user tags: trunk
2021-10-02
01:55
Implement composite puzzle sets check-in: 4919d2c22e user: user tags: trunk
2021-09-30
21:08
Add SOLUTION_MOVE_LIST function, F9 to flash player location, shift and middle mouse button, and a documentation correction. check-in: 28b482dea1 user: user tags: trunk
Changes

Modified README from [95780c926f] to [0087928e14].

162
163
164
165
166
167
168




169
170
171
172
173
174
175
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179







+
+
+
+








* misc/har.c: A small C program for dealing with Hamster archives. This is
not properly a part of Free Hero Mesh (and is not needed in order to use
or compile Free Hero Mesh), but is included since it is likely to be useful
for some users (e.g. to copy pictures/sounds between puzzle sets).

* misc/sokoban.tar.gz: Example puzzle set.

* smallxrm.c and smallxrm.h: This is an implementation of the X resource
manager which is used in Free Hero Mesh but can also be used in other
programs independently from Free Hero Mesh.

(The files in the misc/ directory are not part of Free Hero Mesh nor are
they needed by Free Hero Mesh, but are provided for convenience. They can
safely be ignored or deleted if you are not using them.)


=== Community/discussion ===
235
236
237
238
239
240
241
242
243


244
245
246
247
248
249
250
239
240
241
242
243
244
245


246
247
248
249
250
251
252
253
254







-
-
+
+







 You must use the base name. For example, if the puzzle set files are
 named "sokoban.xclass", "sokoban.class", "sokoban.level", and
 "sokoban.solution", then you must type "heromesh sokoban" to load it.
 (This assumes that the file is in the current directory. If it isn't,
 then you must also specify the directory name, in addition to the puzzle
 set base name.)

 (Later versions of Free Hero Mesh might add support for composite puzzle
 sets, although the current version does not have this.)
 You may also use a composite puzzle set. In this case, use the -z switch
 and pass the name including the file name suffix.

** How to access the NNTP?

 You can use a NNTP client program, often called a "newsreader" program.
 Many email programs can also do NNTP, including Mozilla Thunderbird.
 There are also specialized NNTP programs, including Pan and bystand.
 (Some are text, some are binary, and some are hybrid; you will need one

Modified TODO from [f9befcda8c] to [cf8d84517b].

37
38
39
40
41
42
43
44

45
46
47
48
49
50
37
38
39
40
41
42
43

44
45
46
47
48
49
50







-
+






* SQL
  * Implement the GROUP column in the CLASSES table
  * Allow multiple SQL statements in one binding
* Large fonts (width 8 or 16, height 8-32)
* Branching replay recording
* Slow movement displaying state between triggers
* Warning if file changed when uncommited data exists in the cache database
* Composite puzzle set format (in a single file; read-only)
* Composite puzzle set format (implemented)
  * Optional hypertext help
  * Compressed class definitions (?)
* Option to auto display level titles
* Testing
  * Bizarro world
  * Deferred movement

Modified class.c from [1bc0783ffd] to [1ee8b910eb].

2109
2110
2111
2112
2113
2114
2115
2116

2117
2118
2119
2120
2121
2122
2123
2109
2110
2111
2112
2113
2114
2115

2116
2117
2118
2119
2120
2121
2122
2123







-
+







  int i;
  int gloptr=0;
  Hash*glolocalhash;
  char*nam=sqlite3_mprintf("%s.class",basefilename);
  sqlite3_stmt*vst=0;
  fprintf(stderr,"Loading class definitions...\n");
  if(!nam) fatal("Allocation failed\n");
  classfp=fopen(nam,"r");
  classfp=main_options['z']?composite_slice("class",1):fopen(nam,"r");
  sqlite3_free(nam);
  if(!classfp) fatal("Cannot open class file '%s': %m\n",nam);
  glohash=calloc(HASH_SIZE,sizeof(Hash));
  if(!glohash) fatal("Allocation failed\n");
  glolocalhash=calloc(LOCAL_HASH_SIZE,sizeof(Hash));
  if(!glolocalhash) fatal("Allocation failed\n");
  strcpy(tokenstr,"+"); glohash[look_hash_mac()].id=MAC_ADD;

Modified commandline.doc from [f6413bcb62] to [aab3fc8cb6].

62
63
64
65
66
67
68




69
70
71
72
73
74
75
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79







+
+
+
+







  More verbose error logging.

-x
  Do not start the GUI; accept SQL codes to execute from stdin, and write
  the results to stdout. See sql.doc for the fuunctions and tables that
  may be used. Dot commands are also possible; see the below section.

-z
  Load a composite puzzle set. In this case, the <basename> should be the
  full file name of the puzzle, including the suffix.


=== Dot commands ===

.b0
  Disable terminate on errors.

.b1

Modified game.c from [2ff75549f9] to [25a4dc3668].

726
727
728
729
730
731
732
733

734
735
736
737
738
739
740
726
727
728
729
730
731
732

733
734
735
736
737
738
739
740







-
+







      number=replay_mark;
      goto restart;
    case '^>': // Replay to mark
      inputs_count=0;
      number=replay_mark-replay_pos;
      goto replay;
    case '^E': // Edit
      return -2;
      return main_options['r']?1:-2;
    case '^I': // Toggle inventory display
      side_mode^=1;
      return prev;
    case '^M': // Mark replay position
      replay_mark=replay_pos+inputs_count;
      return prev;
    case '^Q': // Quit

Modified heromesh.h from [9db2b324b2] to [e898c0efa3].

70
71
72
73
74
75
76

77
78
79
80
81
82
83
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+







  }) : \
  stack_protect_mode=='!' ? 1 : \
0))
#else
#define StackProtection() 0
#endif

FILE*composite_slice(const char*suffix,char isfatal);
unsigned char*read_lump_or_userstate(int sol,int lvl,long*sz,char us);
void write_lump(int sol,int lvl,long sz,const unsigned char*data);
void write_userstate(int sol,int lvl,long sz,const unsigned char*data);
const char*load_level(int lvl);
void set_cursor(int id);
const char*log_if_error(const char*t);

Modified main.c from [2fa2744cde] to [4242250daf].

1
2
3
4
5
6
7
8
9
10

11
12
13
14
15
16
17
1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
17









-
+







#if 0
gcc ${CFLAGS:--s -O2} -o ${EXE:-~/bin/heromesh} -Wno-multichar main.c class.o picture.o bindings.o function.o exec.o game.o edit.o picedit.o smallxrm.o sqlite3.o `sdl-config --cflags --libs` -ldl -lpthread -lm
exit
#endif

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

#define _BSD_SOURCE
//#define _GNU_SOURCE
#define HEROMESH_MAIN
#include "SDL.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
62
63
64
65
66
67
68

69
70
71














































































72
73
74
75
76
77
78
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157







+



+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







void*stack_protect_high;
#endif

static const char*globalclassname;
static SDL_Cursor*cursor[77];
static FILE*levelfp;
static FILE*solutionfp;
static FILE*compositefp;
static sqlite3_int64 leveluc,solutionuc;
static sqlite3_stmt*readusercachest;
static char*hpath;

typedef struct {
  FILE*fp;
  sqlite3_uint64 start,length,offset;
} SliceCookie;

static ssize_t slice_read(void*cookie,char*buf,size_t size) {
  SliceCookie*d=cookie;
  fseek(d->fp,d->start+d->offset,SEEK_SET);
  if(size>d->length-d->offset) size=d->length-d->offset;
  d->offset+=size=fread(buf,1,size,d->fp);
  return size;
}

static int slice_seek(void*cookie,off64_t*offset,int whence) {
  SliceCookie*d=cookie;
  switch(whence) {
    case SEEK_SET: d->offset=*offset; break;
    case SEEK_CUR: d->offset+=*offset; break;
    case SEEK_END: d->offset=d->length+*offset; break;
  }
  if(d->offset<0 || d->offset>d->length) return -1;
  return fseek(d->fp,d->start+(*offset=d->offset),SEEK_SET);
}

static int slice_close(void*cookie) {
  free(cookie);
  return 0;
}

FILE*composite_slice(const char*suffix,char isfatal) {
  FILE*fp;
  SliceCookie*d;
  int c,n;
  sqlite3_int64 t;
  rewind(compositefp);
  look:
  n=0;
  if(*suffix>'Z') for(;;) {
    c=fgetc(compositefp);
    if(c==EOF) goto notfound;
    if(!c) goto skip;
    if(c=='.') break;
  }
  for(;;) {
    c=fgetc(compositefp);
    if(c==EOF) goto notfound;
    if(!c) {
      if(suffix[n]) goto skip; else goto found;
    }
    if(c==suffix[n]) {
      n++;
    } else {
      do c=fgetc(compositefp); while(c>0);
      goto skip;
    }
  }
  skip:
  t=fgetc(compositefp)<<16; t|=fgetc(compositefp)<<24;
  t|=fgetc(compositefp); t|=fgetc(compositefp)<<8;
  fseek(compositefp,t,SEEK_CUR);
  goto look;
  found:
  t=fgetc(compositefp)<<16; t|=fgetc(compositefp)<<24;
  t|=fgetc(compositefp); t|=fgetc(compositefp)<<8;
  d=malloc(sizeof(SliceCookie));
  if(!d) fatal("Allocation failed\n");
  d->fp=compositefp;
  d->start=ftell(compositefp);
  d->length=t;
  d->offset=0;
  fp=fopencookie(d,"r",(cookie_io_functions_t){.read=slice_read,.seek=slice_seek,.close=slice_close});
  if(!fp) fatal("Allocation failed\n");
  return fp;
  notfound:
  if(isfatal) fatal("Cannot find '%s' lump in composite puzzle set file\n",suffix);
  return 0;
}

static sqlite3_int64 reset_usercache(FILE*fp,const char*nam,struct stat*stats,const char*suffix) {
  sqlite3_stmt*st;
  sqlite3_int64 t,id;
  char buf[128];
  int i,z;
  if(z=sqlite3_prepare_v2(userdb,"DELETE FROM `USERCACHEINDEX` WHERE `NAME` = ?1;",-1,&st,0)) fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
455
456
457
458
459
460
461

462
463
464
465
466
467
468























































469
470
471
472
473
474
475
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610







+







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







  while((e=sqlite3_step(st))==SQLITE_ROW);
  if(e!=SQLITE_DONE) fatal("SQL error (%d): %s\n",e,sqlite3_errmsg(userdb));
  sqlite3_finalize(st);
}

static void flush_usercache(void) {
  int e;
  if(main_options['r']) return;
  fprintf(stderr,"Flushing user cache...\n");
  if(e=sqlite3_exec(userdb,"BEGIN;",0,0,0)) fatal("SQL error (%d): %s\n",e,sqlite3_errmsg(userdb));
  flush_usercache_1(FIL_LEVEL);
  flush_usercache_1(FIL_SOLUTION);
  if(e=sqlite3_exec(userdb,"COMMIT;",0,0,0)) fatal("SQL error (%d): %s\n",e,sqlite3_errmsg(userdb));
  fprintf(stderr,"Done\n");
}

static void init_composite(void) {
  FILE*fp=compositefp=fopen(basefilename,"r");
  sqlite3_stmt*st;
  sqlite3_int64 t1,t2;
  int z;
  struct stat fst;
  if(!fp) fatal("Cannot open '%s' for reading: %m\n",basefilename);
  fprintf(stderr,"Loading puzzle set...\n");
  if(z=sqlite3_exec(userdb,"BEGIN;",0,0,0)) fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  if(z=sqlite3_prepare_v2(userdb,"SELECT `ID`, `TIME` FROM `USERCACHEINDEX` WHERE `NAME` = CHAR(?2)||'//'||?1;",-1,&st,0)) fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  basefilename=realpath(basefilename,0);
  if(!basefilename) fatal("Cannot find real path of puzzle set: %m\n");
  sqlite3_bind_text(st,1,basefilename,-1,0);
  levelfp=composite_slice("level",1);
  solutionfp=composite_slice("solution",1);
  sqlite3_bind_int(st,2,'L');
  z=sqlite3_step(st);
  if(z==SQLITE_ROW) {
    leveluc=sqlite3_column_int64(st,0);
    t1=sqlite3_column_int64(st,1);
  } else if(z==SQLITE_DONE) {
    leveluc=t1=-1;
  } else {
    fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  }
  sqlite3_reset(st);
  sqlite3_bind_int(st,2,'S');
  z=sqlite3_step(st);
  if(z==SQLITE_ROW) {
    solutionuc=sqlite3_column_int64(st,0);
    t2=sqlite3_column_int64(st,1);
  } else if(z==SQLITE_DONE) {
    solutionuc=t2=-1;
  } else {
    fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  }
  sqlite3_finalize(st);
  if(stat(basefilename,&fst)) fatal("Unable to stat '%s': %m\n",basefilename);
  if(!fst.st_size) fatal("File '%s' has zero size\n",basefilename);
  if(fst.st_mtime>t1 || fst.st_ctime>t1) {
    char*p=sqlite3_mprintf("L//%s",basefilename);
    if(!p) fatal("Allocation failed\n");
    leveluc=reset_usercache(levelfp,p,&fst,".LVL");
    *p='S';
    solutionuc=reset_usercache(solutionfp,p,&fst,".SOL");
    sqlite3_free(p);
  }
  if(z=sqlite3_prepare_v3(userdb,"SELECT `OFFSET`, CASE WHEN ?3 THEN `USERSTATE` ELSE `DATA` END "
   "FROM `USERCACHEDATA` WHERE `FILE` = ?1 AND `LEVEL` = ?2;",-1,SQLITE_PREPARE_PERSISTENT,&readusercachest,0)) {
    fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  }
  if(z=sqlite3_exec(userdb,"COMMIT;",0,0,0)) fatal("SQL error (%d): %s\n",z,sqlite3_errmsg(userdb));
  fprintf(stderr,"Done\n");
}

static void init_usercache(void) {
  sqlite3_stmt*st;
  int z;
  sqlite3_int64 t1,t2;
  char*nam1;
  char*nam2;
894
895
896
897
898
899
900




901
902
903
904
905
906
907
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046







+
+
+
+







  if(!resourcedb) fatal("Allocation of resource database failed\n");
  basefilename=argv[optind++];
  if(argc>optind && argv[1][0]=='=') {
    globalclassname=argv[optind++]+1;
  } else if(find_globalclassname()) {
    globalclassname=strrchr(basefilename,'/');
    globalclassname=globalclassname?globalclassname+1:basefilename;
  }
  if(main_options['z']) {
    if(main_options['p'] || main_options['f'] || main_options['e']) fatal("Switches are conflicting with -z\n");
    main_options['r']=1;
  }
  if(main_options['n']) {
    if(main_options['r']) fatal("Switches -r and -n are conflicting\n");
    main_options['x']=1;
  }
  if(main_options['a']) main_options['r']=main_options['x']=1;
  if(main_options['p']) main_options['r']=1;
923
924
925
926
927
928
929

930
931
932
933
934
935
936

937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953

954
955
956
957
958
959
960
961
962
963
964
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075

1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105







+






-
+

















+











  init_sql();
  load_key_bindings();
  init_screen();
  if(main_options['p']) {
    run_picture_editor();
    return 0;
  }
  if(main_options['z']) init_composite();
  load_pictures();
  if(main_options['T']) {
    printf("argv[0] = %s\n",argv[0]);
    test_mode();
    return 0;
  }
  init_usercache();
  if(!main_options['z']) init_usercache();
  if(main_options['n']) return 0;
  load_classes();
  load_level_index();
  optionquery[1]=Q_maxObjects;
  max_objects=strtoll(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"",0,0)?:0xFFFF0000L;
  set_tracing();
  annihilate();
  optionquery[1]=Q_level;
  if(level_ord=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"",0,10)) {
    if(level_ord>level_nindex) level_ord=level_nindex;
    log_if_error(load_level(-level_ord));
  }
  optionquery[1]=Q_maxTrigger;
  max_trigger=strtol(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"",0,10);
  if(main_options['a']) run_auto_test();
  if(main_options['x']) {
    if(main_options['f']) {
      if(main_options['r']) fatal("Cannot flush user cache; puzzle set is read-only\n");
      flush_usercache();
      return 0;
    }
    fprintf(stderr,"Ready for executing SQL statements.\n");
    no_dead_anim=1;
    do_sql_mode();
    return 0;
  }
  set_autosave();
  for(;;) { if(main_options['e']) run_editor(); else run_game(); }
}

Modified man6/heromesh.6 from [ed5c74f628] to [40aa1aa469].

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
21













-
+







.TH HEROMESH 6 "December 22, 2020"
.so man6/heromesh.str
.SH NAME
heromesh \- Free Hero Mesh (a puzzle game engine)
.SH SYNOPSYS
.B heromesh
.RI "[" "options" "] " "basename" " [" "resources" "]"
.SH DESCRIPTION
Free Hero Mesh is a puzzle game engine, for turn-based grid-based puzzle games.
It is fully deterministic and depends only on the sequence of inputs, not on time or randomness.
Free Hero Mesh allows you to define your own classes of objects using the included programming language.
.PP
The "basename" above is the filename of the puzzle set without the extension.
A puzzle set consists of four files.
A puzzle set consists of four files (except composite puzzle sets).
.PP
The "resources" above is optional resources to override, in the X resource manager format.
.SH OPTIONS
.IP -C
Dump all class data.
.IP -H
Dump the hash table.
43
44
45
46
47
48
49



50
51
52

53
54
55
56
57
58
59
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63







+
+
+



+







Open in read-only mode.
.IP -t
Enable message tracing.
.IP -v
More verbose error logging.
.IP -x
Does not start the GUI; instead, accepts SQL statements from stdin.
.IP -z
Load a composite puzzle set.
In this case, pass the full file name instead only the base name.
.SH FILES
A puzzle set consists of four files, with filename suffixes
.BR ".class" ", " ".xclass" ", " ".level" ", and " ".solution" "."
A composite puzzle set consists of a single file.
.TP
.I ~/.heromeshrc
The configuration file, in X resource manager format.
.TP
.I ~/.heromeshsession
A SQLite database containing user cache data.
.TP

Modified picture.c from [923fd4390d] to [5b37db371e].

665
666
667
668
669
670
671
672

673
674
675
676
677
678
679
665
666
667
668
669
670
671

672
673
674
675
676
677
678
679







-
+







  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");
  fprintf(stderr,"Loading pictures...\n");
  fp=fopen(nam,"r");
  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) {

Modified puzzleset.doc from [3cca5ab492] to [33868fdaba].

16
17
18
19
20
21
22



23
24
25
26
27
28
29
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32







+
+
+








The class definition file is a plain text file, and you are expected to
edit it using any text editor. The other three files are Hamster archives,
and are normally written by Free Hero Mesh, so you do not need to tamper
with the internal contents of those files by yourself. You may extract and
insert parts of the Hamster archives if you are careful; the descriptions
below mention what each lump means.

A puzzle set may also be a "composite puzzle set"; see the below section
for information about composite puzzle sets.

(A "Hamster archive" consists of zero or more lumps, where each lump
consists of a null-terminated ASCII file name, the 32-bit PDP-endian data
size, and then the data.)


=== Class resource file ===
90
91
92
93
94
95
96





















93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
solution is valid.

The solution file is also used for autotesting. In this case, the solution
of each level is executed, and it ensures that a win occurs during the
final turn, and not before. (A puzzle set catalog service may perhaps use
this in order to verify that the puzzles are solvable.)


=== Composite puzzle set ===

A composite puzzle set can have any file name, and is a Hamster archive
containing at least four lumps. The main four lumps have the same contents
as the four main files of a non-composite puzzle set, and their names must
have the correct suffix, although the part of the name before the first
dot can be anything that does not itself have a dot (it is not required to
match the file name of the composite puzzle set, and may be empty).

A composite puzzle set is always read-only. If you wish to modify it, you
must extract them as separate files.

There are other optional lumps, as follows:

* FILE_ID.DIZ: Contains a plain ASCII text description of this puzzle set.
Should have up to nine lines each no longer than 45 ASCII characters, and
the first line is a title of the puzzle set.

(Others may be added later, such as optional compression.)

Modified sql.doc from [a8f6d9974c] to [556d466297].

211
212
213
214
215
216
217
218




219
220
221
222
211
212
213
214
215
216
217

218
219
220
221
222
223
224
225







-
+
+
+
+




CREATE TABLE "USERCACHEINDEX"("ID" INTEGER PRIMARY KEY, "NAME" TEXT,
"TIME" INT);
  The user cache index; contains one record for each .level and .solution
  file of all puzzle sets which have been loaded. NAME is the full path,
  and TIME is the UNIX timestamp. If you want to delete a record, then
  you should also delete all records from USERCACHEDATA whose FILE value
  is the same as the ID value in this table for which it is deleted. You
  should not touch it while Free Hero Mesh is running, though.
  should not touch it while Free Hero Mesh is running, though. If it is a
  composite puzzle set, then NAME consists of the full path prefixed by
  L// for the level file or S// for the solution file (this will then
  probably be followed by a third slash indicating the root directory).

CREATE TEMPORARY TABLE "VARIABLES"("ID" INTEGER PRIMARY KEY, "NAME" TEXT);
  List of all global and local user variables used in the game.