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