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 "Park.h"
11 
12 #include "../Cheats.h"
13 #include "../Context.h"
14 #include "../Date.h"
15 #include "../Game.h"
16 #include "../GameState.h"
17 #include "../OpenRCT2.h"
18 #include "../actions/ParkSetParameterAction.h"
19 #include "../config/Config.h"
20 #include "../core/Memory.hpp"
21 #include "../core/String.hpp"
22 #include "../interface/Colour.h"
23 #include "../interface/Window.h"
24 #include "../localisation/Localisation.h"
25 #include "../management/Award.h"
26 #include "../management/Finance.h"
27 #include "../management/Marketing.h"
28 #include "../management/Research.h"
29 #include "../network/network.h"
30 #include "../peep/Peep.h"
31 #include "../peep/Staff.h"
32 #include "../ride/Ride.h"
33 #include "../ride/RideData.h"
34 #include "../ride/ShopItem.h"
35 #include "../scenario/Scenario.h"
36 #include "../util/Util.h"
37 #include "../windows/Intent.h"
38 #include "Entrance.h"
39 #include "Litter.h"
40 #include "Map.h"
41 #include "Surface.h"
42 
43 #include <algorithm>
44 #include <limits>
45 
46 using namespace OpenRCT2;
47 
48 uint64_t gParkFlags;
49 uint16_t gParkRating;
50 money16 gParkEntranceFee;
51 uint16_t gParkSize;
52 money16 gLandPrice;
53 money16 gConstructionRightsPrice;
54 
55 uint64_t gTotalAdmissions;
56 money64 gTotalIncomeFromAdmissions;
57 
58 money64 gParkValue;
59 money64 gCompanyValue;
60 
61 int16_t gParkRatingCasualtyPenalty;
62 uint8_t gParkRatingHistory[32];
63 uint32_t gGuestsInParkHistory[32];
64 
65 // If this value is more than or equal to 0, the park rating is forced to this value. Used for cheat
66 static int32_t _forcedParkRating = -1;
67 
68 /**
69  * In a difficult guest generation scenario, no guests will be generated if over this value.
70  */
71 uint32_t _suggestedGuestMaximum;
72 
73 /**
74  * Probability out of 65535, of gaining a new guest per game tick.
75  * new guests per second = 40 * (probability / 65535)
76  * With a full park rating, non-overpriced entrance fee, less guests than the suggested maximum and four positive awards,
77  * approximately 1 guest per second can be generated (+60 guests in one minute).
78  */
79 int32_t _guestGenerationProbability;
80 
81 /**
82  * Choose a random peep spawn and iterates through until defined spawn is found.
83  */
get_random_peep_spawn()84 static PeepSpawn* get_random_peep_spawn()
85 {
86     if (!gPeepSpawns.empty())
87     {
88         return &gPeepSpawns[scenario_rand() % gPeepSpawns.size()];
89     }
90 
91     return nullptr;
92 }
93 
park_set_open(bool open)94 void park_set_open(bool open)
95 {
96     auto parkSetParameter = ParkSetParameterAction(open ? ParkParameter::Open : ParkParameter::Close);
97     GameActions::Execute(&parkSetParameter);
98 }
99 
100 /**
101  *
102  *  rct2: 0x00664D05
103  */
update_park_fences(const CoordsXY & coords)104 void update_park_fences(const CoordsXY& coords)
105 {
106     if (map_is_edge(coords))
107         return;
108 
109     auto surfaceElement = map_get_surface_element_at(coords);
110     if (surfaceElement == nullptr)
111         return;
112 
113     uint8_t newFences = 0;
114     if ((surfaceElement->GetOwnership() & OWNERSHIP_OWNED) == 0)
115     {
116         bool fenceRequired = true;
117 
118         TileElement* tileElement = map_get_first_element_at(coords);
119         if (tileElement == nullptr)
120             return;
121         // If an entrance element do not place flags around surface
122         do
123         {
124             if (tileElement->GetType() != TILE_ELEMENT_TYPE_ENTRANCE)
125                 continue;
126 
127             if (tileElement->AsEntrance()->GetEntranceType() != ENTRANCE_TYPE_PARK_ENTRANCE)
128                 continue;
129 
130             if (!(tileElement->IsGhost()))
131             {
132                 fenceRequired = false;
133                 break;
134             }
135         } while (!(tileElement++)->IsLastForTile());
136 
137         if (fenceRequired)
138         {
139             // As map_is_location_in_park sets the error text
140             // will require to back it up.
141             rct_string_id previous_error = gGameCommandErrorText;
142             if (map_is_location_in_park({ coords.x - COORDS_XY_STEP, coords.y }))
143             {
144                 newFences |= 0x8;
145             }
146 
147             if (map_is_location_in_park({ coords.x, coords.y - COORDS_XY_STEP }))
148             {
149                 newFences |= 0x4;
150             }
151 
152             if (map_is_location_in_park({ coords.x + COORDS_XY_STEP, coords.y }))
153             {
154                 newFences |= 0x2;
155             }
156 
157             if (map_is_location_in_park({ coords.x, coords.y + COORDS_XY_STEP }))
158             {
159                 newFences |= 0x1;
160             }
161 
162             gGameCommandErrorText = previous_error;
163         }
164     }
165 
166     if (surfaceElement->GetParkFences() != newFences)
167     {
168         int32_t baseZ = surfaceElement->GetBaseZ();
169         int32_t clearZ = baseZ + 16;
170         map_invalidate_tile({ coords, baseZ, clearZ });
171         surfaceElement->SetParkFences(newFences);
172     }
173 }
174 
update_park_fences_around_tile(const CoordsXY & coords)175 void update_park_fences_around_tile(const CoordsXY& coords)
176 {
177     update_park_fences(coords);
178     update_park_fences({ coords.x + COORDS_XY_STEP, coords.y });
179     update_park_fences({ coords.x - COORDS_XY_STEP, coords.y });
180     update_park_fences({ coords.x, coords.y + COORDS_XY_STEP });
181     update_park_fences({ coords.x, coords.y - COORDS_XY_STEP });
182 }
183 
set_forced_park_rating(int32_t rating)184 void set_forced_park_rating(int32_t rating)
185 {
186     _forcedParkRating = rating;
187     auto& park = GetContext()->GetGameState()->GetPark();
188     gParkRating = park.CalculateParkRating();
189     auto intent = Intent(INTENT_ACTION_UPDATE_PARK_RATING);
190     context_broadcast_intent(&intent);
191 }
192 
get_forced_park_rating()193 int32_t get_forced_park_rating()
194 {
195     return _forcedParkRating;
196 }
197 
park_get_entrance_fee()198 money16 park_get_entrance_fee()
199 {
200     if (gParkFlags & PARK_FLAGS_NO_MONEY)
201     {
202         return 0;
203     }
204     if (!park_entry_price_unlocked())
205     {
206         return 0;
207     }
208     return gParkEntranceFee;
209 }
210 
park_ride_prices_unlocked()211 bool park_ride_prices_unlocked()
212 {
213     if (gParkFlags & PARK_FLAGS_UNLOCK_ALL_PRICES)
214     {
215         return true;
216     }
217     if (gParkFlags & PARK_FLAGS_PARK_FREE_ENTRY)
218     {
219         return true;
220     }
221     return false;
222 }
223 
park_entry_price_unlocked()224 bool park_entry_price_unlocked()
225 {
226     if (gParkFlags & PARK_FLAGS_UNLOCK_ALL_PRICES)
227     {
228         return true;
229     }
230     if (!(gParkFlags & PARK_FLAGS_PARK_FREE_ENTRY))
231     {
232         return true;
233     }
234     return false;
235 }
236 
IsOpen() const237 bool Park::IsOpen() const
238 {
239     return (gParkFlags & PARK_FLAGS_PARK_OPEN) != 0;
240 }
241 
GetParkRating() const242 uint16_t Park::GetParkRating() const
243 {
244     return gParkRating;
245 }
246 
GetParkValue() const247 money64 Park::GetParkValue() const
248 {
249     return gParkValue;
250 }
251 
GetCompanyValue() const252 money64 Park::GetCompanyValue() const
253 {
254     return gCompanyValue;
255 }
256 
Initialise()257 void Park::Initialise()
258 {
259     Name = format_string(STR_UNNAMED_PARK, nullptr);
260     gStaffHandymanColour = COLOUR_BRIGHT_RED;
261     gStaffMechanicColour = COLOUR_LIGHT_BLUE;
262     gStaffSecurityColour = COLOUR_YELLOW;
263     gNumGuestsInPark = 0;
264     gNumGuestsInParkLastWeek = 0;
265     gNumGuestsHeadingForPark = 0;
266     gGuestChangeModifier = 0;
267     gParkRating = 0;
268     _guestGenerationProbability = 0;
269     gTotalRideValueForMoney = 0;
270     gResearchLastItem = std::nullopt;
271     gMarketingCampaigns.clear();
272 
273     research_reset_items();
274     finance_init();
275 
276     set_every_ride_type_not_invented();
277 
278     set_all_scenery_items_invented();
279 
280     gParkEntranceFee = MONEY(10, 00);
281 
282     gPeepSpawns.clear();
283     reset_park_entrance();
284 
285     gResearchPriorities = EnumsToFlags(
286         ResearchCategory::Transport, ResearchCategory::Gentle, ResearchCategory::Rollercoaster, ResearchCategory::Thrill,
287         ResearchCategory::Water, ResearchCategory::Shop, ResearchCategory::SceneryGroup);
288     gResearchFundingLevel = RESEARCH_FUNDING_NORMAL;
289 
290     gGuestInitialCash = MONEY(50, 00);
291     gGuestInitialHappiness = CalculateGuestInitialHappiness(50);
292     gGuestInitialHunger = 200;
293     gGuestInitialThirst = 200;
294     gScenarioObjective.Type = OBJECTIVE_GUESTS_BY;
295     gScenarioObjective.Year = 4;
296     gScenarioObjective.NumGuests = 1000;
297     gLandPrice = MONEY(90, 00);
298     gConstructionRightsPrice = MONEY(40, 00);
299     gParkFlags = PARK_FLAGS_NO_MONEY | PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
300     ResetHistories();
301     finance_reset_history();
302     award_reset();
303 
304     gScenarioName = "";
305     gScenarioDetails = String::ToStd(language_get_string(STR_NO_DETAILS_YET));
306 }
307 
Update(const Date & date)308 void Park::Update(const Date& date)
309 {
310     // Every ~13 seconds
311     if (gCurrentTicks % 512 == 0)
312     {
313         gParkRating = CalculateParkRating();
314         gParkValue = CalculateParkValue();
315         gCompanyValue = CalculateCompanyValue();
316         gTotalRideValueForMoney = CalculateTotalRideValueForMoney();
317         _suggestedGuestMaximum = CalculateSuggestedMaxGuests();
318         _guestGenerationProbability = CalculateGuestGenerationProbability();
319 
320         window_invalidate_by_class(WC_FINANCES);
321         auto intent = Intent(INTENT_ACTION_UPDATE_PARK_RATING);
322         context_broadcast_intent(&intent);
323     }
324     // Every ~102 seconds
325     if (gCurrentTicks % 4096 == 0)
326     {
327         gParkSize = CalculateParkSize();
328         window_invalidate_by_class(WC_PARK_INFORMATION);
329     }
330     // Every new week
331     if (date.IsWeekStart())
332     {
333         UpdateHistories();
334     }
335     GenerateGuests();
336 }
337 
CalculateParkSize() const338 int32_t Park::CalculateParkSize() const
339 {
340     int32_t tiles = 0;
341     tile_element_iterator it;
342     tile_element_iterator_begin(&it);
343     do
344     {
345         if (it.element->GetType() == TILE_ELEMENT_TYPE_SURFACE)
346         {
347             if (it.element->AsSurface()->GetOwnership() & (OWNERSHIP_CONSTRUCTION_RIGHTS_OWNED | OWNERSHIP_OWNED))
348             {
349                 tiles++;
350             }
351         }
352     } while (tile_element_iterator_next(&it));
353 
354     if (tiles != gParkSize)
355     {
356         gParkSize = tiles;
357         window_invalidate_by_class(WC_PARK_INFORMATION);
358     }
359 
360     return tiles;
361 }
362 
CalculateParkRating() const363 int32_t Park::CalculateParkRating() const
364 {
365     if (_forcedParkRating >= 0)
366     {
367         return _forcedParkRating;
368     }
369 
370     int32_t result = 1150;
371     if (gParkFlags & PARK_FLAGS_DIFFICULT_PARK_RATING)
372     {
373         result = 1050;
374     }
375 
376     // Guests
377     {
378         // -150 to +3 based on a range of guests from 0 to 2000
379         result -= 150 - (std::min<int16_t>(2000, gNumGuestsInPark) / 13);
380 
381         // Find the number of happy peeps and the number of peeps who can't find the park exit
382         uint32_t happyGuestCount = 0;
383         uint32_t lostGuestCount = 0;
384         for (auto peep : EntityList<Guest>())
385         {
386             if (!peep->OutsideOfPark)
387             {
388                 if (peep->Happiness > 128)
389                 {
390                     happyGuestCount++;
391                 }
392                 if ((peep->PeepFlags & PEEP_FLAGS_LEAVING_PARK) && (peep->GuestIsLostCountdown < 90))
393                 {
394                     lostGuestCount++;
395                 }
396             }
397         }
398 
399         // Peep happiness -500 to +0
400         result -= 500;
401         if (gNumGuestsInPark > 0)
402         {
403             result += 2 * std::min(250u, (happyGuestCount * 300) / gNumGuestsInPark);
404         }
405 
406         // Up to 25 guests can be lost without affecting the park rating.
407         if (lostGuestCount > 25)
408         {
409             result -= (lostGuestCount - 25) * 7;
410         }
411     }
412 
413     // Rides
414     {
415         int32_t rideCount = 0;
416         int32_t excitingRideCount = 0;
417         int32_t totalRideUptime = 0;
418         int32_t totalRideIntensity = 0;
419         int32_t totalRideExcitement = 0;
420         for (auto& ride : GetRideManager())
421         {
422             totalRideUptime += 100 - ride.downtime;
423             if (ride_has_ratings(&ride))
424             {
425                 totalRideExcitement += ride.excitement / 8;
426                 totalRideIntensity += ride.intensity / 8;
427                 excitingRideCount++;
428             }
429             rideCount++;
430         }
431         result -= 200;
432         if (rideCount > 0)
433         {
434             result += (totalRideUptime / rideCount) * 2;
435         }
436         result -= 100;
437         if (excitingRideCount > 0)
438         {
439             int32_t averageExcitement = totalRideExcitement / excitingRideCount;
440             int32_t averageIntensity = totalRideIntensity / excitingRideCount;
441 
442             averageExcitement -= 46;
443             if (averageExcitement < 0)
444             {
445                 averageExcitement = -averageExcitement;
446             }
447 
448             averageIntensity -= 65;
449             if (averageIntensity < 0)
450             {
451                 averageIntensity = -averageIntensity;
452             }
453 
454             averageExcitement = std::min(averageExcitement / 2, 50);
455             averageIntensity = std::min(averageIntensity / 2, 50);
456             result += 100 - averageExcitement - averageIntensity;
457         }
458 
459         totalRideExcitement = std::min<int16_t>(1000, totalRideExcitement);
460         totalRideIntensity = std::min<int16_t>(1000, totalRideIntensity);
461         result -= 200 - ((totalRideExcitement + totalRideIntensity) / 10);
462     }
463 
464     // Litter
465     {
466         // Counts the amount of litter whose age is min. 7680 ticks (5~ min) old.
467         const auto litterList = EntityList<Litter>();
468         const auto litterCount = std::count_if(
469             litterList.begin(), litterList.end(), [](auto* litter) { return litter->GetAge() >= 7680; });
470 
471         result -= 600 - (4 * (150 - std::min<int32_t>(150, litterCount)));
472     }
473 
474     result -= gParkRatingCasualtyPenalty;
475     result = std::clamp(result, 0, 999);
476     return result;
477 }
478 
CalculateParkValue() const479 money64 Park::CalculateParkValue() const
480 {
481     // Sum ride values
482     money64 result = 0;
483     for (const auto& ride : GetRideManager())
484     {
485         result += CalculateRideValue(&ride);
486     }
487 
488     // +7.00 per guest
489     result += gNumGuestsInPark * MONEY(7, 00);
490 
491     return result;
492 }
493 
CalculateRideValue(const Ride * ride) const494 money64 Park::CalculateRideValue(const Ride* ride) const
495 {
496     money64 result = 0;
497     if (ride != nullptr && ride->value != RIDE_VALUE_UNDEFINED)
498     {
499         const auto& rtd = ride->GetRideTypeDescriptor();
500         result = (ride->value * 10LL) * (static_cast<money64>(ride_customers_in_last_5_minutes(ride)) + rtd.BonusValue * 4);
501     }
502     return result;
503 }
504 
CalculateCompanyValue() const505 money64 Park::CalculateCompanyValue() const
506 {
507     auto result = gParkValue - gBankLoan;
508 
509     // Clamp addition to prevent overflow
510     result = add_clamp_money64(result, finance_get_current_cash());
511 
512     return result;
513 }
514 
CalculateTotalRideValueForMoney() const515 money16 Park::CalculateTotalRideValueForMoney() const
516 {
517     money16 totalRideValue = 0;
518     bool ridePricesUnlocked = park_ride_prices_unlocked() && !(gParkFlags & PARK_FLAGS_NO_MONEY);
519     for (auto& ride : GetRideManager())
520     {
521         if (ride.status != RideStatus::Open)
522             continue;
523         if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
524             continue;
525         if (ride.lifecycle_flags & RIDE_LIFECYCLE_CRASHED)
526             continue;
527 
528         // Add ride value
529         if (ride.value != RIDE_VALUE_UNDEFINED)
530         {
531             money16 rideValue = static_cast<money16>(ride.value);
532             if (ridePricesUnlocked)
533             {
534                 rideValue -= ride.price[0];
535             }
536             if (rideValue > 0)
537             {
538                 totalRideValue += rideValue * 2;
539             }
540         }
541     }
542     return totalRideValue;
543 }
544 
CalculateSuggestedMaxGuests() const545 uint32_t Park::CalculateSuggestedMaxGuests() const
546 {
547     uint32_t suggestedMaxGuests = 0;
548 
549     // TODO combine the two ride loops
550     for (auto& ride : GetRideManager())
551     {
552         if (ride.status != RideStatus::Open)
553             continue;
554         if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
555             continue;
556         if (ride.lifecycle_flags & RIDE_LIFECYCLE_CRASHED)
557             continue;
558 
559         // Add guest score for ride type
560         suggestedMaxGuests += ride.GetRideTypeDescriptor().BonusValue;
561     }
562 
563     // If difficult guest generation, extra guests are available for good rides
564     if (gParkFlags & PARK_FLAGS_DIFFICULT_GUEST_GENERATION)
565     {
566         suggestedMaxGuests = std::min<uint32_t>(suggestedMaxGuests, 1000);
567         for (auto& ride : GetRideManager())
568         {
569             if (ride.lifecycle_flags & RIDE_LIFECYCLE_CRASHED)
570                 continue;
571             if (ride.lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
572                 continue;
573             if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED))
574                 continue;
575             if (!ride.GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_TRACK))
576                 continue;
577             if (!ride.GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_DATA_LOGGING))
578                 continue;
579             if (ride.stations[0].SegmentLength < (600 << 16))
580                 continue;
581             if (ride.excitement < RIDE_RATING(6, 00))
582                 continue;
583 
584             // Bonus guests for good ride
585             suggestedMaxGuests += ride.GetRideTypeDescriptor().BonusValue * 2;
586         }
587     }
588 
589     suggestedMaxGuests = std::min<uint32_t>(suggestedMaxGuests, 65535);
590     return suggestedMaxGuests;
591 }
592 
CalculateGuestGenerationProbability() const593 uint32_t Park::CalculateGuestGenerationProbability() const
594 {
595     // Begin with 50 + park rating
596     uint32_t probability = 50 + std::clamp(gParkRating - 200, 0, 650);
597 
598     // The more guests, the lower the chance of a new one
599     uint32_t numGuests = gNumGuestsInPark + gNumGuestsHeadingForPark;
600     if (numGuests > _suggestedGuestMaximum)
601     {
602         probability /= 4;
603         // Even lower for difficult guest generation
604         if (gParkFlags & PARK_FLAGS_DIFFICULT_GUEST_GENERATION)
605         {
606             probability /= 4;
607         }
608     }
609 
610     // Reduces chance for any more than 7000 guests
611     if (numGuests > 7000)
612     {
613         probability /= 4;
614     }
615 
616     // Penalty for overpriced entrance fee relative to total ride value
617     money16 entranceFee = park_get_entrance_fee();
618     if (entranceFee > gTotalRideValueForMoney)
619     {
620         probability /= 4;
621         // Extra penalty for very overpriced entrance fee
622         if (entranceFee / 2 > gTotalRideValueForMoney)
623         {
624             probability /= 4;
625         }
626     }
627 
628     // Reward or penalties for park awards
629     for (size_t i = 0; i < MAX_AWARDS; i++)
630     {
631         const auto award = &gCurrentAwards[i];
632         if (award->Time != 0)
633         {
634             // +/- 0.25% of the probability
635             if (award_is_positive(award->Type))
636             {
637                 probability += probability / 4;
638             }
639             else
640             {
641                 probability -= probability / 4;
642             }
643         }
644     }
645 
646     return probability;
647 }
648 
CalculateGuestInitialHappiness(uint8_t percentage)649 uint8_t Park::CalculateGuestInitialHappiness(uint8_t percentage)
650 {
651     percentage = std::clamp<uint8_t>(percentage, 15, 98);
652 
653     // The percentages follow this sequence:
654     //   15 17 18 20 21 23 25 26 28 29 31 32 34 36 37 39 40 42 43 45 47 48 50 51 53...
655     // This sequence can be defined as PI*(9+n)/2 (the value is floored)
656     for (uint8_t n = 1; n < 55; n++)
657     {
658         // Avoid floating point math by rescaling PI up.
659         constexpr int32_t SCALE = 100000;
660         constexpr int32_t PI_SCALED = 314159; // PI * SCALE;
661         if (((PI_SCALED * (9 + n)) / SCALE) / 2 >= percentage)
662         {
663             return (9 + n) * 4;
664         }
665     }
666 
667     // This is the lowest possible value:
668     return 40;
669 }
670 
GenerateGuests()671 void Park::GenerateGuests()
672 {
673     // Generate a new guest for some probability
674     if (static_cast<int32_t>(scenario_rand() & 0xFFFF) < _guestGenerationProbability)
675     {
676         bool difficultGeneration = (gParkFlags & PARK_FLAGS_DIFFICULT_GUEST_GENERATION) != 0;
677         if (!difficultGeneration || _suggestedGuestMaximum + 150 >= gNumGuestsInPark)
678         {
679             GenerateGuest();
680         }
681     }
682 
683     // Extra guests generated by advertising campaigns
684     for (const auto& campaign : gMarketingCampaigns)
685     {
686         // Random chance of guest generation
687         auto probability = marketing_get_campaign_guest_generation_probability(campaign.Type);
688         auto random = scenario_rand_max(std::numeric_limits<uint16_t>::max());
689         if (random < probability)
690         {
691             GenerateGuestFromCampaign(campaign.Type);
692         }
693     }
694 }
695 
GenerateGuestFromCampaign(int32_t campaign)696 Guest* Park::GenerateGuestFromCampaign(int32_t campaign)
697 {
698     auto peep = GenerateGuest();
699     if (peep != nullptr)
700     {
701         marketing_set_guest_campaign(peep, campaign);
702     }
703     return peep;
704 }
705 
GenerateGuest()706 Guest* Park::GenerateGuest()
707 {
708     Guest* peep = nullptr;
709     const auto spawn = get_random_peep_spawn();
710     if (spawn != nullptr)
711     {
712         auto direction = direction_reverse(spawn->direction);
713         peep = Guest::Generate({ spawn->x, spawn->y, spawn->z });
714         if (peep != nullptr)
715         {
716             peep->sprite_direction = direction << 3;
717 
718             auto destination = peep->GetLocation().ToTileCentre();
719             peep->SetDestination(destination, 5);
720             peep->PeepDirection = direction;
721             peep->Var37 = 0;
722             peep->State = PeepState::EnteringPark;
723         }
724     }
725     return peep;
726 }
727 
HistoryPushRecord(T history[TSize],T newItem)728 template<typename T, size_t TSize> static void HistoryPushRecord(T history[TSize], T newItem)
729 {
730     for (size_t i = TSize - 1; i > 0; i--)
731     {
732         history[i] = history[i - 1];
733     }
734     history[0] = newItem;
735 }
736 
ResetHistories()737 void Park::ResetHistories()
738 {
739     std::fill(std::begin(gParkRatingHistory), std::end(gParkRatingHistory), ParkRatingHistoryUndefined);
740     std::fill(std::begin(gGuestsInParkHistory), std::end(gGuestsInParkHistory), GuestsInParkHistoryUndefined);
741 }
742 
UpdateHistories()743 void Park::UpdateHistories()
744 {
745     uint8_t guestChangeModifier = 1;
746     int32_t changeInGuestsInPark = static_cast<int32_t>(gNumGuestsInPark) - static_cast<int32_t>(gNumGuestsInParkLastWeek);
747     if (changeInGuestsInPark > -20)
748     {
749         guestChangeModifier++;
750         if (changeInGuestsInPark < 20)
751         {
752             guestChangeModifier = 0;
753         }
754     }
755     gGuestChangeModifier = guestChangeModifier;
756     gNumGuestsInParkLastWeek = gNumGuestsInPark;
757 
758     // Update park rating, guests in park and current cash history
759     HistoryPushRecord<uint8_t, 32>(gParkRatingHistory, CalculateParkRating() / 4);
760     HistoryPushRecord<uint32_t, 32>(gGuestsInParkHistory, gNumGuestsInPark);
761     HistoryPushRecord<money64, std::size(gCashHistory)>(gCashHistory, finance_get_current_cash() - gBankLoan);
762 
763     // Update weekly profit history
764     auto currentWeeklyProfit = gWeeklyProfitAverageDividend;
765     if (gWeeklyProfitAverageDivisor != 0)
766     {
767         currentWeeklyProfit /= gWeeklyProfitAverageDivisor;
768     }
769     HistoryPushRecord<money64, std::size(gWeeklyProfitHistory)>(gWeeklyProfitHistory, currentWeeklyProfit);
770     gWeeklyProfitAverageDividend = 0;
771     gWeeklyProfitAverageDivisor = 0;
772 
773     // Update park value history
774     HistoryPushRecord<money64, std::size(gParkValueHistory)>(gParkValueHistory, gParkValue);
775 
776     // Invalidate relevant windows
777     auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT);
778     context_broadcast_intent(&intent);
779     window_invalidate_by_class(WC_PARK_INFORMATION);
780     window_invalidate_by_class(WC_FINANCES);
781 }
782 
park_is_open()783 int32_t park_is_open()
784 {
785     return GetContext()->GetGameState()->GetPark().IsOpen();
786 }
787 
park_calculate_size()788 int32_t park_calculate_size()
789 {
790     auto tiles = GetContext()->GetGameState()->GetPark().CalculateParkSize();
791     if (tiles != gParkSize)
792     {
793         gParkSize = tiles;
794         window_invalidate_by_class(WC_PARK_INFORMATION);
795     }
796     return tiles;
797 }
798 
calculate_guest_initial_happiness(uint8_t percentage)799 uint8_t calculate_guest_initial_happiness(uint8_t percentage)
800 {
801     return Park::CalculateGuestInitialHappiness(percentage);
802 }
803