Browse Source

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.
pull/2468/head
Anders Jenbo 5 years ago
parent
commit
eda4a5061e
  1. 35
      Source/diablo.cpp
  2. 3
      Source/diablo.h
  3. 2
      Source/dx.cpp
  4. 17
      Source/menu.cpp
  5. 19
      Source/miniwin/miniwin.h
  6. 150
      Source/miniwin/misc_msg.cpp
  7. 3
      Source/movie.cpp
  8. 2
      Source/nthread.cpp
  9. 1
      Source/nthread.h
  10. 6
      Source/options.cpp
  11. 4
      Source/utils/display.cpp

35
Source/diablo.cpp

@ -97,6 +97,9 @@ std::array<Keymapper::ActionIndex, 4> 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

3
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];

2
Source/dx.cpp

@ -333,7 +333,7 @@ void RenderPresent()
}
SDL_RenderPresent(renderer);
if (!sgOptions.Graphics.bVSync) {
if (!sgOptions.Graphics.bVSync && !timedemo) {
LimitFrameRate();
}
} else {

17
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) {

19
Source/miniwin/miniwin.h

@ -1,7 +1,6 @@
#pragma once
#include <cstdint>
#include <cctype>
#include <cmath>
#include <cstdint>
@ -9,6 +8,8 @@
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <fstream>
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);

150
Source/miniwin/misc_msg.cpp

@ -1,9 +1,12 @@
#include <SDL.h>
#include <cstdint>
#include <deque>
#include <string>
#include <sstream>
#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<demoMsg> 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;

3
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

2
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()

1
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);

6
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

4
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;
}

Loading…
Cancel
Save