1 /*****************************************************************************
2 * Copyright (c) 2014-2020 OpenRCT2 developers
3 *
4 * For a complete list of all authors, please refer to contributors.md
5 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6 *
7 * OpenRCT2 is licensed under the GNU General Public License version 3.
8 *****************************************************************************/
9
10 #include "GameAction.h"
11
12 #include "../Context.h"
13 #include "../ReplayManager.h"
14 #include "../core/Guard.hpp"
15 #include "../core/Memory.hpp"
16 #include "../core/MemoryStream.h"
17 #include "../localisation/Localisation.h"
18 #include "../network/network.h"
19 #include "../platform/platform.h"
20 #include "../scenario/Scenario.h"
21 #include "../scripting/Duktape.hpp"
22 #include "../scripting/HookEngine.h"
23 #include "../scripting/ScriptEngine.h"
24 #include "../ui/UiContext.h"
25 #include "../ui/WindowManager.h"
26 #include "../world/MoneyEffect.h"
27 #include "../world/Park.h"
28 #include "../world/Scenery.h"
29
30 #include <algorithm>
31 #include <iterator>
32
33 using namespace OpenRCT2;
34
35 namespace GameActions
36 {
37 struct QueuedGameAction
38 {
39 uint32_t tick;
40 uint32_t uniqueId;
41 GameAction::Ptr action;
42
QueuedGameActionGameActions::QueuedGameAction43 explicit QueuedGameAction(uint32_t t, std::unique_ptr<GameAction>&& ga, uint32_t id)
44 : tick(t)
45 , uniqueId(id)
46 , action(std::move(ga))
47 {
48 }
49
operator <GameActions::QueuedGameAction50 bool operator<(const QueuedGameAction& comp) const
51 {
52 // First sort by tick
53 if (tick < comp.tick)
54 return true;
55 if (tick > comp.tick)
56 return false;
57
58 // If the ticks are equal sort by commandIndex
59 return uniqueId < comp.uniqueId;
60 }
61 };
62
63 static std::multiset<QueuedGameAction> _actionQueue;
64 static uint32_t _nextUniqueId = 0;
65 static bool _suspended = false;
66
SuspendQueue()67 void SuspendQueue()
68 {
69 _suspended = true;
70 }
71
ResumeQueue()72 void ResumeQueue()
73 {
74 _suspended = false;
75 }
76
Enqueue(const GameAction * ga,uint32_t tick)77 void Enqueue(const GameAction* ga, uint32_t tick)
78 {
79 auto action = Clone(ga);
80 Enqueue(std::move(action), tick);
81 }
82
Enqueue(GameAction::Ptr && ga,uint32_t tick)83 void Enqueue(GameAction::Ptr&& ga, uint32_t tick)
84 {
85 if (ga->GetPlayer() == -1 && network_get_mode() != NETWORK_MODE_NONE)
86 {
87 // Server can directly invoke actions and will have no player id assigned
88 // as that normally happens when receiving them over network.
89 ga->SetPlayer(network_get_current_player_id());
90 }
91 _actionQueue.emplace(tick, std::move(ga), _nextUniqueId++);
92 }
93
ProcessQueue()94 void ProcessQueue()
95 {
96 if (_suspended)
97 {
98 // Do nothing if suspended, this is usually the case between connect and map loads.
99 return;
100 }
101
102 const uint32_t currentTick = gCurrentTicks;
103
104 while (_actionQueue.begin() != _actionQueue.end())
105 {
106 // run all the game commands at the current tick
107 const QueuedGameAction& queued = *_actionQueue.begin();
108
109 if (network_get_mode() == NETWORK_MODE_CLIENT)
110 {
111 if (queued.tick < currentTick)
112 {
113 // This should never happen.
114 Guard::Assert(
115 false,
116 "Discarding game action %s (%u) from tick behind current tick, ID: %08X, Action Tick: %08X, Current "
117 "Tick: "
118 "%08X\n",
119 queued.action->GetName(), queued.action->GetType(), queued.uniqueId, queued.tick, currentTick);
120 }
121 else if (queued.tick > currentTick)
122 {
123 return;
124 }
125 }
126
127 // Remove ghost scenery so it doesn't interfere with incoming network command
128 switch (queued.action->GetType())
129 {
130 case GameCommand::PlaceWall:
131 case GameCommand::PlaceLargeScenery:
132 case GameCommand::PlaceBanner:
133 case GameCommand::PlaceScenery:
134 scenery_remove_ghost_tool_placement();
135 break;
136 default:
137 break;
138 }
139
140 GameAction* action = queued.action.get();
141 action->SetFlags(action->GetFlags() | GAME_COMMAND_FLAG_NETWORKED);
142
143 Guard::Assert(action != nullptr);
144
145 GameActions::Result::Ptr result = Execute(action);
146 if (result->Error == GameActions::Status::Ok && network_get_mode() == NETWORK_MODE_SERVER)
147 {
148 // Relay this action to all other clients.
149 network_send_game_action(action);
150 }
151
152 _actionQueue.erase(_actionQueue.begin());
153 }
154 }
155
ClearQueue()156 void ClearQueue()
157 {
158 _actionQueue.clear();
159 }
160
Clone(const GameAction * action)161 GameAction::Ptr Clone(const GameAction* action)
162 {
163 std::unique_ptr<GameAction> ga = GameActions::Create(action->GetType());
164 ga->SetCallback(action->GetCallback());
165
166 // Serialise action data into stream.
167 DataSerialiser dsOut(true);
168 action->Serialise(dsOut);
169
170 // Serialise into new action.
171 IStream& stream = dsOut.GetStream();
172 stream.SetPosition(0);
173
174 DataSerialiser dsIn(false, stream);
175 ga->Serialise(dsIn);
176
177 return ga;
178 }
179
CheckActionInPausedMode(uint32_t actionFlags)180 static bool CheckActionInPausedMode(uint32_t actionFlags)
181 {
182 if (gGamePaused == 0)
183 return true;
184 if (gCheatsBuildInPauseMode)
185 return true;
186 if (actionFlags & GameActions::Flags::AllowWhilePaused)
187 return true;
188 return false;
189 }
190
QueryInternal(const GameAction * action,bool topLevel)191 static GameActions::Result::Ptr QueryInternal(const GameAction* action, bool topLevel)
192 {
193 Guard::ArgumentNotNull(action);
194
195 uint16_t actionFlags = action->GetActionFlags();
196 if (topLevel && !CheckActionInPausedMode(actionFlags))
197 {
198 GameActions::Result::Ptr result = std::make_unique<GameActions::Result>();
199
200 result->Error = GameActions::Status::GamePaused;
201 result->ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
202 result->ErrorMessage = STR_CONSTRUCTION_NOT_POSSIBLE_WHILE_GAME_IS_PAUSED;
203
204 return result;
205 }
206
207 auto result = action->Query();
208
209 if (result->Error == GameActions::Status::Ok)
210 {
211 if (!finance_check_affordability(result->Cost, action->GetFlags()))
212 {
213 result->Error = GameActions::Status::InsufficientFunds;
214 result->ErrorTitle = STR_CANT_DO_THIS;
215 result->ErrorMessage = STR_NOT_ENOUGH_CASH_REQUIRES;
216 Formatter(result->ErrorMessageArgs.data()).Add<uint32_t>(result->Cost);
217 }
218 }
219 return result;
220 }
221
Query(const GameAction * action)222 GameActions::Result::Ptr Query(const GameAction* action)
223 {
224 return QueryInternal(action, true);
225 }
226
QueryNested(const GameAction * action)227 GameActions::Result::Ptr QueryNested(const GameAction* action)
228 {
229 return QueryInternal(action, false);
230 }
231
GetRealm()232 static const char* GetRealm()
233 {
234 if (network_get_mode() == NETWORK_MODE_CLIENT)
235 return "cl";
236 if (network_get_mode() == NETWORK_MODE_SERVER)
237 return "sv";
238 return "sp";
239 }
240
241 struct ActionLogContext_t
242 {
243 MemoryStream output;
244 };
245
LogActionBegin(ActionLogContext_t & ctx,const GameAction * action)246 static void LogActionBegin(ActionLogContext_t& ctx, const GameAction* action)
247 {
248 MemoryStream& output = ctx.output;
249
250 char temp[128] = {};
251 snprintf(
252 temp, sizeof(temp), "[%s] Tick: %u, GA: %s (%08X) (", GetRealm(), gCurrentTicks, action->GetName(),
253 EnumValue(action->GetType()));
254
255 output.Write(temp, strlen(temp));
256
257 DataSerialiser ds(true, ctx.output, true); // Logging mode.
258
259 // Write all parameters into output as text.
260 action->Serialise(ds);
261 }
262
LogActionFinish(ActionLogContext_t & ctx,const GameAction * action,const GameActions::Result::Ptr & result)263 static void LogActionFinish(ActionLogContext_t& ctx, const GameAction* action, const GameActions::Result::Ptr& result)
264 {
265 MemoryStream& output = ctx.output;
266
267 char temp[128] = {};
268
269 if (result->Error != GameActions::Status::Ok)
270 {
271 snprintf(temp, sizeof(temp), ") Failed, %u", static_cast<uint32_t>(result->Error));
272 }
273 else
274 {
275 snprintf(temp, sizeof(temp), ") OK");
276 }
277
278 output.Write(temp, strlen(temp) + 1);
279
280 const char* text = static_cast<const char*>(output.GetData());
281 log_verbose("%s", text);
282
283 network_append_server_log(text);
284 }
285
ExecuteInternal(const GameAction * action,bool topLevel)286 static GameActions::Result::Ptr ExecuteInternal(const GameAction* action, bool topLevel)
287 {
288 Guard::ArgumentNotNull(action);
289
290 uint16_t actionFlags = action->GetActionFlags();
291 uint32_t flags = action->GetFlags();
292
293 auto* replayManager = OpenRCT2::GetContext()->GetReplayManager();
294 if (replayManager != nullptr && (replayManager->IsReplaying() || replayManager->IsNormalising()))
295 {
296 // We only accept replay commands as long the replay is active.
297 if ((flags & GAME_COMMAND_FLAG_REPLAY) == 0)
298 {
299 // TODO: Introduce proper error.
300 GameActions::Result::Ptr result = std::make_unique<GameActions::Result>();
301
302 result->Error = GameActions::Status::GamePaused;
303 result->ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
304 result->ErrorMessage = STR_CONSTRUCTION_NOT_POSSIBLE_WHILE_GAME_IS_PAUSED;
305
306 return result;
307 }
308 }
309
310 GameActions::Result::Ptr result = QueryInternal(action, topLevel);
311 #ifdef ENABLE_SCRIPTING
312 if (result->Error == GameActions::Status::Ok
313 && ((network_get_mode() == NETWORK_MODE_NONE) || (flags & GAME_COMMAND_FLAG_NETWORKED)))
314 {
315 auto& scriptEngine = GetContext()->GetScriptEngine();
316 scriptEngine.RunGameActionHooks(*action, result, false);
317 // Script hooks may now have changed the game action result...
318 }
319 #endif
320 if (result->Error == GameActions::Status::Ok)
321 {
322 if (topLevel)
323 {
324 // Networked games send actions to the server to be run
325 if (network_get_mode() == NETWORK_MODE_CLIENT)
326 {
327 // As a client we have to wait or send it first.
328 if (!(actionFlags & GameActions::Flags::ClientOnly) && !(flags & GAME_COMMAND_FLAG_NETWORKED))
329 {
330 log_verbose("[%s] GameAction::Execute %s (Out)", GetRealm(), action->GetName());
331 network_send_game_action(action);
332
333 return result;
334 }
335 }
336 else if (network_get_mode() == NETWORK_MODE_SERVER)
337 {
338 // If player is the server it would execute right away as where clients execute the commands
339 // at the beginning of the frame, so we have to put them into the queue.
340 if (!(actionFlags & GameActions::Flags::ClientOnly) && !(flags & GAME_COMMAND_FLAG_NETWORKED))
341 {
342 log_verbose("[%s] GameAction::Execute %s (Queue)", GetRealm(), action->GetName());
343 Enqueue(action, gCurrentTicks);
344
345 return result;
346 }
347 }
348 }
349
350 ActionLogContext_t logContext;
351 LogActionBegin(logContext, action);
352
353 // Execute the action, changing the game state
354 result = action->Execute();
355 #ifdef ENABLE_SCRIPTING
356 if (result->Error == GameActions::Status::Ok)
357 {
358 auto& scriptEngine = GetContext()->GetScriptEngine();
359 scriptEngine.RunGameActionHooks(*action, result, true);
360 // Script hooks may now have changed the game action result...
361 }
362 #endif
363
364 LogActionFinish(logContext, action, result);
365
366 // If not top level just give away the result.
367 if (!topLevel)
368 return result;
369
370 // Update money balance
371 if (result->Error == GameActions::Status::Ok && finance_check_money_required(flags) && result->Cost != 0)
372 {
373 finance_payment(result->Cost, result->Expenditure);
374 MoneyEffect::Create(result->Cost, result->Position);
375 }
376
377 if (!(actionFlags & GameActions::Flags::ClientOnly) && result->Error == GameActions::Status::Ok)
378 {
379 if (network_get_mode() != NETWORK_MODE_NONE)
380 {
381 NetworkPlayerId_t playerId = action->GetPlayer();
382
383 int32_t playerIndex = network_get_player_index(playerId.id);
384 Guard::Assert(
385 playerIndex != -1, "Unable to find player %u for game action %u", playerId, action->GetType());
386
387 network_set_player_last_action(playerIndex, action->GetType());
388 if (result->Cost != 0)
389 {
390 network_add_player_money_spent(playerIndex, result->Cost);
391 }
392
393 if (!result->Position.IsNull())
394 {
395 network_set_player_last_action_coord(playerIndex, result->Position);
396 }
397 }
398 else
399 {
400 bool commandExecutes = (flags & GAME_COMMAND_FLAG_GHOST) == 0 && (flags & GAME_COMMAND_FLAG_NO_SPEND) == 0;
401
402 bool recordAction = false;
403 if (replayManager != nullptr)
404 {
405 if (replayManager->IsRecording() && commandExecutes)
406 recordAction = true;
407 else if (replayManager->IsNormalising() && (flags & GAME_COMMAND_FLAG_REPLAY) != 0)
408 recordAction = true; // In normalisation we only feed back actions issued by the replay manager.
409 }
410 if (recordAction)
411 {
412 replayManager->AddGameAction(gCurrentTicks, action);
413 }
414 }
415 }
416
417 // Allow autosave to commence
418 if (gLastAutoSaveUpdate == AUTOSAVE_PAUSE)
419 {
420 gLastAutoSaveUpdate = platform_get_ticks();
421 }
422 }
423
424 // Call callback for asynchronous events
425 auto cb = action->GetCallback();
426 if (cb != nullptr)
427 {
428 cb(action, result.get());
429 }
430
431 // Only show errors when its not a ghost and not a preview and also top level action.
432 bool shouldShowError = !(flags & GAME_COMMAND_FLAG_GHOST) && !(flags & GAME_COMMAND_FLAG_NO_SPEND) && topLevel;
433
434 // In network mode the error should be only shown to the issuer of the action.
435 if (network_get_mode() != NETWORK_MODE_NONE)
436 {
437 // If the action was never networked and query fails locally the player id is not assigned.
438 // So compare only if the action went into the queue otherwise show errors by default.
439 const bool isActionFromNetwork = (action->GetFlags() & GAME_COMMAND_FLAG_NETWORKED) != 0;
440 if (isActionFromNetwork && action->GetPlayer() != network_get_current_player_id())
441 {
442 shouldShowError = false;
443 }
444 }
445
446 if (result->Error != GameActions::Status::Ok && shouldShowError)
447 {
448 auto windowManager = GetContext()->GetUiContext()->GetWindowManager();
449 windowManager->ShowError(result->GetErrorTitle(), result->GetErrorMessage());
450 }
451
452 return result;
453 }
454
Execute(const GameAction * action)455 GameActions::Result::Ptr Execute(const GameAction* action)
456 {
457 return ExecuteInternal(action, true);
458 }
459
ExecuteNested(const GameAction * action)460 GameActions::Result::Ptr ExecuteNested(const GameAction* action)
461 {
462 return ExecuteInternal(action, false);
463 }
464 } // namespace GameActions
465
GetName() const466 const char* GameAction::GetName() const
467 {
468 return GameActions::GetName(_type);
469 }
470
LocationValid(const CoordsXY & coords) const471 bool GameAction::LocationValid(const CoordsXY& coords) const
472 {
473 auto result = map_is_location_valid(coords);
474 if (!result)
475 return false;
476 #ifdef ENABLE_SCRIPTING
477 auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
478 if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::ACTION_LOCATION))
479 {
480 auto ctx = GetContext()->GetScriptEngine().GetContext();
481
482 // Create event args object
483 auto obj = OpenRCT2::Scripting::DukObject(ctx);
484 obj.Set("x", coords.x);
485 obj.Set("y", coords.y);
486 obj.Set("player", _playerId);
487 obj.Set("type", EnumValue(_type));
488
489 auto flags = GetActionFlags();
490 obj.Set("isClientOnly", (flags & GameActions::Flags::ClientOnly) != 0);
491 obj.Set("result", true);
492
493 // Call the subscriptions
494 auto e = obj.Take();
495 hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::ACTION_LOCATION, e, true);
496
497 auto scriptResult = OpenRCT2::Scripting::AsOrDefault(e["result"], true);
498
499 return scriptResult;
500 }
501 #endif
502 return true;
503 }
504