diff --git a/include/decl.h b/include/decl.h index 834b14ab8..711ba614e 100644 --- a/include/decl.h +++ b/include/decl.h @@ -462,6 +462,10 @@ struct ptr_array { }; typedef struct ptr_array ptr_array_t; +/* logging verbosity levels */ +#define LOG_MINIMAL 0 +#define LOG_VERBOSE 1 + #undef E #endif /* DECL_H */ diff --git a/include/extern.h b/include/extern.h index d2ed2ff9e..3c74bfda0 100644 --- a/include/extern.h +++ b/include/extern.h @@ -27,6 +27,13 @@ E void NDECL(newgame); E void FDECL(welcome, (BOOLEAN_P)); E time_t NDECL(get_realtime); E int FDECL(argcheck, (int, char **, enum earlyarg)); +E void NDECL(fuzzer_start); +E void NDECL(fuzzer_stop); +E void NDECL(fuzzer_toggle); +E void VDECL(fuzzer_log, (int, const char *, ...)) PRINTF_F(2, 3); +E void NDECL(fuzzer_check); +E void NDECL(fuzzer_auto_start); +E boolean FDECL(fuzzer_msg_history, (const char *)); /* ### apply.c ### */ @@ -950,6 +957,7 @@ E boolean FDECL(fuzzymatch, (const char *, const char *, const char *, BOOLEAN_P)); E void FDECL(init_random, (int FDECL((*fn), (int)))); E void FDECL(reseed_random, (int FDECL((*fn), (int)))); +E void FDECL(set_random, (unsigned long, int FDECL((*fn), (int)))); E time_t NDECL(getnow); E int NDECL(getyear); #if 0 @@ -2161,6 +2169,7 @@ E int FDECL(rnd, (int)); E int FDECL(d, (int, int)); E int FDECL(rne, (int)); E int FDECL(rnz, (int)); +E unsigned long NDECL(rul); /* ### role.c ### */ diff --git a/include/flag.h b/include/flag.h index 3ea3ada26..50f76c457 100644 --- a/include/flag.h +++ b/include/flag.h @@ -447,6 +447,10 @@ struct instance_flags { chosen_windowport[], but do not switch to it in the midst of options processing */ boolean obsolete; /* obsolete options can point at this, it isn't used */ + boolean fuzzer_auto_start; /* start fuzzer automatically */ + int fuzzer_stop_and_save; /* move when fuzzer stops and saves game */ + boolean fuzzer_saving; /* fuzzer is saving game */ + int verbose_logging_start; /* move when verbose fuzzer logging starts */ }; /* diff --git a/src/allmain.c b/src/allmain.c index 082447b3b..cde2ef9a2 100644 --- a/src/allmain.c +++ b/src/allmain.c @@ -436,6 +436,8 @@ boolean resuming; #ifdef MAIL ckmailstatus(); #endif + fuzzer_check(); + rhack((char *) 0); } if (u.utotype) /* change dungeon level */ @@ -583,6 +585,8 @@ newgame() { int i; + fuzzer_auto_start(); + #ifdef MFLOPPY gameDiskPrompt(); #endif @@ -922,4 +926,131 @@ const char *opts; #endif return; } + +static FILE * g_fuzzer_log_file = NULL; +static int g_fuzzer_log_level = LOG_MINIMAL; + +/* fuzzer_start() starts the fuzzer opening the fuzzer log file */ +void +fuzzer_start() +{ + if (!iflags.debug_fuzzer) { + const char * fq_replay; + + iflags.debug_fuzzer = TRUE; + iflags.fuzzer_auto_start = FALSE; + + nhassert(g_fuzzer_log_file == NULL); + fq_replay = fqname("fuzzer.log", SAVEPREFIX, 0); + + g_fuzzer_log_file = fopen(fq_replay, "w"); + } +} + +/* fuzzer_stop() stops the fuzzer and close the fuzzer log file */ +void +fuzzer_stop() +{ + if (iflags.debug_fuzzer) { + if(g_fuzzer_log_file != NULL) { + fclose(g_fuzzer_log_file); + g_fuzzer_log_file = NULL; + } + } +} + +/* fuzzer_toggle() toggles fuzzer state */ +void +fuzzer_toggle() +{ + if (iflags.debug_fuzzer) + fuzzer_stop(); + else + fuzzer_start(); +} + +/* fuzzer_log() is used to place messages in the file 'fuzzer.log'. This + * log is the primary tool for monitoring fuzzer activity and tracking down + * issues that the fuzzer is able to reproduce. + */ +void +fuzzer_log +VA_DECL2(int, level, const char *, str) +{ + VA_START(str); + VA_INIT(str, char *); + + if (!g_fuzzer_log_file) + return; + + if (iflags.verbose_logging_start != 0 && moves >= iflags.verbose_logging_start) + g_fuzzer_log_level = LOG_VERBOSE; + + if (level <= g_fuzzer_log_level) + Vfprintf(g_fuzzer_log_file, str, VA_ARGS); + + VA_END(); +} + +/* fuzzer_check() is called prior to rhack(0) to allow the fuzzer to + * check if it should stop and to allow it to reseed the game. + */ +void +fuzzer_check() +{ + if (iflags.debug_fuzzer) + { + if (moves >= iflags.fuzzer_stop_and_save) { + iflags.fuzzer_saving = TRUE; + dosave0(); + exit_nhwindows("Goodbye from the fuzzer..."); + fuzzer_stop(); + nh_terminate(EXIT_SUCCESS); + } + + unsigned long seed = rul(); + set_random(seed, rn2); + fuzzer_log(LOG_MINIMAL, "SEED:%ld:%lu\n", moves, seed); + + } +} + +/* fuzzer_auto_start is called when creating a new game to allow + * the fuzzer to start itself. + */ +void +fuzzer_auto_start() +{ + if (iflags.fuzzer_auto_start) { + nhassert(!iflags.debug_fuzzer); + fuzzer_start(); + unsigned long seed = rul(); + set_random(seed, rn2); + fuzzer_log(LOG_MINIMAL, "START:%ld:%lu\n", moves, seed); + } +} + +/* fuzzer_msg_history is called during save file recovery to allow + * the fuzzer to snoop the messages being recovered. The fuzzer + * saves a seed as a message in save files and this is the mechanism + * used to recover that seed if the fuzzer is being auto started. + */ +boolean +fuzzer_msg_history(msg) + const char * msg; +{ + long saved_moves; + unsigned long saved_seed; + if (sscanf(msg, "SEED:%ld:%lu", &saved_moves, &saved_seed) == 2) { + nhassert(saved_moves == moves); + if (iflags.fuzzer_auto_start) { + fuzzer_start(); + set_random(saved_seed, rn2); + fuzzer_log(LOG_MINIMAL, "START:%ld:%lu\n", moves, saved_seed); + } + return TRUE; + } + + return FALSE; +} /*allmain.c*/ diff --git a/src/hacklib.c b/src/hacklib.c index 9fe316517..1f43325df 100644 --- a/src/hacklib.c +++ b/src/hacklib.c @@ -854,7 +854,7 @@ STATIC_DCL struct tm *NDECL(getlt); /* Sets the seed for the random number generator */ #ifdef USE_ISAAC64 -static void +void set_random(seed, fn) unsigned long seed; int FDECL((*fn), (int)); @@ -865,7 +865,7 @@ int FDECL((*fn), (int)); #else /* USE_ISAAC64 */ /*ARGSUSED*/ -static void +void set_random(seed, fn) unsigned long seed; int FDECL((*fn), (int)) UNUSED; @@ -917,7 +917,7 @@ int FDECL((*fn), (int)); { /* only reseed if we are certain that the seed generation is unguessable * by the players. */ - if (has_strong_rngseed) + if (has_strong_rngseed && !iflags.debug_fuzzer) init_random(fn); } @@ -1108,6 +1108,9 @@ phase_of_the_moon() /* 0-7, with 0: new, 4: full */ register struct tm *lt = getlt(); register int epact, diy, goldn; + if(iflags.debug_fuzzer) + return rn2(8); + diy = lt->tm_yday; goldn = (lt->tm_year % 19) + 1; epact = (11 * goldn + 18) % 30; @@ -1122,6 +1125,9 @@ friday_13th() { register struct tm *lt = getlt(); + if(iflags.debug_fuzzer) + return rn2(30); + /* tm_wday (day of week; 0==Sunday) == 5 => Friday */ return (boolean) (lt->tm_wday == 5 && lt->tm_mday == 13); } diff --git a/src/restore.c b/src/restore.c index b6126a227..7d1f6d894 100644 --- a/src/restore.c +++ b/src/restore.c @@ -1231,6 +1231,8 @@ register int fd; panic("restore_msghistory: msg too big (%d)", msgsize); mread(fd, (genericptr_t) msg, msgsize); msg[msgsize] = '\0'; + if(fuzzer_msg_history(msg)) + continue; putmsghistory(msg, TRUE); ++msgcount; } diff --git a/src/rnd.c b/src/rnd.c index 0ca649fbb..5473be949 100644 --- a/src/rnd.c +++ b/src/rnd.c @@ -56,10 +56,21 @@ int FDECL((*fn), (int)); (int) sizeof seed); } +unsigned long +rul() +{ + unsigned long value; + + value = (unsigned long) isaac64_next_uint64(&rnglist[CORE].rng_state); + fuzzer_log(LOG_VERBOSE, "RANDOM:%llu\n", value); + + return value; +} + static int RND(int x) { - return (isaac64_next_uint64(&rnglist[CORE].rng_state) % x); + return (rul() % x); } /* 0 <= rn2(x) < x, but on a different sequence from the "main" rn2; @@ -69,6 +80,8 @@ int rn2_on_display_rng(x) register int x; { + if (iflags.debug_fuzzer) + return rn2(x); return (isaac64_next_uint64(&rnglist[DISP].rng_state) % x); } @@ -94,6 +107,20 @@ register int x; seed *= 2739110765; return (int)((seed >> 16) % (unsigned)x); } + +unsigned long +rul() +{ +#if defined(LINT) && defined(UNIX) + return (unsigned long) rand(); +#else /* LINT */ +#if defined(UNIX) || defined(RANDOM) + return (unsigned long) Rand(); +#else + return (unsigned long) (Rand() >> 3); +#endif /* defined(UNIX) || defined(RANDOM) */ +#endif /* LINT */ +} #endif /* USE_ISAAC64 */ /* 0 <= rn2(x) < x */ diff --git a/src/save.c b/src/save.c index e0af4b8d2..e931eae76 100644 --- a/src/save.c +++ b/src/save.c @@ -139,7 +139,7 @@ dosave0() return 0; #endif - HUP if (iflags.window_inited) { + HUP if (!iflags.debug_fuzzer && iflags.window_inited) { nh_uncompress(fq_save); fd = open_savefile(); if (fd > 0) { @@ -1238,6 +1238,18 @@ int fd, mode; bwrite(fd, (genericptr_t) msg, msglen); ++msgcount; } + /* If the fuzzer is stopping and saving, save a seed as a message. + In 3.7, we will modify the save file format and save the seed + directly in the saved game state. */ + if (iflags.fuzzer_saving) { + char message[BUFSIZ]; + unsigned long seed = rul(); + sprintf(message, "SEED:%ld:%lu", moves, seed); + fuzzer_log(LOG_MINIMAL, "STOP:%ld:%lu\n", moves, seed); + msglen = strlen(message); + bwrite(fd, (genericptr_t) &msglen, sizeof msglen); + bwrite(fd, (genericptr_t) message, msglen); + } bwrite(fd, (genericptr_t) &minusone, sizeof (int)); } debugpline1("Stored %d messages into savefile.", msgcount); diff --git a/sys/winnt/nttty.c b/sys/winnt/nttty.c index a8000bc05..4cca4dcbd 100644 --- a/sys/winnt/nttty.c +++ b/sys/winnt/nttty.c @@ -475,8 +475,14 @@ int *x, *y, *mod; coord cc; DWORD count; really_move_cursor(); - if (iflags.debug_fuzzer) - return randomkey(); + if (iflags.debug_fuzzer) { + int poskey = randomkey(); + if (poskey == 0) { + *x = rn2(console.width); + *y = rn2(console.height); + } + return poskey; + } ch = (program_state.done_hup) ? '\033' : keyboard_handler.pCheckInput( diff --git a/sys/winnt/windmain.c b/sys/winnt/windmain.c index b2bb18e79..d1712ffe4 100644 --- a/sys/winnt/windmain.c +++ b/sys/winnt/windmain.c @@ -336,7 +336,7 @@ attempt_restore: resuming = TRUE; /* not starting new game */ if (discover) You("are in non-scoring discovery mode."); - if (discover || wizard) { + if ((discover || wizard) && !iflags.fuzzer_auto_start) { if (yn("Do you want to keep the save file?") == 'n') (void) delete_savefile(); else { @@ -461,6 +461,23 @@ char *argv[]; case 'X': discover = TRUE, wizard = FALSE; break; + case 'F': + { + iflags.fuzzer_auto_start = 1; + + if (argc > 1 && argv[1][0] != '-') { + argc--; + argv++; + iflags.fuzzer_stop_and_save = atoi(*argv); + + if (argc > 1 && argv[1][0] != '-') { + argc--; + argv++; + iflags.verbose_logging_start = atoi(*argv); + } + } + } + break; #ifdef NEWS case 'n': iflags.news = FALSE; diff --git a/win/win32/scripts/fuzzer/clean.bat b/win/win32/scripts/fuzzer/clean.bat new file mode 100644 index 000000000..a02c48739 --- /dev/null +++ b/win/win32/scripts/fuzzer/clean.bat @@ -0,0 +1,8 @@ +set BIN_DIR=..\..\..\..\bin\Debug\Win32 + +set FUZZER_LOG=%BIN_DIR%\fuzzer.log +set FUZZER_DIR=%BIN_DIR%\fuzzer + +if exist %BIN_DIR%\%USERNAME%* del %BIN_DIR%\%USERNAME%* +if exist %FUZZER_LOG% del %FUZZER_LOG% + diff --git a/win/win32/scripts/fuzzer/longtest.bat b/win/win32/scripts/fuzzer/longtest.bat new file mode 100644 index 000000000..e5e8d1529 --- /dev/null +++ b/win/win32/scripts/fuzzer/longtest.bat @@ -0,0 +1,23 @@ +echo off + +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION + +set STEP_SIZE=5000 +set FINAL_MOVE=500000 +set START_MOVE=5000 + +for /L %%i in (%START_MOVE%, %STEP_SIZE%, %FINAL_MOVE%) do ( + + call runtill.bat %%i + + if ERRORLEVEL 1 ( + echo FAILED getting running to %%i. + exit /b 1 + ) + +) + +echo SUCCESS. + + diff --git a/win/win32/scripts/fuzzer/restore.bat b/win/win32/scripts/fuzzer/restore.bat new file mode 100644 index 000000000..9752fed96 --- /dev/null +++ b/win/win32/scripts/fuzzer/restore.bat @@ -0,0 +1,7 @@ +call clean.bat + +set BIN_DIR=..\..\..\..\bin\Debug\Win32 +set SAVED_GAME=%USERNAME%-wizard.NetHack-saved-game +set FUZZER_DIR=%BIN_DIR%\fuzzer + +copy %FUZZER_DIR%\%SAVED_GAME% %BIN_DIR%\%SAVED_GAME% diff --git a/win/win32/scripts/fuzzer/runtill.bat b/win/win32/scripts/fuzzer/runtill.bat new file mode 100644 index 000000000..ebb5aa4b6 --- /dev/null +++ b/win/win32/scripts/fuzzer/runtill.bat @@ -0,0 +1,73 @@ +REM +REM runtill target_move +REM +echo off + +SETLOCAL ENABLEEXTENSIONS +SETLOCAL ENABLEDELAYEDEXPANSION + +set TARGET_MOVE=%1 + +if %TARGET_MOVE% == "" ( + echo Usage:runtill target_move + goto :eof +) + +set BIN_DIR=..\..\..\..\bin\Debug\Win32 +set SAVED_GAME=%USERNAME%-wizard.NetHack-saved-game +set LOG_FILE=%BIN_DIR%\runtil.log +set FUZZER_LOG=%BIN_DIR%\fuzzer.log +set FUZZER_DIR=%BIN_DIR%\fuzzer +set BASELINE=%FUZZER_DIR%\fuzzer.log + +if not exist %FUZZER_DIR% mkdir %FUZZER_DIR% + +call clean.bat + +if not exist %FUZZER_DIR%\%SAVED_GAME% ( + %BIN_DIR%\nethack -D -F 0 + + copy %BIN_DIR%\%SAVED_GAME% %FUZZER_DIR% +) + +call restore.bat + +%BIN_DIR%\nethack -D -F %TARGET_MOVE% + +move %BIN_DIR%\*.snap %BIN_DIR%\snapshots +copy %FUZZER_LOG% %BASELINE% + +for /f "tokens=2,3 delims=: usebackq" %%i in (`findstr /c:START %BASELINE%`) do ( + set START_SEED=%%j + set START_MOVE=%%i +) + +for /f "tokens=2,3 delims=: usebackq" %%i in (`findstr /c:STOP %BASELINE%`) do ( + set STOP_SEED=%%j + set STOP_MOVE=%%i +) + +if !STOP_MOVE! LSS %TARGET_MOVE% ( + cls + echo FAILED: Failed to reach target move. !STOP_MOVE! is not GTE %TARGET_MOVE%. + exit /b 1 +) + +call restore.bat + +%BIN_DIR%\nethack -D -F %TARGET_MOVE% + +fc %FUZZER_LOG% %BASELINE% + +if ERRORLEVEL 1 ( + cls + echo FAILED: Unable to reproduce same timeline + exit /b 1 +) + +del /q %FUZZER_DIR%\%SAVED_GAME% + +copy %BIN_DIR%\%SAVED_GAME% %FUZZER_DIR% + +echo !START_MOVE! to !STOP_MOVE!. +echo SUCCESS.