|
|
#include "engine/demomode.h" |
|
|
|
|
|
#include <cstdint> |
|
|
#include <cstdio> |
|
|
#include <deque> |
|
|
|
|
|
#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/console.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 LoadingStatus : uint8_t { |
|
|
Success, |
|
|
FileNotFound, |
|
|
UnsupportedVersion, |
|
|
}; |
|
|
|
|
|
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; |
|
|
|
|
|
// These options affect gameplay and are stored in the demo file. |
|
|
struct { |
|
|
uint8_t tickRate = 20; |
|
|
bool runInTown = false; |
|
|
bool theoQuest = false; |
|
|
bool cowQuest = false; |
|
|
bool autoGoldPickup = false; |
|
|
bool autoElixirPickup = false; |
|
|
bool autoOilPickup = false; |
|
|
bool autoPickupInTown = false; |
|
|
bool adriaRefillsMana = false; |
|
|
bool autoEquipWeapons = false; |
|
|
bool autoEquipArmor = false; |
|
|
bool autoEquipHelms = false; |
|
|
bool autoEquipShields = false; |
|
|
bool autoEquipJewelry = false; |
|
|
bool randomizeQuests = false; |
|
|
bool showItemLabels = false; |
|
|
bool autoRefillBelt = false; |
|
|
bool disableCripplingShrines = false; |
|
|
uint8_t numHealPotionPickup = 0; |
|
|
uint8_t numFullHealPotionPickup = 0; |
|
|
uint8_t numManaPotionPickup = 0; |
|
|
uint8_t numFullManaPotionPickup = 0; |
|
|
uint8_t numRejuPotionPickup = 0; |
|
|
uint8_t numFullRejuPotionPickup = 0; |
|
|
} DemoSettings; |
|
|
|
|
|
FILE *DemoRecording; |
|
|
std::deque<DemoMsg> Demo_Message_Queue; |
|
|
uint32_t DemoModeLastTick = 0; |
|
|
|
|
|
int LogicTick = 0; |
|
|
int StartTime = 0; |
|
|
|
|
|
uint16_t DemoGraphicsWidth = 640; |
|
|
uint16_t DemoGraphicsHeight = 480; |
|
|
|
|
|
void ReadSettings(FILE *in, uint8_t version) |
|
|
{ |
|
|
DemoGraphicsWidth = ReadLE16(in); |
|
|
DemoGraphicsHeight = ReadLE16(in); |
|
|
if (version > 0) { |
|
|
DemoSettings.runInTown = ReadByte(in) != 0; |
|
|
DemoSettings.theoQuest = ReadByte(in) != 0; |
|
|
DemoSettings.cowQuest = ReadByte(in) != 0; |
|
|
DemoSettings.autoGoldPickup = ReadByte(in) != 0; |
|
|
DemoSettings.autoElixirPickup = ReadByte(in) != 0; |
|
|
DemoSettings.autoOilPickup = ReadByte(in) != 0; |
|
|
DemoSettings.autoPickupInTown = ReadByte(in) != 0; |
|
|
DemoSettings.adriaRefillsMana = ReadByte(in) != 0; |
|
|
DemoSettings.autoEquipWeapons = ReadByte(in) != 0; |
|
|
DemoSettings.autoEquipArmor = ReadByte(in) != 0; |
|
|
DemoSettings.autoEquipHelms = ReadByte(in) != 0; |
|
|
DemoSettings.autoEquipShields = ReadByte(in) != 0; |
|
|
DemoSettings.autoEquipJewelry = ReadByte(in) != 0; |
|
|
DemoSettings.randomizeQuests = ReadByte(in) != 0; |
|
|
DemoSettings.showItemLabels = ReadByte(in) != 0; |
|
|
DemoSettings.autoRefillBelt = ReadByte(in) != 0; |
|
|
DemoSettings.disableCripplingShrines = ReadByte(in) != 0; |
|
|
DemoSettings.numHealPotionPickup = ReadByte(in); |
|
|
DemoSettings.numFullHealPotionPickup = ReadByte(in); |
|
|
DemoSettings.numManaPotionPickup = ReadByte(in); |
|
|
DemoSettings.numFullManaPotionPickup = ReadByte(in); |
|
|
DemoSettings.numRejuPotionPickup = ReadByte(in); |
|
|
DemoSettings.numFullRejuPotionPickup = ReadByte(in); |
|
|
} else { |
|
|
DemoSettings = {}; |
|
|
} |
|
|
} |
|
|
|
|
|
void WriteSettings(FILE *out) |
|
|
{ |
|
|
WriteLE16(out, gnScreenWidth); |
|
|
WriteLE16(out, gnScreenHeight); |
|
|
WriteByte(out, *sgOptions.Gameplay.runInTown); |
|
|
WriteByte(out, *sgOptions.Gameplay.theoQuest); |
|
|
WriteByte(out, *sgOptions.Gameplay.cowQuest); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoGoldPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoElixirPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoOilPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoPickupInTown); |
|
|
WriteByte(out, *sgOptions.Gameplay.adriaRefillsMana); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoEquipWeapons); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoEquipArmor); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoEquipHelms); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoEquipShields); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoEquipJewelry); |
|
|
WriteByte(out, *sgOptions.Gameplay.randomizeQuests); |
|
|
WriteByte(out, *sgOptions.Gameplay.showItemLabels); |
|
|
WriteByte(out, *sgOptions.Gameplay.autoRefillBelt); |
|
|
WriteByte(out, *sgOptions.Gameplay.disableCripplingShrines); |
|
|
WriteByte(out, *sgOptions.Gameplay.numHealPotionPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.numFullHealPotionPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.numManaPotionPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.numFullManaPotionPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.numRejuPotionPickup); |
|
|
WriteByte(out, *sgOptions.Gameplay.numFullRejuPotionPickup); |
|
|
} |
|
|
|
|
|
#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<SDL_EventType>(dmsg.eventType)) { |
|
|
case SDL_MOUSEMOTION: |
|
|
event.motion.which = 0; |
|
|
event.motion.x = dmsg.motion.x; |
|
|
event.motion.y = dmsg.motion.y; |
|
|
return true; |
|
|
case SDL_MOUSEBUTTONDOWN: |
|
|
case SDL_MOUSEBUTTONUP: |
|
|
event.button.which = 0; |
|
|
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.which = 0; |
|
|
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<interface_mode>(dmsg.eventType - SDL_USEREVENT)); |
|
|
return true; |
|
|
} |
|
|
event.type = static_cast<SDL_EventType>(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<SDLKey>(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.which = 0; |
|
|
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.which = 0; |
|
|
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<SDL_Keymod>(dmsg.key.mod); |
|
|
return true; |
|
|
default: |
|
|
if (dmsg.eventType >= 0x8000) { |
|
|
event.type = CustomEventToSdlEvent(static_cast<interface_mode>(dmsg.eventType - 0x8000)); |
|
|
return true; |
|
|
} |
|
|
event.type = static_cast<SDL_EventType>(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<uint32_t>(msg.type), progressToNextGameTick); |
|
|
break; |
|
|
} |
|
|
#endif // LOG_DEMOMODE_MESSAGES |
|
|
} |
|
|
|
|
|
LoadingStatus LoadDemoMessages(int i) |
|
|
{ |
|
|
const std::string path = StrCat(paths::PrefPath(), "demo_", i, ".dmo"); |
|
|
FILE *demofile = OpenFile(path.c_str(), "rb"); |
|
|
if (demofile == nullptr) { |
|
|
return LoadingStatus::FileNotFound; |
|
|
} |
|
|
|
|
|
const uint8_t version = ReadByte(demofile); |
|
|
if (version != 0 && version != 1) { |
|
|
return LoadingStatus::UnsupportedVersion; |
|
|
} |
|
|
|
|
|
gSaveNumber = ReadLE32(demofile); |
|
|
ReadSettings(demofile, version); |
|
|
|
|
|
while (true) { |
|
|
const uint32_t typeNum = ReadLE32(demofile); |
|
|
if (std::feof(demofile)) |
|
|
break; |
|
|
const auto type = static_cast<DemoMsgType>(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<int32_t>(demofile); |
|
|
msg.wheel.y = ReadLE32<int32_t>(demofile); |
|
|
msg.wheel.mod = ReadLE16(demofile); |
|
|
break; |
|
|
case 0x300: // SDL_KEYDOWN |
|
|
case 0x301: // SDL_KEYUP |
|
|
msg.key.sym = static_cast<SDL_Keycode>(ReadLE32(demofile)); |
|
|
msg.key.mod = static_cast<SDL_Keymod>(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; |
|
|
} |
|
|
} |
|
|
|
|
|
std::fclose(demofile); |
|
|
|
|
|
DemoModeLastTick = SDL_GetTicks(); |
|
|
|
|
|
return LoadingStatus::Success; |
|
|
} |
|
|
|
|
|
void RecordEventHeader(const SDL_Event &event) |
|
|
{ |
|
|
WriteLE32(DemoRecording, static_cast<uint32_t>(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; |
|
|
|
|
|
LoadingStatus status = LoadDemoMessages(demoNumber); |
|
|
switch (status) { |
|
|
case LoadingStatus::Success: |
|
|
return; |
|
|
case LoadingStatus::FileNotFound: |
|
|
printInConsole("Demo file not found"); |
|
|
break; |
|
|
case LoadingStatus::UnsupportedVersion: |
|
|
printInConsole("Unsupported Demo version"); |
|
|
break; |
|
|
} |
|
|
printNewlineInConsole(); |
|
|
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); |
|
|
|
|
|
sgOptions.Gameplay.runInTown.SetValue(DemoSettings.runInTown); |
|
|
sgOptions.Gameplay.theoQuest.SetValue(DemoSettings.theoQuest); |
|
|
sgOptions.Gameplay.cowQuest.SetValue(DemoSettings.cowQuest); |
|
|
sgOptions.Gameplay.autoGoldPickup.SetValue(DemoSettings.autoGoldPickup); |
|
|
sgOptions.Gameplay.autoElixirPickup.SetValue(DemoSettings.autoElixirPickup); |
|
|
sgOptions.Gameplay.autoOilPickup.SetValue(DemoSettings.autoOilPickup); |
|
|
sgOptions.Gameplay.autoPickupInTown.SetValue(DemoSettings.autoPickupInTown); |
|
|
sgOptions.Gameplay.adriaRefillsMana.SetValue(DemoSettings.adriaRefillsMana); |
|
|
sgOptions.Gameplay.autoEquipWeapons.SetValue(DemoSettings.autoEquipWeapons); |
|
|
sgOptions.Gameplay.autoEquipArmor.SetValue(DemoSettings.autoEquipArmor); |
|
|
sgOptions.Gameplay.autoEquipHelms.SetValue(DemoSettings.autoEquipHelms); |
|
|
sgOptions.Gameplay.autoEquipShields.SetValue(DemoSettings.autoEquipShields); |
|
|
sgOptions.Gameplay.autoEquipJewelry.SetValue(DemoSettings.autoEquipJewelry); |
|
|
sgOptions.Gameplay.randomizeQuests.SetValue(DemoSettings.randomizeQuests); |
|
|
sgOptions.Gameplay.showItemLabels.SetValue(DemoSettings.showItemLabels); |
|
|
sgOptions.Gameplay.autoRefillBelt.SetValue(DemoSettings.autoRefillBelt); |
|
|
sgOptions.Gameplay.disableCripplingShrines.SetValue(DemoSettings.disableCripplingShrines); |
|
|
sgOptions.Gameplay.numHealPotionPickup.SetValue(DemoSettings.numHealPotionPickup); |
|
|
sgOptions.Gameplay.numFullHealPotionPickup.SetValue(DemoSettings.numFullHealPotionPickup); |
|
|
sgOptions.Gameplay.numManaPotionPickup.SetValue(DemoSettings.numManaPotionPickup); |
|
|
sgOptions.Gameplay.numFullManaPotionPickup.SetValue(DemoSettings.numFullManaPotionPickup); |
|
|
sgOptions.Gameplay.numRejuPotionPickup.SetValue(DemoSettings.numRejuPotionPickup); |
|
|
sgOptions.Gameplay.numFullRejuPotionPickup.SetValue(DemoSettings.numFullRejuPotionPickup); |
|
|
} |
|
|
|
|
|
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 = std::clamp<int32_t>(fraction, 0, AnimationInfo::baseValueFraction); |
|
|
uint8_t progressToNextGameTick = static_cast<uint8_t>(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<uint32_t>(runGameLoop ? DemoMsgType::GameTick : DemoMsgType::Rendering)); |
|
|
WriteByte(DemoRecording, ProgressToNextGameTick); |
|
|
} |
|
|
|
|
|
void RecordMessage(const SDL_Event &event, uint16_t modState) |
|
|
{ |
|
|
if (!gbRunGame || DemoRecording == nullptr) |
|
|
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<uint32_t>(event.key.keysym.sym)); |
|
|
WriteLE16(DemoRecording, static_cast<uint16_t>(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<uint32_t>(GetCustomEvent(event.type)); |
|
|
RecordEventHeader(stableCustomEvent); |
|
|
} |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
void NotifyGameLoopStart() |
|
|
{ |
|
|
if (IsRecording()) { |
|
|
const std::string path = StrCat(paths::PrefPath(), "demo_", RecordNumber, ".dmo"); |
|
|
DemoRecording = OpenFile(path.c_str(), "wb"); |
|
|
if (DemoRecording == nullptr) { |
|
|
RecordNumber = -1; |
|
|
LogError("Failed to open {} for writing", path); |
|
|
return; |
|
|
} |
|
|
constexpr uint8_t Version = 1; |
|
|
WriteByte(DemoRecording, Version); |
|
|
WriteLE32(DemoRecording, gSaveNumber); |
|
|
WriteSettings(DemoRecording); |
|
|
} |
|
|
|
|
|
if (IsRunning()) { |
|
|
StartTime = SDL_GetTicks(); |
|
|
LogicTick = 0; |
|
|
} |
|
|
} |
|
|
|
|
|
void NotifyGameLoopEnd() |
|
|
{ |
|
|
if (IsRecording()) { |
|
|
std::fclose(DemoRecording); |
|
|
DemoRecording = nullptr; |
|
|
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
|
|
|
|