1#include <base/logger.h>
2#include <base/system.h>
3
4#include <engine/client.h>
5#include <engine/shared/demo.h>
6#include <engine/shared/network.h>
7#include <engine/shared/snapshot.h>
8#include <engine/storage.h>
9
10#include <game/gamecore.h>
11
12#include <memory>
13
14static const char *TOOL_NAME = "demo_extract_chat";
15
16class CClientSnapshotHandler
17{
18public:
19 struct CClientData
20 {
21 char m_aName[MAX_NAME_LENGTH];
22 };
23 CClientData m_aClients[MAX_CLIENTS];
24
25 char m_aaDemoSnapshotData[IClient::NUM_SNAPSHOT_TYPES][CSnapshot::MAX_SIZE];
26 CSnapshot *m_apAltSnapshots[IClient::NUM_SNAPSHOT_TYPES];
27
28 CClientSnapshotHandler() :
29 m_aClients()
30 {
31 mem_zero(block: m_aaDemoSnapshotData, size: sizeof(m_aaDemoSnapshotData));
32
33 for(int SnapshotType = 0; SnapshotType < IClient::NUM_SNAPSHOT_TYPES; SnapshotType++)
34 {
35 m_apAltSnapshots[SnapshotType] = (CSnapshot *)&m_aaDemoSnapshotData[SnapshotType];
36 }
37 }
38
39 int UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo)
40 {
41 CUnpacker Unpacker;
42 CSnapshotBuilder Builder;
43 Builder.Init();
44 CNetObjHandler NetObjHandler;
45
46 int Num = pFrom->NumItems();
47 for(int Index = 0; Index < Num; Index++)
48 {
49 const CSnapshotItem *pFromItem = pFrom->GetItem(Index);
50 const int FromItemSize = pFrom->GetItemSize(Index);
51 const int ItemType = pFrom->GetItemType(Index);
52 const void *pData = pFromItem->Data();
53 Unpacker.Reset(pData, Size: FromItemSize);
54
55 void *pRawObj = NetObjHandler.SecureUnpackObj(Type: ItemType, pUnpacker: &Unpacker);
56 if(!pRawObj)
57 continue;
58
59 const int ItemSize = NetObjHandler.GetUnpackedObjSize(Type: ItemType);
60 void *pObj = Builder.NewItem(Type: pFromItem->Type(), Id: pFromItem->Id(), Size: ItemSize);
61 if(!pObj)
62 return -4;
63
64 mem_copy(dest: pObj, source: pRawObj, size: ItemSize);
65 }
66
67 return Builder.Finish(pSnapdata: pTo);
68 }
69
70 int SnapNumItems(int SnapId)
71 {
72 dbg_assert(SnapId >= 0 && SnapId < IClient::NUM_SNAPSHOT_TYPES, "Invalid SnapId: %d", SnapId);
73 return m_apAltSnapshots[SnapId]->NumItems();
74 }
75
76 IClient::CSnapItem SnapGetItem(int SnapId, int Index)
77 {
78 dbg_assert(SnapId >= 0 && SnapId < IClient::NUM_SNAPSHOT_TYPES, "Invalid SnapId: %d", SnapId);
79 const CSnapshot *pSnapshot = m_apAltSnapshots[SnapId];
80 const CSnapshotItem *pSnapshotItem = m_apAltSnapshots[SnapId]->GetItem(Index);
81 IClient::CSnapItem Item;
82 Item.m_Type = pSnapshot->GetItemType(Index);
83 Item.m_Id = pSnapshotItem->Id();
84 Item.m_pData = pSnapshotItem->Data();
85 Item.m_DataSize = pSnapshot->GetItemSize(Index);
86 return Item;
87 }
88
89 void OnNewSnapshot()
90 {
91 int Num = SnapNumItems(SnapId: IClient::SNAP_CURRENT);
92 for(int i = 0; i < Num; i++)
93 {
94 const IClient::CSnapItem Item = SnapGetItem(SnapId: IClient::SNAP_CURRENT, Index: i);
95
96 if(Item.m_Type == NETOBJTYPE_CLIENTINFO)
97 {
98 const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)Item.m_pData;
99 int ClientId = Item.m_Id;
100 if(ClientId < MAX_CLIENTS)
101 {
102 CClientData *pClient = &m_aClients[ClientId];
103 IntsToStr(pInts: pInfo->m_aName, NumInts: std::size(pInfo->m_aName), pStr: pClient->m_aName, StrSize: sizeof(pClient->m_aName));
104 }
105 }
106 }
107 }
108
109 void OnDemoPlayerSnapshot(void *pData, int Size)
110 {
111 unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE];
112 CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer;
113 const int AltSnapSize = UnpackAndValidateSnapshot(pFrom: (CSnapshot *)pData, pTo: pAltSnapBuffer);
114 if(AltSnapSize < 0)
115 return;
116
117 std::swap(a&: m_apAltSnapshots[IClient::SNAP_PREV], b&: m_apAltSnapshots[IClient::SNAP_CURRENT]);
118 mem_copy(dest: m_apAltSnapshots[IClient::SNAP_CURRENT], source: pAltSnapBuffer, size: AltSnapSize);
119
120 OnNewSnapshot();
121 }
122};
123
124class CDemoPlayerMessageListener : public CDemoPlayer::IListener
125{
126public:
127 CDemoPlayer *m_pDemoPlayer;
128 CClientSnapshotHandler *m_pClientSnapshotHandler;
129
130 void OnDemoPlayerSnapshot(void *pData, int Size) override
131 {
132 m_pClientSnapshotHandler->OnDemoPlayerSnapshot(pData, Size);
133 }
134
135 void OnDemoPlayerMessage(void *pData, int Size) override
136 {
137 CUnpacker Unpacker;
138 Unpacker.Reset(pData, Size);
139 CMsgPacker Packer(NETMSG_EX, true);
140
141 int Msg;
142 bool Sys;
143 CUuid Uuid;
144
145 int Result = UnpackMessageId(pId: &Msg, pSys: &Sys, pUuid: &Uuid, pUnpacker: &Unpacker, pPacker: &Packer);
146 if(Result == UNPACKMESSAGE_ERROR)
147 return;
148
149 if(!Sys)
150 {
151 CNetObjHandler NetObjHandler;
152 void *pRawMsg = NetObjHandler.SecureUnpackMsg(Type: Msg, pUnpacker: &Unpacker);
153 if(!pRawMsg)
154 return;
155
156 const IDemoPlayer::CInfo &Info = m_pDemoPlayer->Info()->m_Info;
157 char aTime[20];
158 str_time(centisecs: (int64_t)(Info.m_CurrentTick - Info.m_FirstTick) / SERVER_TICK_SPEED * 100, format: TIME_HOURS, buffer: aTime, buffer_size: sizeof(aTime));
159
160 if(Msg == NETMSGTYPE_SV_CHAT)
161 {
162 CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg;
163
164 if(pMsg->m_ClientId > -1 && m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientId].m_aName[0] == '\0')
165 return;
166
167 const char *Prefix = pMsg->m_Team > 1 ? "whisper" : (pMsg->m_Team ? "teamchat" : "chat");
168
169 if(pMsg->m_ClientId < 0)
170 {
171 printf(format: "[%s] %s: *** %s\n", aTime, Prefix, pMsg->m_pMessage);
172 return;
173 }
174
175 if(pMsg->m_Team == TEAM_WHISPER_SEND)
176 printf(format: "[%s] %s: -> %s: %s\n", aTime, Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientId].m_aName, pMsg->m_pMessage);
177 else if(pMsg->m_Team == TEAM_WHISPER_RECV)
178 printf(format: "[%s] %s: <- %s: %s\n", aTime, Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientId].m_aName, pMsg->m_pMessage);
179 else
180 printf(format: "[%s] %s: %s: %s\n", aTime, Prefix, m_pClientSnapshotHandler->m_aClients[pMsg->m_ClientId].m_aName, pMsg->m_pMessage);
181 }
182 else if(Msg == NETMSGTYPE_SV_BROADCAST)
183 {
184 CNetMsg_Sv_Broadcast *pMsg = (CNetMsg_Sv_Broadcast *)pRawMsg;
185 char aBroadcast[1024];
186 while((pMsg->m_pMessage = str_next_token(str: pMsg->m_pMessage, delim: "\n", buffer: aBroadcast, buffer_size: sizeof(aBroadcast))))
187 {
188 if(aBroadcast[0] != '\0')
189 {
190 printf(format: "[%s] broadcast: %s\n", aTime, aBroadcast);
191 }
192 }
193 }
194 }
195 }
196};
197
198static int ExtractDemoChat(const char *pDemoFilePath, IStorage *pStorage)
199{
200 std::unique_ptr<CSnapshotDelta> pDemoSnapshotDelta = std::make_unique<CSnapshotDelta>();
201 CDemoPlayer DemoPlayer(pDemoSnapshotDelta.get(), false);
202
203 if(DemoPlayer.Load(pStorage, pConsole: nullptr, pFilename: pDemoFilePath, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE) == -1)
204 {
205 log_error(TOOL_NAME, "Demo file '%s' failed to load: %s", pDemoFilePath, DemoPlayer.ErrorMessage());
206 return -1;
207 }
208
209 CClientSnapshotHandler Handler;
210 CDemoPlayerMessageListener Listener;
211 Listener.m_pDemoPlayer = &DemoPlayer;
212 Listener.m_pClientSnapshotHandler = &Handler;
213 DemoPlayer.SetListener(&Listener);
214
215 const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info();
216 CNetBase::Init();
217 DemoPlayer.Play();
218
219 while(DemoPlayer.IsPlaying())
220 {
221 DemoPlayer.Update(RealTime: false);
222 if(pInfo->m_Info.m_Paused)
223 break;
224 }
225
226 DemoPlayer.Stop();
227
228 return 0;
229}
230
231int main(int argc, const char *argv[])
232{
233 // Create storage before setting logger to avoid log messages from storage creation
234 std::unique_ptr<IStorage> pStorage = CreateLocalStorage();
235
236 CCmdlineFix CmdlineFix(&argc, &argv);
237 log_set_global_logger_default();
238
239 if(!pStorage)
240 {
241 log_error(TOOL_NAME, "Error creating local storage");
242 return -1;
243 }
244
245 if(argc != 2)
246 {
247 log_error(TOOL_NAME, "Usage: %s <demo_filename>", TOOL_NAME);
248 return -1;
249 }
250
251 return ExtractDemoChat(pDemoFilePath: argv[1], pStorage: pStorage.get());
252}
253