Index: fileformat.doc ================================================================== --- fileformat.doc +++ fileformat.doc @@ -135,10 +135,13 @@ * Level version number (16-bits small-endian): If this does not match the level version number in the .LVL lump, then the solution is considered to be invalid. * Flags (8-bits): Specifies which other fields are present. + +* Score (signed 32-bits small-endian; only present if flag bit7 is set): +The score of this level. Lower numbers are better. * Comment (null-terminated; only present if flag bit0 set): Normally contains a user name, but may be any arbitrary text. * Timestamp (64-bits small-endian; only present if flag bit1 set): The Index: game.c ================================================================== --- game.c +++ game.c @@ -1,7 +1,7 @@ #if 0 -gcc ${CFLAGS:--s -O2} -c -Wno-multichar game.c `sdl-config --cflags` +gcc ${CFLAGS:--s -O2} -c -Wno-multichar -fwrapv game.c `sdl-config --cflags` exit #endif /* This program is part of Free Hero Mesh and is public domain. @@ -43,10 +43,11 @@ return 1; } int encode_move_list(FILE*fp) { // Encodes the current replay list into the file; returns the number of bytes. + // Does not write a null terminator. fwrite(replay_list,1,replay_count,fp); return replay_count; } MoveItem decode_move(FILE*fp) { @@ -58,17 +59,18 @@ int decode_move_list(FILE*fp) { // Decodes a move list from the file, and stores it in replay_list and replay_count. // Returns the number of moves (replay_count). MoveItem v; + FILE*o; free(replay_list); replay_list=0; replay_size=0; replay_count=0; - FILE*o=open_memstream((char**)&replay_list,&replay_size); + o=open_memstream((char**)&replay_list,&replay_size); if(!o) fatal("Allocation failed\n"); - while(replay_count<0xFFFD && (v=decode_move(fp))) { + while(replay_count<0xFFFE && (v=decode_move(fp))) { fwrite(&v,1,sizeof(MoveItem),o); replay_count++; } fclose(o); if(replay_count && !replay_list) fatal("Allocation failed\n"); @@ -289,68 +291,112 @@ SDL_UnlockSurface(screen); SDL_Flip(screen); } static void save_replay(void) { - long sz=replay_size; - if(solution_replay || !replay_list) return; - if(sz>8; - replay_list[sz-5]=replay_mark; - replay_list[sz-4]=(level_version+solved-1)>>8; - replay_list[sz-3]=level_version+solved-1; - replay_list[sz-2]=replay_count>>8; - replay_list[sz-1]=replay_count; - write_userstate(FIL_LEVEL,level_id,sz,replay_list); + fp=open_memstream((char**)&buf,&sz); + if(!fp) fatal("Allocation failed\n"); + fputc(0x00,fp); + fputc(0x01,fp); + encode_move_list(fp); + fputc(0x00,fp); + if(solved) { + fputc(0x41,fp); + fputc(level_version,fp); + fputc(level_version>>8,fp); + } + if(replay_mark) { + fputc(0x42,fp); + fputc(replay_mark,fp); + fputc(replay_mark>>8,fp); + } + fclose(fp); + if(!buf) fatal("Allocation failed\n"); + write_userstate(FIL_LEVEL,level_id,sz,buf); + free(buf); } static void load_replay(void) { + FILE*fp=0; + unsigned char*buf=0; long sz; - int i; + int i,j; free(replay_list); + replay_list=0; + replay_count=replay_mark=replay_size=0; if(solution_replay) { - replay_list=read_lump(FIL_SOLUTION,level_id,&sz); - // Solution format: version (16-bits), flag (8-bits), user name (null-terminated), timestamp (64-bits), move list - if(sz>3) { - i=replay_list[0]|(replay_list[1]<<8); - if(i!=level_version) goto notfound; - i=3; - if(replay_list[2]&1) { - while(i=sz || sz-i>0xFFFF) goto notfound; - replay_size=sz; - memmove(replay_list,replay_list+i,replay_count=sz-i); - replay_mark=0; - } else { - goto notfound; - } - } else { - replay_list=read_userstate(FIL_LEVEL,level_id,&sz); - if(sz>=2) { - replay_size=sz; - replay_count=(replay_list[sz-2]<<8)|replay_list[sz-1]; - if(sz-replay_count>=4) replay_mark=(replay_list[replay_count]<<8)|replay_list[replay_count+1]; else replay_mark=0; + gameover_score=NO_SCORE; + if(buf=read_lump(FIL_SOLUTION,level_id,&sz)) { + fp=fmemopen(buf,sz,"r"); + if(!fp) fatal("Allocation failed\n"); + // Solution format: version (16-bits), flag (8-bits), score (32-bits), user name (null-terminated), timestamp (64-bits), move list + if(sz>3) { + i=fgetc(fp); i|=fgetc(fp)<<8; + if(i==level_version) { + j=fgetc(fp); + if(j&128) { + gameover_score=fgetc(fp); + gameover_score|=fgetc(fp)<<8; + gameover_score|=fgetc(fp)<<16; + gameover_score|=fgetc(fp)<<24; + } + if(j&1) while(fgetc(fp)>0); + if(j&2) for(i=0;i<8;i++) fgetc(fp); + decode_move_list(fp); + } + } + } + } else if(buf=read_userstate(FIL_LEVEL,level_id,&sz)) { + fp=fmemopen(buf,sz,"r"); + if(!fp) fatal("Allocation failed\n"); + if(sz>2 && *buf) { + // Old format + replay_count=(buf[sz-2]<<8)|buf[sz-1]; + if(sz-replay_count>=4) replay_mark=(buf[replay_count]<<8)|buf[replay_count+1]; else replay_mark=0; if(sz-replay_count>=6) { - i=(replay_list[replay_count+2]<<8)|replay_list[replay_count+3]; + i=(buf[replay_count+2]<<8)|buf[replay_count+3]; if(i==level_version) solved=1; } + replay_list=malloc(replay_size=sizeof(MoveItem)*replay_count+1); + if(!replay_list) fatal("Allocation failed\n"); + for(i=0;i0); + } else if(i<0x80) { + fgetc(fp); fgetc(fp); + } else if(i<0xC0) { + for(i=0;i<4;i++) fgetc(fp); + } else { + for(i=0;i<8;i++) fgetc(fp); + } + } } } + if(fp) fclose(fp); + free(buf); } static void begin_level(int id) { const char*t; replay_time=0; @@ -1328,11 +1374,11 @@ return n; } static inline void input_move(Uint8 k) { const char*t=execute_turn(k); - if(replay_pos==65534 && !gameover) t="Too many moves played"; + if(replay_pos>0xFFFE && !gameover) t="Too many moves played"; if(t) { screen_message(t); gameover=-1; return; } @@ -1339,22 +1385,21 @@ if(!key_ignored) { if(inserting) { if(replay_pos>=0xFFFE || replay_pos==replay_count) { inserting=0; } else { - if(replay_count>=0xFFFE) replay_count=0xFFFD; + if(replay_count>0xFFFE) replay_count=0xFFFE; if(replay_size<0xFFFF) { replay_list=realloc(replay_list,replay_size=0xFFFF); if(!replay_list) fatal("Allocation failed\n"); } memmove(replay_list+replay_pos+1,replay_list+replay_pos,replay_count-replay_pos); replay_count++; } } if(replay_pos>=replay_size) { - if(replay_size>0xFDFF) replay_size=0xFDFF; - if(replay_size+0x200<=replay_pos) fatal("Confusion in input_move function\n"); + if(replay_size>0xFFFF) replay_size=0xFFFF; replay_list=realloc(replay_list,replay_size+=0x200); if(!replay_list) fatal("Allocation failed\n"); } replay_list[replay_pos++]=k; if(replay_pos>replay_count) replay_count=replay_pos; @@ -1362,49 +1407,62 @@ } static void record_solution(void) { const char*v; const char*com; - Uint8*data; - Uint8*p; - long sz; + FILE*fp; + Uint8 flag; + long n; + unsigned char*buf=0; + size_t sz=0; if(solution_replay) return; - if(data=read_lump(FIL_SOLUTION,level_id,&sz)) { - if(sz<3 || (data[0]|(data[1]<<8))!=level_version || (data[2]&~3)) goto dontkeep; - sz-=3; - if(data[2]&1) sz-=strnlen(data+3,sz); - if(data[2]&2) sz-=8; - if(sz<=0 || sz>replay_pos) goto dontkeep; - free(data); + if(buf=read_lump(FIL_SOLUTION,level_id,&n)) { + if(n<3 || (buf[0]|(buf[1]<<8))!=level_version || (buf[2]&~0x83)) goto dontkeep; + n-=3; + if((buf[2]&128) && n>4) { + Sint32 sco=buf[3]|(buf[4]<<8)|(buf[5]<<16)|(buf[6]<<24); + if(gameover_score!=NO_SCORE && sco<=gameover_score) goto dontkeep; + } else { + if(buf[2]&1) n-=strnlen(buf+3,n); + if(buf[2]&2) n-=8; + if(n<=0 || n>replay_pos) goto dontkeep; + } + free(buf); return; dontkeep: - free(data); + free(buf); + buf=0; } optionquery[1]=Q_solutionComment; com=xrm_get_resource(resourcedb,optionquery,optionquery,2); if(com && !*com) com=0; optionquery[1]=Q_solutionTimestamp; v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:""; - data=malloc(sz=replay_pos+(boolxrm(v,0)?8:0)+(com?strlen(com)+1:0)+3); - if(!data) fatal("Allocation failed\n"); - data[0]=level_version&255; - data[1]=level_version>>8; - data[2]=(boolxrm(v,0)?2:0)|(com?1:0); - p=data+3; - if(com) { - strcpy(p,com); - p+=strlen(com)+1; - } - if(data[2]&2) { - time_t t=time(0); - p[0]=t>>000; p[1]=t>>010; p[2]=t>>020; p[3]=t>>030; - p[4]=t>>040; p[5]=t>>050; p[6]=t>>060; p[7]=t>>070; - p+=8; - } - memcpy(p,replay_list,replay_pos); - write_lump(FIL_SOLUTION,level_id,sz,data); - free(data); + flag=0; + if(com) flag|=1; + if(boolxrm(v,0)) flag|=2; + if(gameover_score!=NO_SCORE) flag|=128; + fp=open_memstream((char**)&buf,&sz); + if(!fp) fatal("Allocation failed\n"); + fputc(level_version,fp); + fputc(level_version>>8,fp); + fputc(flag,fp); + if(flag&128) { + fputc(gameover_score,fp); + fputc(gameover_score>>8,fp); + fputc(gameover_score>>16,fp); + fputc(gameover_score>>24,fp); + } + if(flag&1) fwrite(com,1,strlen(com+1),fp); + n=replay_count; + replay_count=replay_pos; + encode_move_list(fp); + replay_count=n; + fclose(fp); + if(!buf) fatal("Allocation failed\n"); + write_lump(FIL_SOLUTION,level_id,sz,buf); + free(buf); sqlite3_exec(userdb,"UPDATE `LEVELS` SET `SOLVABLE` = 1 WHERE `ID` = LEVEL_ID();",0,0,0); } void run_game(void) { int i; Index: internals.doc ================================================================== --- internals.doc +++ internals.doc @@ -119,5 +119,44 @@ 31 (\x) = Next byte is a character to display as graphic Codes 32-255 are displayed as is, but characters 1-31 cannot be displayed as a graphic unless a \x escape is present. + +=== User state data === + +The user state data for levels in the user cache database can be in the +old format or the new format. + +The old format is: + +* The move list. Only single-byte moves are valid. + +* The replay mark position (a big-endian 16-bit number). + +* The level version number (a big-endian 16-bit number); this will match +the actual level version number if the player has solved this level, or +otherwise will not match. + +* The number of moves in the move list. + +The new format starts with a null byte, and then is followed by any number +of records. The first byte of a record determines its format: + +* 0x01 to 0x3F = Null-terminated + +* 0x41 to 0x7F = Two more bytes + +* 0x81 to 0xBF = Four more bytes + +* 0xC1 to 0xFF = Eight more bytes + +The new format is always small-endian. + +Record types: + +* 0x01 = Replay list + +* 0x41 = Level version, if solved (omitted otherwise) + +* 0x42 = Mark position +