Merge lp:~widelands-dev/widelands/refactor_gameclient into lp:widelands

Proposed by Klaus Halfmann
Status: Merged
Merged at revision: 9125
Proposed branch: lp:~widelands-dev/widelands/refactor_gameclient
Merge into: lp:widelands
Diff against target: 1614 lines (+628/-500)
17 files modified
src/ai/defaultai.cc (+1/-1)
src/logic/game.cc (+25/-26)
src/logic/game.h (+1/-1)
src/logic/game_controller.h (+3/-1)
src/logic/replay_game_controller.cc (+1/-1)
src/logic/replay_game_controller.h (+1/-1)
src/logic/single_player_game_controller.cc (+3/-3)
src/logic/single_player_game_controller.h (+1/-1)
src/network/gameclient.cc (+548/-442)
src/network/gameclient.h (+21/-5)
src/network/gamehost.cc (+8/-8)
src/network/gamehost.h (+1/-1)
src/network/netclient_interface.h (+5/-0)
src/network/nethostproxy.cc (+1/-1)
src/wui/economy_options_window.cc (+4/-4)
src/wui/game_message_menu.cc (+3/-3)
src/wui/warehousewindow.cc (+1/-1)
To merge this branch: bzr merge lp:~widelands-dev/widelands/refactor_gameclient
Reviewer Review Type Date Requested Status
GunChleoc Approve
Klaus Halfmann Needs Resubmitting
Review via email: mp+366743@code.launchpad.net

Commit message

Refactor gameclient.h/.cc for better readability

Description of the change

* Refactor big switch statement in gameclient.cc
* Refactor ::run() to improve readability
* use send_player_command with Ptr instead of Reference to clarify ownership of PlayerCommand.
* Added more comments

Added some TODOs and empty comments where I did not know the details.

There should be no functional change at all.

To post a comment you must log in.
Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Regressiontest are ok: Ran 44 tests in 1377.106s
Will try to do a network game, too.

9090. By Klaus Halfman \<<email address hidden>\>

Avoid extra breaks in GameClient::handle_packet

9091. By Klaus Halfman \<<email address hidden>\>

Fixed typo

9092. By Klaus Halfman \<<email address hidden>\>

fixed comments in netclient_interface.h

Revision history for this message
GunChleoc (gunchleoc) :
9093. By Klaus Halfman \<<email address hidden>\>

Adressed guns comments

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Hello Gun:
* Adressed some comments
* Get the tribe and check whether it exists before you push it
  -> I have no idea (yet) what this code does :-)
* Playercommand is not for current player , not sure if this can normally happen
  lets wait If I ever see this, then we can make it an assert.
* The This / this confusion is because I moved some fucntion into Impl, but they need
  the main class. I could use "outer" or some other name to avoid it.
* send_player_command(Widelands::PlayerCommand* pc) making this a unique_ptr:
  * Will affect a lot of code
  * Some aspects of this code are not clear to me (e.g. Commnands a queued for networking)
  -> lets adress this in a seperate branch, I already regret I touched it :-)
* I would leave "GameClientImpl* d" as is:
  * it is used very often
  * it is used in the local scope only -> no risc to loose control
  * commonly used Pattern in Widelands
* d->settings.usernum == -2,-1, n seems to be used to define the "hello" handshake
  what about "kPreHello" and "kAfterHello" constants?

Maybe we should move the two remaining variables to GameClientImpl, too?

Please check the logic of the main switch statement, I changed some control
flow for better readbility.

I intend to do a similar refactoring with gamehost, too.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4845. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/526796650.
Appveyor build 4626. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_refactor_gameclient-4626.

Revision history for this message
GunChleoc (gunchleoc) wrote :

* Get the tribe and check whether it exists before you push it
  -> I have no idea (yet) what this code does :-)

It's for the tips in the game loading screen. We have tribe-specific lists of tips.

There is a function bool Widelands::tribe_exists(const std::string& tribename) that you can use.

* The This / this confusion is because I moved some fucntion into Impl, but they need
  the main class. I could use "outer" or some other name to avoid it.

I see now - it's just an ugly variable name, yes, please change the name to "game_client" or "parent" or some such.

* send_player_command(Widelands::PlayerCommand* pc) making this a unique_ptr:
  -> lets adress this in a seperate branch, I already regret I touched it :-)

LOL fair enough. Cans or worms are best dealt with separately.

* I would leave "GameClientImpl* d" as is:
  * it is used very often
  * it is used in the local scope only -> no risc to loose control
  * commonly used Pattern in Widelands

That's because it was written before we switched to C++11, so there was no unique_ptr available. You don't have to do it in this branch though.

* d->settings.usernum == -2,-1, n seems to be used to define the "hello" handshake
  what about "kPreHello" and "kAfterHello" constants?

I have found this comment:
  /**
   * Checks if client \ref name exists and \returns int32_t :
   * - the client number if found
   * - -1 if no client was found
   * - -2 if the host is the client (has no client number)
   */

So, kUnknownClient and kUnknownHost?

* Maybe we should move the two remaining variables to GameClientImpl, too?

Consistency is good :)

9094. By Klaus Halfman \<<email address hidden>\>

review GameClient / GameClientImpl relation

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

* Used parent instead of This.
* moved all Variables into Impl

About that -2 magic: I got lost in finding the sematics of user/player etc.
I added a TODO comment with our best guess by now.

I am at the End of my vacation (sigh) so lets get this in before it
starts lingering around for too long.

9095. By Klaus Halfman \<<email address hidden>\>

fixed condition in handle_hello, preallocate tipstext

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Found a first bug, still need some debugging

9096. By Klaus Halfman \<<email address hidden>\>

Cannot create tipstext as expected

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Still get a heap-use-after-free related to
    std::unique_ptr<UI::ProgressWindow> loader_ui(new UI::ProgressWindow());

But this is maybe just a problem as the game crashed with disconnect(CLIENT_CRASHED, )

A have a deja vue around GameClientImpl.modal which is released twice.

Revision history for this message
GunChleoc (gunchleoc) wrote :

You could also do the game tips like this:

    const std::string tribename = get_players_tribe();
    assert(Widelands::tribe_exists(tribename));
    std::unique_ptr<GameTips> tips(new GameTips(*loader, {"general_game", "multiplayer", tribename}));

As to the ASan crash, I suspect that there is a problem with the lifetime of an object caused by the pulling out of stuff into separate functions.

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4871. State: failed. Details: https://travis-ci.org/widelands/widelands/builds/527824464.
Appveyor build 4652. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_refactor_gameclient-4652.

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Using unique_ptr<GameTips> is unneeded, this is only a helper class neede to as long as some progress dialog is open, so using normal scope is ok.
(We should actually cleanup that usage, some other time)

The Problem is the owenership of UI::ProgressWindow* loader

The disconnect code try to close the last 'modal' window.
In case of the loader this is incorrect.

Not sure how to cleanup tis messs by now

9097. By Klaus Halfman \<<email address hidden>\>

first fixes around modal usage

9098. By Klaus Halfman \<<email address hidden>\>

fix: ! operator on settings.savegame was lost

9099. By Klaus Halfman \<<email address hidden>\>

TODO about racecondition

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Found that I lost a '!' while refactoring.

Now a normal network game works as itendend.

I assume that disconnect/double freee has always been there,
I will create a followup bug for that one.
When debugging I triggered a racecondition, I doubt we ever will see that i the wild.

Gun: and all, please take anotheer look

review: Needs Resubmitting
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 4891. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/528361519.
Appveyor build 4672. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_refactor_gameclient-4672.

9100. By Klaus Halfman \<<email address hidden>\>

Merged trunk

Revision history for this message
Klaus Halfmann (klaus-halfmann) wrote :

Now why can we nort merge this one?

Revision history for this message
GunChleoc (gunchleoc) wrote :

Sorry, your recent changes slipped me by. Thanks for the reminder, I will do another review & testing.

9101. By GunChleoc

Small tweak

9102. By GunChleoc

Merged trunk.

Revision history for this message
GunChleoc (gunchleoc) wrote :

I have done some testing and it seems to work fine. I am not getting the double free you mentioned.

@bunnybot merge

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/ai/defaultai.cc'
2--- src/ai/defaultai.cc 2019-05-22 11:23:14 +0000
3+++ src/ai/defaultai.cc 2019-05-25 17:10:52 +0000
4@@ -6612,7 +6612,7 @@
5 const uint16_t new_target = std::max<uint16_t>(default_target * multiplier / 10, 3);
6 assert(new_target > 1);
7
8- game().send_player_command(*new Widelands::CmdSetWareTargetQuantity(
9+ game().send_player_command(new Widelands::CmdSetWareTargetQuantity(
10 gametime, player_number(), observer->economy.serial(), id, new_target));
11 }
12 }
13
14=== modified file 'src/logic/game.cc'
15--- src/logic/game.cc 2019-05-11 18:19:20 +0000
16+++ src/logic/game.cc 2019-05-25 17:10:52 +0000
17@@ -711,7 +711,7 @@
18 * It takes the appropriate action, i.e. either add to the cmd_queue or send
19 * across the network.
20 */
21-void Game::send_player_command(PlayerCommand& pc) {
22+void Game::send_player_command(PlayerCommand* pc) {
23 ctrl_->send_player_command(pc);
24 }
25
26@@ -734,55 +734,55 @@
27
28 // we might want to make these inlines:
29 void Game::send_player_bulldoze(PlayerImmovable& pi, bool const recurse) {
30- send_player_command(*new CmdBulldoze(get_gametime(), pi.owner().player_number(), pi, recurse));
31+ send_player_command(new CmdBulldoze(get_gametime(), pi.owner().player_number(), pi, recurse));
32 }
33
34 void Game::send_player_dismantle(PlayerImmovable& pi) {
35- send_player_command(*new CmdDismantleBuilding(get_gametime(), pi.owner().player_number(), pi));
36+ send_player_command(new CmdDismantleBuilding(get_gametime(), pi.owner().player_number(), pi));
37 }
38
39 void Game::send_player_build(int32_t const pid, const Coords& coords, DescriptionIndex const id) {
40 assert(tribes().building_exists(id));
41- send_player_command(*new CmdBuild(get_gametime(), pid, coords, id));
42+ send_player_command(new CmdBuild(get_gametime(), pid, coords, id));
43 }
44
45 void Game::send_player_build_flag(int32_t const pid, const Coords& coords) {
46- send_player_command(*new CmdBuildFlag(get_gametime(), pid, coords));
47+ send_player_command(new CmdBuildFlag(get_gametime(), pid, coords));
48 }
49
50 void Game::send_player_build_road(int32_t pid, Path& path) {
51- send_player_command(*new CmdBuildRoad(get_gametime(), pid, path));
52+ send_player_command(new CmdBuildRoad(get_gametime(), pid, path));
53 }
54
55 void Game::send_player_flagaction(Flag& flag) {
56- send_player_command(*new CmdFlagAction(get_gametime(), flag.owner().player_number(), flag));
57+ send_player_command(new CmdFlagAction(get_gametime(), flag.owner().player_number(), flag));
58 }
59
60 void Game::send_player_start_stop_building(Building& building) {
61 send_player_command(
62- *new CmdStartStopBuilding(get_gametime(), building.owner().player_number(), building));
63+ new CmdStartStopBuilding(get_gametime(), building.owner().player_number(), building));
64 }
65
66 void Game::send_player_militarysite_set_soldier_preference(Building& building,
67 SoldierPreference my_preference) {
68- send_player_command(*new CmdMilitarySiteSetSoldierPreference(
69+ send_player_command(new CmdMilitarySiteSetSoldierPreference(
70 get_gametime(), building.owner().player_number(), building, my_preference));
71 }
72
73 void Game::send_player_start_or_cancel_expedition(Building& building) {
74 send_player_command(
75- *new CmdStartOrCancelExpedition(get_gametime(), building.owner().player_number(), building));
76+ new CmdStartOrCancelExpedition(get_gametime(), building.owner().player_number(), building));
77 }
78
79 void Game::send_player_enhance_building(Building& building, DescriptionIndex const id) {
80 assert(building.owner().tribe().has_building(id));
81
82 send_player_command(
83- *new CmdEnhanceBuilding(get_gametime(), building.owner().player_number(), building, id));
84+ new CmdEnhanceBuilding(get_gametime(), building.owner().player_number(), building, id));
85 }
86
87 void Game::send_player_evict_worker(Worker& worker) {
88- send_player_command(*new CmdEvictWorker(get_gametime(), worker.owner().player_number(), worker));
89+ send_player_command(new CmdEvictWorker(get_gametime(), worker.owner().player_number(), worker));
90 }
91
92 void Game::send_player_set_ware_priority(PlayerImmovable& imm,
93@@ -790,14 +790,14 @@
94 DescriptionIndex const index,
95 int32_t const prio) {
96 send_player_command(
97- *new CmdSetWarePriority(get_gametime(), imm.owner().player_number(), imm, type, index, prio));
98+ new CmdSetWarePriority(get_gametime(), imm.owner().player_number(), imm, type, index, prio));
99 }
100
101 void Game::send_player_set_input_max_fill(PlayerImmovable& imm,
102 DescriptionIndex const index,
103 WareWorker type,
104 uint32_t const max_fill) {
105- send_player_command(*new CmdSetInputMaxFill(
106+ send_player_command(new CmdSetInputMaxFill(
107 get_gametime(), imm.owner().player_number(), imm, index, type, max_fill));
108 }
109
110@@ -805,50 +805,49 @@
111 TrainingAttribute attr,
112 int32_t const val) {
113 send_player_command(
114- *new CmdChangeTrainingOptions(get_gametime(), ts.owner().player_number(), ts, attr, val));
115+ new CmdChangeTrainingOptions(get_gametime(), ts.owner().player_number(), ts, attr, val));
116 }
117
118 void Game::send_player_drop_soldier(Building& b, int32_t const ser) {
119 assert(ser != -1);
120- send_player_command(*new CmdDropSoldier(get_gametime(), b.owner().player_number(), b, ser));
121+ send_player_command(new CmdDropSoldier(get_gametime(), b.owner().player_number(), b, ser));
122 }
123
124 void Game::send_player_change_soldier_capacity(Building& b, int32_t const val) {
125 send_player_command(
126- *new CmdChangeSoldierCapacity(get_gametime(), b.owner().player_number(), b, val));
127+ new CmdChangeSoldierCapacity(get_gametime(), b.owner().player_number(), b, val));
128 }
129
130 void Game::send_player_enemyflagaction(const Flag& flag,
131 PlayerNumber const who_attacks,
132 const std::vector<Serial>& soldiers) {
133 if (1 < player(who_attacks)
134- .vision(Map::get_index(flag.get_building()->get_position(), map().get_width()))) {
135- send_player_command(*new CmdEnemyFlagAction(get_gametime(), who_attacks, flag, soldiers));
136- }
137+ .vision(Map::get_index(flag.get_building()->get_position(), map().get_width())))
138+ send_player_command(new CmdEnemyFlagAction(get_gametime(), who_attacks, flag, soldiers));
139 }
140
141 void Game::send_player_ship_scouting_direction(Ship& ship, WalkingDir direction) {
142- send_player_command(*new CmdShipScoutDirection(
143+ send_player_command(new CmdShipScoutDirection(
144 get_gametime(), ship.get_owner()->player_number(), ship.serial(), direction));
145 }
146
147 void Game::send_player_ship_construct_port(Ship& ship, Coords coords) {
148- send_player_command(*new CmdShipConstructPort(
149+ send_player_command(new CmdShipConstructPort(
150 get_gametime(), ship.get_owner()->player_number(), ship.serial(), coords));
151 }
152
153 void Game::send_player_ship_explore_island(Ship& ship, IslandExploreDirection direction) {
154- send_player_command(*new CmdShipExploreIsland(
155+ send_player_command(new CmdShipExploreIsland(
156 get_gametime(), ship.get_owner()->player_number(), ship.serial(), direction));
157 }
158
159 void Game::send_player_sink_ship(Ship& ship) {
160 send_player_command(
161- *new CmdShipSink(get_gametime(), ship.get_owner()->player_number(), ship.serial()));
162+ new CmdShipSink(get_gametime(), ship.get_owner()->player_number(), ship.serial()));
163 }
164
165 void Game::send_player_cancel_expedition_ship(Ship& ship) {
166- send_player_command(*new CmdShipCancelExpedition(
167+ send_player_command(new CmdShipCancelExpedition(
168 get_gametime(), ship.get_owner()->player_number(), ship.serial()));
169 }
170
171@@ -856,7 +855,7 @@
172 auto* object = objects().get_object(trade.initiator);
173 assert(object != nullptr);
174 send_player_command(
175- *new CmdProposeTrade(get_gametime(), object->get_owner()->player_number(), trade));
176+ new CmdProposeTrade(get_gametime(), object->get_owner()->player_number(), trade));
177 }
178
179 int Game::propose_trade(const Trade& trade) {
180
181=== modified file 'src/logic/game.h'
182--- src/logic/game.h 2019-05-07 12:14:02 +0000
183+++ src/logic/game.h 2019-05-25 17:10:52 +0000
184@@ -248,7 +248,7 @@
185
186 void enqueue_command(Command* const);
187
188- void send_player_command(Widelands::PlayerCommand&);
189+ void send_player_command(Widelands::PlayerCommand*);
190
191 void send_player_bulldoze(PlayerImmovable&, bool recurse = false);
192 void send_player_dismantle(PlayerImmovable&);
193
194=== modified file 'src/logic/game_controller.h'
195--- src/logic/game_controller.h 2019-02-27 19:00:36 +0000
196+++ src/logic/game_controller.h 2019-05-25 17:10:52 +0000
197@@ -46,7 +46,9 @@
198 }
199
200 virtual void think() = 0;
201- virtual void send_player_command(Widelands::PlayerCommand&) = 0;
202+
203+ // TODO(Klaus Halfmann): Command must be deleted once it was handled.
204+ virtual void send_player_command(Widelands::PlayerCommand*) = 0;
205 virtual int32_t get_frametime() = 0;
206 virtual GameType get_game_type() = 0;
207
208
209=== modified file 'src/logic/replay_game_controller.cc'
210--- src/logic/replay_game_controller.cc 2019-02-23 11:00:49 +0000
211+++ src/logic/replay_game_controller.cc 2019-05-25 17:10:52 +0000
212@@ -60,7 +60,7 @@
213 }
214 }
215
216-void ReplayGameController::send_player_command(Widelands::PlayerCommand&) {
217+void ReplayGameController::send_player_command(Widelands::PlayerCommand*) {
218 throw wexception("Trying to send a player command during replay");
219 }
220
221
222=== modified file 'src/logic/replay_game_controller.h'
223--- src/logic/replay_game_controller.h 2019-02-23 11:00:49 +0000
224+++ src/logic/replay_game_controller.h 2019-05-25 17:10:52 +0000
225@@ -35,7 +35,7 @@
226
227 void think() override;
228
229- void send_player_command(Widelands::PlayerCommand&) override;
230+ void send_player_command(Widelands::PlayerCommand*) override;
231 int32_t get_frametime() override;
232 GameController::GameType get_game_type() override;
233 uint32_t real_speed() override;
234
235=== modified file 'src/logic/single_player_game_controller.cc'
236--- src/logic/single_player_game_controller.cc 2019-02-23 11:00:49 +0000
237+++ src/logic/single_player_game_controller.cc 2019-05-25 17:10:52 +0000
238@@ -74,9 +74,9 @@
239 }
240 }
241
242-void SinglePlayerGameController::send_player_command(Widelands::PlayerCommand& pc) {
243- pc.set_cmdserial(++player_cmdserial_);
244- game_.enqueue_command(&pc);
245+void SinglePlayerGameController::send_player_command(Widelands::PlayerCommand* pc) {
246+ pc->set_cmdserial(++player_cmdserial_);
247+ game_.enqueue_command(pc);
248 }
249
250 int32_t SinglePlayerGameController::get_frametime() {
251
252=== modified file 'src/logic/single_player_game_controller.h'
253--- src/logic/single_player_game_controller.h 2019-02-27 19:00:36 +0000
254+++ src/logic/single_player_game_controller.h 2019-05-25 17:10:52 +0000
255@@ -30,7 +30,7 @@
256 ~SinglePlayerGameController() override;
257
258 void think() override;
259- void send_player_command(Widelands::PlayerCommand&) override;
260+ void send_player_command(Widelands::PlayerCommand*) override;
261 int32_t get_frametime() override;
262 GameController::GameType get_game_type() override;
263 uint32_t real_speed() override;
264
265=== modified file 'src/network/gameclient.cc'
266--- src/network/gameclient.cc 2019-04-29 16:22:08 +0000
267+++ src/network/gameclient.cc 2019-05-25 17:10:52 +0000
268@@ -58,6 +58,8 @@
269 #include "wui/interactive_spectator.h"
270
271 struct GameClientImpl {
272+ bool internet_;
273+
274 GameSettings settings;
275
276 std::string localplayername;
277@@ -91,13 +93,121 @@
278
279 /// Backlog of chat messages
280 std::vector<ChatMessage> chatmessages;
281+
282+ /** File that is eventually transferred via the network if not found at the other side */
283+ std::unique_ptr<NetTransferFile> file_;
284+
285+ void send_hello();
286+ void send_player_command(Widelands::PlayerCommand*);
287+
288+ bool run_map_menu(GameClient* parent);
289+ void run_game(InteractiveGameBase* igb, UI::ProgressWindow*);
290+
291+ InteractiveGameBase* init_game(GameClient* parent, UI::ProgressWindow*);
292+
293 };
294
295+void GameClientImpl::send_hello() {
296+ SendPacket s;
297+ s.unsigned_8(NETCMD_HELLO);
298+ s.unsigned_8(NETWORK_PROTOCOL_VERSION);
299+ s.string(localplayername);
300+ s.string(build_id());
301+ net->send(s);
302+}
303+
304+void GameClientImpl::send_player_command(Widelands::PlayerCommand* pc) {
305+ SendPacket s;
306+ s.unsigned_8(NETCMD_PLAYERCOMMAND);
307+ s.signed_32(game->get_gametime());
308+ pc->serialize(s);
309+ net->send(s);
310+}
311+
312+/**
313+ * Show and run() the fullscreen menu for setting map and mapsettings.
314+ *
315+ * @return true to indicate that run is done.
316+ */
317+bool GameClientImpl::run_map_menu(GameClient* parent) {
318+ FullscreenMenuLaunchMPG lgm(parent, parent);
319+ lgm.set_chat_provider(*parent);
320+ modal = &lgm;
321+ FullscreenMenuBase::MenuTarget code = lgm.run<FullscreenMenuBase::MenuTarget>();
322+ modal = nullptr;
323+ if (code == FullscreenMenuBase::MenuTarget::kBack) {
324+ // if this is an internet game, tell the metaserver that client is back in the lobby.
325+ if (internet_) {
326+ InternetGaming::ref().set_game_done();
327+ }
328+ return true;
329+ }
330+ return false;
331+}
332+
333+/**
334+ * Show progress dialog and load map or saved game.
335+ */
336+InteractiveGameBase* GameClientImpl::init_game(GameClient* parent, UI::ProgressWindow* loader) {
337+
338+ const std::string& tribename = parent->get_players_tribe();
339+ assert(Widelands::tribe_exists(tribename));
340+ GameTips tips(*loader, {"general_game", "multiplayer", tribename});
341+
342+ modal = loader;
343+
344+ loader->step(_("Preparing game"));
345+
346+ game->set_game_controller(parent);
347+ uint8_t const pn = settings.playernum + 1;
348+ game->save_handler().set_autosave_filename(
349+ (boost::format("%s_netclient%u") % kAutosavePrefix % static_cast<unsigned int>(pn)).str());
350+ InteractiveGameBase* igb;
351+ if (pn > 0) {
352+ igb = new InteractivePlayer(*game, g_options.pull_section("global"), pn, true);
353+ } else {
354+ igb = new InteractiveSpectator(*game, g_options.pull_section("global"), true);
355+ }
356+ game -> set_ibase(igb);
357+ igb->set_chat_provider(*parent);
358+ if (settings.savegame) { // savegame
359+ game->init_savegame(loader, settings);
360+ } else { // new map
361+ game->init_newgame(loader, settings);
362+ }
363+ return igb;
364+}
365+
366+
367+/**
368+ * Run the actual game and cleanup when done.
369+ */
370+void GameClientImpl::run_game(InteractiveGameBase* igb, UI::ProgressWindow* loader) {
371+ time.reset(game->get_gametime());
372+ lasttimestamp = game->get_gametime();
373+ lasttimestamp_realtime = SDL_GetTicks();
374+
375+ modal = igb;
376+ game->run(loader, settings.savegame ?
377+ Widelands::Game::Loaded :
378+ settings.scenario ? Widelands::Game::NewMPScenario : Widelands::Game::NewNonScenario,
379+ "", false, (boost::format("netclient_%d") % static_cast<int>(settings.usernum)).str());
380+
381+ // if this is an internet game, tell the metaserver that the game is done.
382+ if (internet_) {
383+ InternetGaming::ref().set_game_done();
384+ }
385+ modal = nullptr;
386+ game = nullptr;
387+}
388+
389 GameClient::GameClient(const std::pair<NetAddress, NetAddress>& host,
390 const std::string& playername,
391 bool internet,
392 const std::string& gamename)
393- : d(new GameClientImpl), internet_(internet) {
394+ : d(new GameClientImpl) {
395+
396+ d->internet_ = internet;
397
398 if (internet) {
399 assert(!gamename.empty());
400@@ -127,7 +237,7 @@
401 d->game = nullptr;
402 d->realspeed = 0;
403 d->desiredspeed = 1000;
404- file_ = nullptr;
405+ d->file_ = nullptr;
406
407 // Get the default win condition script
408 d->settings.win_condition_script = d->settings.win_condition_scripts.front();
409@@ -141,94 +251,41 @@
410 delete d;
411 }
412
413+
414+
415 void GameClient::run() {
416- SendPacket s;
417- s.unsigned_8(NETCMD_HELLO);
418- s.unsigned_8(NETWORK_PROTOCOL_VERSION);
419- s.string(d->localplayername);
420- s.string(build_id());
421- d->net->send(s);
422
423+ d->send_hello();
424 d->settings.multiplayer = true;
425
426 // Fill the list of possible system messages
427 NetworkGamingMessages::fill_map();
428- {
429- FullscreenMenuLaunchMPG lgm(this, this);
430- lgm.set_chat_provider(*this);
431- d->modal = &lgm;
432- FullscreenMenuBase::MenuTarget code = lgm.run<FullscreenMenuBase::MenuTarget>();
433- d->modal = nullptr;
434- if (code == FullscreenMenuBase::MenuTarget::kBack) {
435- // if this is an internet game, tell the metaserver that client is back in the lobby.
436- if (internet_)
437- InternetGaming::ref().set_game_done();
438- return;
439- }
440+
441+ if (d->run_map_menu(this)) {
442+ return; // did not select a Map ...
443 }
444
445 d->server_is_waiting = true;
446
447+ bool write_sync_streams = g_options.pull_section("global").get_bool("write_syncstreams", true);
448 Widelands::Game game;
449- game.set_write_syncstream(g_options.pull_section("global").get_bool("write_syncstreams", true));
450+ game.set_write_syncstream(write_sync_streams);
451
452 try {
453 std::unique_ptr<UI::ProgressWindow> loader_ui(new UI::ProgressWindow());
454- d->modal = loader_ui.get();
455- std::vector<std::string> tipstext;
456- tipstext.push_back("general_game");
457- tipstext.push_back("multiplayer");
458- try {
459- tipstext.push_back(get_players_tribe());
460- } catch (NoTribe) {
461- }
462- GameTips tips(*loader_ui.get(), tipstext);
463-
464- loader_ui->step(_("Preparing game"));
465
466 d->game = &game;
467- game.set_game_controller(this);
468- uint8_t const pn = d->settings.playernum + 1;
469- game.save_handler().set_autosave_filename(
470- (boost::format("%s_netclient%u") % kAutosavePrefix % static_cast<unsigned int>(pn)).str());
471- InteractiveGameBase* igb;
472- if (pn > 0)
473- igb = new InteractivePlayer(game, g_options.pull_section("global"), pn, true);
474- else
475- igb = new InteractiveSpectator(game, g_options.pull_section("global"), true);
476- game.set_ibase(igb);
477- igb->set_chat_provider(*this);
478- if (!d->settings.savegame) { // new map
479- game.init_newgame(loader_ui.get(), d->settings);
480- } else { // savegame
481- game.init_savegame(loader_ui.get(), d->settings);
482- }
483- d->time.reset(game.get_gametime());
484- d->lasttimestamp = game.get_gametime();
485- d->lasttimestamp_realtime = SDL_GetTicks();
486-
487- d->modal = igb;
488- game.run(
489- loader_ui.get(),
490- d->settings.savegame ?
491- Widelands::Game::Loaded :
492- d->settings.scenario ? Widelands::Game::NewMPScenario : Widelands::Game::NewNonScenario,
493- "", false, (boost::format("netclient_%d") % static_cast<int>(d->settings.usernum)).str());
494-
495- // if this is an internet game, tell the metaserver that the game is done.
496- if (internet_)
497- InternetGaming::ref().set_game_done();
498- d->modal = nullptr;
499- d->game = nullptr;
500+ InteractiveGameBase* igb = d->init_game(this, loader_ui.get());
501+ d->run_game(igb, loader_ui.get());
502+
503 } catch (...) {
504 WLApplication::emergency_save(game);
505 d->game = nullptr;
506 disconnect("CLIENT_CRASHED");
507 // We will bounce back to the main menu, so we better log out
508- if (internet_) {
509+ if (d->internet_) {
510 InternetGaming::ref().logout("CLIENT_CRASHED");
511 }
512- d->modal = nullptr;
513 throw;
514 }
515 }
516@@ -237,6 +294,7 @@
517 handle_network();
518
519 if (d->game) {
520+ // TODO(Klaus Halfmann): what kind of time tricks are done here?
521 if (d->realspeed == 0 || d->server_is_waiting)
522 d->time.fastforward();
523 else
524@@ -254,25 +312,28 @@
525 }
526 }
527
528-void GameClient::send_player_command(Widelands::PlayerCommand& pc) {
529+/**
530+ * Send PlayerCommand to server.
531+ *
532+ * @param pc will always be deleted in the end.
533+ */
534+void GameClient::send_player_command(Widelands::PlayerCommand* pc) {
535 assert(d->game);
536- if (pc.sender() != d->settings.playernum + 1) {
537- delete &pc;
538- return;
539+
540+ // TODDO(Klaus Halfmann)should this be an assert?
541+ if (pc->sender() == d->settings.playernum + 1) // allow command for current player only
542+ {
543+ log("[Client]: send playercommand at time %i\n", d->game->get_gametime());
544+
545+ d->send_player_command(pc);
546+
547+ d->lasttimestamp = d->game->get_gametime();
548+ d->lasttimestamp_realtime = SDL_GetTicks();
549+ } else {
550+ log("[Client]: Playercommand is not for current player? %i\n", pc -> sender());
551 }
552
553- log("[Client]: send playercommand at time %i\n", d->game->get_gametime());
554-
555- SendPacket s;
556- s.unsigned_8(NETCMD_PLAYERCOMMAND);
557- s.signed_32(d->game->get_gametime());
558- pc.serialize(s);
559- d->net->send(s);
560-
561- d->lasttimestamp = d->game->get_gametime();
562- d->lasttimestamp_realtime = SDL_GetTicks();
563-
564- delete &pc;
565+ delete pc;
566 }
567
568 int32_t GameClient::get_frametime() {
569@@ -544,6 +605,333 @@
570 }
571 }
572
573+void GameClient::handle_disconnect(RecvPacket& packet) {
574+ uint8_t number = packet.unsigned_8();
575+ std::string reason = packet.string();
576+ if (number == 1)
577+ disconnect(reason, "", false);
578+ else {
579+ std::string arg = packet.string();
580+ disconnect(reason, arg, false);
581+ }
582+}
583+
584+/**
585+ * Hello from the other side
586+ */
587+void GameClient::handle_hello(RecvPacket& packet) {
588+ if (d->settings.usernum != -2) // TODO(Klaus Halfmann): if the host is the client ?.
589+ throw ProtocolException(NETCMD_HELLO); // I am talkimg with myself? Bad idea
590+ uint8_t const version = packet.unsigned_8();
591+ if (version != NETWORK_PROTOCOL_VERSION)
592+ throw DisconnectException("DIFFERENT_PROTOCOL_VERS");
593+ d->settings.usernum = packet.unsigned_32(); // TODO(Klaus Halfmann): usernum is int8_t.
594+ d->settings.playernum = -1;
595+}
596+
597+/**
598+ * Give a pong for a ping
599+ */
600+void GameClient::handle_ping(RecvPacket&) {
601+ SendPacket s;
602+ s.unsigned_8(NETCMD_PONG);
603+ d->net->send(s);
604+
605+ log("[Client] Pong!\n");
606+}
607+
608+/**
609+ * New Map name was sent.
610+ */
611+void GameClient::handle_setting_map(RecvPacket& packet) {
612+ d->settings.mapname = packet.string();
613+ d->settings.mapfilename = g_fs->FileSystem::fix_cross_file(packet.string());
614+ d->settings.savegame = packet.unsigned_8() == 1;
615+ d->settings.scenario = packet.unsigned_8() == 1;
616+ log("[Client] SETTING_MAP '%s' '%s'\n", d->settings.mapname.c_str(),
617+ d->settings.mapfilename.c_str());
618+
619+ // New map was set, so we clean up the buffer of a previously requested file
620+ d->file_.reset(nullptr);
621+}
622+
623+/**
624+ *
625+ */
626+// TODO(Klaus Halfmann): refactor this until it can be understood, move into impl.
627+void GameClient::handle_new_file(RecvPacket& packet) {
628+ std::string path = g_fs->FileSystem::fix_cross_file(packet.string());
629+ uint32_t bytes = packet.unsigned_32();
630+ std::string md5 = packet.string();
631+
632+ // Check whether the file or a file with that name already exists
633+ if (g_fs->file_exists(path)) {
634+ // If the file is a directory, we have to rename the file and replace it with the version
635+ // of the host. If it is a zipped file, we can check, whether the host and the client have
636+ // got the same file.
637+ if (!g_fs->is_directory(path)) {
638+ FileRead fr;
639+ fr.open(*g_fs, path);
640+ if (bytes == fr.get_size()) {
641+ std::unique_ptr<char[]> complete(new char[bytes]);
642+ if (!complete) {
643+ throw wexception("Out of memory");
644+ }
645+ fr.data_complete(complete.get(), bytes);
646+ // TODO(Klaus Halfmann): compute MD5 on the fly in FileRead...
647+ SimpleMD5Checksum md5sum;
648+ md5sum.data(complete.get(), bytes);
649+ md5sum.finish_checksum();
650+ std::string localmd5 = md5sum.get_checksum().str();
651+ if (localmd5 == md5)
652+ // everything is alright we now have the file.
653+ return;
654+ }
655+ }
656+ // Don't overwrite the file, better rename the original one
657+ try {
658+ g_fs->fs_rename(path, backup_file_name(path));
659+ } catch (const FileError& e) {
660+ log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
661+ "%s\n",
662+ e.what());
663+ // TODO(Arty): What now? It just means the next step will fail
664+ // or possibly result in some corrupt file
665+ }
666+ }
667+
668+ // Yes we need the file!
669+ SendPacket s;
670+ s.unsigned_8(NETCMD_NEW_FILE_AVAILABLE);
671+ d->net->send(s);
672+
673+ d->file_.reset(new NetTransferFile());
674+ d->file_->bytes = bytes;
675+ d->file_->filename = path;
676+ d->file_->md5sum = md5;
677+ size_t position = path.rfind(g_fs->file_separator(), path.size() - 2);
678+ if (position != std::string::npos) {
679+ path.resize(position);
680+ g_fs->ensure_directory_exists(path);
681+ }
682+}
683+
684+/**
685+ *
686+ */
687+// TODO(Klaus Halfmann): refactor this until it can be understood, move into impl.
688+void GameClient::handle_file_part(RecvPacket& packet) {
689+ // Only go on, if we are waiting for a file part at the moment. It can happen, that an
690+ // "unrequested" part is send by the server if the map was changed just a moment ago
691+ // and there was an outstanding request from the client.
692+ if (!d->file_)
693+ return; // silently ignore
694+
695+ uint32_t part = packet.unsigned_32();
696+ uint32_t size = packet.unsigned_32();
697+
698+ // Send an answer
699+ SendPacket s;
700+ s.unsigned_8(NETCMD_FILE_PART);
701+ s.unsigned_32(part);
702+ s.string(d->file_->md5sum);
703+ d->net->send(s);
704+
705+ FilePart fp;
706+
707+ char buf[NETFILEPARTSIZE];
708+ assert(size <= NETFILEPARTSIZE);
709+
710+ // TODO(Klaus Halfmann): read directcly into FilePart?
711+ if (packet.data(buf, size) != size)
712+ log("Readproblem. Will try to go on anyways\n");
713+ memcpy(fp.part, &buf[0], size);
714+ d->file_->parts.push_back(fp);
715+
716+ // Write file to disk as soon as all parts arrived
717+ uint32_t left = (d->file_->bytes - NETFILEPARTSIZE * part);
718+ if (left <= NETFILEPARTSIZE) {
719+ FileWrite fw;
720+ left = d->file_->bytes;
721+ uint32_t i = 0;
722+ // Put all data together
723+ while (left > 0) {
724+ uint32_t writeout = (left > NETFILEPARTSIZE) ? NETFILEPARTSIZE : left;
725+ fw.data(d->file_->parts[i].part, writeout, FileWrite::Pos::null());
726+ left -= writeout;
727+ ++i;
728+ }
729+ // Now really write the file
730+ fw.write(*g_fs, d->file_->filename.c_str());
731+
732+ // Check for consistence
733+ FileRead fr;
734+ fr.open(*g_fs, d->file_->filename);
735+
736+ std::unique_ptr<char[]> complete(new char[d->file_->bytes]);
737+
738+ fr.data_complete(complete.get(), d->file_->bytes);
739+ SimpleMD5Checksum md5sum;
740+ md5sum.data(complete.get(), d->file_->bytes);
741+ md5sum.finish_checksum();
742+ std::string localmd5 = md5sum.get_checksum().str();
743+ if (localmd5 != d->file_->md5sum) {
744+ // Something went wrong! We have to rerequest the file.
745+ s.reset();
746+ s.unsigned_8(NETCMD_NEW_FILE_AVAILABLE);
747+ d->net->send(s);
748+ // Notify the players
749+ s.reset();
750+ s.unsigned_8(NETCMD_CHAT);
751+ s.string(_("/me 's file failed md5 checksumming."));
752+ d->net->send(s);
753+ try {
754+ g_fs->fs_unlink(d->file_->filename);
755+ } catch (const FileError& e) {
756+ log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
757+ "%s\n",
758+ e.what());
759+ }
760+ }
761+ // Check file for validity
762+ bool invalid = false;
763+ if (d->settings.savegame) {
764+ // Saved game check - does Widelands recognize the file as saved game?
765+ Widelands::Game game;
766+ try {
767+ Widelands::GameLoader gl(d->file_->filename, game);
768+ } catch (...) {
769+ invalid = true;
770+ }
771+ } else {
772+ // Map check - does Widelands recognize the file as map?
773+ Widelands::Map map;
774+ std::unique_ptr<Widelands::MapLoader> ml = map.get_correct_loader(d->file_->filename);
775+ if (!ml) {
776+ invalid = true;
777+ }
778+ }
779+ if (invalid) {
780+ try {
781+ g_fs->fs_unlink(d->file_->filename);
782+ // Restore original file, if there was one before
783+ if (g_fs->file_exists(backup_file_name(d->file_->filename)))
784+ g_fs->fs_rename(backup_file_name(d->file_->filename), d->file_->filename);
785+ } catch (const FileError& e) {
786+ log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
787+ "%s\n", e.what());
788+ }
789+ s.reset();
790+ s.unsigned_8(NETCMD_CHAT);
791+ s.string(_("/me checked the received file. Although md5 check summing succeeded, "
792+ "I can not handle the file."));
793+ d->net->send(s);
794+ }
795+ }
796+}
797+
798+/**
799+ *
800+ */
801+void GameClient::handle_setting_tribes(RecvPacket& packet) {
802+ d->settings.tribes.clear();
803+ for (uint8_t i = packet.unsigned_8(); i; --i) {
804+ Widelands::TribeBasicInfo info = Widelands::get_tribeinfo(packet.string());
805+
806+ // Get initializations (we have to do this locally, for translations)
807+ LuaInterface lua;
808+ info.initializations.clear();
809+ for (uint8_t j = packet.unsigned_8(); j > 0; --j) {
810+ std::string const initialization_script = packet.string();
811+ std::unique_ptr<LuaTable> t = lua.run_script(initialization_script);
812+ t->do_not_warn_about_unaccessed_keys();
813+ info.initializations.push_back(Widelands::TribeBasicInfo::Initialization(
814+ initialization_script, t->get_string("descname"), t->get_string("tooltip")));
815+ }
816+ d->settings.tribes.push_back(info);
817+ }
818+}
819+
820+/**
821+ *
822+ */
823+void GameClient::handle_setting_allplayers(RecvPacket& packet) {
824+ d->settings.players.resize(packet.unsigned_8());
825+ for (uint8_t i = 0; i < d->settings.players.size(); ++i) {
826+ receive_one_player(i, packet);
827+ }
828+ // Map changes are finished here
829+ Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap));
830+}
831+
832+/**
833+ *
834+ */
835+void GameClient::handle_playercommand(RecvPacket& packet) {
836+ if (!d->game)
837+ throw DisconnectException("PLAYERCMD_WO_GAME");
838+
839+ int32_t const time = packet.signed_32();
840+ Widelands::PlayerCommand& plcmd = *Widelands::PlayerCommand::deserialize(packet);
841+ plcmd.set_duetime(time);
842+ d->game->enqueue_command(&plcmd);
843+ d->time.receive(time);
844+}
845+
846+/**
847+ *
848+ */
849+void GameClient::handle_syncrequest(RecvPacket& packet) {
850+ if (!d->game)
851+ throw DisconnectException("SYNCREQUEST_WO_GAME");
852+ int32_t const time = packet.signed_32();
853+ d->time.receive(time);
854+ d->game->enqueue_command(new CmdNetCheckSync(time, [this] { sync_report_callback(); }));
855+ d->game->report_sync_request();
856+}
857+
858+/**
859+ *
860+ */
861+void GameClient::handle_chat(RecvPacket& packet) {
862+ ChatMessage c("");
863+ c.playern = packet.signed_16();
864+ c.sender = packet.string();
865+ c.msg = packet.string();
866+ if (packet.unsigned_8())
867+ c.recipient = packet.string();
868+ d->chatmessages.push_back(c);
869+ Notifications::publish(c);
870+}
871+
872+/**
873+ *
874+ */
875+void GameClient::handle_system_message(RecvPacket& packet) {
876+ const std::string code = packet.string();
877+ const std::string arg1 = packet.string();
878+ const std::string arg2 = packet.string();
879+ const std::string arg3 = packet.string();
880+ ChatMessage c(NetworkGamingMessages::get_message(code, arg1, arg2, arg3));
881+ c.playern = UserSettings::none(); // == System message
882+ // c.sender remains empty to indicate a system message
883+ d->chatmessages.push_back(c);
884+ Notifications::publish(c);
885+}
886+
887+/**
888+ *
889+ */
890+void GameClient::handle_desync(RecvPacket&) {
891+ log("[Client] received NETCMD_INFO_DESYNC. Trying to salvage some "
892+ "information for debugging.\n");
893+ if (d->game) {
894+ d->game->save_syncstream(true);
895+ // We don't know our playernumber, so report as -1
896+ d->game->report_desync(-1);
897+ }
898+}
899+
900 /**
901 * Handle one packet received from the host.
902 *
903@@ -552,363 +940,80 @@
904 void GameClient::handle_packet(RecvPacket& packet) {
905 uint8_t cmd = packet.unsigned_8();
906
907- if (cmd == NETCMD_DISCONNECT) {
908- uint8_t number = packet.unsigned_8();
909- std::string reason = packet.string();
910- if (number == 1)
911- disconnect(reason, "", false);
912- else {
913- std::string arg = packet.string();
914- disconnect(reason, arg, false);
915- }
916- return;
917- }
918-
919- if (d->settings.usernum == -2) {
920- if (cmd != NETCMD_HELLO)
921+ switch (cmd) {
922+ case NETCMD_DISCONNECT:
923+ return handle_disconnect(packet);
924+ case NETCMD_HELLO:
925+ return handle_hello(packet);
926+ case NETCMD_PING:
927+ return handle_ping(packet);
928+ case NETCMD_SETTING_MAP:
929+ return handle_setting_map(packet);
930+ case NETCMD_NEW_FILE_AVAILABLE:
931+ return handle_new_file(packet);
932+ case NETCMD_FILE_PART:
933+ return handle_file_part(packet);
934+ case NETCMD_SETTING_TRIBES:
935+ return handle_setting_tribes(packet);
936+ case NETCMD_SETTING_ALLPLAYERS:
937+ return handle_setting_allplayers(packet);
938+ case NETCMD_SETTING_PLAYER: {
939+ uint8_t player = packet.unsigned_8();
940+ receive_one_player(player, packet);
941+ }
942+ break;
943+ case NETCMD_SETTING_ALLUSERS: {
944+ d->settings.users.resize(packet.unsigned_8());
945+ for (uint32_t i = 0; i < d->settings.users.size(); ++i)
946+ receive_one_user(i, packet);
947+ }
948+ break;
949+ case NETCMD_SETTING_USER: {
950+ uint32_t user = packet.unsigned_32();
951+ receive_one_user(user, packet);
952+ }
953+ break;
954+ case NETCMD_SET_PLAYERNUMBER: {
955+ int32_t number = packet.signed_32();
956+ d->settings.playernum = number;
957+ d->settings.users.at(d->settings.usernum).position = number;
958+ }
959+ break;
960+ case NETCMD_WIN_CONDITION:
961+ d->settings.win_condition_script = g_fs->FileSystem::fix_cross_file(packet.string());
962+ break;
963+ case NETCMD_PEACEFUL_MODE:
964+ d->settings.peaceful = packet.unsigned_8();
965+ break;
966+ case NETCMD_LAUNCH:
967+ if (!d->modal || d->game) {
968+ throw DisconnectException("UNEXPECTED_LAUNCH");
969+ }
970+ d->modal->end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kOk);
971+ break;
972+ case NETCMD_SETSPEED:
973+ d->realspeed = packet.unsigned_16();
974+ log("[Client] speed: %u.%03u\n", d->realspeed / 1000, d->realspeed % 1000);
975+ break;
976+ case NETCMD_TIME:
977+ d->time.receive(packet.signed_32());
978+ break;
979+ case NETCMD_WAIT:
980+ log("[Client]: server is waiting.\n");
981+ d->server_is_waiting = true;
982+ break;
983+ case NETCMD_PLAYERCOMMAND:
984+ return handle_playercommand(packet);
985+ case NETCMD_SYNCREQUEST:
986+ return handle_syncrequest(packet);
987+ case NETCMD_CHAT:
988+ return handle_chat(packet);
989+ case NETCMD_SYSTEM_MESSAGE_CODE:
990+ return handle_system_message(packet);
991+ case NETCMD_INFO_DESYNC:
992+ return handle_desync(packet);
993+ default:
994 throw ProtocolException(cmd);
995- uint8_t const version = packet.unsigned_8();
996- if (version != NETWORK_PROTOCOL_VERSION)
997- throw DisconnectException("DIFFERENT_PROTOCOL_VERS");
998- d->settings.usernum = packet.unsigned_32();
999- d->settings.playernum = -1;
1000- return;
1001- }
1002-
1003- switch (cmd) {
1004- case NETCMD_PING: {
1005- SendPacket s;
1006- s.unsigned_8(NETCMD_PONG);
1007- d->net->send(s);
1008-
1009- log("[Client] Pong!\n");
1010- break;
1011- }
1012-
1013- case NETCMD_SETTING_MAP: {
1014- d->settings.mapname = packet.string();
1015- d->settings.mapfilename = g_fs->FileSystem::fix_cross_file(packet.string());
1016- d->settings.savegame = packet.unsigned_8() == 1;
1017- d->settings.scenario = packet.unsigned_8() == 1;
1018- log("[Client] SETTING_MAP '%s' '%s'\n", d->settings.mapname.c_str(),
1019- d->settings.mapfilename.c_str());
1020-
1021- // New map was set, so we clean up the buffer of a previously requested file
1022- file_.reset(nullptr);
1023- break;
1024- }
1025-
1026- case NETCMD_NEW_FILE_AVAILABLE: {
1027- std::string path = g_fs->FileSystem::fix_cross_file(packet.string());
1028- uint32_t bytes = packet.unsigned_32();
1029- std::string md5 = packet.string();
1030-
1031- // Check whether the file or a file with that name already exists
1032- if (g_fs->file_exists(path)) {
1033- // If the file is a directory, we have to rename the file and replace it with the version
1034- // of the
1035- // host. If it is a ziped file, we can check, whether the host and the client have got the
1036- // same file.
1037- if (!g_fs->is_directory(path)) {
1038- FileRead fr;
1039- fr.open(*g_fs, path);
1040- if (bytes == fr.get_size()) {
1041- std::unique_ptr<char[]> complete(new char[bytes]);
1042- if (!complete)
1043- throw wexception("Out of memory");
1044-
1045- fr.data_complete(complete.get(), bytes);
1046- SimpleMD5Checksum md5sum;
1047- md5sum.data(complete.get(), bytes);
1048- md5sum.finish_checksum();
1049- std::string localmd5 = md5sum.get_checksum().str();
1050- if (localmd5 == md5)
1051- // everything is alright we already have the file.
1052- return;
1053- }
1054- }
1055- // Don't overwrite the file, better rename the original one
1056- try {
1057- g_fs->fs_rename(path, backup_file_name(path));
1058- } catch (const FileError& e) {
1059- log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
1060- "%s\n",
1061- e.what());
1062- // TODO(Arty): What now? It just means the next step will fail
1063- // or possibly result in some corrupt file
1064- }
1065- }
1066-
1067- // Yes we need the file!
1068- SendPacket s;
1069- s.unsigned_8(NETCMD_NEW_FILE_AVAILABLE);
1070- d->net->send(s);
1071-
1072- file_.reset(new NetTransferFile());
1073- file_->bytes = bytes;
1074- file_->filename = path;
1075- file_->md5sum = md5;
1076- size_t position = path.rfind(g_fs->file_separator(), path.size() - 2);
1077- if (position != std::string::npos) {
1078- path.resize(position);
1079- g_fs->ensure_directory_exists(path);
1080- }
1081- break;
1082- }
1083-
1084- case NETCMD_FILE_PART: {
1085- // Only go on, if we are waiting for a file part at the moment. It can happen, that an
1086- // "unrequested"
1087- // part is send by the server if the map was changed just a moment ago and there was an
1088- // outstanding
1089- // request from the client.
1090- if (!file_)
1091- return; // silently ignore
1092-
1093- uint32_t part = packet.unsigned_32();
1094- uint32_t size = packet.unsigned_32();
1095-
1096- // Send an answer
1097- SendPacket s;
1098- s.unsigned_8(NETCMD_FILE_PART);
1099- s.unsigned_32(part);
1100- s.string(file_->md5sum);
1101- d->net->send(s);
1102-
1103- FilePart fp;
1104-
1105- char buf[NETFILEPARTSIZE];
1106- assert(size <= NETFILEPARTSIZE);
1107-
1108- if (packet.data(buf, size) != size)
1109- log("Readproblem. Will try to go on anyways\n");
1110- memcpy(fp.part, &buf[0], size);
1111- file_->parts.push_back(fp);
1112-
1113- // Write file to disk as soon as all parts arrived
1114- uint32_t left = (file_->bytes - NETFILEPARTSIZE * part);
1115- if (left <= NETFILEPARTSIZE) {
1116- FileWrite fw;
1117- left = file_->bytes;
1118- uint32_t i = 0;
1119- // Put all data together
1120- while (left > 0) {
1121- uint32_t writeout = (left > NETFILEPARTSIZE) ? NETFILEPARTSIZE : left;
1122- fw.data(file_->parts[i].part, writeout, FileWrite::Pos::null());
1123- left -= writeout;
1124- ++i;
1125- }
1126- // Now really write the file
1127- fw.write(*g_fs, file_->filename.c_str());
1128-
1129- // Check for consistence
1130- FileRead fr;
1131- fr.open(*g_fs, file_->filename);
1132-
1133- std::unique_ptr<char[]> complete(new char[file_->bytes]);
1134-
1135- fr.data_complete(complete.get(), file_->bytes);
1136- SimpleMD5Checksum md5sum;
1137- md5sum.data(complete.get(), file_->bytes);
1138- md5sum.finish_checksum();
1139- std::string localmd5 = md5sum.get_checksum().str();
1140- if (localmd5 != file_->md5sum) {
1141- // Something went wrong! We have to rerequest the file.
1142- s.reset();
1143- s.unsigned_8(NETCMD_NEW_FILE_AVAILABLE);
1144- d->net->send(s);
1145- // Notify the players
1146- s.reset();
1147- s.unsigned_8(NETCMD_CHAT);
1148- s.string(_("/me 's file failed md5 checksumming."));
1149- d->net->send(s);
1150- try {
1151- g_fs->fs_unlink(file_->filename);
1152- } catch (const FileError& e) {
1153- log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
1154- "%s\n",
1155- e.what());
1156- }
1157- }
1158- // Check file for validity
1159- bool invalid = false;
1160- if (d->settings.savegame) {
1161- // Saved game check - does Widelands recognize the file as saved game?
1162- Widelands::Game game;
1163- try {
1164- Widelands::GameLoader gl(file_->filename, game);
1165- } catch (...) {
1166- invalid = true;
1167- }
1168- } else {
1169- // Map check - does Widelands recognize the file as map?
1170- Widelands::Map map;
1171- std::unique_ptr<Widelands::MapLoader> ml = map.get_correct_loader(file_->filename);
1172- if (!ml)
1173- invalid = true;
1174- }
1175- if (invalid) {
1176- try {
1177- g_fs->fs_unlink(file_->filename);
1178- // Restore original file, if there was one before
1179- if (g_fs->file_exists(backup_file_name(file_->filename)))
1180- g_fs->fs_rename(backup_file_name(file_->filename), file_->filename);
1181- } catch (const FileError& e) {
1182- log("file error in GameClient::handle_packet: case NETCMD_FILE_PART: "
1183- "%s\n",
1184- e.what());
1185- }
1186- s.reset();
1187- s.unsigned_8(NETCMD_CHAT);
1188- s.string(_("/me checked the received file. Although md5 check summing succeeded, "
1189- "I can not handle the file."));
1190- d->net->send(s);
1191- }
1192- }
1193- break;
1194- }
1195-
1196- case NETCMD_SETTING_TRIBES: {
1197- d->settings.tribes.clear();
1198- for (uint8_t i = packet.unsigned_8(); i; --i) {
1199- Widelands::TribeBasicInfo info = Widelands::get_tribeinfo(packet.string());
1200-
1201- // Get initializations (we have to do this locally, for translations)
1202- LuaInterface lua;
1203- info.initializations.clear();
1204- for (uint8_t j = packet.unsigned_8(); j; --j) {
1205- std::string const initialization_script = packet.string();
1206- std::unique_ptr<LuaTable> t = lua.run_script(initialization_script);
1207- t->do_not_warn_about_unaccessed_keys();
1208- info.initializations.push_back(Widelands::TribeBasicInfo::Initialization(
1209- initialization_script, t->get_string("descname"), t->get_string("tooltip")));
1210- }
1211- d->settings.tribes.push_back(info);
1212- }
1213- break;
1214- }
1215-
1216- case NETCMD_SETTING_ALLPLAYERS: {
1217- d->settings.players.resize(packet.unsigned_8());
1218- for (uint8_t i = 0; i < d->settings.players.size(); ++i) {
1219- receive_one_player(i, packet);
1220- }
1221- // Map changes are finished here
1222- Notifications::publish(NoteGameSettings(NoteGameSettings::Action::kMap));
1223- break;
1224- }
1225- case NETCMD_SETTING_PLAYER: {
1226- uint8_t player = packet.unsigned_8();
1227- receive_one_player(player, packet);
1228- break;
1229- }
1230- case NETCMD_SETTING_ALLUSERS: {
1231- d->settings.users.resize(packet.unsigned_8());
1232- for (uint32_t i = 0; i < d->settings.users.size(); ++i)
1233- receive_one_user(i, packet);
1234- break;
1235- }
1236- case NETCMD_SETTING_USER: {
1237- uint32_t user = packet.unsigned_32();
1238- receive_one_user(user, packet);
1239- break;
1240- }
1241- case NETCMD_SET_PLAYERNUMBER: {
1242- int32_t number = packet.signed_32();
1243- d->settings.playernum = number;
1244- d->settings.users.at(d->settings.usernum).position = number;
1245- break;
1246- }
1247- case NETCMD_WIN_CONDITION: {
1248- d->settings.win_condition_script = g_fs->FileSystem::fix_cross_file(packet.string());
1249- break;
1250- }
1251- case NETCMD_PEACEFUL_MODE: {
1252- d->settings.peaceful = packet.unsigned_8();
1253- break;
1254- }
1255-
1256- case NETCMD_LAUNCH: {
1257- if (!d->modal || d->game) {
1258- throw DisconnectException("UNEXPECTED_LAUNCH");
1259- }
1260- d->modal->end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kOk);
1261- break;
1262- }
1263-
1264- case NETCMD_SETSPEED:
1265- d->realspeed = packet.unsigned_16();
1266- log("[Client] speed: %u.%03u\n", d->realspeed / 1000, d->realspeed % 1000);
1267- break;
1268-
1269- case NETCMD_TIME:
1270- d->time.receive(packet.signed_32());
1271- break;
1272-
1273- case NETCMD_WAIT:
1274- log("[Client]: server is waiting.\n");
1275- d->server_is_waiting = true;
1276- break;
1277-
1278- case NETCMD_PLAYERCOMMAND: {
1279- if (!d->game)
1280- throw DisconnectException("PLAYERCMD_WO_GAME");
1281-
1282- int32_t const time = packet.signed_32();
1283- Widelands::PlayerCommand& plcmd = *Widelands::PlayerCommand::deserialize(packet);
1284- plcmd.set_duetime(time);
1285- d->game->enqueue_command(&plcmd);
1286- d->time.receive(time);
1287- break;
1288- }
1289-
1290- case NETCMD_SYNCREQUEST: {
1291- if (!d->game)
1292- throw DisconnectException("SYNCREQUEST_WO_GAME");
1293- int32_t const time = packet.signed_32();
1294- d->time.receive(time);
1295- d->game->enqueue_command(new CmdNetCheckSync(time, [this] { sync_report_callback(); }));
1296- d->game->report_sync_request();
1297- break;
1298- }
1299-
1300- case NETCMD_CHAT: {
1301- ChatMessage c("");
1302- c.playern = packet.signed_16();
1303- c.sender = packet.string();
1304- c.msg = packet.string();
1305- if (packet.unsigned_8())
1306- c.recipient = packet.string();
1307- d->chatmessages.push_back(c);
1308- Notifications::publish(c);
1309- break;
1310- }
1311-
1312- case NETCMD_SYSTEM_MESSAGE_CODE: {
1313- const std::string code = packet.string();
1314- const std::string arg1 = packet.string();
1315- const std::string arg2 = packet.string();
1316- const std::string arg3 = packet.string();
1317- ChatMessage c(NetworkGamingMessages::get_message(code, arg1, arg2, arg3));
1318- c.playern = UserSettings::none(); // == System message
1319- // c.sender remains empty to indicate a system message
1320- d->chatmessages.push_back(c);
1321- Notifications::publish(c);
1322- break;
1323- }
1324-
1325- case NETCMD_INFO_DESYNC:
1326- log("[Client] received NETCMD_INFO_DESYNC. Trying to salvage some "
1327- "information for debugging.\n");
1328- if (d->game) {
1329- d->game->save_syncstream(true);
1330- // We don't know our playernumber, so report as -1
1331- d->game->report_desync(-1);
1332- }
1333- break;
1334-
1335- default:
1336- throw ProtocolException(cmd);
1337 }
1338 }
1339
1340@@ -917,7 +1022,7 @@
1341 */
1342 void GameClient::handle_network() {
1343 // if this is an internet game, handle the metaserver network
1344- if (internet_)
1345+ if (d->internet_)
1346 InternetGaming::ref().handle_metaserver_communication();
1347 try {
1348 assert(d->net != nullptr);
1349@@ -965,7 +1070,7 @@
1350
1351 bool const trysave = showmsg && d->game;
1352
1353- if (showmsg) {
1354+ if (showmsg && d->modal) { // can only show a message with a valid modal parent window
1355 std::string msg;
1356 if (arg.empty())
1357 msg = NetworkGamingMessages::get_message(reason);
1358@@ -985,8 +1090,9 @@
1359 if (trysave)
1360 WLApplication::emergency_save(*d->game);
1361
1362+ // TODO(Klaus Halfmann): Some of the modal windows are now handled by unique_ptr resulting in a double free.
1363 if (d->modal) {
1364 d->modal->end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kBack);
1365- d->modal = nullptr;
1366 }
1367+ d->modal = nullptr;
1368 }
1369
1370=== modified file 'src/network/gameclient.h'
1371--- src/network/gameclient.h 2019-04-29 16:22:08 +0000
1372+++ src/network/gameclient.h 2019-05-25 17:10:52 +0000
1373@@ -29,14 +29,16 @@
1374 #include "network/netclient_interface.h"
1375
1376 struct GameClientImpl;
1377+class InteractiveGameBase;
1378
1379-// TODO(unknown): Use composition instead of inheritance
1380 /**
1381 * GameClient manages the lifetime of a network game in which this computer
1382 * participates as a client.
1383 *
1384 * This includes running the game setup screen and the actual game after
1385 * launch, as well as dealing with the actual network protocol.
1386+ *
1387+ * @param internet TODO(Klaus Halfmann): true: coonnect into the open internet via proxy, false connect locally / via IP.
1388 */
1389 struct GameClient : public GameController, public GameSettingsProvider, public ChatProvider {
1390 GameClient(const std::pair<NetAddress, NetAddress>& host,
1391@@ -50,7 +52,7 @@
1392
1393 // GameController interface
1394 void think() override;
1395- void send_player_command(Widelands::PlayerCommand&) override;
1396+ void send_player_command(Widelands::PlayerCommand*) override;
1397 int32_t get_frametime() override;
1398 GameController::GameType get_game_type() override;
1399
1400@@ -115,7 +117,21 @@
1401
1402 void sync_report_callback();
1403
1404- void handle_packet(RecvPacket&);
1405+ void handle_hello(RecvPacket& packet);
1406+ void handle_disconnect(RecvPacket& packet);
1407+ void handle_ping(RecvPacket& packet);
1408+ void handle_new_file(RecvPacket& packet);
1409+ void handle_syncrequest(RecvPacket& packet);
1410+ void handle_setting_map(RecvPacket& packet);
1411+ void handle_file_part(RecvPacket& packet);
1412+ void handle_setting_tribes(RecvPacket& packet);
1413+ void handle_setting_allplayers(RecvPacket& packet);
1414+ void handle_playercommand(RecvPacket& packet);
1415+ void handle_chat(RecvPacket& packet);
1416+ void handle_system_message(RecvPacket& packet);
1417+ void handle_desync(RecvPacket& packet);
1418+ void handle_packet(RecvPacket& packet);
1419+
1420 void handle_network();
1421 void send_time();
1422 void receive_one_player(uint8_t number, StreamRead&);
1423@@ -125,9 +141,9 @@
1424 bool sendreason = true,
1425 bool showmsg = true);
1426
1427- std::unique_ptr<NetTransferFile> file_;
1428+
1429 GameClientImpl* d;
1430- bool internet_;
1431+
1432 };
1433
1434 #endif // end of include guard: WL_NETWORK_GAMECLIENT_H
1435
1436=== modified file 'src/network/gamehost.cc'
1437--- src/network/gamehost.cc 2019-05-11 22:55:40 +0000
1438+++ src/network/gamehost.cc 2019-05-25 17:10:52 +0000
1439@@ -761,15 +761,15 @@
1440 }
1441 }
1442
1443-void GameHost::send_player_command(Widelands::PlayerCommand& pc) {
1444- pc.set_duetime(d->committed_networktime + 1);
1445+void GameHost::send_player_command(Widelands::PlayerCommand* pc) {
1446+ pc->set_duetime(d->committed_networktime + 1);
1447
1448 SendPacket packet;
1449 packet.unsigned_8(NETCMD_PLAYERCOMMAND);
1450- packet.signed_32(pc.duetime());
1451- pc.serialize(packet);
1452+ packet.signed_32(pc->duetime());
1453+ pc-> serialize(packet);
1454 broadcast(packet);
1455- d->game->enqueue_command(&pc);
1456+ d->game->enqueue_command(pc);
1457
1458 committed_network_time(d->committed_networktime + 1);
1459 }
1460@@ -2181,11 +2181,11 @@
1461 if (!d->game)
1462 throw DisconnectException("PLAYERCMD_WO_GAME");
1463 int32_t time = r.signed_32();
1464- Widelands::PlayerCommand& plcmd = *Widelands::PlayerCommand::deserialize(r);
1465+ Widelands::PlayerCommand* plcmd = Widelands::PlayerCommand::deserialize(r);
1466 log("[Host]: Client %u (%u) sent player command %u for %u, time = %i\n", i, client.playernum,
1467- static_cast<unsigned int>(plcmd.id()), plcmd.sender(), time);
1468+ static_cast<unsigned int>(plcmd->id()), plcmd->sender(), time);
1469 receive_client_time(i, time);
1470- if (plcmd.sender() != client.playernum + 1)
1471+ if (plcmd->sender() != client.playernum + 1)
1472 throw DisconnectException("PLAYERCMD_FOR_OTHER");
1473 send_player_command(plcmd);
1474 } break;
1475
1476=== modified file 'src/network/gamehost.h'
1477--- src/network/gamehost.h 2019-05-04 10:47:44 +0000
1478+++ src/network/gamehost.h 2019-05-25 17:10:52 +0000
1479@@ -50,7 +50,7 @@
1480
1481 // GameController interface
1482 void think() override;
1483- void send_player_command(Widelands::PlayerCommand&) override;
1484+ void send_player_command(Widelands::PlayerCommand*) override;
1485 int32_t get_frametime() override;
1486 GameController::GameType get_game_type() override;
1487
1488
1489=== modified file 'src/network/netclient_interface.h'
1490--- src/network/netclient_interface.h 2019-02-23 11:00:49 +0000
1491+++ src/network/netclient_interface.h 2019-05-25 17:10:52 +0000
1492@@ -27,6 +27,7 @@
1493 /**
1494 * NetClient manages the network connection for a network game in which this computer
1495 * participates as a client.
1496+ *
1497 * This class provides the interface all NetClient implementation have to follow.
1498 * Currently two implementations exists: A "real" NetClient for local games and a
1499 * NetClientProxy which relays commands over a relay server.
1500@@ -42,18 +43,21 @@
1501
1502 /**
1503 * Returns whether the client is connected.
1504+ *
1505 * \return \c true if the connection is open, \c false otherwise.
1506 */
1507 virtual bool is_connected() const = 0;
1508
1509 /**
1510 * Closes the connection.
1511+ *
1512 * If you want to send a goodbye-message to the host, do so before calling this.
1513 */
1514 virtual void close() = 0;
1515
1516 /**
1517 * Tries to receive a packet.
1518+ *
1519 * \return A pointer to a packet if one packet is available, an invalid pointer otherwise.
1520 * Calling this on a closed connection will return an invalid pointer.
1521 */
1522@@ -61,6 +65,7 @@
1523
1524 /**
1525 * Sends a packet.
1526+ *
1527 * Calling this on a closed connection will silently fail.
1528 * \param packet The packet to send.
1529 */
1530
1531=== modified file 'src/network/nethostproxy.cc'
1532--- src/network/nethostproxy.cc 2018-03-03 10:48:14 +0000
1533+++ src/network/nethostproxy.cc 2019-05-25 17:10:52 +0000
1534@@ -256,7 +256,7 @@
1535 conn_->receive(&cmd);
1536 uint8_t id;
1537 conn_->receive(&id);
1538- assert(clients_.count(id));
1539+ assert(clients_.count(id)); // TODO(Klaus Halfmann): As of a race condition this may not always hold.
1540 clients_.at(id).state_ = Client::State::kDisconnected;
1541 }
1542 break;
1543
1544=== modified file 'src/wui/economy_options_window.cc'
1545--- src/wui/economy_options_window.cc 2019-02-23 11:00:49 +0000
1546+++ src/wui/economy_options_window.cc 2019-05-25 17:10:52 +0000
1547@@ -189,11 +189,11 @@
1548 // Don't allow negative new amount.
1549 if (amount >= 0 || -amount <= static_cast<int>(tq.permanent)) {
1550 if (is_wares) {
1551- game.send_player_command(*new Widelands::CmdSetWareTargetQuantity(
1552+ game.send_player_command(new Widelands::CmdSetWareTargetQuantity(
1553 game.get_gametime(), player_->player_number(), serial_, index,
1554 tq.permanent + amount));
1555 } else {
1556- game.send_player_command(*new Widelands::CmdSetWorkerTargetQuantity(
1557+ game.send_player_command(new Widelands::CmdSetWorkerTargetQuantity(
1558 game.get_gametime(), player_->player_number(), serial_, index,
1559 tq.permanent + amount));
1560 }
1561@@ -209,10 +209,10 @@
1562 for (const Widelands::DescriptionIndex& index : items) {
1563 if (display_.ware_selected(index)) {
1564 if (is_wares) {
1565- game.send_player_command(*new Widelands::CmdResetWareTargetQuantity(
1566+ game.send_player_command(new Widelands::CmdResetWareTargetQuantity(
1567 game.get_gametime(), player_->player_number(), serial_, index));
1568 } else {
1569- game.send_player_command(*new Widelands::CmdResetWorkerTargetQuantity(
1570+ game.send_player_command(new Widelands::CmdResetWorkerTargetQuantity(
1571 game.get_gametime(), player_->player_number(), serial_, index));
1572 }
1573 }
1574
1575=== modified file 'src/wui/game_message_menu.cc'
1576--- src/wui/game_message_menu.cc 2019-04-18 16:50:35 +0000
1577+++ src/wui/game_message_menu.cc 2019-05-25 17:10:52 +0000
1578@@ -336,7 +336,7 @@
1579 // Maybe the message was removed since think?
1580 if (message->status() == Message::Status::kNew) {
1581 Widelands::Game& game = iplayer().game();
1582- game.send_player_command(*new Widelands::CmdMessageSetStatusRead(
1583+ game.send_player_command(new Widelands::CmdMessageSetStatusRead(
1584 game.get_gametime(), player.player_number(), id));
1585 }
1586 centerviewbtn_->set_enabled(message->position());
1587@@ -439,12 +439,12 @@
1588 switch (mode) {
1589 case Inbox:
1590 // Archive highlighted message
1591- game.send_player_command(*new Widelands::CmdMessageSetStatusArchived(
1592+ game.send_player_command(new Widelands::CmdMessageSetStatusArchived(
1593 game.get_gametime(), plnum, MessageId(selected_record)));
1594 break;
1595 case Archive:
1596 // Restore highlighted message
1597- game.send_player_command(*new Widelands::CmdMessageSetStatusRead(
1598+ game.send_player_command(new Widelands::CmdMessageSetStatusRead(
1599 game.get_gametime(), plnum, MessageId(selected_record)));
1600 break;
1601 }
1602
1603=== modified file 'src/wui/warehousewindow.cc'
1604--- src/wui/warehousewindow.cc 2019-02-23 11:00:49 +0000
1605+++ src/wui/warehousewindow.cc 2019-05-25 17:10:52 +0000
1606@@ -160,7 +160,7 @@
1607
1608 for (const Widelands::DescriptionIndex& index : indices) {
1609 if (display_.ware_selected(index)) {
1610- gb_.game().send_player_command(*new Widelands::CmdSetStockPolicy(
1611+ gb_.game().send_player_command(new Widelands::CmdSetStockPolicy(
1612 gb_.game().get_gametime(), wh_.owner().player_number(), wh_, is_workers, index,
1613 newpolicy));
1614 }

Subscribers

People subscribed via source and target branches

to status/vote changes: