#include "engine/demomode.h" #include #include #include #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #include "controls/plrctrls.h" #include "engine/events.hpp" #include "gmenu.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 { // #define LOG_DEMOMODE_MESSAGES // #define LOG_DEMOMODE_MESSAGES_MOUSEMOTION // #define LOG_DEMOMODE_MESSAGES_RENDERING // #define LOG_DEMOMODE_MESSAGES_GAMETICK namespace { enum class DemoMsgType : uint8_t { 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 { uint32_t sym; uint16_t mod; }; struct DemoMsg { DemoMsgType type; uint8_t progressToNextGameTick; uint32_t 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; #if SDL_VERSION_ATLEAST(2, 0, 0) bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { event.type = dmsg.eventType; switch (static_cast(dmsg.eventType)) { case SDL_MOUSEMOTION: event.motion.x = dmsg.motion.x; event.motion.y = dmsg.motion.y; return true; 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; return true; case SDL_MOUSEWHEEL: event.wheel.x = dmsg.wheel.x; event.wheel.y = dmsg.wheel.y; modState = dmsg.wheel.mod; return true; 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; return true; default: if (dmsg.eventType >= SDL_USEREVENT) { event.type = CustomEventToSdlEvent(static_cast(dmsg.eventType - SDL_USEREVENT)); return true; } event.type = static_cast(0); LogWarn("Unsupported demo event (type={:x})", dmsg.eventType); return false; } } #else SDLKey Sdl2ToSdl1Key(uint32_t key) { if ((key & (1 << 30)) != 0) { constexpr uint32_t Keys1Start = 57; constexpr SDLKey Keys1[] { SDLK_CAPSLOCK, SDLK_F1, SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5, SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, SDLK_F10, SDLK_F11, SDLK_F12, SDLK_PRINTSCREEN, SDLK_SCROLLLOCK, SDLK_PAUSE, SDLK_INSERT, SDLK_HOME, SDLK_PAGEUP, SDLK_DELETE, SDLK_END, SDLK_PAGEDOWN, SDLK_RIGHT, SDLK_LEFT, SDLK_DOWN, SDLK_UP, SDLK_NUMLOCKCLEAR, SDLK_KP_DIVIDE, SDLK_KP_MULTIPLY, SDLK_KP_MINUS, SDLK_KP_PLUS, SDLK_KP_ENTER, SDLK_KP_1, SDLK_KP_2, SDLK_KP_3, SDLK_KP_4, SDLK_KP_5, SDLK_KP_6, SDLK_KP_7, SDLK_KP_8, SDLK_KP_9, SDLK_KP_0, SDLK_KP_PERIOD }; constexpr uint32_t Keys2Start = 224; constexpr SDLKey Keys2[] { SDLK_LCTRL, SDLK_LSHIFT, SDLK_LALT, SDLK_LGUI, SDLK_RCTRL, SDLK_RSHIFT, SDLK_RALT, SDLK_RGUI, SDLK_MODE }; const uint32_t scancode = key & ~(1 << 30); if (scancode >= Keys1Start) { if (scancode < Keys1Start + sizeof(Keys1) / sizeof(Keys1[0])) return Keys1[scancode - Keys1Start]; if (scancode >= Keys2Start && scancode < Keys2Start + sizeof(Keys2) / sizeof(Keys2[0])) return Keys2[scancode - Keys2Start]; } LogWarn("Demo: unknown key {:d}", key); return SDLK_UNKNOWN; } if (key <= 122) { return static_cast(key); } LogWarn("Demo: unknown key {:d}", key); return SDLK_UNKNOWN; } uint8_t Sdl2ToSdl1MouseButton(uint8_t button) { switch (button) { case 4: return SDL_BUTTON_X1; case 5: return SDL_BUTTON_X2; default: return button; } } bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { switch (dmsg.eventType) { case 0x400: event.type = SDL_MOUSEMOTION; event.motion.x = dmsg.motion.x; event.motion.y = dmsg.motion.y; return true; case 0x401: case 0x402: event.type = dmsg.eventType == 0x401 ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.which = 0; event.button.button = Sdl2ToSdl1MouseButton(dmsg.button.button); event.button.state = dmsg.eventType == 0x401 ? SDL_PRESSED : SDL_RELEASED; event.button.x = dmsg.button.x; event.button.y = dmsg.button.y; modState = dmsg.button.mod; return true; case 0x403: // SDL_MOUSEWHEEL if (dmsg.wheel.y == 0) { LogWarn("Demo: unsupported event (mouse wheel y == 0)"); return false; } event.type = SDL_MOUSEBUTTONDOWN; event.button.button = dmsg.wheel.y > 0 ? SDL_BUTTON_WHEELUP : SDL_BUTTON_WHEELDOWN; modState = dmsg.wheel.mod; return true; case 0x300: case 0x301: event.type = dmsg.eventType == 0x300 ? SDL_KEYDOWN : SDL_KEYUP; event.key.which = 0; event.key.state = dmsg.eventType == 0x300 ? SDL_PRESSED : SDL_RELEASED; event.key.keysym.sym = Sdl2ToSdl1Key(dmsg.key.sym); event.key.keysym.mod = static_cast(dmsg.key.mod); return true; default: if (dmsg.eventType >= 0x8000) { event.type = CustomEventToSdlEvent(static_cast(dmsg.eventType - 0x8000)); return true; } event.type = static_cast(0); LogWarn("Demo: unsupported event (type={:x})", dmsg.eventType); return false; } } #endif void LogDemoMessage(const DemoMsg &msg) { #ifdef LOG_DEMOMODE_MESSAGES const uint8_t progressToNextGameTick = msg.progressToNextGameTick; switch (msg.type) { case DemoMsgType::Message: { const uint32_t eventType = msg.eventType; switch (eventType) { case 0x400: // SDL_MOUSEMOTION #ifdef LOG_DEMOMODE_MESSAGES_MOUSEMOTION Log("🖱️ Message {:>3} MOUSEMOTION {} {}", progressToNextGameTick, msg.motion.x, msg.motion.y); #endif break; case 0x401: // SDL_MOUSEBUTTONDOWN case 0x402: // SDL_MOUSEBUTTONUP Log("🖱️ Message {:>3} {} {} {} {} 0x{:x}", progressToNextGameTick, eventType == 0x401 ? "MOUSEBUTTONDOWN" : "MOUSEBUTTONUP", msg.button.button, msg.button.x, msg.button.y, msg.button.mod); break; case 0x403: // SDL_MOUSEWHEEL Log("🖱️ Message {:>3} MOUSEWHEEL {} {} 0x{:x}", progressToNextGameTick, msg.wheel.x, msg.wheel.y, msg.wheel.mod); break; case 0x300: // SDL_KEYDOWN case 0x301: // SDL_KEYUP Log("🔤 Message {:>3} {} 0x{:x} 0x{:x}", progressToNextGameTick, eventType == 0x300 ? "KEYDOWN" : "KEYUP", msg.key.sym, msg.key.mod); break; case 0x100: // SDL_QUIT Log("❎ Message {:>3} QUIT", progressToNextGameTick); break; default: Log("📨 Message {:>3} USEREVENT 0x{:x}", progressToNextGameTick, eventType); break; } } break; case DemoMsgType::GameTick: #ifdef LOG_DEMOMODE_MESSAGES_GAMETICK Log("⏲️ GameTick {:>3}", progressToNextGameTick); #endif break; case DemoMsgType::Rendering: #ifdef LOG_DEMOMODE_MESSAGES_RENDERING Log("🖼️ Rendering {:>3}", progressToNextGameTick); #endif break; default: LogError("INVALID DEMO MODE MESSAGE {} {:>3}", static_cast(msg.type), progressToNextGameTick); break; } #endif // LOG_DEMOMODE_MESSAGES } 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 (true) { const uint32_t typeNum = ReadLE32(demofile); if (demofile.eof()) break; const auto type = static_cast(typeNum); const uint8_t progressToNextGameTick = ReadByte(demofile); switch (type) { case DemoMsgType::Message: { const uint32_t eventType = ReadLE32(demofile); DemoMsg msg { type, progressToNextGameTick, eventType, {} }; switch (eventType) { case 0x400: // SDL_MOUSEMOTION msg.motion.x = ReadLE16(demofile); msg.motion.y = ReadLE16(demofile); break; case 0x401: // SDL_MOUSEBUTTONDOWN case 0x402: // SDL_MOUSEBUTTONUP msg.button.button = ReadByte(demofile); msg.button.x = ReadLE16(demofile); msg.button.y = ReadLE16(demofile); msg.button.mod = ReadLE16(demofile); break; case 0x403: // SDL_MOUSEWHEEL msg.wheel.x = ReadLE32(demofile); msg.wheel.y = ReadLE32(demofile); msg.wheel.mod = ReadLE16(demofile); break; case 0x300: // SDL_KEYDOWN case 0x301: // SDL_KEYUP msg.key.sym = static_cast(ReadLE32(demofile)); msg.key.mod = static_cast(ReadLE16(demofile)); break; case 0x100: // SDL_QUIT break; default: if (eventType < 0x8000) { // SDL_USEREVENT app_fatal(StrCat("Unknown event ", eventType)); } break; } Demo_Message_Queue.push_back(msg); break; } default: Demo_Message_Queue.push_back(DemoMsg { type, progressToNextGameTick, 0, {} }); break; } } demofile.close(); DemoModeLastTick = SDL_GetTicks(); return true; } void RecordEventHeader(const SDL_Event &event) { WriteLE32(DemoRecording, static_cast(DemoMsgType::Message)); WriteByte(DemoRecording, ProgressToNextGameTick); 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); } forceResolution = Size(DemoGraphicsWidth, DemoGraphicsHeight); } 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(); LogDemoMessage(dmsg); 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 { int32_t fraction = ticksElapsed * AnimationInfo::baseValueFraction / gnTickDelay; fraction = clamp(fraction, 0, AnimationInfo::baseValueFraction); uint8_t progressToNextGameTick = static_cast(fraction); if (dmsg.type == DemoMsgType::GameTick || dmsg.progressToNextGameTick > progressToNextGameTick) { // we are ahead of the replay => add a additional rendering for smoothness if (gbRunGame && PauseMode == 0 && (gbIsMultiplayer || !gmenu_is_active()) && gbProcessPlayers) // if game is not running or paused there is no next gametick in the near future ProgressToNextGameTick = progressToNextGameTick; processInput = false; drawGame = true; return false; } } } ProgressToNextGameTick = 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) { if (CurrentEventHandler == DisableInputEventHandler) return false; 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(); LogDemoMessage(dmsg); if (dmsg.type == DemoMsgType::Message) { const bool hasEvent = CreateSdlEvent(dmsg, *event, *modState); ProgressToNextGameTick = dmsg.progressToNextGameTick; Demo_Message_Queue.pop_front(); return hasEvent; } } return false; } void RecordGameLoopResult(bool runGameLoop) { WriteLE32(DemoRecording, static_cast(runGameLoop ? DemoMsgType::GameTick : DemoMsgType::Rendering)); WriteByte(DemoRecording, ProgressToNextGameTick); } void RecordMessage(const SDL_Event &event, uint16_t modState) { if (!gbRunGame || !DemoRecording.is_open()) return; if (CurrentEventHandler == DisableInputEventHandler) 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 comparison cause reference is not present."); break; case HeroCompareResult::Same: SDL_Log("Timedemo: Same outcome as initial run. :)"); break; case HeroCompareResult::Difference: Log("Timedemo: Different outcome than initial run. ;(\n{}", compareResult.message); break; } } } } // namespace demo } // namespace devilution