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 "NewsItem.h"
11
12 #include "../Context.h"
13 #include "../Input.h"
14 #include "../OpenRCT2.h"
15 #include "../audio/audio.h"
16 #include "../interface/Window.h"
17 #include "../interface/Window_internal.h"
18 #include "../localisation/Date.h"
19 #include "../localisation/Localisation.h"
20 #include "../management/Research.h"
21 #include "../peep/Peep.h"
22 #include "../ride/Ride.h"
23 #include "../ride/Vehicle.h"
24 #include "../util/Util.h"
25 #include "../windows/Intent.h"
26 #include "../world/Entity.h"
27 #include "../world/Location.hpp"
28
29 News::ItemQueues gNewsItems;
30
Current()31 News::Item& News::ItemQueues::Current()
32 {
33 return Recent.front();
34 }
35
Current() const36 const News::Item& News::ItemQueues::Current() const
37 {
38 return Recent.front();
39 }
40
IsValidIndex(int32_t index)41 bool News::IsValidIndex(int32_t index)
42 {
43 if (index >= News::MaxItems)
44 {
45 log_error("Tried to get news item past MAX_NEWS.");
46 return false;
47 }
48 return true;
49 }
50
GetItem(int32_t index)51 News::Item* News::GetItem(int32_t index)
52 {
53 return gNewsItems.At(index);
54 }
55
operator [](size_t index)56 News::Item& News::ItemQueues::operator[](size_t index)
57 {
58 return const_cast<News::Item&>(const_cast<const News::ItemQueues&>(*this)[index]);
59 }
60
operator [](size_t index) const61 const News::Item& News::ItemQueues::operator[](size_t index) const
62 {
63 if (index < Recent.capacity())
64 return Recent[index];
65
66 return Archived[index - Recent.capacity()];
67 }
68
At(int32_t index)69 News::Item* News::ItemQueues::At(int32_t index)
70 {
71 return const_cast<News::Item*>(const_cast<const News::ItemQueues&>(*this).At(index));
72 }
73
At(int32_t index) const74 const News::Item* News::ItemQueues::At(int32_t index) const
75 {
76 if (News::IsValidIndex(index))
77 {
78 return &(*this)[index];
79 }
80
81 return nullptr;
82 }
83
IsQueueEmpty()84 bool News::IsQueueEmpty()
85 {
86 return gNewsItems.IsEmpty();
87 }
88
IsEmpty() const89 bool News::ItemQueues::IsEmpty() const
90 {
91 return Recent.empty();
92 }
93
94 /**
95 *
96 * rct2: 0x0066DF32
97 */
Clear()98 void News::ItemQueues::Clear()
99 {
100 Recent.clear();
101 Archived.clear();
102 }
103
InitQueue()104 void News::InitQueue()
105 {
106 gNewsItems.Clear();
107 assert(gNewsItems.IsEmpty());
108
109 // Throttles for warning types (PEEP_*_WARNING)
110 for (auto& warningThrottle : gPeepWarningThrottle)
111 {
112 warningThrottle = 0;
113 }
114
115 auto intent = Intent(INTENT_ACTION_INVALIDATE_TICKER_NEWS);
116 context_broadcast_intent(&intent);
117 }
118
IncrementTicks()119 uint16_t News::ItemQueues::IncrementTicks()
120 {
121 return ++Current().Ticks;
122 }
123
TickCurrent()124 static void TickCurrent()
125 {
126 int32_t ticks = gNewsItems.IncrementTicks();
127 // Only play news item sound when in normal playing mode
128 if (ticks == 1 && (gScreenFlags == SCREEN_FLAGS_PLAYING))
129 {
130 // Play sound
131 OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::NewsItem, 0, context_get_width() / 2);
132 }
133 }
134
RemoveTime() const135 int32_t News::ItemQueues::RemoveTime() const
136 {
137 if (!Recent[5].IsEmpty() && !Recent[4].IsEmpty() && !Recent[3].IsEmpty() && !Recent[2].IsEmpty())
138 {
139 return 256;
140 }
141 return 320;
142 }
143
CurrentShouldBeArchived() const144 bool News::ItemQueues::CurrentShouldBeArchived() const
145 {
146 return Current().Ticks >= RemoveTime();
147 }
148
149 /**
150 *
151 * rct2: 0x0066E252
152 */
UpdateCurrentItem()153 void News::UpdateCurrentItem()
154 {
155 // Check if there is a current news item
156 if (gNewsItems.IsEmpty())
157 return;
158
159 auto intent = Intent(INTENT_ACTION_INVALIDATE_TICKER_NEWS);
160 context_broadcast_intent(&intent);
161
162 // Update the current news item
163 TickCurrent();
164
165 // Removal of current news item
166 if (gNewsItems.CurrentShouldBeArchived())
167 gNewsItems.ArchiveCurrent();
168 }
169
170 /**
171 *
172 * rct2: 0x0066E377
173 */
CloseCurrentItem()174 void News::CloseCurrentItem()
175 {
176 gNewsItems.ArchiveCurrent();
177 }
178
ArchiveCurrent()179 void News::ItemQueues::ArchiveCurrent()
180 {
181 // Check if there is a current message
182 if (IsEmpty())
183 return;
184
185 Archived.push_back(Current());
186
187 // Invalidate the news window
188 window_invalidate_by_class(WC_RECENT_NEWS);
189
190 // Dequeue the current news item, shift news up
191 Recent.pop_front();
192
193 // Invalidate current news item bar
194 auto intent = Intent(INTENT_ACTION_INVALIDATE_TICKER_NEWS);
195 context_broadcast_intent(&intent);
196 }
197
198 /**
199 * Get the (x,y,z) coordinates of the subject of a news item.
200 * If the new item is no longer valid, return LOCATION_NULL in the x-coordinate
201 *
202 * rct2: 0x0066BA74
203 */
GetSubjectLocation(News::ItemType type,int32_t subject)204 std::optional<CoordsXYZ> News::GetSubjectLocation(News::ItemType type, int32_t subject)
205 {
206 std::optional<CoordsXYZ> subjectLoc{ std::nullopt };
207
208 switch (type)
209 {
210 case News::ItemType::Ride:
211 {
212 Ride* ride = get_ride(static_cast<ride_id_t>(subject));
213 if (ride == nullptr || ride->overall_view.IsNull())
214 {
215 break;
216 }
217 auto rideViewCentre = ride->overall_view.ToTileCentre();
218 subjectLoc = CoordsXYZ{ rideViewCentre, tile_element_height(rideViewCentre) };
219 break;
220 }
221 case News::ItemType::PeepOnRide:
222 {
223 auto peep = TryGetEntity<Peep>(subject);
224 if (peep == nullptr)
225 break;
226
227 subjectLoc = peep->GetLocation();
228 if (subjectLoc->x != LOCATION_NULL)
229 break;
230
231 if (peep->State != PeepState::OnRide && peep->State != PeepState::EnteringRide)
232 {
233 subjectLoc = std::nullopt;
234 break;
235 }
236
237 // Find which ride peep is on
238 Ride* ride = get_ride(peep->CurrentRide);
239 if (ride == nullptr || !(ride->lifecycle_flags & RIDE_LIFECYCLE_ON_TRACK))
240 {
241 subjectLoc = std::nullopt;
242 break;
243 }
244
245 // Find the first car of the train peep is on
246 auto sprite = TryGetEntity<Vehicle>(ride->vehicles[peep->CurrentTrain]);
247 // Find the actual car peep is on
248 for (int32_t i = 0; i < peep->CurrentCar && sprite != nullptr; i++)
249 {
250 sprite = TryGetEntity<Vehicle>(sprite->next_vehicle_on_train);
251 }
252 if (sprite != nullptr)
253 {
254 subjectLoc = sprite->GetLocation();
255 }
256 break;
257 }
258 case News::ItemType::Peep:
259 {
260 auto peep = TryGetEntity<Peep>(subject);
261 if (peep != nullptr)
262 {
263 subjectLoc = peep->GetLocation();
264 }
265 break;
266 }
267 case News::ItemType::Blank:
268 {
269 auto subjectUnsigned = static_cast<uint32_t>(subject);
270 auto subjectXY = CoordsXY{ static_cast<int16_t>(subjectUnsigned & 0xFFFF),
271 static_cast<int16_t>(subjectUnsigned >> 16) };
272 if (!subjectXY.IsNull())
273 {
274 subjectLoc = CoordsXYZ{ subjectXY, tile_element_height(subjectXY) };
275 }
276 break;
277 }
278 default:
279 break;
280 }
281 return subjectLoc;
282 }
283
FirstOpenOrNewSlot()284 News::Item* News::ItemQueues::FirstOpenOrNewSlot()
285 {
286 for (auto emptySlots = Recent.capacity() - Recent.size(); emptySlots < 2; ++emptySlots)
287 {
288 ArchiveCurrent();
289 }
290
291 auto res = Recent.end();
292 // The for loop above guarantees there is always an extra element to use
293 assert(Recent.capacity() - Recent.size() >= 2);
294 auto newsItem = res + 1;
295 newsItem->Type = News::ItemType::Null;
296
297 return &*res;
298 }
299
300 /**
301 *
302 * rct2: 0x0066DF55
303 */
AddItemToQueue(News::ItemType type,rct_string_id string_id,uint32_t assoc,const Formatter & formatter)304 News::Item* News::AddItemToQueue(News::ItemType type, rct_string_id string_id, uint32_t assoc, const Formatter& formatter)
305 {
306 utf8 buffer[256];
307
308 // overflows possible?
309 format_string(buffer, 256, string_id, formatter.Data());
310 return News::AddItemToQueue(type, buffer, assoc);
311 }
312
AddItemToQueue(News::ItemType type,const utf8 * text,uint32_t assoc)313 News::Item* News::AddItemToQueue(News::ItemType type, const utf8* text, uint32_t assoc)
314 {
315 News::Item* newsItem = gNewsItems.FirstOpenOrNewSlot();
316 newsItem->Type = type;
317 newsItem->Flags = 0;
318 newsItem->Assoc = assoc; // Make optional for Award, Money, Graph and Null
319 newsItem->Ticks = 0;
320 newsItem->MonthYear = static_cast<uint16_t>(gDateMonthsElapsed);
321 newsItem->Day = ((days_in_month[date_get_month(newsItem->MonthYear)] * gDateMonthTicks) >> 16) + 1;
322 newsItem->Text = text;
323
324 return newsItem;
325 }
326
327 /**
328 * Checks if News::ItemType requires an assoc
329 * @return A boolean if assoc is required.
330 */
331
CheckIfItemRequiresAssoc(News::ItemType type)332 bool News::CheckIfItemRequiresAssoc(News::ItemType type)
333 {
334 switch (type)
335 {
336 case News::ItemType::Null:
337 case News::ItemType::Award:
338 case News::ItemType::Money:
339 case News::ItemType::Graph:
340 return false;
341 default:
342 return true; // Everything else requires assoc
343 }
344 }
345
346 /**
347 * Opens the window/tab for the subject of the news item
348 *
349 * rct2: 0x0066EBE6
350 *
351 */
OpenSubject(News::ItemType type,int32_t subject)352 void News::OpenSubject(News::ItemType type, int32_t subject)
353 {
354 switch (type)
355 {
356 case News::ItemType::Ride:
357 {
358 auto intent = Intent(WC_RIDE);
359 intent.putExtra(INTENT_EXTRA_RIDE_ID, subject);
360 context_open_intent(&intent);
361 break;
362 }
363 case News::ItemType::PeepOnRide:
364 case News::ItemType::Peep:
365 {
366 auto peep = TryGetEntity<Peep>(subject);
367 if (peep != nullptr)
368 {
369 auto intent = Intent(WC_PEEP);
370 intent.putExtra(INTENT_EXTRA_PEEP, peep);
371 context_open_intent(&intent);
372 }
373 break;
374 }
375 case News::ItemType::Money:
376 context_open_window(WC_FINANCES);
377 break;
378 case News::ItemType::Research:
379 {
380 auto item = ResearchItem(subject, ResearchCategory::Transport, 0);
381 if (item.type == Research::EntryType::Ride)
382 {
383 auto intent = Intent(INTENT_ACTION_NEW_RIDE_OF_TYPE);
384 intent.putExtra(INTENT_EXTRA_RIDE_TYPE, item.baseRideType);
385 intent.putExtra(INTENT_EXTRA_RIDE_ENTRY_INDEX, item.entryIndex);
386 context_open_intent(&intent);
387 break;
388 }
389
390 // Check if window is already open
391 auto window = window_bring_to_front_by_class(WC_SCENERY);
392 if (window == nullptr)
393 {
394 window = window_find_by_class(WC_TOP_TOOLBAR);
395 if (window != nullptr)
396 {
397 window->Invalidate();
398 if (!tool_set(window, WC_TOP_TOOLBAR__WIDX_SCENERY, Tool::Arrow))
399 {
400 input_set_flag(INPUT_FLAG_6, true);
401 context_open_window(WC_SCENERY);
402 }
403 }
404 }
405
406 // Switch to new scenery tab
407 window = window_find_by_class(WC_SCENERY);
408 if (window != nullptr)
409 window_event_mouse_down_call(window, WC_SCENERY__WIDX_SCENERY_TAB_1 + subject);
410 break;
411 }
412 case News::ItemType::Peeps:
413 {
414 auto intent = Intent(WC_GUEST_LIST);
415 intent.putExtra(INTENT_EXTRA_GUEST_LIST_FILTER, static_cast<int32_t>(GuestListFilterType::GuestsThinkingX));
416 intent.putExtra(INTENT_EXTRA_RIDE_ID, subject);
417 context_open_intent(&intent);
418 break;
419 }
420 case News::ItemType::Award:
421 context_open_window_view(WV_PARK_AWARDS);
422 break;
423 case News::ItemType::Graph:
424 context_open_window_view(WV_PARK_RATING);
425 break;
426 case News::ItemType::Null:
427 case News::ItemType::Blank:
428 case News::ItemType::Count:
429 break;
430 }
431 }
432
433 /**
434 *
435 * rct2: 0x0066E407
436 */
DisableNewsItems(News::ItemType type,uint32_t assoc)437 void News::DisableNewsItems(News::ItemType type, uint32_t assoc)
438 {
439 // TODO: write test invalidating windows
440 gNewsItems.ForeachRecentNews([type, assoc](auto& newsItem) {
441 if (type == newsItem.Type && assoc == newsItem.Assoc)
442 {
443 newsItem.SetFlags(News::ItemFlags::HasButton);
444 if (&newsItem == &gNewsItems.Current())
445 {
446 auto intent = Intent(INTENT_ACTION_INVALIDATE_TICKER_NEWS);
447 context_broadcast_intent(&intent);
448 }
449 }
450 });
451
452 gNewsItems.ForeachArchivedNews([type, assoc](auto& newsItem) {
453 if (type == newsItem.Type && assoc == newsItem.Assoc)
454 {
455 newsItem.SetFlags(News::ItemFlags::HasButton);
456 window_invalidate_by_class(WC_RECENT_NEWS);
457 }
458 });
459 }
460
AddItemToQueue(News::Item * newNewsItem)461 void News::AddItemToQueue(News::Item* newNewsItem)
462 {
463 News::Item* newsItem = gNewsItems.FirstOpenOrNewSlot();
464 *newsItem = *newNewsItem;
465 }
466
RemoveItem(int32_t index)467 void News::RemoveItem(int32_t index)
468 {
469 if (index < 0 || index >= News::MaxItems)
470 return;
471
472 // News item is already null, no need to remove it
473 if (gNewsItems[index].Type == News::ItemType::Null)
474 return;
475
476 size_t newsBoundary = index < News::ItemHistoryStart ? News::ItemHistoryStart : News::MaxItems;
477 for (size_t i = index; i < newsBoundary - 1; i++)
478 {
479 gNewsItems[i] = gNewsItems[i + 1];
480 }
481 gNewsItems[newsBoundary - 1].Type = News::ItemType::Null;
482 }
483