2016-01-09 14:56:28 -05:00
|
|
|
#include "STDInclude.hpp"
|
|
|
|
|
|
|
|
namespace Components
|
|
|
|
{
|
2016-01-10 06:25:31 -05:00
|
|
|
Theatre::Container Theatre::DemoContainer;
|
|
|
|
|
2016-01-09 14:56:28 -05:00
|
|
|
char Theatre::BaselineSnapshot[131072] = { 0 };
|
|
|
|
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()
|
|
|
|
{
|
|
|
|
int sequence = (*Game::serverMessageSequence - 1);
|
|
|
|
Game::FS_Write(&sequence, 4, *Game::demoFile);
|
|
|
|
}
|
|
|
|
|
2016-01-24 07:06:52 -05:00
|
|
|
void Theatre::StoreBaseline(PBYTE snapshotMsg)
|
2016-01-09 14:56:28 -05:00
|
|
|
{
|
|
|
|
// Store offset and length
|
2016-01-24 07:06:52 -05:00
|
|
|
Theatre::BaselineSnapshotMsgLen = *reinterpret_cast<int*>(snapshotMsg + 20);
|
|
|
|
Theatre::BaselineSnapshotMsgOff = *reinterpret_cast<int*>(snapshotMsg + 28) - 7;
|
2016-01-09 14:56:28 -05:00
|
|
|
|
|
|
|
// Copy to our snapshot buffer
|
2016-06-19 11:40:30 -04:00
|
|
|
std::memcpy(Theatre::BaselineSnapshot, *reinterpret_cast<DWORD**>(snapshotMsg + 8), *reinterpret_cast<DWORD*>(snapshotMsg + 20));
|
2016-01-24 07:06:52 -05:00
|
|
|
}
|
2016-01-09 14:56:28 -05:00
|
|
|
|
2016-01-24 07:06:52 -05:00
|
|
|
void __declspec(naked) Theatre::BaselineStoreStub()
|
|
|
|
{
|
|
|
|
_asm
|
2016-01-09 14:56:28 -05:00
|
|
|
{
|
2016-01-24 07:06:52 -05:00
|
|
|
push edi
|
|
|
|
call Theatre::StoreBaseline
|
|
|
|
pop edi
|
|
|
|
|
2016-01-09 14:56:28 -05:00
|
|
|
mov edx, 5ABEF5h
|
|
|
|
jmp edx
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::WriteBaseline()
|
|
|
|
{
|
|
|
|
static char bufData[131072];
|
|
|
|
static char cmpData[131072];
|
|
|
|
|
|
|
|
Game::msg_t buf;
|
|
|
|
|
|
|
|
Game::MSG_Init(&buf, bufData, 131072);
|
|
|
|
Game::MSG_WriteData(&buf, &Theatre::BaselineSnapshot[Theatre::BaselineSnapshotMsgOff], Theatre::BaselineSnapshotMsgLen - Theatre::BaselineSnapshotMsgOff);
|
|
|
|
Game::MSG_WriteByte(&buf, 6);
|
|
|
|
|
|
|
|
int compressedSize = Game::MSG_WriteBitsCompress(false, buf.data, cmpData, buf.cursize);
|
|
|
|
int fileCompressedSize = compressedSize + 4;
|
|
|
|
|
|
|
|
int byte8 = 8;
|
|
|
|
char byte0 = 0;
|
|
|
|
|
|
|
|
Game::FS_Write(&byte0, 1, *Game::demoFile);
|
|
|
|
Game::FS_Write(Game::serverMessageSequence, 4, *Game::demoFile);
|
|
|
|
Game::FS_Write(&fileCompressedSize, 4, *Game::demoFile);
|
|
|
|
Game::FS_Write(&byte8, 4, *Game::demoFile);
|
|
|
|
|
|
|
|
for (int i = 0; i < compressedSize; i += 1024)
|
|
|
|
{
|
|
|
|
int size = min(compressedSize - i, 1024);
|
|
|
|
|
|
|
|
if (i + size >= sizeof(cmpData))
|
|
|
|
{
|
|
|
|
Logger::Print("Error: Writing compressed demo baseline exceeded buffer\n");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
Game::FS_Write(&cmpData[i], size, *Game::demoFile);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void __declspec(naked) Theatre::BaselineToFileStub()
|
|
|
|
{
|
|
|
|
__asm
|
|
|
|
{
|
|
|
|
call Theatre::WriteBaseline
|
|
|
|
|
|
|
|
// Restore overwritten operation
|
|
|
|
mov ecx, 0A5E9C4h
|
|
|
|
mov [ecx], 0
|
|
|
|
|
|
|
|
// Return to original code
|
|
|
|
mov ecx, 5A863Ah
|
|
|
|
jmp ecx
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void __declspec(naked) Theatre::AdjustTimeDeltaStub()
|
|
|
|
{
|
|
|
|
__asm
|
|
|
|
{
|
|
|
|
mov eax, Game::demoPlaying
|
|
|
|
mov eax, [eax]
|
|
|
|
test al, al
|
|
|
|
jz continue
|
|
|
|
|
|
|
|
// delta doesn't drift for demos
|
|
|
|
retn
|
|
|
|
|
|
|
|
continue:
|
|
|
|
mov eax, 5A1AD0h
|
|
|
|
jmp eax
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void __declspec(naked) Theatre::ServerTimedOutStub()
|
|
|
|
{
|
|
|
|
__asm
|
|
|
|
{
|
|
|
|
mov eax, Game::demoPlaying
|
|
|
|
mov eax, [eax]
|
|
|
|
test al, al
|
|
|
|
jz continue
|
|
|
|
|
|
|
|
mov eax, 5A8E70h
|
|
|
|
jmp eax
|
|
|
|
|
|
|
|
continue:
|
|
|
|
mov eax, 0B2BB90h
|
|
|
|
mov esi, 5A8E08h
|
|
|
|
jmp esi
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void __declspec(naked) Theatre::UISetActiveMenuStub()
|
|
|
|
{
|
|
|
|
__asm
|
|
|
|
{
|
2016-01-10 06:25:31 -05:00
|
|
|
mov eax, Game::demoPlaying
|
|
|
|
mov eax, [eax]
|
|
|
|
test al, al
|
|
|
|
jz continue
|
|
|
|
|
|
|
|
mov eax, 4CB49Ch
|
|
|
|
jmp eax
|
|
|
|
|
|
|
|
continue:
|
2016-01-09 14:56:28 -05:00
|
|
|
mov ecx, [esp + 10h]
|
|
|
|
push 10h
|
|
|
|
push ecx
|
|
|
|
mov eax, 4CB3F6h
|
|
|
|
jmp eax
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-10 06:25:31 -05:00
|
|
|
void Theatre::RecordStub(int channel, char* message, char* file)
|
|
|
|
{
|
|
|
|
Game::Com_Printf(channel, message, file);
|
|
|
|
|
|
|
|
Theatre::DemoContainer.CurrentInfo.Name = file;
|
|
|
|
Theatre::DemoContainer.CurrentInfo.Mapname = Dvar::Var("mapname").Get<const char*>();
|
|
|
|
Theatre::DemoContainer.CurrentInfo.Gametype = Dvar::Var("g_gametype").Get<const char*>();
|
|
|
|
Theatre::DemoContainer.CurrentInfo.Author = Steam::SteamFriends()->GetPersonaName();
|
2016-06-30 06:11:51 -04:00
|
|
|
Theatre::DemoContainer.CurrentInfo.Length = Game::Sys_Milliseconds();
|
2016-01-10 06:25:31 -05:00
|
|
|
std::time(&Theatre::DemoContainer.CurrentInfo.TimeStamp);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::StopRecordStub(int channel, char* message)
|
|
|
|
{
|
|
|
|
Game::Com_Printf(channel, message);
|
|
|
|
|
|
|
|
// Store correct length
|
2016-06-30 06:11:51 -04:00
|
|
|
Theatre::DemoContainer.CurrentInfo.Length = Game::Sys_Milliseconds() - Theatre::DemoContainer.CurrentInfo.Length;
|
2016-01-10 06:25:31 -05:00
|
|
|
|
|
|
|
// Write metadata
|
|
|
|
FileSystem::FileWriter meta(Utils::VA("%s.json", Theatre::DemoContainer.CurrentInfo.Name.data()));
|
|
|
|
meta.Write(json11::Json(Theatre::DemoContainer.CurrentInfo).dump());
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::LoadDemos()
|
|
|
|
{
|
|
|
|
Theatre::DemoContainer.CurrentSelection = 0;
|
|
|
|
Theatre::DemoContainer.Demos.clear();
|
|
|
|
|
|
|
|
auto demos = FileSystem::GetFileList("demos/", "dm_13");
|
|
|
|
|
|
|
|
for (auto demo : demos)
|
|
|
|
{
|
|
|
|
FileSystem::File meta(Utils::VA("demos/%s.json", demo.data()));
|
|
|
|
|
|
|
|
if (meta.Exists())
|
|
|
|
{
|
|
|
|
std::string error;
|
|
|
|
json11::Json metaObject = json11::Json::parse(meta.GetBuffer(), error);
|
|
|
|
|
|
|
|
if (metaObject.is_object())
|
|
|
|
{
|
|
|
|
Theatre::Container::DemoInfo info;
|
|
|
|
|
|
|
|
info.Name = demo.substr(0, demo.find_last_of("."));
|
|
|
|
info.Author = metaObject["author"].string_value();
|
|
|
|
info.Gametype = metaObject["gametype"].string_value();
|
|
|
|
info.Mapname = metaObject["mapname"].string_value();
|
|
|
|
info.Length = (int)metaObject["length"].number_value();
|
|
|
|
info.TimeStamp = _atoi64(metaObject["timestamp"].string_value().data());
|
|
|
|
|
|
|
|
Theatre::DemoContainer.Demos.push_back(info);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reverse, latest demo first!
|
|
|
|
std::reverse(Theatre::DemoContainer.Demos.begin(), Theatre::DemoContainer.Demos.end());
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::DeleteDemo()
|
|
|
|
{
|
|
|
|
if (Theatre::DemoContainer.CurrentSelection < Theatre::DemoContainer.Demos.size())
|
|
|
|
{
|
|
|
|
Theatre::Container::DemoInfo info = Theatre::DemoContainer.Demos[Theatre::DemoContainer.CurrentSelection];
|
|
|
|
|
|
|
|
Logger::Print("Deleting demo %s...\n", info.Name.data());
|
|
|
|
|
|
|
|
FileSystem::DeleteFile("demos", info.Name + ".dm_13");
|
|
|
|
FileSystem::DeleteFile("demos", info.Name + ".dm_13.json");
|
|
|
|
|
2016-01-11 05:07:25 -05:00
|
|
|
// Reset our ui_demo_* dvars here, because the theater menu needs it.
|
2016-01-12 19:29:22 -05:00
|
|
|
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("");
|
2016-01-11 05:07:25 -05:00
|
|
|
|
2016-01-10 06:25:31 -05:00
|
|
|
// Reload demos
|
|
|
|
Theatre::LoadDemos();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::PlayDemo()
|
|
|
|
{
|
|
|
|
if (Theatre::DemoContainer.CurrentSelection < Theatre::DemoContainer.Demos.size())
|
|
|
|
{
|
|
|
|
Command::Execute(Utils::VA("demo %s", Theatre::DemoContainer.Demos[Theatre::DemoContainer.CurrentSelection].Name.data()), true);
|
|
|
|
Command::Execute("demoback", false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned int Theatre::GetDemoCount()
|
|
|
|
{
|
|
|
|
return Theatre::DemoContainer.Demos.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Omit column here
|
|
|
|
const char* Theatre::GetDemoText(unsigned int item, int column)
|
|
|
|
{
|
|
|
|
if (item < Theatre::DemoContainer.Demos.size())
|
|
|
|
{
|
|
|
|
Theatre::Container::DemoInfo info = Theatre::DemoContainer.Demos[item];
|
|
|
|
|
|
|
|
return Utils::VA("%s on %s", Game::UI_LocalizeGameType(info.Gametype.data()), Game::UI_LocalizeMapName(info.Mapname.data()));
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::SelectDemo(unsigned int index)
|
|
|
|
{
|
|
|
|
if (index < Theatre::DemoContainer.Demos.size())
|
|
|
|
{
|
|
|
|
Theatre::DemoContainer.CurrentSelection = index;
|
|
|
|
Theatre::Container::DemoInfo info = Theatre::DemoContainer.Demos[index];
|
|
|
|
|
|
|
|
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()));
|
2016-01-11 06:09:31 -05:00
|
|
|
Dvar::Var("ui_demo_length").Set(Utils::FormatTimeSpan(info.Length));
|
2016-01-10 06:25:31 -05:00
|
|
|
Dvar::Var("ui_demo_author").Set(info.Author);
|
|
|
|
Dvar::Var("ui_demo_date").Set(std::asctime(std::localtime(&info.TimeStamp)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-10 09:48:49 -05:00
|
|
|
uint32_t Theatre::InitCGameStub()
|
|
|
|
{
|
|
|
|
if (Dvar::Var("cl_autoRecord").Get<bool>() && !*Game::demoPlaying)
|
|
|
|
{
|
|
|
|
std::vector<std::string> files;
|
|
|
|
std::vector<std::string> demos = FileSystem::GetFileList("demos/", "dm_13");
|
|
|
|
|
|
|
|
for (auto demo : demos)
|
|
|
|
{
|
|
|
|
if (Utils::StartsWith(demo, "auto_"))
|
|
|
|
{
|
|
|
|
files.push_back(demo);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int numDel = files.size() - Dvar::Var("cl_demosKeep").Get<int>();
|
|
|
|
|
2016-01-24 13:58:13 -05:00
|
|
|
for (int i = 0; i < numDel; ++i)
|
2016-01-10 09:48:49 -05:00
|
|
|
{
|
|
|
|
Logger::Print("Deleting old demo %s\n", files[i].data());
|
|
|
|
FileSystem::DeleteFile("demos", files[i].data());
|
|
|
|
FileSystem::DeleteFile("demos", Utils::VA("%s.json", files[i].data()));
|
|
|
|
}
|
|
|
|
|
|
|
|
Command::Execute(Utils::VA("record auto_%I64d", time(0)), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Utils::Hook::Call<DWORD()>(0x42BBB0)();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::MapChangeStub()
|
|
|
|
{
|
|
|
|
if (*Game::demoRecording)
|
|
|
|
{
|
|
|
|
Command::Execute("stoprecord", true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Utils::Hook::Call<void()>(0x464A60)();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Theatre::MapChangeSVStub(char* a1, char* a2)
|
|
|
|
{
|
|
|
|
if (*Game::demoRecording)
|
|
|
|
{
|
|
|
|
Command::Execute("stoprecord", true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Utils::Hook::Call<void(char*, char*)>(0x487C50)(a1, a2);
|
|
|
|
}
|
|
|
|
|
2016-01-09 14:56:28 -05:00
|
|
|
Theatre::Theatre()
|
|
|
|
{
|
2016-01-10 09:48:49 -05:00
|
|
|
Dvar::Register<bool>("cl_autoRecord", true, Game::dvar_flag::DVAR_FLAG_SAVED, "Automatically record games.");
|
|
|
|
Dvar::Register<int>("cl_demosKeep", 30, 1, 999, Game::dvar_flag::DVAR_FLAG_SAVED, "How many demos to keep with autorecord.");
|
|
|
|
|
2016-01-09 14:56:28 -05:00
|
|
|
Utils::Hook(0x5A8370, Theatre::GamestateWriteStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x5A85D2, Theatre::RecordGamestateStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x5ABE36, Theatre::BaselineStoreStub, HOOK_JUMP).Install()->Quick();
|
|
|
|
Utils::Hook(0x5A8630, Theatre::BaselineToFileStub, HOOK_JUMP).Install()->Quick();
|
|
|
|
Utils::Hook(0x4CB3EF, Theatre::UISetActiveMenuStub, HOOK_JUMP).Install()->Quick();
|
|
|
|
Utils::Hook(0x50320E, Theatre::AdjustTimeDeltaStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x5A8E03, Theatre::ServerTimedOutStub, HOOK_JUMP).Install()->Quick();
|
|
|
|
|
2016-01-10 06:25:31 -05:00
|
|
|
// Hook commands to enforce metadata generation
|
|
|
|
Utils::Hook(0x5A82AE, Theatre::RecordStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x5A8156, Theatre::StopRecordStub, HOOK_CALL).Install()->Quick();
|
|
|
|
|
2016-01-10 09:48:49 -05:00
|
|
|
// Autorecording
|
|
|
|
Utils::Hook(0x5A1D6A, Theatre::InitCGameStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x4A712A, Theatre::MapChangeStub, HOOK_CALL).Install()->Quick();
|
|
|
|
Utils::Hook(0x5AA91C, Theatre::MapChangeSVStub, HOOK_CALL).Install()->Quick();
|
|
|
|
|
2016-01-10 06:25:31 -05:00
|
|
|
// UIScripts
|
|
|
|
UIScript::Add("loadDemos", Theatre::LoadDemos);
|
|
|
|
UIScript::Add("launchDemo", Theatre::PlayDemo);
|
|
|
|
UIScript::Add("deleteDemo", Theatre::DeleteDemo);
|
|
|
|
|
|
|
|
// Feeder
|
|
|
|
UIFeeder::Add(10.0f, Theatre::GetDemoCount, Theatre::GetDemoText, Theatre::SelectDemo);
|
|
|
|
|
2016-01-09 14:56:28 -05:00
|
|
|
// set the configstrings stuff to load the default (empty) string table; this should allow demo recording on all gametypes/maps
|
|
|
|
if(!Dedicated::IsDedicated()) Utils::Hook::Set<char*>(0x47440B, "mp/defaultStringTable.csv");
|
|
|
|
|
2016-01-10 06:25:31 -05:00
|
|
|
// Change font size
|
|
|
|
Utils::Hook::Set<BYTE>(0x5AC854, 2);
|
|
|
|
Utils::Hook::Set<BYTE>(0x5AC85A, 2);
|
2016-01-11 06:09:31 -05:00
|
|
|
|
|
|
|
// Command::Add("democycle", [] (Command::Params params)
|
|
|
|
// {
|
|
|
|
// // Cmd_FollowCycle_f
|
|
|
|
// Utils::Hook::Call<void(Game::gentity_t*, int)>(0x458ED0)(Game::g_entities, -1);
|
|
|
|
// });
|
2016-01-09 14:56:28 -05:00
|
|
|
}
|
|
|
|
}
|