Index: config.doc
==================================================================
--- config.doc
+++ config.doc
@@ -138,10 +138,15 @@
 .saveSolutions
   If true, then solutions are saved if you solve a level in less moves
   than the currently recorded solution (or if no solution is already
   recorded). This has no effect in read-only mode.
 
+.saveSolutions.private
+  If true, then solutions are saved like .saveSolutions but they are saved
+  privately to the user cache database, instead of in the solution file.
+  This works even in read-only mode.
+
 .screenFlags
   SDL flags: d = double buffer, f = full screen, h = use hardware surface,
   n = no window frame, p = hardware palette, r = allow the window to be
   resized, y = asynchronous blit, z = no parachute. Some flags might not
   work if the window manager does not support them.

Index: function.c
==================================================================
--- function.c
+++ function.c
@@ -62,10 +62,18 @@
   } else {
     sqlite3_free(sqlite3_str_finish(str));
     sqlite3_result_zeroblob(cxt,0);
   }
 }
+
+static void fn_best_move_list(sqlite3_context*cxt,int argc,sqlite3_value**argv) {
+  if(best_list) sqlite3_result_blob(cxt,best_list,strlen(best_list),SQLITE_TRANSIENT);
+}
+
+static void fn_best_score(sqlite3_context*cxt,int argc,sqlite3_value**argv) {
+  if(best_list && best_score!=NO_SCORE) sqlite3_result_int64(cxt,best_score);
+}
 
 static void fn_byte(sqlite3_context*cxt,int argc,sqlite3_value**argv) {
   Uint8*s=malloc(argc+1);
   int i;
   if(!s) {
@@ -1776,10 +1784,12 @@
 );
 
 void init_sql_functions(sqlite3_int64*ptr0,sqlite3_int64*ptr1) {
   sqlite3_create_function(userdb,"BASENAME",0,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_basename,0,0);
   sqlite3_create_function(userdb,"BCAT",-1,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_bcat,0,0);
+  sqlite3_create_function(userdb,"BEST_MOVE_LIST",0,SQLITE_UTF8,0,fn_best_move_list,0,0);
+  sqlite3_create_function(userdb,"BEST_SCORE",0,SQLITE_UTF8,0,fn_best_score,0,0);
   sqlite3_create_function(userdb,"BYTE",-1,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_byte,0,0);
   sqlite3_create_function(userdb,"CL",1,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_cl,0,0);
   sqlite3_create_function(userdb,"CLASS_DATA",2,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_class_data,0,0);
   sqlite3_create_function(userdb,"CVALUE",1,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_cvalue,0,0);
   sqlite3_create_function(userdb,"HASH",2,SQLITE_UTF8|SQLITE_DETERMINISTIC,0,fn_hash,0,0);

Index: game.c
==================================================================
--- game.c
+++ game.c
@@ -1,7 +1,7 @@
 #if 0
-gcc ${CFLAGS:--s -O2} -c -Wno-multichar -fwrapv game.c `sdl-config --cflags`
+gcc ${CFLAGS:--s -O2} -c -Wno-unused-result -Wno-multichar -fwrapv game.c `sdl-config --cflags`
 exit
 #endif
 
 /*
   This program is part of Free Hero Mesh and is public domain.
@@ -21,23 +21,27 @@
 
 MoveItem*replay_list;
 size_t replay_size;
 Uint16 replay_count,replay_pos,replay_mark;
 Uint8 solution_replay=255;
+char*best_list;
+Sint32 best_score=NO_SCORE;
 
 static volatile Uint8 timerflag;
 static int exam_scroll;
 static MoveItem*inputs;
 static size_t inputs_size;
 static int inputs_count;
 static Uint8 side_mode=255;
 static Uint8 should_record_solution;
+static Uint8 should_record_private_solution;
 static Uint8 replay_speed;
 static Uint8 replay_time;
 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.
 
 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.
   fputc(v,fp);
@@ -90,14 +94,17 @@
   v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
   side_mode=boolxrm(v,1);
   optionquery[1]=Q_replaySpeed;
   v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
   replay_speed=strtol(v,0,10)?:16;
+  optionquery[1]=Q_saveSolutions;
+  optionquery[2]=Q_private;
+  v=xrm_get_resource(resourcedb,optionquery,optionquery,3)?:"";
+  should_record_private_solution=boolxrm(v,0);
   if(main_options['r']) {
     should_record_solution=0;
   } else {
-    optionquery[1]=Q_saveSolutions;
     v=xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"";
     should_record_solution=boolxrm(v,0);
   }
   solution_replay=0;
   optionquery[1]=Q_autoWin;
@@ -311,10 +318,19 @@
   fputc(0x00,fp);
   if(solved) {
     fputc(0x41,fp);
     fputc(level_version,fp);
     fputc(level_version>>8,fp);
+    if(best_list) {
+      fputc(0x02,fp);
+      fwrite(best_list,1,strlen(best_list)+1,fp);
+      fputc(0x81,fp);
+      fputc(best_score,fp);
+      fputc(best_score>>8,fp);
+      fputc(best_score>>16,fp);
+      fputc(best_score>>24,fp);
+    }
   }
   if(replay_mark) {
     fputc(0x42,fp);
     fputc(replay_mark,fp);
     fputc(replay_mark>>8,fp);
@@ -331,10 +347,12 @@
   long sz;
   int i,j;
   free(replay_list);
   replay_list=0;
   replay_count=replay_mark=replay_size=0;
+  free(best_list);
+  best_list=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");
@@ -356,10 +374,11 @@
       }
     }
   } else if(buf=read_userstate(FIL_LEVEL,level_id,&sz)) {
     fp=fmemopen(buf,sz,"r");
     if(!fp) fatal("Allocation failed\n");
+    best_score=NO_SCORE;
     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) {
@@ -375,18 +394,29 @@
       while((i=fgetc(fp))!=EOF) switch(i) {
         case 0x01: // Replay list
           if(replay_list) goto skip;
           decode_move_list(fp);
           break;
+        case 0x02: // Best list
+          if(best_list) goto skip;
+          dum_size=0;
+          getdelim(&best_list,&dum_size,0,fp);
+          break;
         case 0x41: // Solved version
           i=fgetc(fp); i|=fgetc(fp)<<8;
           if(i==level_version) solved=1;
           break;
         case 0x42: // Mark
           replay_mark=fgetc(fp);
           replay_mark|=fgetc(fp)<<8;
           break;
+        case 0x81: // Best score
+          best_score=fgetc(fp);
+          best_score|=fgetc(fp)<<8;
+          best_score|=fgetc(fp)<<16;
+          best_score|=fgetc(fp)<<24;
+          break;
         default: skip:
           if(i<0x40) {
             while(fgetc(fp)>0);
           } else if(i<0x80) {
             fgetc(fp); fgetc(fp);
@@ -393,10 +423,15 @@
           } else if(i<0xC0) {
             for(i=0;i<4;i++) fgetc(fp);
           } else {
             for(i=0;i<8;i++) fgetc(fp);
           }
+      }
+      if(best_list && !solved) {
+        free(best_list);
+        best_list=0;
+        best_score=NO_SCORE;
       }
     }
   }
   if(fp) fclose(fp);
   free(buf);
@@ -1465,10 +1500,32 @@
   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);
 }
+
+static void record_private_solution(void) {
+  FILE*fp;
+  char*buf=0;
+  int n;
+  if(solution_replay) return;
+  if(gameover_score==NO_SCORE) gameover_score=replay_pos;
+  if(best_list && best_score!=NO_SCORE && gameover_score>best_score) return;
+  dum_size=0;
+  fp=open_memstream(&buf,&dum_size);
+  if(!fp) fatal("Allocation failed\n");
+  n=replay_count;
+  replay_count=replay_pos;
+  encode_move_list(fp);
+  replay_count=n;
+  fclose(fp);
+  if(buf) {
+    free(best_list);
+    best_list=buf;
+    best_score=gameover_score;
+  }
+}
 
 void run_game(void) {
   int i;
   SDL_Event ev;
   set_caption();
@@ -1533,10 +1590,11 @@
           inputs_count=0;
           if(saved_inserting) inserting=1,saved_inserting=0;
           no_dead_anim=0;
           if(gameover==1) {
             if(should_record_solution) record_solution();
+            if(should_record_private_solution) record_private_solution();
             if(!solution_replay && !solved) sqlite3_exec(userdb,"UPDATE `LEVELS` SET `SOLVED` = 1 WHERE `ID` = LEVEL_ID();",0,0,0);
             if(autowin) do_autowin();
           }
         }
         redraw_game();

Index: heromesh.h
==================================================================
--- heromesh.h
+++ heromesh.h
@@ -324,10 +324,12 @@
 
 extern MoveItem*replay_list;
 extern size_t replay_size;
 extern Uint16 replay_count,replay_pos,replay_mark;
 extern Uint8 solution_replay;
+extern char*best_list;
+extern Sint32 best_score;
 
 int encode_move(FILE*fp,MoveItem v);
 int encode_move_list(FILE*fp);
 MoveItem decode_move(FILE*fp);
 int decode_move_list(FILE*fp);

Index: internals.doc
==================================================================
--- internals.doc
+++ internals.doc
@@ -153,10 +153,14 @@
 The new format is always small-endian.
 
 Record types:
 
 * 0x01 = Replay list
+
+* 0x02 = Best personal solution
 
 * 0x41 = Level version, if solved (omitted otherwise)
 
 * 0x42 = Mark position
+
+* 0x81 = Best personal score
 

Index: quarks
==================================================================
--- quarks
+++ quarks
@@ -185,11 +185,10 @@
 numLock
 
 ! Mouse
 editClick
 gameClick
-allowMouseWarp
 middle
 ! (left and right are listed with keys)
 ! (the key modifiers are also valid here)
 
 ! Class
@@ -198,10 +197,11 @@
 
 ! Solutions
 saveSolutions
 solutionComment
 solutionTimestamp
+private
 
 ! Picture editor
 picedit
 macro
 

Index: quarks.h
==================================================================
--- quarks.h
+++ quarks.h
@@ -160,17 +160,17 @@
 #define Q_alt 161
 #define Q_meta 162
 #define Q_numLock 163
 #define Q_editClick 164
 #define Q_gameClick 165
-#define Q_allowMouseWarp 166
-#define Q_middle 167
-#define Q_class 168
-#define Q_quiz 169
-#define Q_saveSolutions 170
-#define Q_solutionComment 171
-#define Q_solutionTimestamp 172
+#define Q_middle 166
+#define Q_class 167
+#define Q_quiz 168
+#define Q_saveSolutions 169
+#define Q_solutionComment 170
+#define Q_solutionTimestamp 171
+#define Q_private 172
 #define Q_picedit 173
 #define Q_macro 174
 #define Q_sqlFile 175
 #define Q_sqlInit 176
 #define Q_sqlExtensions 177
@@ -357,17 +357,17 @@
   "alt",
   "meta",
   "numLock",
   "editClick",
   "gameClick",
-  "allowMouseWarp",
   "middle",
   "class",
   "quiz",
   "saveSolutions",
   "solutionComment",
   "solutionTimestamp",
+  "private",
   "picedit",
   "macro",
   "sqlFile",
   "sqlInit",
   "sqlExtensions",

Index: sql.doc
==================================================================
--- sql.doc
+++ sql.doc
@@ -12,10 +12,20 @@
   counting the switches or the program name).
 
 BCAT(...)
   Concatenate several blobs. Nulls are skipped, if any.
 
+BEST_MOVE_LIST()
+  Returns the best personal move list in the user cache file (only valid if
+  .saveSolution.private is true), as a blob. If there isn't any saved, then
+  this is null.
+
+BEST_SCORE()
+  Returns the best score from the best move list (if it has been saved).
+  If no score has been explicitly specified, then this is equal to the
+  number of moves. If no score has been stored, then it is null.
+
 BYTE(...)
   Make a blob; each argument is a number, of which the low 8-bits are used
   to make the value of one byte in the blob, so the size of the blob will
   then be the same as the number of arguments.