Merge lp:~widelands-dev/widelands/market1 into lp:widelands
- market1
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 8462 |
Proposed branch: | lp:~widelands-dev/widelands/market1 |
Merge into: | lp:widelands |
Diff against target: |
1398 lines (+786/-64) 32 files modified
data/tribes/buildings/markets/barbarians/market/init.lua (+1/-3) src/economy/request.h (+2/-5) src/economy/ware_instance.cc (+2/-1) src/game_io/game_class_packet.cc (+2/-0) src/logic/CMakeLists.txt (+7/-6) src/logic/game.cc (+82/-0) src/logic/game.h (+10/-0) src/logic/map_objects/bob.cc (+4/-0) src/logic/map_objects/bob.h (+1/-0) src/logic/map_objects/tribes/market.cc (+222/-1) src/logic/map_objects/tribes/market.h (+48/-1) src/logic/map_objects/tribes/productionsite.cc (+5/-2) src/logic/map_objects/tribes/productionsite.h (+0/-7) src/logic/map_objects/tribes/tribes.cc (+1/-0) src/logic/map_objects/tribes/tribes.h (+0/-1) src/logic/map_objects/tribes/worker.cc (+83/-0) src/logic/map_objects/tribes/worker.h (+5/-0) src/logic/message.h (+2/-1) src/logic/playercommand.cc (+93/-1) src/logic/playercommand.h (+23/-0) src/logic/queue_cmd_factory.cc (+2/-0) src/logic/queue_cmd_ids.h (+2/-1) src/logic/trade_agreement.h (+54/-0) src/map_io/map_buildingdata_packet.cc (+1/-0) src/scripting/lua_map.cc (+76/-27) src/scripting/lua_map.h (+2/-0) src/wui/game_message_menu.cc (+2/-0) src/wui/ware_statistics_menu.cc (+1/-2) src/wui/ware_statistics_menu.h (+0/-1) test/maps/market_trading.wmf/scripting/init.lua (+12/-2) test/maps/market_trading.wmf/scripting/test_market_can_be_built.lua (+2/-2) test/maps/market_trading.wmf/scripting/test_simple_trade.lua (+39/-0) |
To merge this branch: | bzr merge lp:~widelands-dev/widelands/market1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
SirVer | Needs Resubmitting | ||
TiborB | Approve | ||
Review via email: mp+331233@code.launchpad.net |
Commit message
First working land-based trading implementation.
A trade works like this: A player proposes to another players market to initiate a trade. A trade consists of the three pieces of information: what wares will I sent, what wares do I want from you and how often do we exchange. For example I'll send 3 logs whenever you send 1 plank and 5 iron and we do ship these batches 10 times.
Once a trade is initiated, both players will require the correct number of oxen for a batch, i.e. p1 requires 3 (for 3 logs) and p2 requires 6 (1 plank + 5 iron). Once all workers and wares for one batch have arrived in the markets, both markets will launch one caravan and the items gets exchanged.
This is a minimal implementation and not ready for use in-game. Highlights of stuff still missing:
- UI (clicking a market will crash the game).
- denying/accepting a proposed trade. Every proposed trade is auto-accepted atm.
- releasing trade carriers and left-over wares once a trade is cancelled or fulfilled.
- load/save/serialize for networking for all commands and new map objects.
Implemented:
- Adds Lua functions to propose a trade from Lua.
- Adds a PlayerCommand to propose a trade from Lua, but this is still unused.
- Adds logic to trade between two Markets.
- Adds a test that does a simple trade.
The test is run with the regular regression test runs, but can be run standalone using:
./build/
Description of the change
SirVer (sirver) wrote : | # |
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 2686. State: errored. Details: https:/
Appveyor build 2506. State: success. Details: https:/
SirVer (sirver) wrote : | # |
I am unsure as of now how, but this breaks the casern and something in seafaring. I have to investigate how I broke this.
SirVer (sirver) wrote : | # |
Found it - a copy & pasto. Great to have tests catching stuff like this :)
bunnybot (widelandsofficial) wrote : | # |
Continuous integration builds have changed state:
Travis build 2689. State: passed. Details: https:/
Appveyor build 2509. State: success. Details: https:/
TiborB (tiborb95) wrote : | # |
I compiled the branch and "tested" it, though actually there is not much to test.
I did as well some AI-only test so at least I can say that this does not affect normal play.
So IMHO it can go.
Did not read the code though
GunChleoc (gunchleoc) wrote : | # |
I added a bunch of small nits & ideas.
I don't know when I'll be able to check in again, so please do merge once you're happy.
SirVer (sirver) wrote : | # |
Thank you for the code review! All addressed.
@bunnybot merge
GunChleoc (gunchleoc) wrote : | # |
I added some more comments for a follow-up branch.
Preview Diff
1 | === modified file 'data/tribes/buildings/markets/barbarians/market/init.lua' |
2 | --- data/tribes/buildings/markets/barbarians/market/init.lua 2017-09-15 19:53:28 +0000 |
3 | +++ data/tribes/buildings/markets/barbarians/market/init.lua 2017-10-03 20:35:46 +0000 |
4 | @@ -50,7 +50,5 @@ |
5 | prohibited_till = 1000 |
6 | }, |
7 | |
8 | - working_positions = { |
9 | - barbarians_ox = 10, |
10 | - }, |
11 | + carrier = "barbarians_ox", |
12 | } |
13 | |
14 | === modified file 'src/economy/request.h' |
15 | --- src/economy/request.h 2017-01-25 18:55:59 +0000 |
16 | +++ src/economy/request.h 2017-10-03 20:35:46 +0000 |
17 | @@ -123,6 +123,7 @@ |
18 | // callbacks for WareInstance/Worker code |
19 | void transfer_finish(Game&, Transfer&); |
20 | void transfer_fail(Game&, Transfer&); |
21 | + void cancel_transfer(uint32_t idx); |
22 | |
23 | void set_requirements(const Requirements& r) { |
24 | requirements_ = r; |
25 | @@ -131,13 +132,9 @@ |
26 | return requirements_; |
27 | } |
28 | |
29 | + |
30 | private: |
31 | int32_t get_base_required_time(EditorGameBase&, uint32_t nr) const; |
32 | - |
33 | -public: |
34 | - void cancel_transfer(uint32_t idx); |
35 | - |
36 | -private: |
37 | void remove_transfer(uint32_t idx); |
38 | uint32_t find_transfer(Transfer&); |
39 | |
40 | |
41 | === modified file 'src/economy/ware_instance.cc' |
42 | --- src/economy/ware_instance.cc 2017-08-16 10:14:29 +0000 |
43 | +++ src/economy/ware_instance.cc 2017-10-03 20:35:46 +0000 |
44 | @@ -309,8 +309,9 @@ |
45 | |
46 | // Update whether we have a Supply or not |
47 | if (!transfer_ || !transfer_->get_request()) { |
48 | - if (!supply_) |
49 | + if (!supply_) { |
50 | supply_ = new IdleWareSupply(*this); |
51 | + } |
52 | } else { |
53 | delete supply_; |
54 | supply_ = nullptr; |
55 | |
56 | === modified file 'src/game_io/game_class_packet.cc' |
57 | --- src/game_io/game_class_packet.cc 2017-01-25 18:55:59 +0000 |
58 | +++ src/game_io/game_class_packet.cc 2017-10-03 20:35:46 +0000 |
59 | @@ -61,6 +61,8 @@ |
60 | // Write gametime |
61 | fw.unsigned_32(game.gametime_); |
62 | |
63 | + // TODO(sirver,trading): save/load trade_agreements and related data. |
64 | + |
65 | // We do not care for players, since they were set |
66 | // on game initialization to match Map::scenario_player_[names|tribes] |
67 | // or vice versa, so this is handled by map loader |
68 | |
69 | === modified file 'src/logic/CMakeLists.txt' |
70 | --- src/logic/CMakeLists.txt 2017-09-15 12:33:58 +0000 |
71 | +++ src/logic/CMakeLists.txt 2017-10-03 20:35:46 +0000 |
72 | @@ -95,14 +95,14 @@ |
73 | findimmovable.h |
74 | findnode.cc |
75 | findnode.h |
76 | + game.cc |
77 | + game.h |
78 | game_data_error.cc |
79 | game_data_error.h |
80 | - game.cc |
81 | - game.h |
82 | + map.cc |
83 | + map.h |
84 | map_revision.cc |
85 | map_revision.h |
86 | - map.cc |
87 | - map.h |
88 | mapastar.cc |
89 | mapastar.h |
90 | mapdifferenceregion.cc |
91 | @@ -114,18 +114,18 @@ |
92 | mapregion.h |
93 | maptriangleregion.cc |
94 | maptriangleregion.h |
95 | + message.h |
96 | message_id.h |
97 | message_queue.h |
98 | - message.h |
99 | nodecaps.h |
100 | objective.h |
101 | path.cc |
102 | path.h |
103 | pathfield.cc |
104 | pathfield.h |
105 | - player_area.h |
106 | player.cc |
107 | player.h |
108 | + player_area.h |
109 | playercommand.cc |
110 | playercommand.h |
111 | playersmanager.cc |
112 | @@ -138,6 +138,7 @@ |
113 | roadtype.h |
114 | save_handler.cc |
115 | save_handler.h |
116 | + trade_agreement.h |
117 | widelands_geometry_io.cc |
118 | widelands_geometry_io.h |
119 | map_objects/bob.cc |
120 | |
121 | === modified file 'src/logic/game.cc' |
122 | --- src/logic/game.cc 2017-08-30 12:01:47 +0000 |
123 | +++ src/logic/game.cc 2017-10-03 20:35:46 +0000 |
124 | @@ -49,6 +49,7 @@ |
125 | #include "logic/cmd_luascript.h" |
126 | #include "logic/game_settings.h" |
127 | #include "logic/map_objects/tribes/carrier.h" |
128 | +#include "logic/map_objects/tribes/market.h" |
129 | #include "logic/map_objects/tribes/militarysite.h" |
130 | #include "logic/map_objects/tribes/ship.h" |
131 | #include "logic/map_objects/tribes/soldier.h" |
132 | @@ -753,6 +754,87 @@ |
133 | get_gametime(), ship.get_owner()->player_number(), ship.serial())); |
134 | } |
135 | |
136 | +void Game::send_player_propose_trade(const Trade& trade) { |
137 | + auto* object = objects().get_object(trade.initiator); |
138 | + assert(object != nullptr); |
139 | + send_player_command( |
140 | + *new CmdProposeTrade(get_gametime(), object->get_owner()->player_number(), trade)); |
141 | +} |
142 | + |
143 | +int Game::propose_trade(const Trade& trade) { |
144 | + // TODO(sirver,trading): Check if a trade is possible (i.e. if there is a |
145 | + // path between the two markets); |
146 | + const int id = next_trade_agreement_id_; |
147 | + ++next_trade_agreement_id_; |
148 | + |
149 | + auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator)); |
150 | + auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver)); |
151 | + // This is only ever called through a PlayerCommand and that already made |
152 | + // sure that the objects still exist. Since no time has passed, they should |
153 | + // not have vanished under us. |
154 | + assert(initiator != nullptr); |
155 | + assert(receiver != nullptr); |
156 | + |
157 | + receiver->removed.connect( |
158 | + [this, id](const uint32_t /* serial */) { cancel_trade(id); }); |
159 | + initiator->removed.connect( |
160 | + [this, id](const uint32_t /* serial */) { cancel_trade(id); }); |
161 | + |
162 | + receiver->send_message(*this, Message::Type::kTradeOfferReceived, _("Trade Offer"), |
163 | + receiver->descr().icon_filename(), receiver->descr().descname(), |
164 | + _("This market has received a new trade offer."), true); |
165 | + trade_agreements_[id] = TradeAgreement{TradeAgreement::State::kProposed, trade}; |
166 | + |
167 | + // TODO(sirver,trading): this should be done through another player_command, but I |
168 | + // want to get to the trade logic implementation now. |
169 | + accept_trade(id); |
170 | + return id; |
171 | +} |
172 | + |
173 | +void Game::accept_trade(const int trade_id) { |
174 | + auto it = trade_agreements_.find(trade_id); |
175 | + if (it == trade_agreements_.end()) { |
176 | + log("Game::accept_trade: Trade %d has vanished. Ignoring.\n", trade_id); |
177 | + return; |
178 | + } |
179 | + const Trade& trade = it->second.trade; |
180 | + auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator)); |
181 | + auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver)); |
182 | + if (initiator == nullptr || receiver == nullptr) { |
183 | + cancel_trade(trade_id); |
184 | + return; |
185 | + } |
186 | + |
187 | + initiator->new_trade(trade_id, trade.items_to_send, trade.num_batches, trade.receiver); |
188 | + receiver->new_trade(trade_id, trade.items_to_receive, trade.num_batches, trade.initiator); |
189 | + |
190 | + // TODO(sirver,trading): Message the users that the trade has been accepted. |
191 | +} |
192 | + |
193 | +void Game::cancel_trade(int trade_id) { |
194 | + // The trade id might be long gone - since we never disconnect from the |
195 | + // 'removed' signal of the two buildings, we might be invoked long after the |
196 | + // trade was deleted for other reasons. |
197 | + const auto it = trade_agreements_.find(trade_id); |
198 | + if (it == trade_agreements_.end()) { |
199 | + return; |
200 | + } |
201 | + const auto& trade = it->second.trade; |
202 | + |
203 | + auto* initiator = dynamic_cast<Market*>(objects().get_object(trade.initiator)); |
204 | + if (initiator != nullptr) { |
205 | + initiator->cancel_trade(trade_id); |
206 | + // TODO(sirver,trading): Send message to owner that the trade has been canceled. |
207 | + } |
208 | + |
209 | + auto* receiver = dynamic_cast<Market*>(objects().get_object(trade.receiver)); |
210 | + if (receiver != nullptr) { |
211 | + receiver->cancel_trade(trade_id); |
212 | + // TODO(sirver,trading): Send message to owner that the trade has been canceled. |
213 | + } |
214 | + trade_agreements_.erase(trade_id); |
215 | +} |
216 | + |
217 | LuaGameInterface& Game::lua() { |
218 | return static_cast<LuaGameInterface&>(EditorGameBase::lua()); |
219 | } |
220 | |
221 | === modified file 'src/logic/game.h' |
222 | --- src/logic/game.h 2017-08-18 00:17:39 +0000 |
223 | +++ src/logic/game.h 2017-10-03 20:35:46 +0000 |
224 | @@ -27,6 +27,7 @@ |
225 | #include "logic/cmd_queue.h" |
226 | #include "logic/editor_game_base.h" |
227 | #include "logic/save_handler.h" |
228 | +#include "logic/trade_agreement.h" |
229 | #include "random/random.h" |
230 | #include "scripting/logic.h" |
231 | |
232 | @@ -207,6 +208,7 @@ |
233 | void send_player_ship_explore_island(Ship&, IslandExploreDirection); |
234 | void send_player_sink_ship(Ship&); |
235 | void send_player_cancel_expedition_ship(Ship&); |
236 | + void send_player_propose_trade(const Trade& trade); |
237 | |
238 | InteractivePlayer* get_ipl(); |
239 | |
240 | @@ -244,6 +246,11 @@ |
241 | |
242 | void set_auto_speed(bool); |
243 | |
244 | + // TODO(sirver,trading): document these functions once the interface settles. |
245 | + int propose_trade(const Trade& trade); |
246 | + void accept_trade(int trade_id); |
247 | + void cancel_trade(int trade_id); |
248 | + |
249 | private: |
250 | void sync_reset(); |
251 | |
252 | @@ -308,6 +315,9 @@ |
253 | std::unique_ptr<ReplayWriter> replaywriter_; |
254 | |
255 | GeneralStatsVector general_stats_; |
256 | + int next_trade_agreement_id_ = 1; |
257 | + // Maps from trade agreement id to the agreement. |
258 | + std::map<int, TradeAgreement> trade_agreements_; |
259 | |
260 | /// For save games and statistics generation |
261 | std::string win_condition_displayname_; |
262 | |
263 | === modified file 'src/logic/map_objects/bob.cc' |
264 | --- src/logic/map_objects/bob.cc 2017-08-20 08:34:02 +0000 |
265 | +++ src/logic/map_objects/bob.cc 2017-10-03 20:35:46 +0000 |
266 | @@ -390,6 +390,10 @@ |
267 | state.ivar1 = 0; |
268 | } |
269 | |
270 | +bool Bob::is_idle() { |
271 | + return get_state(taskIdle); |
272 | +} |
273 | + |
274 | /** |
275 | * Move along a predefined path. |
276 | * \par ivar1 the step number. |
277 | |
278 | === modified file 'src/logic/map_objects/bob.h' |
279 | --- src/logic/map_objects/bob.h 2017-06-24 08:47:46 +0000 |
280 | +++ src/logic/map_objects/bob.h 2017-10-03 20:35:46 +0000 |
281 | @@ -278,6 +278,7 @@ |
282 | // TODO(feature-Hasi50): correct (?) Send a signal that may switch to some other \ref Task |
283 | void send_signal(Game&, char const*); |
284 | void start_task_idle(Game&, uint32_t anim, int32_t timeout); |
285 | + bool is_idle(); |
286 | |
287 | /// This can fail (and return false). Therefore the caller must check the |
288 | /// result and find something else for the bob to do. Otherwise there will |
289 | |
290 | === modified file 'src/logic/map_objects/tribes/market.cc' |
291 | --- src/logic/map_objects/tribes/market.cc 2017-09-18 13:43:08 +0000 |
292 | +++ src/logic/map_objects/tribes/market.cc 2017-10-03 20:35:46 +0000 |
293 | @@ -19,8 +19,11 @@ |
294 | |
295 | #include "logic/map_objects/tribes/market.h" |
296 | |
297 | +#include <memory> |
298 | + |
299 | #include "base/i18n.h" |
300 | #include "logic/map_objects/tribes/productionsite.h" |
301 | +#include "logic/map_objects/tribes/tribes.h" |
302 | |
303 | namespace Widelands { |
304 | |
305 | @@ -30,7 +33,12 @@ |
306 | : BuildingDescr(init_descname, MapObjectType::MARKET, table, egbase) { |
307 | i18n::Textdomain td("tribes"); |
308 | |
309 | - parse_working_positions(egbase, table.get_table("working_positions").get(), &working_positions_); |
310 | + DescriptionIndex const woi = egbase.tribes().worker_index(table.get_string("carrier")); |
311 | + if (!egbase.tribes().worker_exists(woi)) { |
312 | + throw wexception("The tribe does not define the worker in 'carrier'."); |
313 | + } |
314 | + carrier_ = woi; |
315 | + |
316 | // TODO(sirver,trading): Add actual logic here. |
317 | } |
318 | |
319 | @@ -38,10 +46,223 @@ |
320 | return *new Market(*this); |
321 | } |
322 | |
323 | +int Market::TradeOrder::num_wares_per_batch() const { |
324 | + int sum = 0; |
325 | + for (const auto& item_pair : items) { |
326 | + sum += item_pair.second; |
327 | + } |
328 | + return sum; |
329 | +} |
330 | + |
331 | +bool Market::TradeOrder::fulfilled() const { |
332 | + return num_shipped_batches == initial_num_batches; |
333 | +} |
334 | + |
335 | +// TODO(sirver,trading): This needs to implement 'set_economy'. Maybe common code can be shared. |
336 | Market::Market(const MarketDescr& the_descr) : Building(the_descr) { |
337 | } |
338 | |
339 | Market::~Market() { |
340 | } |
341 | |
342 | +void Market::new_trade(const int trade_id, |
343 | + const BillOfMaterials& items, |
344 | + const int num_batches, |
345 | + const Serial other_side) { |
346 | + trade_orders_[trade_id] = TradeOrder{items, num_batches, 0, other_side, 0, nullptr, {}}; |
347 | + auto& trade_order = trade_orders_[trade_id]; |
348 | + |
349 | + // Request one worker for each item in a batch. |
350 | + trade_order.worker_request.reset( |
351 | + new Request(*this, descr().carrier(), Market::worker_arrived_callback, wwWORKER)); |
352 | + trade_order.worker_request->set_count(trade_order.num_wares_per_batch()); |
353 | + |
354 | + // Make sure we have a wares queue for each item in this. |
355 | + for (const auto& entry : items) { |
356 | + ensure_wares_queue_exists(entry.first); |
357 | + } |
358 | +} |
359 | + |
360 | +void Market::cancel_trade(const int trade_id) { |
361 | + // TODO(sirver,trading): Launch workers, release no longer required wares and delete now unneeded 'WaresQueue's |
362 | + trade_orders_.erase(trade_id); |
363 | +} |
364 | + |
365 | +void Market::worker_arrived_callback( |
366 | + Game& game, Request& rq, DescriptionIndex /* widx */, Worker* const w, PlayerImmovable& target) { |
367 | + auto& market = dynamic_cast<Market&>(target); |
368 | + |
369 | + assert(w); |
370 | + assert(w->get_location(game) == &market); |
371 | + |
372 | + for (auto& trade_order_pair : market.trade_orders_) { |
373 | + auto& trade_order = trade_order_pair.second; |
374 | + if (trade_order.worker_request.get() != &rq) { |
375 | + continue; |
376 | + } |
377 | + |
378 | + if (rq.get_count() == 0) { |
379 | + // Erase this request. |
380 | + trade_order.worker_request.reset(); |
381 | + } |
382 | + w->start_task_idle(game, 0, -1); |
383 | + trade_order.workers.push_back(w); |
384 | + Notifications::publish(NoteBuilding(market.serial(), NoteBuilding::Action::kWorkersChanged)); |
385 | + market.try_launching_batch(&game); |
386 | + return; |
387 | + } |
388 | + NEVER_HERE(); // We should have found and handled a match by now. |
389 | +} |
390 | + |
391 | +void Market::ware_arrived_callback(Game& g, InputQueue*, DescriptionIndex, Worker*, void* data) { |
392 | + Market& market = *static_cast<Market*>(data); |
393 | + market.try_launching_batch(&g); |
394 | +} |
395 | + |
396 | +void Market::try_launching_batch(Game* game) { |
397 | + for (auto& pair : trade_orders_) { |
398 | + if (!is_ready_to_launch_batch(pair.first)) { |
399 | + continue; |
400 | + } |
401 | + |
402 | + auto* other_market = |
403 | + dynamic_cast<Market*>(game->objects().get_object(pair.second.other_side)); |
404 | + if (other_market == nullptr) { |
405 | + // TODO(sirver,trading): Can this even happen? Where is this function called from? |
406 | + // The other market seems to have vanished. The game tracks this and |
407 | + // should soon delete this trade request from us. We just ignore it. |
408 | + continue; |
409 | + } |
410 | + if (!other_market->is_ready_to_launch_batch(pair.first)) { |
411 | + continue; |
412 | + } |
413 | + launch_batch(pair.first, game); |
414 | + other_market->launch_batch(pair.first, game); |
415 | + break; |
416 | + } |
417 | +} |
418 | + |
419 | +bool Market::is_ready_to_launch_batch(const int trade_id) { |
420 | + const auto it = trade_orders_.find(trade_id); |
421 | + if (it == trade_orders_.end()) { |
422 | + return false; |
423 | + } |
424 | + auto& trade_order = it->second; |
425 | + assert(!trade_order.fulfilled()); |
426 | + |
427 | + // Do we have all necessary wares for a batch? |
428 | + for (const auto& item_pair : trade_order.items) { |
429 | + const auto wares_it = wares_queue_.find(item_pair.first); |
430 | + if (wares_it == wares_queue_.end()) { |
431 | + return false; |
432 | + } |
433 | + if (wares_it->second->get_filled() < item_pair.second) { |
434 | + return false; |
435 | + } |
436 | + } |
437 | + |
438 | + // Do we have enough people to carry wares? |
439 | + int num_available_carriers = 0; |
440 | + for (auto* worker : trade_order.workers) { |
441 | + num_available_carriers += worker->is_idle() ? 1 : 0; |
442 | + } |
443 | + return num_available_carriers == trade_order.num_wares_per_batch(); |
444 | +} |
445 | + |
446 | +void Market::launch_batch(const int trade_id, Game* game) { |
447 | + assert(is_ready_to_launch_batch(trade_id)); |
448 | + auto& trade_order = trade_orders_.at(trade_id); |
449 | + |
450 | + // Do we have all necessary wares for a batch? |
451 | + int worker_index = 0; |
452 | + for (const auto& item_pair : trade_order.items) { |
453 | + for (size_t i = 0; i < item_pair.second; ++i) { |
454 | + Worker* carrier = trade_order.workers.at(worker_index); |
455 | + ++worker_index; |
456 | + assert(carrier->is_idle()); |
457 | + |
458 | + // Give the carrier a ware. |
459 | + WareInstance* ware = |
460 | + new WareInstance(item_pair.first, game->tribes().get_ware_descr(item_pair.first)); |
461 | + ware->init(*game); |
462 | + carrier->set_carried_ware(*game, ware); |
463 | + |
464 | + // We have to remove this item from our economy. Otherwise it would be |
465 | + // considered idle (since it has no transport associated with it) and |
466 | + // the engine would want to transfer it to the next warehouse. |
467 | + ware->set_economy(nullptr); |
468 | + wares_queue_.at(item_pair.first) |
469 | + ->set_filled(wares_queue_.at(item_pair.first)->get_filled() - 1); |
470 | + |
471 | + // Send the carrier going. |
472 | + carrier->reset_tasks(*game); |
473 | + carrier->start_task_carry_trade_item( |
474 | + *game, trade_id, ObjectPointer(game->objects().get_object(trade_order.other_side))); |
475 | + } |
476 | + } |
477 | +} |
478 | + |
479 | +void Market::ensure_wares_queue_exists(int ware_index) { |
480 | + if (wares_queue_.count(ware_index) > 0) { |
481 | + return; |
482 | + } |
483 | + wares_queue_[ware_index] = |
484 | + std::unique_ptr<WaresQueue>(new WaresQueue(*this, ware_index, kMaxPerItemTradeBatchSize)); |
485 | + wares_queue_[ware_index]->set_callback(Market::ware_arrived_callback, this); |
486 | +} |
487 | + |
488 | +InputQueue& Market::inputqueue(DescriptionIndex index, WareWorker ware_worker) { |
489 | + assert(ware_worker == wwWARE); |
490 | + auto it = wares_queue_.find(index); |
491 | + if (it != wares_queue_.end()) { |
492 | + return *it->second; |
493 | + } |
494 | + // The parent will throw an exception. |
495 | + return Building::inputqueue(index, ware_worker); |
496 | +} |
497 | + |
498 | +void Market::cleanup(EditorGameBase& egbase) { |
499 | + for (auto& pair : wares_queue_) { |
500 | + pair.second->cleanup(); |
501 | + } |
502 | + Building::cleanup(egbase); |
503 | +} |
504 | + |
505 | +void Market::traded_ware_arrived(const int trade_id, const DescriptionIndex ware_index, Game* game) { |
506 | + auto& trade_order = trade_orders_.at(trade_id); |
507 | + |
508 | + WareInstance* ware = new WareInstance(ware_index, game->tribes().get_ware_descr(ware_index)); |
509 | + ware->init(*game); |
510 | + |
511 | + // TODO(sirver,trading): This is a hack. We should have a worker that |
512 | + // carriers stuff out. At the moment this assumes this market is barbarians |
513 | + // (which is always correct right now), creates a carrier for each received |
514 | + // ware to drop it off. The carrier then leaves the building and goes home. |
515 | + const WorkerDescr& w_desc = |
516 | + *game->tribes().get_worker_descr(game->tribes().worker_index("barbarians_carrier")); |
517 | + auto& worker = w_desc.create(*game, owner(), this, position_); |
518 | + worker.start_task_dropoff(*game, *ware); |
519 | + trade_order.received_traded_wares_in_this_batch += 1; |
520 | + owner().ware_produced(ware_index); |
521 | + |
522 | + auto* other_market = dynamic_cast<Market*>(game->objects().get_object(trade_order.other_side)); |
523 | + assert(other_market != nullptr); |
524 | + other_market->owner().ware_consumed(ware_index, 1); |
525 | + auto& other_trade_order = other_market->trade_orders_.at(trade_id); |
526 | + if (trade_order.received_traded_wares_in_this_batch == other_trade_order.num_wares_per_batch() && |
527 | + other_trade_order.received_traded_wares_in_this_batch == trade_order.num_wares_per_batch()) { |
528 | + // This batch is completed. |
529 | + ++trade_order.num_shipped_batches; |
530 | + trade_order.received_traded_wares_in_this_batch = 0; |
531 | + ++other_trade_order.num_shipped_batches; |
532 | + other_trade_order.received_traded_wares_in_this_batch = 0; |
533 | + if (trade_order.fulfilled()) { |
534 | + assert(other_trade_order.fulfilled()); |
535 | + // TODO(sirver,trading): This is not quite correct. This should for |
536 | + // example send a differnet message than actually canceling a trade. |
537 | + game->cancel_trade(trade_id); |
538 | + } |
539 | + } |
540 | +} |
541 | + |
542 | } // namespace Widelands |
543 | |
544 | === modified file 'src/logic/map_objects/tribes/market.h' |
545 | --- src/logic/map_objects/tribes/market.h 2017-09-15 12:33:58 +0000 |
546 | +++ src/logic/map_objects/tribes/market.h 2017-10-03 20:35:46 +0000 |
547 | @@ -20,6 +20,10 @@ |
548 | #ifndef WL_LOGIC_MAP_OBJECTS_TRIBES_MARKET_H |
549 | #define WL_LOGIC_MAP_OBJECTS_TRIBES_MARKET_H |
550 | |
551 | +#include <memory> |
552 | + |
553 | +#include "economy/request.h" |
554 | +#include "economy/wares_queue.h" |
555 | #include "logic/map_objects/tribes/building.h" |
556 | |
557 | namespace Widelands { |
558 | @@ -32,8 +36,10 @@ |
559 | |
560 | Building& create_object() const override; |
561 | |
562 | + DescriptionIndex carrier() const { return carrier_; } |
563 | + |
564 | private: |
565 | - BillOfMaterials working_positions_; |
566 | + DescriptionIndex carrier_; |
567 | }; |
568 | |
569 | class Market : public Building { |
570 | @@ -42,7 +48,48 @@ |
571 | explicit Market(const MarketDescr& descr); |
572 | ~Market() override; |
573 | |
574 | + void new_trade(int trade_id, const BillOfMaterials& items, int num_batches, Serial other_side); |
575 | + void cancel_trade(int trade_id); |
576 | + |
577 | + InputQueue& inputqueue(DescriptionIndex, WareWorker) override; |
578 | + void cleanup(EditorGameBase&) override; |
579 | + |
580 | + void try_launching_batch(Game* game); |
581 | + void traded_ware_arrived(int trade_id, DescriptionIndex ware_index, Game* game); |
582 | + |
583 | private: |
584 | + struct TradeOrder { |
585 | + BillOfMaterials items; |
586 | + int initial_num_batches; |
587 | + int num_shipped_batches; |
588 | + Serial other_side; |
589 | + |
590 | + int received_traded_wares_in_this_batch; |
591 | + |
592 | + // The invariant here is that worker.size() + worker_request.get_count() |
593 | + // == 'num_wares_per_batch()' |
594 | + std::unique_ptr<Request> worker_request; |
595 | + std::vector<Worker*> workers; |
596 | + |
597 | + // The number of individual wares in 'items', i.e. the sum of all '.second's. |
598 | + int num_wares_per_batch() const; |
599 | + |
600 | + // True if the 'num_shipped_batches' equals the 'initial_num_batches' |
601 | + bool fulfilled() const; |
602 | + }; |
603 | + |
604 | + static void |
605 | + worker_arrived_callback(Game&, Request&, DescriptionIndex, Worker*, PlayerImmovable&); |
606 | + static void |
607 | + ware_arrived_callback(Game& g, InputQueue* q, DescriptionIndex ware, Worker* worker, void* data); |
608 | + |
609 | + void ensure_wares_queue_exists(int ware_index); |
610 | + bool is_ready_to_launch_batch(int trade_id); |
611 | + void launch_batch(int trade_id, Game* game); |
612 | + |
613 | + std::map<int, TradeOrder> trade_orders_; // Key is 'trade_id's. |
614 | + std::map<int, std::unique_ptr<WaresQueue>> wares_queue_; // Key is 'ware_index'. |
615 | + |
616 | DISALLOW_COPY_AND_ASSIGN(Market); |
617 | }; |
618 | |
619 | |
620 | === modified file 'src/logic/map_objects/tribes/productionsite.cc' |
621 | --- src/logic/map_objects/tribes/productionsite.cc 2017-09-15 12:33:58 +0000 |
622 | +++ src/logic/map_objects/tribes/productionsite.cc 2017-10-03 20:35:46 +0000 |
623 | @@ -49,8 +49,9 @@ |
624 | |
625 | constexpr size_t STATISTICS_VECTOR_LENGTH = 20; |
626 | |
627 | -} // namespace |
628 | - |
629 | +// Parses the descriptions of the working positions from 'items_table' and |
630 | +// fills in 'working_positions'. Throws an error if the table contains invalid |
631 | +// values. |
632 | void parse_working_positions(const EditorGameBase& egbase, |
633 | LuaTable* items_table, |
634 | BillOfMaterials* working_positions) { |
635 | @@ -71,6 +72,8 @@ |
636 | } |
637 | } |
638 | |
639 | +} // namespace |
640 | + |
641 | /* |
642 | ============================================================================== |
643 | |
644 | |
645 | === modified file 'src/logic/map_objects/tribes/productionsite.h' |
646 | --- src/logic/map_objects/tribes/productionsite.h 2017-09-18 13:43:08 +0000 |
647 | +++ src/logic/map_objects/tribes/productionsite.h 2017-10-03 20:35:46 +0000 |
648 | @@ -375,13 +375,6 @@ |
649 | } |
650 | }; |
651 | |
652 | -// Parses the descriptions of the working positions from 'items_table' and |
653 | -// fills in 'working_positions'. Throws an error if the table contains invalid |
654 | -// values. |
655 | -void parse_working_positions(const EditorGameBase& egbase, |
656 | - LuaTable* items_table, |
657 | - BillOfMaterials* working_positions); |
658 | - |
659 | } // namespace Widelands |
660 | |
661 | #endif // end of include guard: WL_LOGIC_MAP_OBJECTS_TRIBES_PRODUCTIONSITE_H |
662 | |
663 | === modified file 'src/logic/map_objects/tribes/tribes.cc' |
664 | --- src/logic/map_objects/tribes/tribes.cc 2017-09-15 12:33:58 +0000 |
665 | +++ src/logic/map_objects/tribes/tribes.cc 2017-10-03 20:35:46 +0000 |
666 | @@ -24,6 +24,7 @@ |
667 | #include "base/wexception.h" |
668 | #include "graphic/graphic.h" |
669 | #include "logic/game_data_error.h" |
670 | +#include "logic/map_objects/tribes/market.h" |
671 | |
672 | namespace Widelands { |
673 | |
674 | |
675 | === modified file 'src/logic/map_objects/tribes/tribes.h' |
676 | --- src/logic/map_objects/tribes/tribes.h 2017-09-15 12:33:58 +0000 |
677 | +++ src/logic/map_objects/tribes/tribes.h 2017-10-03 20:35:46 +0000 |
678 | @@ -38,7 +38,6 @@ |
679 | #include "logic/map_objects/tribes/tribe_descr.h" |
680 | #include "logic/map_objects/tribes/ware_descr.h" |
681 | #include "logic/map_objects/tribes/warehouse.h" |
682 | -#include "logic/map_objects/tribes/market.h" |
683 | #include "logic/map_objects/tribes/worker_descr.h" |
684 | #include "scripting/lua_table.h" |
685 | |
686 | |
687 | === modified file 'src/logic/map_objects/tribes/worker.cc' |
688 | --- src/logic/map_objects/tribes/worker.cc 2017-08-20 17:45:42 +0000 |
689 | +++ src/logic/map_objects/tribes/worker.cc 2017-10-03 20:35:46 +0000 |
690 | @@ -47,6 +47,7 @@ |
691 | #include "logic/map_objects/terrain_affinity.h" |
692 | #include "logic/map_objects/tribes/carrier.h" |
693 | #include "logic/map_objects/tribes/dismantlesite.h" |
694 | +#include "logic/map_objects/tribes/market.h" |
695 | #include "logic/map_objects/tribes/soldier.h" |
696 | #include "logic/map_objects/tribes/tribe_descr.h" |
697 | #include "logic/map_objects/tribes/warehouse.h" |
698 | @@ -1594,6 +1595,88 @@ |
699 | send_signal(game, "update"); |
700 | } |
701 | |
702 | +// The task when a worker is part of the caravan that is trading items. |
703 | +const Bob::Task Worker::taskCarryTradeItem = { |
704 | + "carry_trade_item", static_cast<Bob::Ptr>(&Worker::carry_trade_item_update), nullptr, nullptr, true}; |
705 | + |
706 | +void Worker::start_task_carry_trade_item(Game& game, |
707 | + const int trade_id, |
708 | + ObjectPointer other_market) { |
709 | + push_task(game, taskCarryTradeItem); |
710 | + auto& state = top_state(); |
711 | + state.ivar1 = 0; |
712 | + state.ivar2 = trade_id; |
713 | + state.objvar1 = other_market; |
714 | +} |
715 | + |
716 | +// This is a state machine: leave building, go to the other market, drop off |
717 | +// wares, and return. |
718 | +void Worker::carry_trade_item_update(Game& game, State& state) { |
719 | + // Reset any signals that are not related to location |
720 | + std::string signal = get_signal(); |
721 | + signal_handled(); |
722 | + if (!signal.empty()) { |
723 | + // TODO(sirver,trading): Remove once signals are correctly handled. |
724 | + log("carry_trade_item_update: signal received: %s\n", signal.c_str()); |
725 | + } |
726 | + if (signal == "evict") { |
727 | + return pop_task(game); |
728 | + } |
729 | + |
730 | + // First of all, make sure we're outside |
731 | + if (state.ivar1 == 0) { |
732 | + start_task_leavebuilding(game, false); |
733 | + ++state.ivar1; |
734 | + return; |
735 | + } |
736 | + |
737 | + auto* other_market = dynamic_cast<Market*>(state.objvar1.get(game)); |
738 | + if (state.ivar1 == 1) { |
739 | + // Arrived on site. Move to the building and advance our state. |
740 | + if (other_market->base_flag().get_position() == get_position()) { |
741 | + ++state.ivar1; |
742 | + return start_task_move( |
743 | + game, WALK_NW, descr().get_right_walk_anims(does_carry_ware()), true); |
744 | + } |
745 | + |
746 | + // Otherwise continue making progress towards the other market. |
747 | + if (!start_task_movepath(game, other_market->base_flag().get_position(), 5, |
748 | + descr().get_right_walk_anims(does_carry_ware()))) { |
749 | + molog("carry_trade_item_update: Could not move to other flag.\n"); |
750 | + // TODO(sirver,trading): something needs to happen here. |
751 | + } |
752 | + return; |
753 | + } |
754 | + |
755 | + if (state.ivar1 == 2) { |
756 | + WareInstance* const ware = fetch_carried_ware(game); |
757 | + other_market->traded_ware_arrived(state.ivar2, ware->descr_index(), &game); |
758 | + ware->remove(game); |
759 | + ++state.ivar1; |
760 | + start_task_move(game, WALK_SE, descr().get_right_walk_anims(does_carry_ware()), true); |
761 | + return; |
762 | + } |
763 | + |
764 | + if (state.ivar1 == 3) { |
765 | + ++state.ivar1; |
766 | + start_task_return(game, false); |
767 | + return; |
768 | + } |
769 | + |
770 | + if (state.ivar1 == 4) { |
771 | + pop_task(game); |
772 | + start_task_idle(game, 0, -1); |
773 | + dynamic_cast<Market*>(get_location(game))->try_launching_batch(&game); |
774 | + return; |
775 | + } |
776 | + NEVER_HERE(); |
777 | +} |
778 | + |
779 | +void Worker::update_task_carry_trade_item(Game& game) { |
780 | + if (top_state().task == &taskCarryTradeItem) |
781 | + send_signal(game, "update"); |
782 | +} |
783 | + |
784 | /** |
785 | * Evict the worker from its current building. |
786 | */ |
787 | |
788 | === modified file 'src/logic/map_objects/tribes/worker.h' |
789 | --- src/logic/map_objects/tribes/worker.h 2017-06-24 08:47:46 +0000 |
790 | +++ src/logic/map_objects/tribes/worker.h 2017-10-03 20:35:46 +0000 |
791 | @@ -169,6 +169,9 @@ |
792 | void start_task_leavebuilding(Game&, bool changelocation); |
793 | void start_task_fugitive(Game&); |
794 | |
795 | + void start_task_carry_trade_item(Game& game, int trade_id, ObjectPointer other_market); |
796 | + void update_task_carry_trade_item(Game&); |
797 | + |
798 | void |
799 | start_task_geologist(Game&, uint8_t attempts, uint8_t radius, const std::string& subcommand); |
800 | |
801 | @@ -208,6 +211,7 @@ |
802 | static const Task taskFugitive; |
803 | static const Task taskGeologist; |
804 | static const Task taskScout; |
805 | + static const Task taskCarryTradeItem; |
806 | |
807 | private: |
808 | // task details |
809 | @@ -232,6 +236,7 @@ |
810 | void fugitive_update(Game&, State&); |
811 | void geologist_update(Game&, State&); |
812 | void scout_update(Game&, State&); |
813 | + void carry_trade_item_update(Game&, State&); |
814 | |
815 | // Program commands |
816 | bool run_mine(Game&, State&, const Action&); |
817 | |
818 | === modified file 'src/logic/message.h' |
819 | --- src/logic/message.h 2017-02-19 16:56:59 +0000 |
820 | +++ src/logic/message.h 2017-10-03 20:35:46 +0000 |
821 | @@ -48,7 +48,8 @@ |
822 | kWarfare, // everything starting from here is warfare |
823 | kWarfareSiteDefeated, |
824 | kWarfareSiteLost, |
825 | - kWarfareUnderAttack |
826 | + kWarfareUnderAttack, |
827 | + kTradeOfferReceived, |
828 | }; |
829 | |
830 | /** |
831 | |
832 | === modified file 'src/logic/playercommand.cc' |
833 | --- src/logic/playercommand.cc 2017-06-25 21:56:53 +0000 |
834 | +++ src/logic/playercommand.cc 2017-10-03 20:35:46 +0000 |
835 | @@ -29,6 +29,7 @@ |
836 | #include "io/streamwrite.h" |
837 | #include "logic/game.h" |
838 | #include "logic/map_objects/map_object.h" |
839 | +#include "logic/map_objects/tribes/market.h" |
840 | #include "logic/map_objects/tribes/soldier.h" |
841 | #include "logic/map_objects/tribes/tribe_descr.h" |
842 | #include "logic/player.h" |
843 | @@ -52,6 +53,25 @@ |
844 | return mol.get<T>(object_index).serial(); |
845 | } |
846 | |
847 | +void serialize_bill_of_materials(const BillOfMaterials& bill, StreamWrite* ser) { |
848 | + ser->unsigned_32(bill.size()); |
849 | + for (const WareAmount& amount : bill) { |
850 | + ser->unsigned_8(amount.first); |
851 | + ser->unsigned_32(amount.second); |
852 | + } |
853 | +} |
854 | + |
855 | +BillOfMaterials deserialize_bill_of_materials(StreamRead* des) { |
856 | + BillOfMaterials bill; |
857 | + const int count = des->unsigned_32(); |
858 | + for (int i = 0; i < count; ++i) { |
859 | + const auto index = des->unsigned_8(); |
860 | + const auto amount = des->unsigned_32(); |
861 | + bill.push_back(std::make_pair(index, amount)); |
862 | + } |
863 | + return bill; |
864 | +} |
865 | + |
866 | } // namespace |
867 | |
868 | // NOTE keep numbers of existing entries as they are to ensure backward compatible savegame loading |
869 | @@ -86,7 +106,8 @@ |
870 | PLCMD_SHIP_EXPLORE = 27, |
871 | PLCMD_SHIP_CONSTRUCT = 28, |
872 | PLCMD_SHIP_SINK = 29, |
873 | - PLCMD_SHIP_CANCELEXPEDITION = 30 |
874 | + PLCMD_SHIP_CANCELEXPEDITION = 30, |
875 | + PLCMD_PROPOSE_TRADE = 31, |
876 | }; |
877 | |
878 | /*** class PlayerCommand ***/ |
879 | @@ -1784,4 +1805,75 @@ |
880 | fw.unsigned_8(ware_); |
881 | fw.unsigned_8(static_cast<uint8_t>(policy_)); |
882 | } |
883 | + |
884 | +CmdProposeTrade::CmdProposeTrade(uint32_t time, PlayerNumber pn, const Trade& trade) |
885 | + : PlayerCommand(time, pn), trade_(trade) { |
886 | +} |
887 | + |
888 | +CmdProposeTrade::CmdProposeTrade() : PlayerCommand() { |
889 | +} |
890 | + |
891 | +void CmdProposeTrade::execute(Game& game) { |
892 | + Player* plr = game.get_player(sender()); |
893 | + if (plr == nullptr) { |
894 | + return; |
895 | + } |
896 | + |
897 | + Market* initiator = dynamic_cast<Market*>(game.objects().get_object(trade_.initiator)); |
898 | + if (initiator == nullptr) { |
899 | + log("CmdProposeTrade: initiator vanished or is not a market.\n"); |
900 | + return; |
901 | + } |
902 | + if (&initiator->owner() != plr) { |
903 | + log("CmdProposeTrade: sender %u, but market owner %u\n", sender(), |
904 | + initiator->owner().player_number()); |
905 | + return; |
906 | + } |
907 | + Market* receiver = dynamic_cast<Market*>(game.objects().get_object(trade_.receiver)); |
908 | + if (receiver == nullptr) { |
909 | + log("CmdProposeTrade: receiver vanished or is not a market.\n"); |
910 | + return; |
911 | + } |
912 | + if (&initiator->owner() == &receiver->owner()) { |
913 | + log("CmdProposeTrade: Sending and receiving player are the same.\n"); |
914 | + return; |
915 | + } |
916 | + |
917 | + // TODO(sirver,trading): Maybe check connectivity between markets here and |
918 | + // report errors. |
919 | + game.propose_trade(trade_); |
920 | +} |
921 | + |
922 | +CmdProposeTrade::CmdProposeTrade(StreamRead& des) : PlayerCommand(0, des.unsigned_8()) { |
923 | + trade_.initiator = des.unsigned_32(); |
924 | + trade_.receiver = des.unsigned_32(); |
925 | + trade_.items_to_send = deserialize_bill_of_materials(&des); |
926 | + trade_.items_to_receive = deserialize_bill_of_materials(&des); |
927 | + trade_.num_batches = des.signed_32(); |
928 | +} |
929 | + |
930 | +void CmdProposeTrade::serialize(StreamWrite& ser) { |
931 | + ser.unsigned_8(PLCMD_PROPOSE_TRADE); |
932 | + ser.unsigned_8(sender()); |
933 | + ser.unsigned_32(trade_.initiator); |
934 | + ser.unsigned_32(trade_.receiver); |
935 | + serialize_bill_of_materials(trade_.items_to_send, &ser); |
936 | + serialize_bill_of_materials(trade_.items_to_receive, &ser); |
937 | + ser.signed_32(trade_.num_batches); |
938 | +} |
939 | + |
940 | +void CmdProposeTrade::read(FileRead& /* fr */, |
941 | + EditorGameBase& /* egbase */, |
942 | + MapObjectLoader& /* mol */) { |
943 | + // TODO(sirver,trading): Implement this. |
944 | + NEVER_HERE(); |
945 | +} |
946 | + |
947 | +void CmdProposeTrade::write(FileWrite& /* fw */, |
948 | + EditorGameBase& /* egbase */, |
949 | + MapObjectSaver& /* mos */) { |
950 | + // TODO(sirver,trading): Implement this. |
951 | + NEVER_HERE(); |
952 | +} |
953 | + |
954 | } |
955 | |
956 | === modified file 'src/logic/playercommand.h' |
957 | --- src/logic/playercommand.h 2017-08-16 13:23:15 +0000 |
958 | +++ src/logic/playercommand.h 2017-10-03 20:35:46 +0000 |
959 | @@ -846,6 +846,29 @@ |
960 | DescriptionIndex ware_; |
961 | Warehouse::StockPolicy policy_; |
962 | }; |
963 | + |
964 | +struct CmdProposeTrade : PlayerCommand { |
965 | + CmdProposeTrade(uint32_t time, PlayerNumber pn, const Trade& trade); |
966 | + |
967 | + QueueCommandTypes id() const override { |
968 | + return QueueCommandTypes::kProposeTrade; |
969 | + } |
970 | + |
971 | + void execute(Game& game) override; |
972 | + |
973 | + // Network (de-)serialization |
974 | + explicit CmdProposeTrade(StreamRead& des); |
975 | + void serialize(StreamWrite& ser) override; |
976 | + |
977 | + // Savegame functions |
978 | + CmdProposeTrade(); |
979 | + void write(FileWrite&, EditorGameBase&, MapObjectSaver&) override; |
980 | + void read(FileRead&, EditorGameBase&, MapObjectLoader&) override; |
981 | + |
982 | +private: |
983 | + Trade trade_; |
984 | +}; |
985 | + |
986 | } |
987 | |
988 | #endif // end of include guard: WL_LOGIC_PLAYERCOMMAND_H |
989 | |
990 | === modified file 'src/logic/queue_cmd_factory.cc' |
991 | --- src/logic/queue_cmd_factory.cc 2017-01-25 18:55:59 +0000 |
992 | +++ src/logic/queue_cmd_factory.cc 2017-10-03 20:35:46 +0000 |
993 | @@ -78,6 +78,8 @@ |
994 | return *new CmdEvictWorker(); |
995 | case QueueCommandTypes::kMilitarysiteSetSoldierPreference: |
996 | return *new CmdMilitarySiteSetSoldierPreference(); |
997 | + case QueueCommandTypes::kProposeTrade: |
998 | + return *new CmdProposeTrade(); |
999 | case QueueCommandTypes::kSinkShip: |
1000 | return *new CmdShipSink(); |
1001 | case QueueCommandTypes::kShipCancelExpedition: |
1002 | |
1003 | === modified file 'src/logic/queue_cmd_ids.h' |
1004 | --- src/logic/queue_cmd_ids.h 2017-01-25 18:55:59 +0000 |
1005 | +++ src/logic/queue_cmd_ids.h 2017-10-03 20:35:46 +0000 |
1006 | @@ -70,7 +70,8 @@ |
1007 | |
1008 | kEvictWorker, |
1009 | |
1010 | - kMilitarysiteSetSoldierPreference, // 26 |
1011 | + kMilitarysiteSetSoldierPreference, |
1012 | + kProposeTrade, // 27 |
1013 | |
1014 | kSinkShip = 121, |
1015 | kShipCancelExpedition, |
1016 | |
1017 | === added file 'src/logic/trade_agreement.h' |
1018 | --- src/logic/trade_agreement.h 1970-01-01 00:00:00 +0000 |
1019 | +++ src/logic/trade_agreement.h 2017-10-03 20:35:46 +0000 |
1020 | @@ -0,0 +1,54 @@ |
1021 | +/* |
1022 | + * Copyright (C) 2006-2017 by the Widelands Development Team |
1023 | + * |
1024 | + * This program is free software; you can redistribute it and/or |
1025 | + * modify it under the terms of the GNU General Public License |
1026 | + * as published by the Free Software Foundation; either version 2 |
1027 | + * of the License, or (at your option) any later version. |
1028 | + * |
1029 | + * This program is distributed in the hope that it will be useful, |
1030 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1031 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1032 | + * GNU General Public License for more details. |
1033 | + * |
1034 | + * You should have received a copy of the GNU General Public License |
1035 | + * along with this program; if not, write to the Free Software |
1036 | + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
1037 | + * |
1038 | + */ |
1039 | + |
1040 | +#ifndef WL_LOGIC_TRADE_AGREEMENT_H |
1041 | +#define WL_LOGIC_TRADE_AGREEMENT_H |
1042 | + |
1043 | +#include "logic/map_objects/map_object.h" |
1044 | + |
1045 | +namespace Widelands { |
1046 | + |
1047 | +// TODO(sirver,trading): Document everything in here. |
1048 | + |
1049 | +// Maximum number of a single ware that can be contained in a trade batch. |
1050 | +constexpr int kMaxPerItemTradeBatchSize = 15; |
1051 | + |
1052 | +struct Trade { |
1053 | + BillOfMaterials items_to_send; |
1054 | + BillOfMaterials items_to_receive; |
1055 | + int num_batches; |
1056 | + Serial initiator; |
1057 | + Serial receiver; |
1058 | +}; |
1059 | + |
1060 | +// TODO(sirver,trading): This class should probably be private to 'Game'. |
1061 | +struct TradeAgreement { |
1062 | + enum class State { |
1063 | + kProposed, |
1064 | + kRunning, |
1065 | + }; |
1066 | + |
1067 | + State state; |
1068 | + Trade trade; |
1069 | +}; |
1070 | + |
1071 | +} // namespace Widelands |
1072 | + |
1073 | + |
1074 | +#endif // end of include guard: WL_LOGIC_TRADE_AGREEMENT_H |
1075 | |
1076 | === modified file 'src/map_io/map_buildingdata_packet.cc' |
1077 | --- src/map_io/map_buildingdata_packet.cc 2017-08-20 08:34:02 +0000 |
1078 | +++ src/map_io/map_buildingdata_packet.cc 2017-10-03 20:35:46 +0000 |
1079 | @@ -339,6 +339,7 @@ |
1080 | } |
1081 | } |
1082 | |
1083 | + // TODO(sirver,trading): Pull out and reuse this for market workers. |
1084 | assert(warehouse.incorporated_workers_.empty()); |
1085 | { |
1086 | uint16_t const nrworkers = fr.unsigned_16(); |
1087 | |
1088 | === modified file 'src/scripting/lua_map.cc' |
1089 | --- src/scripting/lua_map.cc 2017-09-18 13:43:08 +0000 |
1090 | +++ src/scripting/lua_map.cc 2017-10-03 20:35:46 +0000 |
1091 | @@ -33,6 +33,7 @@ |
1092 | #include "logic/map_objects/immovable.h" |
1093 | #include "logic/map_objects/terrain_affinity.h" |
1094 | #include "logic/map_objects/tribes/carrier.h" |
1095 | +#include "logic/map_objects/tribes/market.h" |
1096 | #include "logic/map_objects/tribes/ship.h" |
1097 | #include "logic/map_objects/tribes/soldier.h" |
1098 | #include "logic/map_objects/tribes/tribes.h" |
1099 | @@ -594,6 +595,48 @@ |
1100 | } |
1101 | return 0; |
1102 | } |
1103 | + |
1104 | +// Parses a table of name/count pairs as given from Lua. |
1105 | +void parse_wares_workers(lua_State* L, |
1106 | + int table_index, |
1107 | + const TribeDescr& tribe, |
1108 | + InputMap* ware_workers_list, |
1109 | + bool is_ware) { |
1110 | + luaL_checktype(L, table_index, LUA_TTABLE); |
1111 | + lua_pushnil(L); |
1112 | + while (lua_next(L, table_index) != 0) { |
1113 | + if (is_ware) { |
1114 | + if (tribe.ware_index(luaL_checkstring(L, -2)) == INVALID_INDEX) { |
1115 | + report_error(L, "Illegal ware %s", luaL_checkstring(L, -2)); |
1116 | + } |
1117 | + ware_workers_list->insert( |
1118 | + std::make_pair(std::make_pair(tribe.ware_index(luaL_checkstring(L, -2)), |
1119 | + Widelands::WareWorker::wwWARE), |
1120 | + luaL_checkuint32(L, -1))); |
1121 | + } else { |
1122 | + if (tribe.worker_index(luaL_checkstring(L, -2)) == INVALID_INDEX) { |
1123 | + report_error(L, "Illegal worker %s", luaL_checkstring(L, -2)); |
1124 | + } |
1125 | + ware_workers_list->insert( |
1126 | + std::make_pair(std::make_pair(tribe.worker_index(luaL_checkstring(L, -2)), |
1127 | + Widelands::WareWorker::wwWORKER), |
1128 | + luaL_checkuint32(L, -1))); |
1129 | + } |
1130 | + lua_pop(L, 1); |
1131 | + } |
1132 | +} |
1133 | + |
1134 | +BillOfMaterials parse_wares_as_bill_of_material(lua_State* L, int table_index, |
1135 | + const TribeDescr& tribe) { |
1136 | + InputMap input_map; |
1137 | + parse_wares_workers(L, table_index, tribe, &input_map, true /* is_ware */); |
1138 | + BillOfMaterials result; |
1139 | + for (const auto& pair : input_map) { |
1140 | + result.push_back(std::make_pair(pair.first.first, pair.second)); |
1141 | + } |
1142 | + return result; |
1143 | +} |
1144 | + |
1145 | } // namespace |
1146 | |
1147 | /* |
1148 | @@ -782,7 +825,6 @@ |
1149 | |
1150 | // We either received, two items string,int: |
1151 | if (nargs == 3) { |
1152 | - |
1153 | result = RequestedWareWorker::kSingle; |
1154 | if (is_ware) { |
1155 | if (tribe.ware_index(luaL_checkstring(L, 2)) == INVALID_INDEX) { |
1156 | @@ -803,32 +845,7 @@ |
1157 | } else { |
1158 | result = RequestedWareWorker::kList; |
1159 | // or we got a table with name:quantity |
1160 | - luaL_checktype(L, 2, LUA_TTABLE); |
1161 | - lua_pushnil(L); |
1162 | - while (lua_next(L, 2) != 0) { |
1163 | - if (is_ware) { |
1164 | - if (tribe.ware_index(luaL_checkstring(L, -2)) == INVALID_INDEX) { |
1165 | - report_error(L, "Illegal ware %s", luaL_checkstring(L, -2)); |
1166 | - } |
1167 | - } else { |
1168 | - if (tribe.worker_index(luaL_checkstring(L, -2)) == INVALID_INDEX) { |
1169 | - report_error(L, "Illegal worker %s", luaL_checkstring(L, -2)); |
1170 | - } |
1171 | - } |
1172 | - |
1173 | - if (is_ware) { |
1174 | - ware_workers_list->insert( |
1175 | - std::make_pair(std::make_pair(tribe.ware_index(luaL_checkstring(L, -2)), |
1176 | - Widelands::WareWorker::wwWARE), |
1177 | - luaL_checkuint32(L, -1))); |
1178 | - } else { |
1179 | - ware_workers_list->insert( |
1180 | - std::make_pair(std::make_pair(tribe.worker_index(luaL_checkstring(L, -2)), |
1181 | - Widelands::WareWorker::wwWORKER), |
1182 | - luaL_checkuint32(L, -1))); |
1183 | - } |
1184 | - lua_pop(L, 1); |
1185 | - } |
1186 | + parse_wares_workers(L, 2, tribe, ware_workers_list, is_ware); |
1187 | } |
1188 | return result; |
1189 | } |
1190 | @@ -2762,6 +2779,8 @@ |
1191 | trading over land with other players. See the parent classes for more |
1192 | properties. |
1193 | */ |
1194 | +// TODO(sirver,trading): Expose the properties of MarketDescription here once |
1195 | +// the interface settles. |
1196 | const char LuaMarketDescription::className[] = "MarketDescription"; |
1197 | const MethodType<LuaMarketDescription> LuaMarketDescription::Methods[] = { |
1198 | {nullptr, nullptr}, |
1199 | @@ -5083,6 +5102,7 @@ |
1200 | */ |
1201 | const char LuaMarket::className[] = "Market"; |
1202 | const MethodType<LuaMarket> LuaMarket::Methods[] = { |
1203 | + METHOD(LuaMarket, propose_trade), |
1204 | // TODO(sirver,trading): Implement and fix documentation. |
1205 | // METHOD(LuaMarket, set_wares), |
1206 | // METHOD(LuaMarket, get_wares), |
1207 | @@ -5106,6 +5126,35 @@ |
1208 | ========================================================== |
1209 | */ |
1210 | |
1211 | +/* RST |
1212 | + .. method:: propose_trade(other_market, num_batches, items_to_send, items_to_receive) |
1213 | + |
1214 | + TODO(sirver,trading): document |
1215 | + |
1216 | + :returns: :const:`nil` |
1217 | +*/ |
1218 | +int LuaMarket::propose_trade(lua_State* L) { |
1219 | + if (lua_gettop(L) != 5) { |
1220 | + report_error(L, "Takes 4 arguments."); |
1221 | + } |
1222 | + Game& game = get_game(L); |
1223 | + Market* self = get(L, game); |
1224 | + Market* other_market = (*get_user_class<LuaMarket>(L, 2))->get(L, game); |
1225 | + const int num_batches = luaL_checkinteger(L, 3); |
1226 | + |
1227 | + const BillOfMaterials items_to_send = parse_wares_as_bill_of_material(L, 4, self->owner().tribe()); |
1228 | + // TODO(sirver,trading): unsure if correct. Test inter-tribe trading, i.e. |
1229 | + // barbarians trading with empire, but shipping atlantean only wares. |
1230 | + const BillOfMaterials items_to_receive = parse_wares_as_bill_of_material(L, 5, self->owner().tribe()); |
1231 | + const int trade_id = game.propose_trade( |
1232 | + Trade{items_to_send, items_to_receive, num_batches, self->serial(), other_market->serial()}); |
1233 | + |
1234 | + // TODO(sirver,trading): Wrap 'Trade' into its own Lua class? |
1235 | + lua_pushint32(L, trade_id); |
1236 | + return 1; |
1237 | +} |
1238 | + |
1239 | + |
1240 | /* |
1241 | ========================================================== |
1242 | C METHODS |
1243 | |
1244 | === modified file 'src/scripting/lua_map.h' |
1245 | --- src/scripting/lua_map.h 2017-09-20 21:27:25 +0000 |
1246 | +++ src/scripting/lua_map.h 2017-10-03 20:35:46 +0000 |
1247 | @@ -29,6 +29,7 @@ |
1248 | #include "logic/game.h" |
1249 | #include "logic/map_objects/tribes/constructionsite.h" |
1250 | #include "logic/map_objects/tribes/dismantlesite.h" |
1251 | +#include "logic/map_objects/tribes/market.h" |
1252 | #include "logic/map_objects/tribes/militarysite.h" |
1253 | #include "logic/map_objects/tribes/productionsite.h" |
1254 | #include "logic/map_objects/tribes/ship.h" |
1255 | @@ -1125,6 +1126,7 @@ |
1256 | /* |
1257 | * Lua Methods |
1258 | */ |
1259 | + int propose_trade(lua_State* L); |
1260 | |
1261 | /* |
1262 | * C Methods |
1263 | |
1264 | === modified file 'src/wui/game_message_menu.cc' |
1265 | --- src/wui/game_message_menu.cc 2017-08-17 15:34:45 +0000 |
1266 | +++ src/wui/game_message_menu.cc 2017-10-03 20:35:46 +0000 |
1267 | @@ -487,6 +487,7 @@ |
1268 | case Widelands::Message::Type::kWarfareSiteDefeated: |
1269 | case Widelands::Message::Type::kWarfareSiteLost: |
1270 | case Widelands::Message::Type::kWarfareUnderAttack: |
1271 | + case Widelands::Message::Type::kTradeOfferReceived: |
1272 | set_filter_messages_tooltips(); |
1273 | message_filter_ = Widelands::Message::Type::kAllMessages; |
1274 | geologistsbtn_->set_perm_pressed(false); |
1275 | @@ -580,6 +581,7 @@ |
1276 | case Widelands::Message::Type::kWarfareSiteDefeated: |
1277 | case Widelands::Message::Type::kWarfareSiteLost: |
1278 | case Widelands::Message::Type::kWarfareUnderAttack: |
1279 | + case Widelands::Message::Type::kTradeOfferReceived: |
1280 | return "images/wui/messages/message_new.png"; |
1281 | } |
1282 | NEVER_HERE(); |
1283 | |
1284 | === modified file 'src/wui/ware_statistics_menu.cc' |
1285 | --- src/wui/ware_statistics_menu.cc 2017-08-08 17:39:40 +0000 |
1286 | +++ src/wui/ware_statistics_menu.cc 2017-10-03 20:35:46 +0000 |
1287 | @@ -103,8 +103,7 @@ |
1288 | ®istry, |
1289 | kPlotWidth + 2 * kSpacing, |
1290 | 270, |
1291 | - _("Ware Statistics")), |
1292 | - parent_(&parent) { |
1293 | + _("Ware Statistics")) { |
1294 | uint8_t const nr_wares = parent.get_player()->egbase().tribes().nrwares(); |
1295 | |
1296 | // Init color sets |
1297 | |
1298 | === modified file 'src/wui/ware_statistics_menu.h' |
1299 | --- src/wui/ware_statistics_menu.h 2017-08-18 14:27:26 +0000 |
1300 | +++ src/wui/ware_statistics_menu.h 2017-10-03 20:35:46 +0000 |
1301 | @@ -37,7 +37,6 @@ |
1302 | void set_time(int32_t); |
1303 | |
1304 | private: |
1305 | - InteractivePlayer* parent_; |
1306 | WuiPlotArea* plot_production_; |
1307 | WuiPlotArea* plot_consumption_; |
1308 | WuiPlotArea* plot_stock_; |
1309 | |
1310 | === modified file 'test/maps/market_trading.wmf/scripting/init.lua' |
1311 | --- test/maps/market_trading.wmf/scripting/init.lua 2017-09-18 13:43:08 +0000 |
1312 | +++ test/maps/market_trading.wmf/scripting/init.lua 2017-10-03 20:35:46 +0000 |
1313 | @@ -1,6 +1,6 @@ |
1314 | -include "scripting/lunit.lua" |
1315 | include "scripting/coroutine.lua" |
1316 | include "scripting/infrastructure.lua" |
1317 | +include "scripting/lunit.lua" |
1318 | include "scripting/ui.lua" |
1319 | |
1320 | game = wl.Game() |
1321 | @@ -22,7 +22,17 @@ |
1322 | end |
1323 | end |
1324 | |
1325 | +function place_markets() |
1326 | + prefilled_buildings(p1, { "barbarians_market", 22, 27 }) |
1327 | + market_p1 = map:get_field(22, 27).immovable |
1328 | + connected_road(p1, market_p1.flag, "tr,tl|", true) |
1329 | + |
1330 | + prefilled_buildings(p2, { "barbarians_market", 31, 27 }) |
1331 | + market_p2 = map:get_field(31, 27).immovable |
1332 | + connected_road(p2, market_p2.flag, "tr,tl|", true) |
1333 | +end |
1334 | + |
1335 | full_headquarters(p1, 22, 25) |
1336 | -full_headquarters(p2, 32, 25) |
1337 | +full_headquarters(p2, 31, 25) |
1338 | |
1339 | game.desired_speed = 50000 |
1340 | |
1341 | === renamed file 'test/maps/market_trading.wmf/scripting/test_market_can_be_build.lua' => 'test/maps/market_trading.wmf/scripting/test_market_can_be_built.lua' |
1342 | --- test/maps/market_trading.wmf/scripting/test_market_can_be_build.lua 2017-09-18 13:43:08 +0000 |
1343 | +++ test/maps/market_trading.wmf/scripting/test_market_can_be_built.lua 2017-10-03 20:35:46 +0000 |
1344 | @@ -1,8 +1,8 @@ |
1345 | run(function() |
1346 | sleep(2000) |
1347 | |
1348 | - market = p2:place_building("barbarians_market", map:get_field(35, 25), true, true) |
1349 | - connected_road(p2, market.flag, "l,l,l|", true) |
1350 | + market = p2:place_building("barbarians_market", map:get_field(31, 27), true, true) |
1351 | + connected_road(p2, market.flag, "tr,tl|", true) |
1352 | |
1353 | while #p2:get_buildings("barbarians_market") == 0 do |
1354 | sleep(10000) |
1355 | |
1356 | === added file 'test/maps/market_trading.wmf/scripting/test_simple_trade.lua' |
1357 | --- test/maps/market_trading.wmf/scripting/test_simple_trade.lua 1970-01-01 00:00:00 +0000 |
1358 | +++ test/maps/market_trading.wmf/scripting/test_simple_trade.lua 2017-10-03 20:35:46 +0000 |
1359 | @@ -0,0 +1,39 @@ |
1360 | +run(function() |
1361 | + sleep(2000) |
1362 | + place_markets() |
1363 | + market_p2:propose_trade(market_p1, 5, { log = 3 }, { granite = 2, iron = 1 }) |
1364 | + |
1365 | + local p1_initial = { |
1366 | + iron = p1:get_wares("iron"), |
1367 | + log = p1:get_wares("log"), |
1368 | + granite = p1:get_wares("granite"), |
1369 | + } |
1370 | + local p2_initial = { |
1371 | + iron = p1:get_wares("iron"), |
1372 | + log = p1:get_wares("log"), |
1373 | + granite = p1:get_wares("granite"), |
1374 | + } |
1375 | + |
1376 | + -- We await until one ware we trade has the right count for one player. |
1377 | + -- Then, we'll sleep half as long as we already waited to make sure that no |
1378 | + -- additional batches are shipped. Then we check all stocks for the correct |
1379 | + -- numbers. |
1380 | + local start_time = game.time |
1381 | + while p2:get_wares("iron") - p2_initial["iron"] < 5 do |
1382 | + sleep(10000) |
1383 | + end |
1384 | + |
1385 | + sleep(math.ceil((game.time - start_time) / 2)) |
1386 | + |
1387 | + assert_equal(5, p2:get_wares("iron") - p2_initial["iron"]) |
1388 | + assert_equal(10, p2:get_wares("granite") - p2_initial["granite"]) |
1389 | + assert_equal(-15, p2:get_wares("log") - p2_initial["log"]) |
1390 | + |
1391 | + assert_equal(-5, p1:get_wares("iron") - p1_initial["iron"]) |
1392 | + assert_equal(-10, p1:get_wares("granite") - p1_initial["granite"]) |
1393 | + assert_equal(15, p1:get_wares("log") - p1_initial["log"]) |
1394 | + |
1395 | + print("# All Tests passed.") |
1396 | + wl.ui.MapView():close() |
1397 | +end) |
1398 | + |
Still very big, but the smallest functional piece I could make. Forward looking I'll probably have essentially one branch per TODO which should make the diffs even smaller.