Merge lp:~widelands-dev/widelands/bug-better-syncstreams into lp:widelands

Proposed by Notabilis
Status: Merged
Merged at revision: 8981
Proposed branch: lp:~widelands-dev/widelands/bug-better-syncstreams
Merge into: lp:widelands
Diff against target: 544 lines (+273/-11)
16 files modified
data/tribes/buildings/productionsites/atlanteans/mill/init.lua (+1/-1)
src/economy/economy.cc (+2/-1)
src/economy/request.cc (+1/-1)
src/logic/cmd_queue.cc (+1/-2)
src/logic/filesystem_constants.h (+1/-0)
src/logic/game.cc (+51/-0)
src/logic/game.h (+66/-1)
src/logic/map_objects/bob.cc (+1/-0)
src/logic/map_objects/map_object.cc (+7/-1)
src/logic/map_objects/tribes/battle.cc (+1/-1)
src/logic/map_objects/tribes/program_result.h (+1/-0)
src/logic/map_objects/tribes/worker.cc (+2/-2)
src/network/gameclient.cc (+5/-1)
src/network/gamehost.cc (+3/-0)
src/wlapplication.cc (+7/-0)
utils/syncstream/syncexcerpt-to-text.py (+123/-0)
To merge this branch: bzr merge lp:~widelands-dev/widelands/bug-better-syncstreams
Reviewer Review Type Date Requested Status
GunChleoc Approve
Review via email: mp+361922@code.launchpad.net

Commit message

Print more information in syncstreams. Create additional smaller syncstream files containing the last few seconds leading to a desync.

Description of the change

[Ready for review and merge]

Since I am not really able to get useful information out of the existing syncstream files, this branch adds further information to the syncstream describing the type of the syncstream entries.

Additionally, create a smaller syncstream extract file next to the syncstream file which contains only the last few seconds. This should be enough to debug the desync but reduces the size of the to-be-uploaded files.

To post a comment you must log in.
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4409. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/481989790.
Appveyor build 4200. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_bug_better_syncstreams-4200.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4411. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/482186977.
Appveyor build 4201. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_bug_better_syncstreams-4201.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I think this is an excellent idea, so I've done a preliminary code review. Let me know when you are finished for a final review.

Revision history for this message
Notabilis (notabilis27) wrote :

Thanks for the review. I answered some of your comments below and will push a merge-ready version later on.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Answered.

Revision history for this message
Notabilis (notabilis27) wrote :

As far as I am concerned, this branch is ready for review and merge now.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Code LGTM, not tested

review: Approve
Revision history for this message
Notabilis (notabilis27) wrote :

I tested(?) it by enforcing a desync (modifying player.cc and using std::rand() as ID for new economies, then changing economy targets ingame). The new *.wse files were created and could be parsed by the python script. No idea what else can be tested.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have done some similar testing. I'd like to see a few changes:

1. WLApplication:cleanup_replays() does not clean up old wse files
2. There are no instructions in the Python script.
   I'd like to at least see a usage hint when it's called without parameters.

Revision history for this message
Notabilis (notabilis27) wrote :

I wrote some documentation, do you think it is understandable and sufficient?

Revision history for this message
GunChleoc (gunchleoc) wrote :

Looks good to me, let's have it!

@bunnybot merge

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/tribes/buildings/productionsites/atlanteans/mill/init.lua'
2--- data/tribes/buildings/productionsites/atlanteans/mill/init.lua 2018-09-06 08:21:35 +0000
3+++ data/tribes/buildings/productionsites/atlanteans/mill/init.lua 2019-02-10 11:27:32 +0000
4@@ -74,7 +74,7 @@
5 }
6 },
7 produce_blackroot_flour = {
8- -- TRANSLATORS: Completed/Skipped/Did not start grinding blackrootbecause ...
9+ -- TRANSLATORS: Completed/Skipped/Did not start grinding blackroot because ...
10 descname = _"grinding blackroot",
11 actions = {
12 -- No check whether we need blackroot_flour because blackroots cannot be used for anything else.
13
14=== modified file 'src/economy/economy.cc'
15--- src/economy/economy.cc 2019-02-09 08:42:15 +0000
16+++ src/economy/economy.cc 2019-02-10 11:27:32 +0000
17@@ -711,6 +711,7 @@
18 // alerts, so add info to the sync stream here.
19 {
20 ::StreamWrite& ss = game.syncstream();
21+ ss.unsigned_8(SyncEntry::kProcessRequests);
22 ss.unsigned_8(req.get_type());
23 ss.unsigned_8(req.get_index());
24 ss.unsigned_32(req.target().serial());
25@@ -1051,7 +1052,7 @@
26 // to avoid potential future problems caused by the supplies_ changing
27 // under us in some way.
28 ::StreamWrite& ss = game.syncstream();
29- ss.unsigned_32(0x02decafa); // appears as facade02 in sync stream
30+ ss.unsigned_8(SyncEntry::kHandleActiveSupplies);
31 ss.unsigned_32(assignments.size());
32
33 for (const auto& temp_assignment : assignments) {
34
35=== modified file 'src/economy/request.cc'
36--- src/economy/request.cc 2018-12-13 07:24:01 +0000
37+++ src/economy/request.cc 2019-02-10 11:27:32 +0000
38@@ -380,7 +380,7 @@
39 assert(is_open());
40
41 ::StreamWrite& ss = game.syncstream();
42- ss.unsigned_32(0x01decafa); // appears as facade01 in sync stream
43+ ss.unsigned_8(SyncEntry::kStartTransfer);
44 ss.unsigned_32(target().serial());
45 ss.unsigned_32(supp.get_position(game)->serial());
46
47
48=== modified file 'src/logic/cmd_queue.cc'
49--- src/logic/cmd_queue.cc 2018-12-13 07:24:01 +0000
50+++ src/logic/cmd_queue.cc 2019-02-10 11:27:32 +0000
51@@ -114,8 +114,7 @@
52
53 if (dynamic_cast<GameLogicCommand*>(&c)) {
54 StreamWrite& ss = game_.syncstream();
55- static uint8_t const tag[] = {0xde, 0xad, 0x00};
56- ss.data(tag, 3); // provide an easy-to-find pattern as debugging aid
57+ ss.unsigned_8(SyncEntry::kRunQueue);
58 ss.unsigned_32(c.duetime());
59 ss.unsigned_32(static_cast<uint32_t>(c.id()));
60 }
61
62=== modified file 'src/logic/filesystem_constants.h'
63--- src/logic/filesystem_constants.h 2018-11-09 08:00:35 +0000
64+++ src/logic/filesystem_constants.h 2019-02-10 11:27:32 +0000
65@@ -49,6 +49,7 @@
66 const std::string kReplayDir = "replays";
67 const std::string kReplayExtension = ".wrpl";
68 const std::string kSyncstreamExtension = ".wss";
69+const std::string kSyncstreamExcerptExtension = ".wse";
70 // The time in seconds for how long old replays/syncstreams should be kept
71 // around, in seconds. Right now this is 4 weeks.
72 constexpr double kReplayKeepAroundTime = 4 * 7 * 24 * 60 * 60;
73
74=== modified file 'src/logic/game.cc'
75--- src/logic/game.cc 2019-02-09 08:42:15 +0000
76+++ src/logic/game.cc 2019-02-10 11:27:32 +0000
77@@ -37,6 +37,7 @@
78 #include "base/macros.h"
79 #include "base/time_string.h"
80 #include "base/warning.h"
81+#include "build_info.h"
82 #include "economy/economy.h"
83 #include "game_io/game_loader.h"
84 #include "game_io/game_preload_packet.h"
85@@ -87,6 +88,8 @@
86 void Game::SyncWrapper::start_dump(const std::string& fname) {
87 dumpfname_ = fname + kSyncstreamExtension;
88 dump_.reset(g_fs->open_stream_write(dumpfname_));
89+ current_excerpt_id_ = 0;
90+ excerpts_buffer_[current_excerpt_id_].clear();
91 }
92
93 void Game::SyncWrapper::data(void const* const sync_data, size_t const size) {
94@@ -114,6 +117,8 @@
95 log("Writing to syncstream file %s failed. Stop synctream dump.\n", dumpfname_.c_str());
96 dump_.reset();
97 }
98+ assert(current_excerpt_id_ < kExcerptSize);
99+ excerpts_buffer_[current_excerpt_id_].append(static_cast<const char*>(sync_data), size);
100 }
101
102 target_.data(sync_data, size);
103@@ -604,6 +609,51 @@
104 }
105
106 /**
107+ * Switches to the next part of the syncstream excerpt.
108+ */
109+void Game::report_sync_request() {
110+ syncwrapper_.current_excerpt_id_ = (syncwrapper_.current_excerpt_id_ + 1) % SyncWrapper::kExcerptSize;
111+ syncwrapper_.excerpts_buffer_[syncwrapper_.current_excerpt_id_].clear();
112+}
113+
114+/**
115+ * Triggers writing of syncstream excerpt and adds the playernumber of the desynced player
116+ * to the stream.
117+ * Playernumber should be negative when called by network clients
118+ */
119+void Game::report_desync(int32_t playernumber) {
120+ if (syncwrapper_.dumpfname_.empty()) {
121+ log("Error: A desync occurred but no filename for the syncstream has been set.");
122+ return;
123+ }
124+ // Replace .wss extension of syncstream file with .wse extension for syncstream extract
125+ std::string filename = syncwrapper_.dumpfname_;
126+ assert(syncwrapper_.dumpfname_.length() > kSyncstreamExtension.length());
127+ filename.replace(filename.length() - kSyncstreamExtension.length(),
128+ kSyncstreamExtension.length(), kSyncstreamExcerptExtension);
129+ std::unique_ptr<StreamWrite> file(g_fs->open_stream_write(filename));
130+ assert(file != nullptr);
131+ // Write revision, branch and build type of this build to the file
132+ file->unsigned_32(build_id().length());
133+ file->text(build_id());
134+ file->unsigned_32(build_type().length());
135+ file->text(build_type());
136+ file->signed_32(playernumber);
137+ // Write our buffers to the file. Start with the oldest one
138+ const size_t i2 = (syncwrapper_.current_excerpt_id_ + 1) % SyncWrapper::kExcerptSize;
139+ size_t i = i2;
140+ do {
141+ file->text(syncwrapper_.excerpts_buffer_[i]);
142+ syncwrapper_.excerpts_buffer_[i].clear();
143+ i = (i + 1) % SyncWrapper::kExcerptSize;
144+ } while (i != i2);
145+ file->unsigned_8(SyncEntry::kDesync);
146+ file->signed_32(playernumber);
147+ // Restart buffers
148+ syncwrapper_.current_excerpt_id_ = 0;
149+}
150+
151+/**
152 * Calculate the current synchronization checksum and copy
153 * it into the given array, without affecting the subsequent
154 * checksumming process.
155@@ -625,6 +675,7 @@
156 */
157 uint32_t Game::logic_rand() {
158 uint32_t const result = rng().rand();
159+ syncstream().unsigned_8(SyncEntry::kRandom);
160 syncstream().unsigned_32(result);
161 return result;
162 }
163
164=== modified file 'src/logic/game.h'
165--- src/logic/game.h 2018-12-13 07:24:01 +0000
166+++ src/logic/game.h 2019-02-10 11:27:32 +0000
167@@ -63,6 +63,55 @@
168 gs_ending
169 };
170
171+// The entry types that are written to the syncstream
172+// The IDs are a number in the higher 4 bits and the length in bytes in the lower 4 bits
173+// Keep this synchronized with utils/syncstream/syncexcerpt-to-text.py
174+enum SyncEntry : uint8_t {
175+ // Used in:
176+ // game.cc Game::report_desync()
177+ // Parameters:
178+ // s32 id of desynced user, -1 when written on client
179+ kDesync = 0x14,
180+ // map_object.cc CmdDestroyMapObject::execute()
181+ // u32 object serial
182+ kDestroyObject = 0x24,
183+ // economy.cc Economy::process_requests()
184+ // u8 request type
185+ // u8 request index
186+ // u32 target serial
187+ kProcessRequests = 0x36,
188+ // economy.cc Economy::handle_active_supplies()
189+ // u32 assignments size
190+ kHandleActiveSupplies = 0x44,
191+ // request.cc Request::start_transfer()
192+ // u32 target serial
193+ // u32 source(?) serial
194+ kStartTransfer = 0x58,
195+ // cmd_queue.cc CmdQueue::run_queue()
196+ // u32 duetime
197+ // u32 command id
198+ kRunQueue = 0x68,
199+ // game.h Game::logic_rand_seed()
200+ // u32 random seed
201+ kRandomSeed = 0x74,
202+ // game.cc Game::logic_rand()
203+ // u32 random value
204+ kRandom = 0x84,
205+ // map_object.cc CmdAct::execute()
206+ // u32 object serial
207+ // u8 object type (see map_object.h MapObjectType)
208+ kCmdAct = 0x95,
209+ // battle.cc Battle::Battle()
210+ // u32 first soldier serial
211+ // u32 second soldier serial
212+ kBattle = 0xA8,
213+ // bob.cc Bob::set_position()
214+ // u32 bob serial
215+ // s16 position x
216+ // s16 position y
217+ kBobSetPosition = 0xB8
218+};
219+
220 class Player;
221 class MapLoader;
222 class PlayerCommand;
223@@ -187,9 +236,13 @@
224
225 void logic_rand_seed(uint32_t const seed) {
226 rng().seed(seed);
227+ syncstream().unsigned_8(SyncEntry::kRandomSeed);
228+ syncstream().unsigned_32(seed);
229 }
230
231 StreamWrite& syncstream();
232+ void report_sync_request();
233+ void report_desync(int32_t playernumber);
234 Md5Checksum get_sync_hash() const;
235
236 void enqueue_command(Command* const);
237@@ -279,7 +332,8 @@
238 target_(target),
239 counter_(0),
240 next_diskspacecheck_(0),
241- syncstreamsave_(false) {
242+ syncstreamsave_(false),
243+ current_excerpt_id_(0) {
244 }
245
246 ~SyncWrapper() override;
247@@ -304,6 +358,17 @@
248 std::unique_ptr<StreamWrite> dump_;
249 std::string dumpfname_;
250 bool syncstreamsave_;
251+ // Use a cyclic buffer for storing parts of the syncstream
252+ // Currently used buffer
253+ size_t current_excerpt_id_;
254+ // (Arbitrary) count of buffers
255+ // Syncreports seem to be requested from the network clients every game second so this
256+ // buffer should be big enough to store the last 32 seconds of the game actions leading
257+ // up to the desync
258+ static constexpr size_t kExcerptSize = 32;
259+ // Array of byte buffers
260+ // std::string is used as a binary buffer here
261+ std::string excerpts_buffer_[kExcerptSize];
262 } syncwrapper_;
263
264 GameController* ctrl_;
265
266=== modified file 'src/logic/map_objects/bob.cc'
267--- src/logic/map_objects/bob.cc 2018-12-13 07:24:01 +0000
268+++ src/logic/map_objects/bob.cc 2019-02-10 11:27:32 +0000
269@@ -893,6 +893,7 @@
270 // randomly generated movements.
271 if (upcast(Game, game, &egbase)) {
272 StreamWrite& ss = game->syncstream();
273+ ss.unsigned_8(SyncEntry::kBobSetPosition);
274 ss.unsigned_32(serial());
275 ss.signed_16(coords.x);
276 ss.signed_16(coords.y);
277
278=== modified file 'src/logic/map_objects/map_object.cc'
279--- src/logic/map_objects/map_object.cc 2018-12-13 07:24:01 +0000
280+++ src/logic/map_objects/map_object.cc 2019-02-10 11:27:32 +0000
281@@ -49,6 +49,7 @@
282 }
283
284 void CmdDestroyMapObject::execute(Game& game) {
285+ game.syncstream().unsigned_8(SyncEntry::kDestroyObject);
286 game.syncstream().unsigned_32(obj_serial);
287
288 if (MapObject* obj = game.objects().get_object(obj_serial))
289@@ -94,10 +95,15 @@
290 }
291
292 void CmdAct::execute(Game& game) {
293+ game.syncstream().unsigned_8(SyncEntry::kCmdAct);
294 game.syncstream().unsigned_32(obj_serial);
295
296- if (MapObject* const obj = game.objects().get_object(obj_serial))
297+ if (MapObject* const obj = game.objects().get_object(obj_serial)) {
298+ game.syncstream().unsigned_8(static_cast<uint8_t>(obj->descr().type()));
299 obj->act(game, arg);
300+ } else {
301+ game.syncstream().unsigned_8(static_cast<uint8_t>(MapObjectType::MAPOBJECT));
302+ }
303 // the object must queue the next CMD_ACT itself if necessary
304 }
305
306
307=== modified file 'src/logic/map_objects/tribes/battle.cc'
308--- src/logic/map_objects/tribes/battle.cc 2018-12-13 07:24:01 +0000
309+++ src/logic/map_objects/tribes/battle.cc 2019-02-10 11:27:32 +0000
310@@ -65,7 +65,7 @@
311 assert(first_soldier->get_owner() != second_soldier->get_owner());
312 {
313 StreamWrite& ss = game.syncstream();
314- ss.unsigned_32(0x00e111ba); // appears as ba111e00 in a hexdump
315+ ss.unsigned_8(SyncEntry::kBattle);
316 ss.unsigned_32(first_soldier->serial());
317 ss.unsigned_32(second_soldier->serial());
318 }
319
320=== modified file 'src/logic/map_objects/tribes/program_result.h'
321--- src/logic/map_objects/tribes/program_result.h 2018-12-13 07:24:01 +0000
322+++ src/logic/map_objects/tribes/program_result.h 2019-02-10 11:27:32 +0000
323@@ -21,6 +21,7 @@
324 #define WL_LOGIC_MAP_OBJECTS_TRIBES_PROGRAM_RESULT_H
325
326 namespace Widelands {
327+// Don't change this values, they are used as hardcoded array indices
328 enum ProgramResult { None = 0, Failed = 1, Completed = 2, Skipped = 3 };
329 enum ProgramResultHandlingMethod { Fail, Complete, Skip, Continue, Repeat };
330 } // namespace Widelands
331
332=== modified file 'src/logic/map_objects/tribes/worker.cc'
333--- src/logic/map_objects/tribes/worker.cc 2018-12-13 07:24:01 +0000
334+++ src/logic/map_objects/tribes/worker.cc 2019-02-10 11:27:32 +0000
335@@ -2651,7 +2651,7 @@
336 }
337 }
338
339-/** Check if militray sites have becom visible by now.
340+/** Check if militray sites have become visible by now.
341 *
342 * After the pop in prepare_scouts_worklist,
343 * the latest entry of scouts_worklist is the next MS to visit (if known)
344@@ -2684,7 +2684,7 @@
345 const Player& player,
346 std::vector<ImmovableFound>& found_sites) {
347
348- // If there are many enemy sites, push a random walk request into queue evry third finding.
349+ // If there are many enemy sites, push a random walk request into queue every third finding.
350 uint32_t haveabreak = 3;
351
352 for (const ImmovableFound& vu : found_sites) {
353
354=== modified file 'src/network/gameclient.cc'
355--- src/network/gameclient.cc 2018-12-13 07:24:01 +0000
356+++ src/network/gameclient.cc 2019-02-10 11:27:32 +0000
357@@ -860,6 +860,7 @@
358 int32_t const time = packet.signed_32();
359 d->time.receive(time);
360 d->game->enqueue_command(new CmdNetCheckSync(time, [this] { sync_report_callback(); }));
361+ d->game->report_sync_request();
362 break;
363 }
364
365@@ -891,8 +892,11 @@
366 case NETCMD_INFO_DESYNC:
367 log("[Client] received NETCMD_INFO_DESYNC. Trying to salvage some "
368 "information for debugging.\n");
369- if (d->game)
370+ if (d->game) {
371 d->game->save_syncstream(true);
372+ // We don't know our playernumber, so report as -1
373+ d->game->report_desync(-1);
374+ }
375 break;
376
377 default:
378
379=== modified file 'src/network/gamehost.cc'
380--- src/network/gamehost.cc 2019-02-09 09:03:23 +0000
381+++ src/network/gamehost.cc 2019-02-10 11:27:32 +0000
382@@ -1875,6 +1875,7 @@
383 }
384
385 log("[Host]: Requesting sync reports for time %i\n", d->syncreport_time);
386+ d->game->report_sync_request();
387
388 SendPacket packet;
389 packet.unsigned_8(NETCMD_SYNCREQUEST);
390@@ -1916,6 +1917,8 @@
391 i, d->syncreport.str().c_str(), client.syncreport.str().c_str());
392
393 d->game->save_syncstream(true);
394+ // Create syncstream excerpt and add faulting player number
395+ d->game->report_desync(i);
396
397 SendPacket packet;
398 packet.unsigned_8(NETCMD_INFO_DESYNC);
399
400=== modified file 'src/wlapplication.cc'
401--- src/wlapplication.cc 2018-12-13 07:24:01 +0000
402+++ src/wlapplication.cc 2019-02-10 11:27:32 +0000
403@@ -1470,6 +1470,13 @@
404 if (is_autogenerated_and_expired(filename)) {
405 log("Delete syncstream %s\n", filename.c_str());
406 g_fs->fs_unlink(filename);
407+ assert(filename.length() > kSyncstreamExtension.length());
408+ std::string filename_wse = filename.substr(0, filename.length() - kSyncstreamExtension.length());
409+ filename_wse += kSyncstreamExcerptExtension;
410+ if (g_fs->file_exists(filename_wse)) {
411+ log("Delete syncstream excerpt %s\n", filename_wse.c_str());
412+ g_fs->fs_unlink(filename_wse);
413+ }
414 }
415 }
416
417
418=== added file 'utils/syncstream/syncexcerpt-to-text.py'
419--- utils/syncstream/syncexcerpt-to-text.py 1970-01-01 00:00:00 +0000
420+++ utils/syncstream/syncexcerpt-to-text.py 2019-02-10 11:27:32 +0000
421@@ -0,0 +1,123 @@
422+#!/usr/bin/env python3
423+
424+import struct
425+import sys
426+
427+# WARNING!!
428+# Keep this file in sync with the enum SyncEntry in src/logic/game.h
429+
430+def handle_1(f):
431+ print("Desync:")
432+ # Read 4 bytes and interprete them as an signed 32 bit integer
433+ print(" Desynced player:", struct.unpack('<i', f.read(4))[0])
434+
435+def handle_2(f):
436+ print("DestroyObject:")
437+ print(" Serial:", struct.unpack('<I', f.read(4))[0])
438+
439+def handle_3(f):
440+ print("ProcessRequests:")
441+ print(" Type:", struct.unpack('<B', f.read(1))[0])
442+ print(" Index:", struct.unpack('<B', f.read(1))[0])
443+ print(" Serial:", struct.unpack('<I', f.read(4))[0])
444+
445+def handle_4(f):
446+ print("HandleActiveSupplies:")
447+ print(" Size:", struct.unpack('<I', f.read(4))[0])
448+
449+def handle_5(f):
450+ print("StartTransfer:")
451+ print(" Target serial:", struct.unpack('<I', f.read(4))[0])
452+ print(" Source serial:", struct.unpack('<I', f.read(4))[0])
453+
454+def handle_6(f):
455+ print("RunQueue:")
456+ print(" Duetime:", struct.unpack('<I', f.read(4))[0])
457+ print(" Command:", struct.unpack('<I', f.read(4))[0])
458+
459+def handle_7(f):
460+ print("RandomSeed:")
461+ print(" Seed:", struct.unpack('<I', f.read(4))[0])
462+
463+def handle_8(f):
464+ print("Random:")
465+ print(" Number:", struct.unpack('<I', f.read(4))[0])
466+
467+def handle_9(f):
468+ print("CmdAct:")
469+ print(" Serial:", struct.unpack('<I', f.read(4))[0])
470+ # See MapObjectType in map_object.h for decoding the type
471+ print(" Actor type:", struct.unpack('<B', f.read(1))[0])
472+
473+def handle_A(f):
474+ print("Battle:")
475+ print(" Serial first soldier:", struct.unpack('<I', f.read(4))[0])
476+ print(" Serial second soldier:", struct.unpack('<I', f.read(4))[0])
477+
478+def handle_B(f):
479+ print("BobSetPosition:")
480+ print(" Serial:", struct.unpack('<I', f.read(4))[0])
481+ print(" Position X:", struct.unpack('<h', f.read(2))[0])
482+ print(" Position y:", struct.unpack('<h', f.read(2))[0])
483+
484+handlers = {
485+ 1: handle_1,
486+ 2: handle_2,
487+ 3: handle_3,
488+ 4: handle_4,
489+ 5: handle_5,
490+ 6: handle_6,
491+ 7: handle_7,
492+ 8: handle_8,
493+ 9: handle_9,
494+ 10: handle_A,
495+ 11: handle_B
496+ }
497+
498+
499+if len(sys.argv) != 2:
500+ print("Usage: %s filename.wse > outfile.txt\n" % sys.argv[0])
501+
502+ print("Parses the hex-data in the given Widelands Syncstream Excerpt")
503+ print("into human readable form and echos it to stdout.\n")
504+
505+ print("You should do this with both the *.wse files of the host and")
506+ print("the desyncing client. Which player desynced is also reported")
507+ print("at the beginning of the output file of the host.")
508+ print("Afterwards, the resulting files can be compared with the")
509+ print("'diff' command. For correctly syncronized games the resulting")
510+ print("files should be the same, but most likely there will be some")
511+ print("small differences in the files (e.g., commands that are only")
512+ print("executed for one of the players).")
513+ print("There probably will be a large block of commands present at")
514+ print("the beginning of only one of the files, this is normal and")
515+ print("can be ignored.\n")
516+
517+ print("To find out more about the commands and where in the Widelands")
518+ print("code they are used, see the enum SyncEntry in src/logic/game.h")
519+ sys.exit(1)
520+
521+with open(sys.argv[1], "rb") as f:
522+ print("Created with %s(%s)"
523+ % (str(f.read(struct.unpack('<I', f.read(4))[0]).decode()),
524+ str(f.read(struct.unpack('<I', f.read(4))[0]).decode())),
525+ end='')
526+ playernumber = struct.unpack('<i', f.read(4))[0]
527+ if playernumber < 0:
528+ print(" as client")
529+ else:
530+ print(" as host, desynced client = %i" % playernumber)
531+ byte = f.read(1)
532+ while byte:
533+ # Parse byte
534+ b = ord(byte)
535+ cmd = b >> 4
536+ length = b & 0x0F
537+ if cmd in handlers:
538+ handlers[cmd](f)
539+ else:
540+ # Unknown command. Skip as many bytes as the entry is long
541+ print("Warning: Unknown command code '%s', skipping %i bytes" % (cmd, length))
542+ for i in range(0, length):
543+ byte = f.read(1)
544+ byte = f.read(1)

Subscribers

People subscribed via source and target branches

to status/vote changes: