Index: README ================================================================== --- README +++ README @@ -164,10 +164,14 @@ 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.) @@ -237,12 +241,12 @@ "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. Index: TODO ================================================================== --- TODO +++ TODO @@ -39,12 +39,12 @@ * 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 Index: class.c ================================================================== --- class.c +++ class.c @@ -2111,11 +2111,11 @@ 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)); Index: commandline.doc ================================================================== --- commandline.doc +++ commandline.doc @@ -64,10 +64,14 @@ -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 should be the + full file name of the puzzle, including the suffix. + === Dot commands === .b0 Disable terminate on errors. Index: game.c ================================================================== --- game.c +++ game.c @@ -728,11 +728,11 @@ 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; Index: heromesh.h ================================================================== --- heromesh.h +++ heromesh.h @@ -72,10 +72,11 @@ 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); Index: main.c ================================================================== --- main.c +++ main.c @@ -5,11 +5,11 @@ /* 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 #include #include @@ -64,13 +64,92 @@ 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]; @@ -457,17 +536,73 @@ 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; @@ -896,10 +1031,14 @@ 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; } @@ -925,17 +1064,18 @@ 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; @@ -949,10 +1089,11 @@ 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; Index: man6/heromesh.6 ================================================================== --- man6/heromesh.6 +++ man6/heromesh.6 @@ -9,11 +9,11 @@ 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. @@ -45,13 +45,17 @@ 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 Index: picture.c ================================================================== --- picture.c +++ picture.c @@ -667,11 +667,11 @@ 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; Index: puzzleset.doc ================================================================== --- puzzleset.doc +++ puzzleset.doc @@ -18,10 +18,13 @@ 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.) @@ -92,5 +95,26 @@ 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.) + Index: sql.doc ================================================================== --- sql.doc +++ sql.doc @@ -213,10 +213,13 @@ 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.