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