#include "engine/demomode.h" #include #include #include #include #include "controls/plrctrls.h" #include "menu.h" #include "nthread.h" #include "options.h" #include "pfile.h" #include "utils/display.h" #include "utils/endian_stream.hpp" #include "utils/paths.h" #include "utils/str_cat.hpp" namespace devilution { namespace { enum class DemoMsgType { GameTick = 0, Rendering = 1, Message = 2, }; struct MouseMotionEventData { uint16_t x; uint16_t y; }; struct MouseButtonEventData { uint8_t button; uint16_t x; uint16_t y; uint16_t mod; }; struct MouseWheelEventData { int32_t x; int32_t y; uint16_t mod; }; struct KeyEventData { SDL_Keycode sym; SDL_Keymod mod; }; struct DemoMsg { DemoMsgType type; float progressToNextGameTick; SDL_EventType eventType; union { MouseMotionEventData motion; MouseButtonEventData button; MouseWheelEventData wheel; KeyEventData key; }; }; int DemoNumber = -1; bool Timedemo = false; int RecordNumber = -1; bool CreateDemoReference = false; std::ofstream DemoRecording; std::deque Demo_Message_Queue; uint32_t DemoModeLastTick = 0; int LogicTick = 0; int StartTime = 0; uint16_t DemoGraphicsWidth = 640; uint16_t DemoGraphicsHeight = 480; bool LoadDemoMessages(int i) { std::ifstream demofile; demofile.open(StrCat(paths::PrefPath(), "demo_", i, ".dmo"), std::fstream::binary); if (!demofile.is_open()) { return false; } const uint8_t version = ReadByte(demofile); if (version != 0) { return false; } gSaveNumber = ReadLE32(demofile); DemoGraphicsWidth = ReadLE16(demofile); DemoGraphicsHeight = ReadLE16(demofile); while (!demofile.eof()) { const uint32_t typeNum = ReadLE32(demofile); const auto type = static_cast(typeNum); const float progressToNextGameTick = ReadLEFloat(demofile); switch (type) { case DemoMsgType::Message: { const auto eventType = static_cast(ReadLE32(demofile)); DemoMsg msg { type, progressToNextGameTick, eventType, {} }; switch (eventType) { case SDL_MOUSEMOTION: msg.motion.x = ReadLE16(demofile); msg.motion.y = ReadLE16(demofile); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: msg.button.button = ReadByte(demofile); msg.button.x = ReadLE16(demofile); msg.button.y = ReadLE16(demofile); msg.button.mod = ReadLE16(demofile); break; #ifndef USE_SDL1 case SDL_MOUSEWHEEL: msg.wheel.x = ReadLE32(demofile); msg.wheel.y = ReadLE32(demofile); msg.wheel.mod = ReadLE16(demofile); break; #endif case SDL_KEYDOWN: case SDL_KEYUP: msg.key.sym = static_cast(ReadLE32(demofile)); msg.key.mod = static_cast(ReadLE16(demofile)); break; case SDL_QUIT: break; default: if (eventType < SDL_USEREVENT) { app_fatal(StrCat("Unknown event ", static_cast(eventType))); } break; } Demo_Message_Queue.push_back(msg); break; } default: Demo_Message_Queue.push_back(DemoMsg { type, progressToNextGameTick, static_cast(0), {} }); break; } } demofile.close(); DemoModeLastTick = SDL_GetTicks(); return true; } void RecordEventHeader(const SDL_Event &event) { WriteLE32(DemoRecording, static_cast(DemoMsgType::Message)); WriteLEFloat(DemoRecording, gfProgressToNextGameTick); WriteLE32(DemoRecording, event.type); } } // namespace namespace demo { void InitPlayBack(int demoNumber, bool timedemo) { DemoNumber = demoNumber; Timedemo = timedemo; ControlMode = ControlTypes::KeyboardAndMouse; if (!LoadDemoMessages(demoNumber)) { SDL_Log("Unable to load demo file"); diablo_quit(1); } } void InitRecording(int recordNumber, bool createDemoReference) { RecordNumber = recordNumber; CreateDemoReference = createDemoReference; } void OverrideOptions() { #ifndef USE_SDL1 sgOptions.Graphics.fitToScreen.SetValue(false); #endif #if SDL_VERSION_ATLEAST(2, 0, 0) sgOptions.Graphics.hardwareCursor.SetValue(false); #endif if (Timedemo) { #ifndef USE_SDL1 sgOptions.Graphics.vSync.SetValue(false); #endif sgOptions.Graphics.limitFPS.SetValue(false); } } bool IsRunning() { return DemoNumber != -1; } bool IsRecording() { return RecordNumber != -1; }; bool GetRunGameLoop(bool &drawGame, bool &processInput) { if (Demo_Message_Queue.empty()) app_fatal("Demo queue empty"); const DemoMsg dmsg = Demo_Message_Queue.front(); if (dmsg.type == DemoMsgType::Message) app_fatal("Unexpected Message"); if (Timedemo) { // disable additonal rendering to speedup replay drawGame = dmsg.type == DemoMsgType::GameTick && !HeadlessMode; } else { int currentTickCount = SDL_GetTicks(); int ticksElapsed = currentTickCount - DemoModeLastTick; bool tickDue = ticksElapsed >= gnTickDelay; drawGame = false; if (tickDue) { if (dmsg.type == DemoMsgType::GameTick) { DemoModeLastTick = currentTickCount; } } else { float progressToNextGameTick = clamp((float)ticksElapsed / (float)gnTickDelay, 0.F, 1.F); if (dmsg.type == DemoMsgType::GameTick || dmsg.progressToNextGameTick > progressToNextGameTick) { // we are ahead of the replay => add a additional rendering for smoothness gfProgressToNextGameTick = progressToNextGameTick; processInput = false; drawGame = true; return false; } } } gfProgressToNextGameTick = dmsg.progressToNextGameTick; Demo_Message_Queue.pop_front(); if (dmsg.type == DemoMsgType::GameTick) LogicTick++; return dmsg.type == DemoMsgType::GameTick; } bool FetchMessage(SDL_Event *event, uint16_t *modState) { SDL_Event e; if (SDL_PollEvent(&e) != 0) { if (e.type == SDL_QUIT) { *event = e; return true; } if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) { Demo_Message_Queue.clear(); DemoNumber = -1; Timedemo = false; last_tick = SDL_GetTicks(); } if (e.type == SDL_KEYDOWN && IsAnyOf(e.key.keysym.sym, SDLK_KP_PLUS, SDLK_PLUS) && sgGameInitInfo.nTickRate < 255) { sgGameInitInfo.nTickRate++; sgOptions.Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; } if (e.type == SDL_KEYDOWN && IsAnyOf(e.key.keysym.sym, SDLK_KP_MINUS, SDLK_MINUS) && sgGameInitInfo.nTickRate > 1) { sgGameInitInfo.nTickRate--; sgOptions.Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; } } if (!Demo_Message_Queue.empty()) { const DemoMsg dmsg = Demo_Message_Queue.front(); if (dmsg.type == DemoMsgType::Message) { event->type = dmsg.eventType; switch (dmsg.eventType) { case SDL_MOUSEMOTION: event->motion.x = dmsg.motion.x; event->motion.y = dmsg.motion.y; break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: event->button.button = dmsg.button.button; event->button.state = dmsg.eventType == SDL_MOUSEBUTTONDOWN ? SDL_PRESSED : SDL_RELEASED; event->button.x = dmsg.button.x; event->button.y = dmsg.button.y; *modState = dmsg.button.mod; break; #ifndef USE_SDL1 case SDL_MOUSEWHEEL: event->wheel.x = dmsg.wheel.x; event->wheel.y = dmsg.wheel.y; *modState = dmsg.wheel.mod; break; #endif case SDL_KEYDOWN: case SDL_KEYUP: event->key.state = dmsg.eventType == SDL_KEYDOWN ? SDL_PRESSED : SDL_RELEASED; event->key.keysym.sym = dmsg.key.sym; event->key.keysym.mod = dmsg.key.mod; break; default: if (dmsg.eventType >= SDL_USEREVENT) { event->type = CustomEventToSdlEvent(static_cast(dmsg.eventType - SDL_USEREVENT)); } break; } gfProgressToNextGameTick = dmsg.progressToNextGameTick; Demo_Message_Queue.pop_front(); return true; } } return false; } void RecordGameLoopResult(bool runGameLoop) { WriteLE32(DemoRecording, static_cast(runGameLoop ? DemoMsgType::GameTick : DemoMsgType::Rendering)); WriteLEFloat(DemoRecording, gfProgressToNextGameTick); } void RecordMessage(const SDL_Event &event, uint16_t modState) { if (!gbRunGame || !DemoRecording.is_open()) return; switch (event.type) { case SDL_MOUSEMOTION: RecordEventHeader(event); WriteLE16(DemoRecording, event.motion.x); WriteLE16(DemoRecording, event.motion.y); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: RecordEventHeader(event); WriteByte(DemoRecording, event.button.button); WriteLE16(DemoRecording, event.button.x); WriteLE16(DemoRecording, event.button.y); WriteLE16(DemoRecording, modState); break; #ifndef USE_SDL1 case SDL_MOUSEWHEEL: RecordEventHeader(event); WriteLE32(DemoRecording, event.wheel.x); WriteLE32(DemoRecording, event.wheel.y); WriteLE16(DemoRecording, modState); break; #endif case SDL_KEYDOWN: case SDL_KEYUP: RecordEventHeader(event); WriteLE32(DemoRecording, static_cast(event.key.keysym.sym)); WriteLE16(DemoRecording, static_cast(event.key.keysym.mod)); break; #ifndef USE_SDL1 case SDL_WINDOWEVENT: if (event.window.type == SDL_WINDOWEVENT_CLOSE) { SDL_Event quitEvent; quitEvent.type = SDL_QUIT; RecordEventHeader(quitEvent); } break; #endif case SDL_QUIT: RecordEventHeader(event); break; default: if (IsCustomEvent(event.type)) { SDL_Event stableCustomEvent; stableCustomEvent.type = SDL_USEREVENT + static_cast(GetCustomEvent(event.type)); RecordEventHeader(stableCustomEvent); } break; } } void NotifyGameLoopStart() { if (IsRecording()) { DemoRecording.open(StrCat(paths::PrefPath(), "demo_", RecordNumber, ".dmo"), std::fstream::trunc | std::fstream::binary); constexpr uint8_t Version = 0; WriteByte(DemoRecording, Version); WriteLE32(DemoRecording, gSaveNumber); WriteLE16(DemoRecording, gnScreenWidth); WriteLE16(DemoRecording, gnScreenHeight); } if (IsRunning()) { StartTime = SDL_GetTicks(); LogicTick = 0; } } void NotifyGameLoopEnd() { if (IsRecording()) { DemoRecording.close(); if (CreateDemoReference) pfile_write_hero_demo(RecordNumber); RecordNumber = -1; CreateDemoReference = false; } if (IsRunning() && !HeadlessMode) { float seconds = (SDL_GetTicks() - StartTime) / 1000.0f; SDL_Log("%d frames, %.2f seconds: %.1f fps", LogicTick, seconds, LogicTick / seconds); gbRunGameResult = false; gbRunGame = false; HeroCompareResult compareResult = pfile_compare_hero_demo(DemoNumber, false); switch (compareResult.status) { case HeroCompareResult::ReferenceNotFound: SDL_Log("Timedemo: No final comparision cause reference is not present."); break; case HeroCompareResult::Same: SDL_Log("Timedemo: Same outcome as inital run. :)"); break; case HeroCompareResult::Difference: Log("Timedemo: Different outcome then inital run. ;(\n{}", compareResult.message); break; } } } } // namespace demo } // namespace devilution