Index: bindings.doc ================================================================== --- bindings.doc +++ bindings.doc @@ -119,10 +119,13 @@ Cancel dead animation. 'lo' Flash the specified location briefly. +'ls' + Load save state slot (0 to 7). + 'mi' Import a move list. The argument is a operating system command, that when executed will write the move list to stdout, with one byte per move (the Hero Mesh key codes). @@ -138,10 +141,16 @@ 'rs' Adjust replay speed by the specified number (negative to make faster, or positive to make slower; zero leaves it unchanged). +'ss' + Save save state slot (0 to 7). + +'xs' + Exchange save state slot (0 to 7). + 'xy' Input a move using coordinate input. Coordinates are 1-based. === Editor commands === Index: default.heromeshrc ================================================================== --- default.heromeshrc +++ default.heromeshrc @@ -5,10 +5,11 @@ ?.traceAll: false ?.showInventory: 0 ?.maxTrigger: 32767 ?.pasteCommand: xclip -o ?.codepage: /home/user/freeheromesh/codepage.har +?.saveSolutions.private: true ! Game inputs ?.gameKey.A: 'A ?.gameKey.B: 'B ?.gameKey.C: 'C @@ -91,12 +92,12 @@ ?.gameKey.up: 'UP ! Game inputs with alt ?.gameKey.alt.9: 'F9 ?.gameKey.alt.0: 'F10 -?.gameKey.alt.1: 'F11 -?.gameKey.alt.2: 'F12 +?.gameKey.alt.minus: 'F11 +?.gameKey.alt.equals: 'F12 ?.gameKey.alt.B: 'BREAK ?.gameKey.alt.C: 'CAPSLOCK ?.gameKey.alt.D: 'DELETE ?.gameKey.alt.N: 'NUMLOCK ?.gameKey.alt.O: 'SCRLOCK @@ -130,10 +131,11 @@ ?.gameKey.f5: ^M ?.gameKey.f6: ^< ?.gameKey.f7: ^> ?.gameKey.f8: ^s ?.gameKey.shift.f8: select 'ml',solution_move_list(1); +?.gameKey.ctrl.f8: select 'ml',best_move_list(); ?.gameKey.f9: select 'lo',xy(o.x,o.y) from objects o,classes c on(o.class=c.id) where c.player; ?.gameKey.tab: ^I ?.gameKey.alt.G: ^g ?.gameKey.alt.P: ^p ?.gameKey.alt.R: select 'go',id from levels where not solved order by random() limit 1; @@ -143,10 +145,34 @@ ?.gameKey.delete: ^- ?.gameKey.ctrl.delete: ^D ?.gameKey.insert: ^+ ?.gameKey.alt.kp_minus: select 'go',-ord from levels where ord<$level and not solved order by ord desc limit 1; ?.gameKey.alt.kp_plus: select 'go',-ord from levels where ord>$level and not solved order by ord asc limit 1; +?.gameKey.shift.1: select 'ss',0; +?.gameKey.shift.2: select 'ss',1; +?.gameKey.shift.3: select 'ss',2; +?.gameKey.shift.4: select 'ss',3; +?.gameKey.shift.5: select 'ss',4; +?.gameKey.shift.6: select 'ss',5; +?.gameKey.shift.7: select 'ss',6; +?.gameKey.shift.8: select 'ss',7; +?.gameKey.ctrl.1: select 'ls',0; +?.gameKey.ctrl.2: select 'ls',1; +?.gameKey.ctrl.3: select 'ls',2; +?.gameKey.ctrl.4: select 'ls',3; +?.gameKey.ctrl.5: select 'ls',4; +?.gameKey.ctrl.6: select 'ls',5; +?.gameKey.ctrl.7: select 'ls',6; +?.gameKey.ctrl.8: select 'ls',7; +?.gameKey.alt.1: select 'xs',0; +?.gameKey.alt.2: select 'xs',1; +?.gameKey.alt.3: select 'xs',2; +?.gameKey.alt.4: select 'xs',3; +?.gameKey.alt.5: select 'xs',4; +?.gameKey.alt.6: select 'xs',5; +?.gameKey.alt.7: select 'xs',6; +?.gameKey.alt.8: select 'xs',7; ! Editor key bindings ?.editKey.1: select 'mr',0; ?.editKey.2: select 'mr',1; ?.editKey.3: select 'mr',2; Index: game.c ================================================================== --- game.c +++ game.c @@ -39,10 +39,19 @@ static Uint8 solved; static Uint8 inserting,saved_inserting; static sqlite3_stmt*autowin; static size_t dum_size; // not used by Free Hero Mesh, but needed by some C library functions. +typedef struct { + char*data; + size_t size; + Uint16 mark,pos; +} SaveState; + +static SaveState savestates[8]; +static Uint8 has_savestates; + int encode_move(FILE*fp,MoveItem v) { // Encodes a single move and writes the encoded move to the file. // Returns the number of bytes of the encoded move. if(v>=8 && v<256) { fputc(v,fp); @@ -332,10 +341,11 @@ SDL_UnlockSurface(screen); SDL_Flip(screen); } static void save_replay(void) { + int i; unsigned char*buf=0; size_t sz=0; FILE*fp; if(solution_replay || !replay_list || !replay_count) return; if(gameover==1) solved=1; @@ -361,10 +371,22 @@ } if(replay_mark) { fputc(0x42,fp); fputc(replay_mark,fp); fputc(replay_mark>>8,fp); + } + if(has_savestates) for(i=0;i<8;i++) if(savestates[i].data) { + fputc(i+0x30,fp); + fputs(savestates[i].data,fp); + fputc(0,fp); + if(savestates[i].mark || savestates[i].pos) { + fputc(i+0xB0,fp); + fputc(savestates[i].mark,fp); + fputc(savestates[i].mark>>8,fp); + fputc(savestates[i].pos,fp); + fputc(savestates[i].pos>>8,fp); + } } fclose(fp); if(!buf) fatal("Allocation failed\n"); write_userstate(FIL_LEVEL,level_id,sz,buf); free(buf); @@ -378,10 +400,19 @@ free(replay_list); replay_list=0; replay_count=replay_mark=replay_size=0; free(best_list); best_list=0; + if(has_savestates) { + for(i=0;i<8;i++) { + free(savestates[i].data); + savestates[i].data=0; + savestates[i].size=0; + savestates[i].mark=savestates[i].pos=0; + } + has_savestates=0; + } if(solution_replay) { gameover_score=NO_SCORE; if(buf=read_lump(FIL_SOLUTION,level_id,&sz)) { fp=fmemopen(buf,sz,"r"); if(!fp) fatal("Allocation failed\n"); @@ -427,10 +458,15 @@ break; case 0x02: // Best list if(best_list) goto skip; dum_size=0; getdelim(&best_list,&dum_size,0,fp); + break; + case 0x30 ... 0x37: // Save states + if(savestates[i&7].data) goto skip; + getdelim(&savestates[i&7].data,&savestates[i&7].size,0,fp); + has_savestates=1; break; case 0x41: // Solved version i=fgetc(fp); i|=fgetc(fp)<<8; if(i==level_version) solved=1; break; @@ -442,10 +478,16 @@ best_score=fgetc(fp); best_score|=fgetc(fp)<<8; best_score|=fgetc(fp)<<16; best_score|=fgetc(fp)<<24; break; + case 0xB0 ... 0xB7: // Save states + savestates[i&7].mark=fgetc(fp); + savestates[i&7].mark|=fgetc(fp)<<8; + savestates[i&7].pos=fgetc(fp); + savestates[i&7].pos|=fgetc(fp)<<8; + break; default: skip: if(i<0x40) { while(fgetc(fp)>0); } else if(i<0x80) { fgetc(fp); fgetc(fp); @@ -463,10 +505,49 @@ } } if(fp) fclose(fp); free(buf); } + +static int exchange_state(int slot,int how) { + // Return value is replay position of save state (-1 if error) + // slot = 0 to 7 + // how = 'l' (load), 's' (save), 'x' (exchange) + SaveState*ss=savestates+slot; + SaveState v=*ss; + FILE*fp; + if(how!='s' && !v.data) { + screen_message("Nonexisting save state"); + return -1; + } + if(how!='l') { + if(ss->data) { + if(how=='s') free(ss->data); + ss->data=0; + ss->size=0; + } + if(replay_count) { + has_savestates=1; + fp=open_memstream(&ss->data,&ss->size); + if(!fp) fatal("Allocation failed\n"); + encode_move_list(fp); + fputc(0,fp); + fclose(fp); + } + ss->mark=replay_mark; + ss->pos=replay_pos; + } + if(how!='s') { + fp=fmemopen(v.data,v.size,"r"); + if(!fp) fatal("Allocation failed\n"); + decode_move_list(fp); + fclose(fp); + if(how=='x') free(v.data); + replay_mark=v.mark; + } + return v.pos; +} static void begin_level(int id) { const char*t; replay_time=0; if(replay_count) save_replay(); @@ -1367,10 +1448,18 @@ begin_level(number); return 1; case 'lo': // Locate me locate_me(number&63?:64,number/64?:64); return prev; + case 'ls': // Load state + if(solution_replay) { + screen_message("You cannot load states during solution replay"); + return -3; + } + number=exchange_state(number&7,'l'); + if(number<0) return -3; + goto restart; case 'mi': // Move list import if(argc<2 || solution_replay) break; if(replay_pos) begin_level(level_id); do_import_moves(sqlite3_column_text(args,1)); return 1; @@ -1388,10 +1477,21 @@ // fall through case 'rS': // Replay speed (absolute) if(number<1) number=1; else if(number>255) number=255; replay_speed=number; return prev; + case 'ss': // Save state + exchange_state(number&7,'s'); + return prev; + case 'xs': // Exchange state + if(solution_replay) { + screen_message("You cannot load states during solution replay"); + return -3; + } + number=exchange_state(number&7,'x'); + if(number<0) return -3; + goto restart; case 'xy': // Coordinate input if(argc<3 || !has_xy_input) break; argc=sqlite3_column_int(args,1); number=sqlite3_column_int(args,2); if(argc<1 || argc>pfwidth || number<1 || number>pfheight) return 0; Index: game.doc ================================================================== --- game.doc +++ game.doc @@ -94,10 +94,14 @@ If you solve a level yourself, you can push CTRL+S to record the solution. The solution will be recorded in the puzzle set once the user cache is flushed as described below. If the .saveSolution resource is true, then it will automatically save the solution (although not necessarily to the puzzle set files). + +There are also save state slots, which are separate for each level; you +can have up to eight such save states recorded at once. Use SHIFT to save, +CTRL to load, or ALT to exchange, together with a number 1 to 8. IMPORTANT NOTE: Saving the solutions here will only save them in the user cache database, not to the puzzle set file. To save them to the puzzle set file, you must invoke heromesh -f to flush the user cache. The local replay list and mark will not be saved in the puzzle set though; they are @@ -155,11 +159,12 @@ SHIFT+F4 Rewind 1000 moves F5 Set mark F6 Rewind to mark F7 Replay to mark F8 Toggle solution replay - SHIFT+F8 Copy solution + SHIFT+F8 Load solution + CTRL+F8 Load personal best F9 Flash player position F10 SQL queries ESC Restart level TAB Toggle inventory/replay display KP ENTER Restart level @@ -175,12 +180,15 @@ ALT+[ Increase slow replay speed ALT+] Decrease slow replay speed INS Toggle insertion mode DEL Delete a move CTRL+DEL Delete all moves forward + SHIFT+num Save state 1 to 8 + CTRL+num Load state 1 to 8 + ALT+num Exchange state 1 to 8 Mouse (in grid): MIDDLE Describe topmost object RIGHT Inspect objects (main world) SHIFT+RIGHT Inspect objects (bizarro world) Index: internals.doc ================================================================== --- internals.doc +++ internals.doc @@ -197,12 +197,17 @@ Record types: * 0x01 = Replay list * 0x02 = Best personal solution + +* 0x30 to 0x37 = Replay list of save state * 0x41 = Level version, if solved (omitted otherwise) * 0x42 = Mark position * 0x81 = Best personal score + +* 0xB0 to 0xB7 = Save state data; low 16-bits is mark position and high +16-bits is current replay position