iw4x-client/src/Components/Modules/Theatre.cpp

376 lines
9.8 KiB
C++
Raw Normal View History

2022-02-27 07:53:44 -05:00
#include <STDInclude.hpp>
2017-01-19 16:23:59 -05:00
namespace Components
{
Theatre::DemoInfo Theatre::CurrentInfo;
unsigned int Theatre::CurrentSelection;
std::vector<Theatre::DemoInfo> Theatre::Demos;
char Theatre::BaselineSnapshot[131072] = {0};
2017-01-19 16:23:59 -05:00
int Theatre::BaselineSnapshotMsgLen;
int Theatre::BaselineSnapshotMsgOff;
void Theatre::GamestateWriteStub(Game::msg_t* msg, char byte)
{
Game::MSG_WriteLong(msg, 0);
Game::MSG_WriteByte(msg, byte);
}
void Theatre::RecordGamestateStub()
{
const auto sequence = (*Game::serverMessageSequence - 1);
Game::FS_WriteToDemo(&sequence, 4, *Game::demoFile);
2017-01-19 16:23:59 -05:00
}
void Theatre::StoreBaseline(PBYTE snapshotMsg)
{
// Store offset and length
2022-12-05 13:45:14 -05:00
BaselineSnapshotMsgLen = *reinterpret_cast<int*>(snapshotMsg + 20);
BaselineSnapshotMsgOff = *reinterpret_cast<int*>(snapshotMsg + 28) - 7;
2017-01-19 16:23:59 -05:00
// Copy to our snapshot buffer
2022-12-05 13:45:14 -05:00
std::memcpy(BaselineSnapshot, *reinterpret_cast<DWORD**>(snapshotMsg + 8), *reinterpret_cast<DWORD*>(snapshotMsg + 20));
2017-01-19 16:23:59 -05:00
}
__declspec(naked) void Theatre::BaselineStoreStub()
{
_asm
{
push edi
2022-12-05 13:45:14 -05:00
call StoreBaseline
2017-01-19 16:23:59 -05:00
pop edi
mov edx, 5ABEF5h
jmp edx
}
}
void Theatre::WriteBaseline()
{
2022-08-13 11:19:45 -04:00
static unsigned char bufData[131072];
2022-12-05 13:45:14 -05:00
static unsigned char cmpData[131072];
2017-01-19 16:23:59 -05:00
Game::msg_t buf;
Game::MSG_Init(&buf, bufData, 131072);
2022-12-05 13:45:14 -05:00
Game::MSG_WriteData(&buf, &BaselineSnapshot[BaselineSnapshotMsgOff], BaselineSnapshotMsgLen - BaselineSnapshotMsgOff);
2017-01-19 16:23:59 -05:00
Game::MSG_WriteByte(&buf, 6);
2022-12-05 13:45:14 -05:00
const auto compressedSize = Game::MSG_WriteBitsCompress(false, buf.data, cmpData, buf.cursize);
const auto fileCompressedSize = compressedSize + 4;
2017-01-19 16:23:59 -05:00
int byte8 = 8;
char byte0 = 0;
Game::FS_WriteToDemo(&byte0, 1, *Game::demoFile);
Game::FS_WriteToDemo(Game::serverMessageSequence, 4, *Game::demoFile);
Game::FS_WriteToDemo(&fileCompressedSize, 4, *Game::demoFile);
Game::FS_WriteToDemo(&byte8, 4, *Game::demoFile);
2017-01-19 16:23:59 -05:00
for (auto i = 0; i < compressedSize; i += 1024)
2017-01-19 16:23:59 -05:00
{
const auto size = std::min(compressedSize - i, 1024);
2017-01-19 16:23:59 -05:00
if (i + size >= sizeof(cmpData))
{
2022-07-21 12:56:16 -04:00
Logger::PrintError(Game::CON_CHANNEL_ERROR, "Writing compressed demo baseline exceeded buffer\n");
2017-01-19 16:23:59 -05:00
break;
}
Game::FS_WriteToDemo(&cmpData[i], size, *Game::demoFile);
2017-01-19 16:23:59 -05:00
}
}
__declspec(naked) void Theatre::BaselineToFileStub()
{
__asm
{
2017-02-01 07:44:25 -05:00
pushad
2022-12-05 13:45:14 -05:00
call WriteBaseline
2017-02-01 07:44:25 -05:00
popad
2017-01-19 16:23:59 -05:00
// Restore overwritten operation
mov ecx, 0A5E9C4h
mov [ecx], 0
// Return to original code
2017-02-01 07:44:25 -05:00
push 5A863Ah
retn
2017-01-19 16:23:59 -05:00
}
}
__declspec(naked) void Theatre::AdjustTimeDeltaStub()
{
__asm
{
mov eax, Game::demoPlaying
mov eax, [eax]
test al, al
jz continue
// delta doesn't drift for demos
retn
continue:
2017-02-01 07:44:25 -05:00
push 5A1AD0h
retn
2017-01-19 16:23:59 -05:00
}
}
__declspec(naked) void Theatre::ServerTimedOutStub()
{
__asm
{
mov eax, Game::demoPlaying
mov eax, [eax]
test al, al
jz continue
mov eax, 5A8E70h
jmp eax
continue:
mov eax, 0B2BB90h
2017-02-01 07:44:25 -05:00
push 5A8E08h
retn
2017-01-19 16:23:59 -05:00
}
}
__declspec(naked) void Theatre::UISetActiveMenuStub()
{
__asm
{
mov eax, Game::demoPlaying
mov eax, [eax]
test al, al
jz continue
mov eax, 4CB49Ch
jmp eax
continue:
mov ecx, [esp + 10h]
push 10h
push ecx
2017-02-01 07:44:25 -05:00
push 4CB3F6h
retn
2017-01-19 16:23:59 -05:00
}
}
void Theatre::RecordStub(int channel, char* message, char* file)
{
Game::Com_Printf(channel, message, file);
2022-12-05 13:45:14 -05:00
CurrentInfo.name = file;
CurrentInfo.mapname = (*Game::sv_mapname)->current.string;
CurrentInfo.gametype = (*Game::sv_gametype)->current.string;
CurrentInfo.author = Steam::SteamFriends()->GetPersonaName();
CurrentInfo.length = Game::Sys_Milliseconds();
std::time(&CurrentInfo.timeStamp);
2017-01-19 16:23:59 -05:00
}
void Theatre::StopRecordStub(int channel, char* message)
{
Game::Com_Printf(channel, message);
// Store correct length
2022-12-05 13:45:14 -05:00
CurrentInfo.length = Game::Sys_Milliseconds() - CurrentInfo.length;
2017-01-19 16:23:59 -05:00
// Write metadata
2022-12-05 13:45:14 -05:00
FileSystem::FileWriter meta(std::format("{}.json", CurrentInfo.name));
meta.write(nlohmann::json(CurrentInfo.to_json()).dump());
2017-01-19 16:23:59 -05:00
}
2022-08-24 10:38:14 -04:00
void Theatre::LoadDemos([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info)
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
CurrentSelection = 0;
Demos.clear();
2017-01-19 16:23:59 -05:00
const auto demos = FileSystem::GetFileList("demos/", "dm_13");
2017-01-19 16:23:59 -05:00
for (auto demo : demos)
{
FileSystem::File meta(Utils::String::VA("demos/%s.json", demo.data()));
if (meta.exists())
{
nlohmann::json metaObject;
try
{
metaObject = nlohmann::json::parse(meta.getBuffer());
2022-12-05 13:45:14 -05:00
DemoInfo demoInfo;
demoInfo.name = demo.substr(0, demo.find_last_of("."));
demoInfo.author = metaObject["author"].get<std::string>();
demoInfo.gametype = metaObject["gametype"].get<std::string>();
demoInfo.mapname = metaObject["mapname"].get<std::string>();
demoInfo.length = metaObject["length"].get<int>();
auto timestamp = metaObject["timestamp"].get<std::string>();
demoInfo.timeStamp = _atoi64(timestamp.data());
Demos.push_back(demoInfo);
}
catch (const nlohmann::json::parse_error& ex)
2017-01-19 16:23:59 -05:00
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "Json Parse Error: {}\n", ex.what());
2017-01-19 16:23:59 -05:00
}
}
}
// Reverse, latest demo first!
2022-12-05 13:45:14 -05:00
std::ranges::reverse(Demos);
2017-01-19 16:23:59 -05:00
}
2022-08-24 10:38:14 -04:00
void Theatre::DeleteDemo([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info)
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
if (CurrentSelection < Demos.size())
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
auto demoInfo = Demos.at(CurrentSelection);
2017-01-19 16:23:59 -05:00
2022-08-24 10:38:14 -04:00
Logger::Print("Deleting demo {}...\n", demoInfo.name);
2017-01-19 16:23:59 -05:00
2022-08-24 10:38:14 -04:00
FileSystem::_DeleteFile("demos", demoInfo.name + ".dm_13");
FileSystem::_DeleteFile("demos", demoInfo.name + ".dm_13.json");
2017-01-19 16:23:59 -05:00
// Reset our ui_demo_* dvars here, because the theater menu needs it.
Dvar::Var("ui_demo_mapname").set("");
Dvar::Var("ui_demo_mapname_localized").set("");
Dvar::Var("ui_demo_gametype").set("");
Dvar::Var("ui_demo_length").set("");
Dvar::Var("ui_demo_author").set("");
Dvar::Var("ui_demo_date").set("");
// Reload demos
2022-12-05 13:45:14 -05:00
LoadDemos(UIScript::Token(), info);
2017-01-19 16:23:59 -05:00
}
}
2022-08-24 10:38:14 -04:00
void Theatre::PlayDemo([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info)
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
if (CurrentSelection < Demos.size())
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
Command::Execute(std::format("demo {}", Demos[CurrentSelection].name), true);
2017-01-19 16:23:59 -05:00
Command::Execute("demoback", false);
}
}
unsigned int Theatre::GetDemoCount()
{
2022-12-05 13:45:14 -05:00
return Demos.size();
2017-01-19 16:23:59 -05:00
}
// Omit column here
const char* Theatre::GetDemoText(unsigned int item, int /*column*/)
{
2022-12-05 13:45:14 -05:00
if (item < Demos.size())
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
auto info = Demos.at(item);
2017-01-19 16:23:59 -05:00
return Utils::String::VA("%s on %s", Game::UI_LocalizeGameType(info.gametype.data()), Game::UI_LocalizeMapName(info.mapname.data()));
}
return "";
}
void Theatre::SelectDemo(unsigned int index)
{
2022-12-05 13:45:14 -05:00
if (index < Demos.size())
2017-01-19 16:23:59 -05:00
{
2022-12-05 13:45:14 -05:00
CurrentSelection = index;
auto info = Demos.at(index);
2017-01-19 16:23:59 -05:00
tm time;
2022-12-05 13:45:14 -05:00
char buffer[1000] = {0};
2017-01-19 16:23:59 -05:00
localtime_s(&time, &info.timeStamp);
asctime_s(buffer, sizeof buffer, &time);
Dvar::Var("ui_demo_mapname").set(info.mapname);
Dvar::Var("ui_demo_mapname_localized").set(Game::UI_LocalizeMapName(info.mapname.data()));
Dvar::Var("ui_demo_gametype").set(Game::UI_LocalizeGameType(info.gametype.data()));
Dvar::Var("ui_demo_length").set(Utils::String::FormatTimeSpan(info.length));
Dvar::Var("ui_demo_author").set(info.author);
Dvar::Var("ui_demo_date").set(buffer);
}
}
uint32_t Theatre::InitCGameStub()
{
if (Dvar::Var("cl_autoRecord").get<bool>() && !*Game::demoPlaying)
{
std::vector<std::string> files;
2022-12-05 13:45:14 -05:00
auto demos = FileSystem::GetFileList("demos/", "dm_13");
2017-01-19 16:23:59 -05:00
for (auto demo : demos)
{
if (Utils::String::StartsWith(demo, "auto_"))
{
files.push_back(demo);
}
}
2022-12-05 13:45:14 -05:00
auto numDel = static_cast<int>(files.size()) - Dvar::Var("cl_demosKeep").get<int>();
2017-01-19 16:23:59 -05:00
2022-12-05 13:45:14 -05:00
for (auto i = 0; i < numDel; ++i)
2017-01-19 16:23:59 -05:00
{
2022-06-12 17:07:53 -04:00
Logger::Print("Deleting old demo {}\n", files[i]);
2022-08-11 06:44:03 -04:00
FileSystem::_DeleteFile("demos", files[i].data());
FileSystem::_DeleteFile("demos", Utils::String::VA("%s.json", files[i].data()));
2017-01-19 16:23:59 -05:00
}
Command::Execute(Utils::String::VA("record auto_%lld", time(nullptr)), true);
2017-01-19 16:23:59 -05:00
}
return Utils::Hook::Call<DWORD()>(0x42BBB0)();
}
void Theatre::MapChangeStub()
{
2022-12-05 13:45:14 -05:00
StopRecording();
2017-01-19 16:23:59 -05:00
Utils::Hook::Call<void()>(0x464A60)();
}
void Theatre::StopRecording()
2017-01-19 16:23:59 -05:00
{
if (*Game::demoRecording)
{
Command::Execute("stoprecord", true);
}
}
Theatre::Theatre()
{
Dvar::Register<bool>("cl_autoRecord", true, Game::DVAR_ARCHIVE, "Automatically record games.");
Dvar::Register<int>("cl_demosKeep", 30, 1, 999, Game::DVAR_ARCHIVE, "How many demos to keep with autorecord.");
2017-01-19 16:23:59 -05:00
2022-12-05 13:45:14 -05:00
Utils::Hook(0x5A8370, GamestateWriteStub, HOOK_CALL).install()->quick();
Utils::Hook(0x5A85D2, RecordGamestateStub, HOOK_CALL).install()->quick();
Utils::Hook(0x5ABE36, BaselineStoreStub, HOOK_JUMP).install()->quick();
Utils::Hook(0x5A8630, BaselineToFileStub, HOOK_JUMP).install()->quick();
Utils::Hook(0x4CB3EF, UISetActiveMenuStub, HOOK_JUMP).install()->quick();
Utils::Hook(0x50320E, AdjustTimeDeltaStub, HOOK_CALL).install()->quick();
Utils::Hook(0x5A8E03, ServerTimedOutStub, HOOK_JUMP).install()->quick();
2017-01-19 16:23:59 -05:00
// Hook commands to enforce metadata generation
2022-12-05 13:45:14 -05:00
Utils::Hook(0x5A82AE, RecordStub, HOOK_CALL).install()->quick();
Utils::Hook(0x5A8156, StopRecordStub, HOOK_CALL).install()->quick();
2017-01-19 16:23:59 -05:00
// Autorecording
2022-12-05 13:45:14 -05:00
Utils::Hook(0x5A1D6A, InitCGameStub, HOOK_CALL).install()->quick();
Utils::Hook(0x4A712A, MapChangeStub, HOOK_CALL).install()->quick();
2017-01-19 16:23:59 -05:00
// UIScripts
2022-12-05 13:45:14 -05:00
UIScript::Add("loadDemos", LoadDemos);
UIScript::Add("launchDemo", PlayDemo);
UIScript::Add("deleteDemo", DeleteDemo);
2017-01-19 16:23:59 -05:00
// Feeder
2022-12-05 13:45:14 -05:00
UIFeeder::Add(10.0f, GetDemoCount, GetDemoText, SelectDemo);
2017-01-19 16:23:59 -05:00
// set the configstrings stuff to load the default (empty) string table; this should allow demo recording on all gametypes/maps
2020-12-09 14:13:34 -05:00
if (!Dedicated::IsEnabled()) Utils::Hook::Set<const char*>(0x47440B, "mp/defaultStringTable.csv");
2017-01-19 16:23:59 -05:00
// Change font size
Utils::Hook::Set<BYTE>(0x5AC854, 2);
Utils::Hook::Set<BYTE>(0x5AC85A, 2);
}
}