From eda4a5061eaf723bc66ce9f4427412730a38c197 Mon Sep 17 00:00:00 2001 From: Anders Jenbo Date: Tue, 25 May 2021 23:26:39 +0200 Subject: [PATCH] Implement demo recording and playback This records all keyboard and mouse events to a file and lets you play it back at a later point with a differen game speed. --- Source/diablo.cpp | 35 ++++++++- Source/diablo.h | 3 + Source/dx.cpp | 2 +- Source/menu.cpp | 17 +++- Source/miniwin/miniwin.h | 19 ++++- Source/miniwin/misc_msg.cpp | 150 +++++++++++++++++++++++++++++++++++- Source/movie.cpp | 3 + Source/nthread.cpp | 2 +- Source/nthread.h | 1 + Source/options.cpp | 6 +- Source/utils/display.cpp | 4 +- 11 files changed, 229 insertions(+), 13 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 00356fd5c..32a2445c0 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -97,6 +97,9 @@ std::array quickSpellActionIndexes; bool gbForceWindowed = false; bool leveldebug = false; +int recordDemo = -1; +bool demoMode = false; +bool timedemo = false; #ifdef _DEBUG bool monstdebug = false; _monster_id DebugMonsters[10]; @@ -782,6 +785,7 @@ void RunGameLoop(interface_mode uMsg) { WNDPROC saveProc; tagMSG msg; + int startTime; nthread_ignore_mutex(true); StartGame(uMsg); @@ -802,6 +806,12 @@ void RunGameLoop(interface_mode uMsg) #ifdef GPERF_HEAP_FIRST_GAME_ITERATION unsigned run_game_iteration = 0; #endif + + int logicTick = 0; + + if (timedemo) + startTime = SDL_GetTicks(); + while (gbRunGame) { while (FetchMessage(&msg)) { if (msg.message == DVL_WM_QUIT) { @@ -825,12 +835,20 @@ void RunGameLoop(interface_mode uMsg) game_loop(gbGameLoopStartup); gbGameLoopStartup = false; DrawAndBlit(); + logicTick++; #ifdef GPERF_HEAP_FIRST_GAME_ITERATION if (run_game_iteration++ == 0) HeapProfilerDump("first_game_iteration"); #endif } + if (timedemo) { + float secounds = (SDL_GetTicks() - startTime) / 1000.0; + SDL_Log("%d frames, %.2f seconds: %.1f fps", logicTick, secounds, logicTick / secounds); + gbRunGameResult = false; + gbRunGame = false; + } + if (gbIsMultiplayer) { pfile_write_hero(/*writeGameData=*/false, /*clearTables=*/true); } @@ -865,6 +883,9 @@ void RunGameLoop(interface_mode uMsg) printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "-x", _("Run in windowed mode")); printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--verbose", _("Enable verbose logging")); printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--spawn", _("Force spawn mode even if diabdat.mpq is found")); + printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--record <#>", _("Record a demo file")); + printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--demo <#>", _("Play a demo file")); + printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--timedemo", _("Disable all frame limiting during demo playback")); printInConsole("%s", _(/* TRANSLATORS: Commandline Option */ "\nHellfire options:\n")); printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--diablo", _("Force diablo mode even if hellfire.mpq is found")); printInConsole(" %-20s %-30s\n", /* TRANSLATORS: Commandline Option */ "--nestart", _("Use alternate nest palette")); @@ -898,6 +919,16 @@ void DiabloParseFlags(int argc, char **argv) paths::SetBasePath(argv[++i]); } else if (strcasecmp("--save-dir", argv[i]) == 0) { paths::SetPrefPath(argv[++i]); + } else if (strcasecmp("--demo", argv[i]) == 0) { + demoMode = true; + if (!LoadDemoMessages(SDL_atoi(argv[++i]))) { + SDL_Log("Unable to load demo file"); + diablo_quit(1); + } + } else if (strcasecmp("--timedemo", argv[i]) == 0) { + timedemo = true; + } else if (strcasecmp("--record", argv[i]) == 0) { + recordDemo = SDL_atoi(argv[++i]); } else if (strcasecmp("--config-dir", argv[i]) == 0) { paths::SetConfigPath(argv[++i]); } else if (strcasecmp("--lang-dir", argv[i]) == 0) { @@ -1052,8 +1083,10 @@ void DiabloDeinit() { FreeItemGFX(); - if (sbWasOptionsLoaded) + if (sbWasOptionsLoaded && !demoMode) SaveOptions(); + if (demoRecording.is_open()) + demoRecording.close(); if (was_snd_init) effects_cleanup_sfx(); #ifndef NOSOUND diff --git a/Source/diablo.h b/Source/diablo.h index 734ec8891..565df5486 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -103,6 +103,9 @@ void diablo_color_cyc_logic(); extern Keymapper keymapper; extern bool gbForceWindowed; extern bool leveldebug; +extern int recordDemo; +extern bool demoMode; +extern bool timedemo; #ifdef _DEBUG extern bool monstdebug; extern _monster_id DebugMonsters[10]; diff --git a/Source/dx.cpp b/Source/dx.cpp index c6275fd58..1ad5a6a02 100644 --- a/Source/dx.cpp +++ b/Source/dx.cpp @@ -333,7 +333,7 @@ void RenderPresent() } SDL_RenderPresent(renderer); - if (!sgOptions.Graphics.bVSync) { + if (!sgOptions.Graphics.bVSync && !timedemo) { LimitFrameRate(); } } else { diff --git a/Source/menu.cpp b/Source/menu.cpp index 3ca61e827..f0a78a2b8 100644 --- a/Source/menu.cpp +++ b/Source/menu.cpp @@ -76,12 +76,20 @@ void PlayIntro() RefreshMusic(); } +bool Dummy_GetHeroInfo(_uiheroinfo *pInfo) +{ + return true; +} + } // namespace bool mainmenu_select_hero_dialog(GameData *gameData) { _selhero_selections dlgresult = SELHERO_NEW_DUNGEON; - if (!gbIsMultiplayer) { + if (demoMode) { + pfile_ui_set_hero_infos(Dummy_GetHeroInfo); + gbLoadGame = true; + } else if (!gbIsMultiplayer) { UiSelHeroSingDialog( pfile_ui_set_hero_infos, pfile_ui_save_create, @@ -108,6 +116,9 @@ bool mainmenu_select_hero_dialog(GameData *gameData) pfile_read_player_from_save(gSaveNumber, MyPlayerId); + if (recordDemo != -1) + CreateDemoFile(recordDemo); + return true; } @@ -121,7 +132,9 @@ void mainmenu_loop() do { menu = MAINMENU_NONE; - if (!UiMainMenuDialog(gszProductName, &menu, effects_play_sound, 30)) + if (demoMode) + menu = MAINMENU_SINGLE_PLAYER; + else if (!UiMainMenuDialog(gszProductName, &menu, effects_play_sound, 30)) app_fatal("%s", _("Unable to display mainmenu")); switch (menu) { diff --git a/Source/miniwin/miniwin.h b/Source/miniwin/miniwin.h index 223e483c9..0b262ad05 100644 --- a/Source/miniwin/miniwin.h +++ b/Source/miniwin/miniwin.h @@ -1,7 +1,6 @@ #pragma once #include - #include #include #include @@ -9,6 +8,8 @@ #include #include #include +#include +#include namespace devilution { @@ -32,6 +33,20 @@ struct tagMSG { int32_t lParam; }; +enum class DemoMsgType { + Rendering = 1, + Message = 2, +}; + +struct demoMsg { + DemoMsgType type; + uint32_t message; + int32_t wParam; + int32_t lParam; +}; + +extern std::ofstream demoRecording; + // // Everything else // @@ -41,6 +56,8 @@ void FocusOnCharInfo(); bool GetAsyncKeyState(int vKey); +void CreateDemoFile(int i); +bool LoadDemoMessages(int i); bool FetchMessage(tagMSG *lpMsg); bool TranslateMessage(const tagMSG *lpMsg); diff --git a/Source/miniwin/misc_msg.cpp b/Source/miniwin/misc_msg.cpp index 3d5016ea3..1e8f6d8cc 100644 --- a/Source/miniwin/misc_msg.cpp +++ b/Source/miniwin/misc_msg.cpp @@ -1,9 +1,12 @@ #include #include #include +#include +#include #include "control.h" #include "controls/controller.h" +#include "utils/paths.h" #include "controls/controller_motion.h" #include "controls/game_controls.h" #include "controls/plrctrls.h" @@ -19,6 +22,9 @@ #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/stubs.h" +#include "menu.h" +#include "nthread.h" +#include "storm/storm.h" #ifdef __SWITCH__ #include "platform/switch/docking.h" @@ -297,7 +303,7 @@ bool BlurInventory() return true; } -bool FetchMessage(tagMSG *lpMsg) +bool FetchMessage_Real(tagMSG *lpMsg) { #ifdef __SWITCH__ HandleDocking(); @@ -468,7 +474,7 @@ bool FetchMessage(tagMSG *lpMsg) if (key == -1) return FalseAvail(e.type == SDL_KEYDOWN ? "SDL_KEYDOWN" : "SDL_KEYUP", e.key.keysym.sym); lpMsg->message = e.type == SDL_KEYDOWN ? DVL_WM_KEYDOWN : DVL_WM_KEYUP; - lpMsg->wParam = (DWORD)key; + lpMsg->wParam = (uint32_t)key; // HACK: Encode modifier in lParam for TranslateMessage later lpMsg->lParam = e.key.keysym.mod << 16; } break; @@ -585,11 +591,149 @@ bool FetchMessage(tagMSG *lpMsg) return true; } +std::ofstream demoRecording; +static std::deque demo_message_queue; + +void CreateDemoFile(int i) +{ + char demoFilename[16]; + snprintf(demoFilename, 15, "demo_%d.dmo", i); + demoRecording.open(paths::PrefPath() + demoFilename, std::fstream::trunc); + + demoRecording << "0," << gSaveNumber << "," << gnScreenWidth << "," << gnScreenHeight << "\n"; +} + +void SaveDemoMessage(tagMSG *lpMsg) +{ + demoRecording << tick << ",0," << lpMsg->message << "," << lpMsg->wParam << "," << lpMsg->lParam << "\n"; +} + +void PumpDemoMessage(int tick, uint32_t message, int32_t wParam, int32_t lParam) +{ + demoMsg msg; + msg.type = demoMsgType; + msg.message = message; + msg.wParam = wParam; + msg.lParam = lParam; + + demo_message_queue.push_back(msg); +} + +bool LoadDemoMessages(int i) +{ + std::ifstream demofile; + char demoFilename[16]; + snprintf(demoFilename, 15, "demo_%d.dmo", i); + demofile.open(paths::PrefPath() + demoFilename); + if (!demofile.is_open()) { + return false; + } + + std::string line, number; + + std::getline(demofile, line); + std::stringstream header(line); + + std::getline(header, number, ','); // Demo version + if (std::stoi(number) != 0) { + return false; + } + + std::getline(header, number, ','); + gSaveNumber = std::stoi(number); + + std::getline(header, number, ','); + uint32_t width = std::stoi(number); + sgOptions.Graphics.nWidth = width; + + std::getline(header, number, ','); + uint32_t height = std::stoi(number); + sgOptions.Graphics.nHeight = height; + + while (std::getline(demofile, line)) { + std::stringstream command(line); + + std::getline(command, number, ','); + int tick = std::stoi(number); + + std::getline(command, number, ','); + int type = std::stoi(number); + + switch (type) { + case 0: + std::getline(command, number, ','); + uint32_t message = std::stoi(number); + std::getline(command, number, ','); + int32_t wParam = std::stoi(number); + std::getline(command, number, ','); + int32_t lParam = std::stoi(number); + PumpDemoMessage(tick, message, wParam, lParam); + break; + } + } + + demofile.close(); + + return true; +} + +bool DemoMessage(tagMSG *lpMsg) +{ + SDL_Event e; + if (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) { + lpMsg->message = DVL_WM_QUIT; + lpMsg->lParam = 0; + lpMsg->wParam = 0; + return true; + } + if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) { + demo_message_queue.clear(); + message_queue.clear(); + demoMode = false; + timedemo = false; + last_tick = SDL_GetTicks(); + } + } + + if (!demo_message_queue.empty()) { + demoMsg dmsg = demo_message_queue.front(); + if (dmsg.type == DemoMsgType::Message) { + lpMsg->message = dmsg.message; + lpMsg->lParam = dmsg.lParam; + lpMsg->wParam = dmsg.wParam; + demo_message_queue.pop_front(); + return true; + } + } + + lpMsg->message = 0; + lpMsg->lParam = 0; + lpMsg->wParam = 0; + + return false; +} + +bool FetchMessage(tagMSG *lpMsg, int tick) +{ + bool available; + + if (!demoMode) + available = FetchMessage_Real(lpMsg); + else + available = DemoMessage(lpMsg, tick); + + if (recordDemo != -1 && available && tick > -1) + SaveDemoMessage(lpMsg, tick); + + return available; +} + bool TranslateMessage(const tagMSG *lpMsg) { if (lpMsg->message == DVL_WM_KEYDOWN) { int key = lpMsg->wParam; - unsigned mod = (DWORD)lpMsg->lParam >> 16; + unsigned mod = (uint32_t)lpMsg->lParam >> 16; bool shift = (mod & KMOD_SHIFT) != 0; bool caps = (mod & KMOD_CAPS) != 0; diff --git a/Source/movie.cpp b/Source/movie.cpp index 048e01dd6..58a572edd 100644 --- a/Source/movie.cpp +++ b/Source/movie.cpp @@ -28,6 +28,9 @@ bool loop_movie; */ void play_movie(const char *pszMovie, bool userCanClose) { + if (timedemo) + return; + movie_playing = true; #ifndef NOSOUND diff --git a/Source/nthread.cpp b/Source/nthread.cpp index 17d8a978e..7a803ddf1 100644 --- a/Source/nthread.cpp +++ b/Source/nthread.cpp @@ -19,6 +19,7 @@ uint32_t gdwTurnsInTransit; uintptr_t glpMsgTbl[MAX_PLRS]; uint32_t gdwLargestMsgSize; uint32_t gdwNormalMsgSize; +int last_tick; float gfProgressToNextGameTick = 0.0; namespace { @@ -31,7 +32,6 @@ uint32_t turn_upper_bit; bool sgbTicsOutOfSync; char sgbPacketCountdown; bool sgbThreadIsRunning; -int last_tick; SdlThread Thread; void NthreadHandler() diff --git a/Source/nthread.h b/Source/nthread.h index 29a15bb22..507a2ce47 100644 --- a/Source/nthread.h +++ b/Source/nthread.h @@ -16,6 +16,7 @@ extern uintptr_t glpMsgTbl[MAX_PLRS]; extern uint32_t gdwLargestMsgSize; extern uint32_t gdwNormalMsgSize; extern float gfProgressToNextGameTick; // the progress as a fraction (0.0f to 1.0f) in time to the next game tick +extern int last_tick; void nthread_terminate_game(const char *pszFcn); uint32_t nthread_send_and_recv_turn(uint32_t curTurn, int turnDelta); diff --git a/Source/options.cpp b/Source/options.cpp index 072a2eeea..10201bf51 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -194,8 +194,10 @@ void LoadOptions() sgOptions.Audio.nBufferSize = GetIniInt("Audio", "Buffer Size", DEFAULT_AUDIO_BUFFER_SIZE); sgOptions.Audio.nResamplingQuality = GetIniInt("Audio", "Resampling Quality", DEFAULT_AUDIO_RESAMPLING_QUALITY); - sgOptions.Graphics.nWidth = GetIniInt("Graphics", "Width", DEFAULT_WIDTH); - sgOptions.Graphics.nHeight = GetIniInt("Graphics", "Height", DEFAULT_HEIGHT); + if (!demoMode) { + sgOptions.Graphics.nWidth = GetIniInt("Graphics", "Width", DEFAULT_WIDTH); + sgOptions.Graphics.nHeight = GetIniInt("Graphics", "Height", DEFAULT_HEIGHT); + } #ifndef __vita__ sgOptions.Graphics.bFullscreen = GetIniBool("Graphics", "Fullscreen", true); #else diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index 436d85dd4..295299fa5 100644 --- a/Source/utils/display.cpp +++ b/Source/utils/display.cpp @@ -172,7 +172,7 @@ bool SpawnWindow(const char *lpWindowName) int width = sgOptions.Graphics.nWidth; int height = sgOptions.Graphics.nHeight; - if (sgOptions.Graphics.bUpscale && sgOptions.Graphics.bFitToScreen) { + if (sgOptions.Graphics.bUpscale && sgOptions.Graphics.bFitToScreen && !demoMode) { CalculatePreferdWindowSize(width, height); } AdjustToScreenGeometry(width, height); @@ -220,7 +220,7 @@ bool SpawnWindow(const char *lpWindowName) #ifndef USE_SDL1 Uint32 rendererFlags = SDL_RENDERER_ACCELERATED; - if (sgOptions.Graphics.bVSync) { + if (sgOptions.Graphics.bVSync && !timedemo) { rendererFlags |= SDL_RENDERER_PRESENTVSYNC; }