diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2aef79299..f28de9571 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2630,6 +2630,7 @@ if(TOOLS)
config_retrieve.cpp
config_store.cpp
crapnet.cpp
+ demo_extract_chat.cpp
dilate.cpp
dummy_map.cpp
map_convert_07.cpp
@@ -2999,6 +3000,7 @@ if(TOOLS)
set(TARGET_TOOLS
config_retrieve
config_store
+ demo_extract_chat
dilate
map_convert_07
map_diff
diff --git a/src/tools/demo_extract_chat.cpp b/src/tools/demo_extract_chat.cpp
new file mode 100644
index 000000000..729ddcd67
--- /dev/null
+++ b/src/tools/demo_extract_chat.cpp
@@ -0,0 +1,245 @@
+#include
+#include
+#include
+#include
+
+static const char *TOOL_NAME = "demo_extract_chat";
+
+class CClientSnapshotHandler
+{
+public:
+ struct CClientData
+ {
+ char m_aName[MAX_NAME_LENGTH];
+ };
+ CClientData m_aClients[MAX_CLIENTS];
+
+ CSnapshotStorage::CHolder m_aDemoSnapshotHolders[IClient::NUM_SNAPSHOT_TYPES];
+ char m_aaaDemoSnapshotData[IClient::NUM_SNAPSHOT_TYPES][2][CSnapshot::MAX_SIZE];
+ CSnapshotStorage::CHolder *m_apSnapshots[IClient::NUM_SNAPSHOT_TYPES];
+
+ CClientSnapshotHandler() :
+ m_aClients(), m_aDemoSnapshotHolders()
+ {
+ mem_zero(m_aaaDemoSnapshotData, sizeof(m_aaaDemoSnapshotData));
+
+ for(int SnapshotType = 0; SnapshotType < IClient::NUM_SNAPSHOT_TYPES; SnapshotType++)
+ {
+ m_apSnapshots[SnapshotType] = &m_aDemoSnapshotHolders[SnapshotType];
+ m_apSnapshots[SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemoSnapshotData[SnapshotType][0];
+ m_apSnapshots[SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemoSnapshotData[SnapshotType][1];
+ m_apSnapshots[SnapshotType]->m_SnapSize = 0;
+ m_apSnapshots[SnapshotType]->m_AltSnapSize = 0;
+ m_apSnapshots[SnapshotType]->m_Tick = -1;
+ }
+ }
+
+ int UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
+ {
+ CUnpacker Unpacker;
+ CSnapshotBuilder Builder;
+ Builder.Init();
+ CNetObjHandler NetObjHandler;
+
+ int Num = pFrom->NumItems();
+ for(int Index = 0; Index < Num; Index++)
+ {
+ const CSnapshotItem *pFromItem = pFrom->GetItem(Index);
+ const int FromItemSize = pFrom->GetItemSize(Index);
+ const int ItemType = pFrom->GetItemType(Index);
+ const void *pData = pFromItem->Data();
+ Unpacker.Reset(pData, FromItemSize);
+
+ void *pRawObj = NetObjHandler.SecureUnpackObj(ItemType, &Unpacker);
+ if(!pRawObj)
+ continue;
+
+ const int ItemSize = NetObjHandler.GetUnpackedObjSize(ItemType);
+ void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->ID(), ItemSize);
+ if(!pObj)
+ return -4;
+
+ mem_copy(pObj, pRawObj, ItemSize);
+ }
+
+ return Builder.Finish(pTo);
+ }
+
+ int SnapNumItems(int SnapID)
+ {
+ dbg_assert(SnapID >= 0 && SnapID < IClient::NUM_SNAPSHOT_TYPES, "invalid SnapID");
+ if(!m_apSnapshots[SnapID])
+ return 0;
+ return m_apSnapshots[SnapID]->m_pAltSnap->NumItems();
+ }
+
+ void *SnapGetItem(int SnapID, int Index, IClient::CSnapItem *pItem)
+ {
+ dbg_assert(SnapID >= 0 && SnapID < IClient::NUM_SNAPSHOT_TYPES, "invalid SnapID");
+ const CSnapshotItem *pSnapshotItem = m_apSnapshots[SnapID]->m_pAltSnap->GetItem(Index);
+ pItem->m_DataSize = m_apSnapshots[SnapID]->m_pAltSnap->GetItemSize(Index);
+ pItem->m_Type = m_apSnapshots[SnapID]->m_pAltSnap->GetItemType(Index);
+ pItem->m_ID = pSnapshotItem->ID();
+ return (void *)pSnapshotItem->Data();
+ }
+
+ void OnNewSnapshot()
+ {
+ int Num = SnapNumItems(IClient::SNAP_CURRENT);
+ for(int i = 0; i < Num; i++)
+ {
+ IClient::CSnapItem Item;
+ const void *pData = SnapGetItem(IClient::SNAP_CURRENT, i, &Item);
+
+ if(Item.m_Type == NETOBJTYPE_CLIENTINFO)
+ {
+ const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)pData;
+ int ClientID = Item.m_ID;
+ if(ClientID < MAX_CLIENTS)
+ {
+ CClientData *pClient = &m_aClients[ClientID];
+ IntsToStr(&pInfo->m_Name0, 4, pClient->m_aName);
+ }
+ }
+ }
+ }
+
+ void OnDemoPlayerSnapshot(void *pData, int Size)
+ {
+ unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
+ CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
+ const int AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer);
+ if(AltSnapSize < 0)
+ return;
+
+ std::swap(m_apSnapshots[IClient::SNAP_PREV], m_apSnapshots[IClient::SNAP_CURRENT]);
+ mem_copy(m_apSnapshots[IClient::SNAP_CURRENT]->m_pSnap, pData, Size);
+ mem_copy(m_apSnapshots[IClient::SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize);
+
+ OnNewSnapshot();
+ }
+};
+
+class CDemoPlayerMessageListener : public CDemoPlayer::IListener
+{
+public:
+ CDemoPlayer *m_pDemoPlayer;
+ CClientSnapshotHandler *m_pClientSnapshotHandler;
+
+ void OnDemoPlayerSnapshot(void *pData, int Size) override
+ {
+ m_pClientSnapshotHandler->OnDemoPlayerSnapshot(pData, Size);
+ }
+
+ void OnDemoPlayerMessage(void *pData, int Size) override
+ {
+ CUnpacker Unpacker;
+ Unpacker.Reset(pData, Size);
+ CMsgPacker Packer(NETMSG_EX, true);
+
+ int Msg;
+ bool Sys;
+ CUuid Uuid;
+
+ int Result = UnpackMessageID(&Msg, &Sys, &Uuid, &Unpacker, &Packer);
+ if(Result == UNPACKMESSAGE_ERROR)
+ return;
+
+ if(!Sys)
+ {
+ CNetObjHandler NetObjHandler;
+ void *pRawMsg = NetObjHandler.SecureUnpackMsg(Msg, &Unpacker);
+ if(!pRawMsg)
+ return;
+
+ if(Msg == NETMSGTYPE_SV_CHAT)
+ {
+ CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
+
+ if(pMsg->m_ClientID > -1 && m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientID].m_aName[0] == '\0')
+ return;
+
+ const char *Prefix = pMsg->m_Team > 1 ? "whisper" : (pMsg->m_Team ? "teamchat" : "chat");
+
+ if(pMsg->m_ClientID < 0)
+ {
+ printf("%s: *** %s\n", Prefix, pMsg->m_pMessage);
+ return;
+ }
+
+ if(pMsg->m_Team == 2) // WHISPER SEND
+ printf("%s: -> %s: %s\n", Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientID].m_aName, pMsg->m_pMessage);
+ else if(pMsg->m_Team == 3) // WHISPER RECEIVE
+ printf("%s: <- %s: %s\n", Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientID].m_aName, pMsg->m_pMessage);
+ else
+ printf("%s: %s: %s\n", Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientID].m_aName, pMsg->m_pMessage);
+ }
+ else if(Msg == NETMSGTYPE_SV_BROADCAST)
+ {
+ CNetMsg_Sv_Broadcast *pMsg = (CNetMsg_Sv_Broadcast *)pRawMsg;
+ char aBroadcast[1024];
+ while((pMsg->m_pMessage = str_next_token(pMsg->m_pMessage, "\n", aBroadcast, sizeof(aBroadcast))))
+ {
+ if(aBroadcast[0] != '\0')
+ {
+ printf("broadcast: %s\n", aBroadcast);
+ }
+ }
+ }
+ }
+ }
+};
+
+int Process(const char *pDemoFilePath, IStorage *pStorage)
+{
+ CSnapshotDelta DemoSnapshotDelta;
+ CDemoPlayer DemoPlayer(&DemoSnapshotDelta, false);
+
+ if(DemoPlayer.Load(pStorage, nullptr, pDemoFilePath, IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
+ {
+ dbg_msg(TOOL_NAME, "Demo file '%s' failed to load: %s", pDemoFilePath, DemoPlayer.ErrorMessage());
+ return -1;
+ }
+
+ CClientSnapshotHandler Handler;
+ CDemoPlayerMessageListener Listener;
+ Listener.m_pDemoPlayer = &DemoPlayer;
+ Listener.m_pClientSnapshotHandler = &Handler;
+ DemoPlayer.SetListener(&Listener);
+
+ const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
+ CNetBase::Init();
+ DemoPlayer.Play();
+
+ while(DemoPlayer.IsPlaying())
+ {
+ DemoPlayer.Update(false);
+ if(pInfo->m_Info.m_Paused)
+ break;
+ }
+
+ DemoPlayer.Stop();
+
+ return 0;
+}
+
+int main(int argc, const char *argv[])
+{
+ IStorage *pStorage = CreateLocalStorage();
+ if(!pStorage)
+ {
+ dbg_msg(TOOL_NAME, "Error loading storage");
+ return -1;
+ }
+
+ CCmdlineFix CmdlineFix(&argc, &argv);
+ log_set_global_logger_default();
+
+ if(argc != 2)
+ {
+ dbg_msg(TOOL_NAME, "Usage: %s [demo_filename]", TOOL_NAME);
+ return -1;
+ }
+
+ return Process(argv[1], pStorage);
+}