diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index c5387c41..77dfdce9 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -34,15 +34,22 @@ internal sealed class RealTimeCore private readonly CustomTimeBar timeBar; private readonly RealTimeEventManager eventManager; private readonly MethodPatcher patcher; + private readonly VanillaEvents vanillaEvents; private bool isEnabled; - private RealTimeCore(TimeAdjustment timeAdjustment, CustomTimeBar timeBar, RealTimeEventManager eventManager, MethodPatcher patcher) + private RealTimeCore( + TimeAdjustment timeAdjustment, + CustomTimeBar timeBar, + RealTimeEventManager eventManager, + MethodPatcher patcher, + VanillaEvents vanillaEvents) { this.timeAdjustment = timeAdjustment; this.timeBar = timeBar; this.eventManager = eventManager; this.patcher = patcher; + this.vanillaEvents = vanillaEvents; isEnabled = true; } @@ -135,7 +142,9 @@ public static RealTimeCore Run(ConfigurationProvider configProvi customTimeBar.Enable(gameDate); customTimeBar.CityEventClick += CustomTimeBarCityEventClick; - var result = new RealTimeCore(timeAdjustment, customTimeBar, eventManager, patcher); + var vanillaEvents = VanillaEvents.Customize(); + + var result = new RealTimeCore(timeAdjustment, customTimeBar, eventManager, patcher, vanillaEvents); eventManager.EventsChanged += result.CityEventsChanged; var statistics = new Statistics(timeInfo, localizationProvider); @@ -192,6 +201,8 @@ public void Stop() Log.Info($"The 'Real Time' mod reverts method patches."); patcher.Revert(); + vanillaEvents.Revert(); + timeAdjustment.Disable(); timeBar.CityEventClick -= CustomTimeBarCityEventClick; timeBar.Disable(); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 706bf515..8d433be5 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -134,8 +134,9 @@ private void DoScheduledEvacuation(ref CitizenSchedule schedule, TAI instance, u private bool ProcessCitizenInShelter(ref CitizenSchedule schedule, ref TCitizen citizen) { ushort shelter = CitizenProxy.GetVisitBuilding(ref citizen); - if (BuildingMgr.BuildingHasFlags(shelter, Building.Flags.Downgrading)) + if (BuildingMgr.BuildingHasFlags(shelter, Building.Flags.Downgrading) && schedule.ScheduledState == ResidentState.InShelter) { + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); schedule.Schedule(ResidentState.Unknown); return true; } @@ -202,7 +203,7 @@ private ScheduleAction UpdateCitizenState(ref TCitizen citizen, ref CitizenSched return ScheduleAction.ProcessState; } - if (CitizenProxy.GetVisitBuilding(ref citizen) == currentBuilding) + if (CitizenProxy.GetVisitBuilding(ref citizen) == currentBuilding && schedule.WorkStatus != WorkStatus.Working) { // A citizen may visit their own work building (e.g. shopping) goto case Citizen.Location.Visit; @@ -212,6 +213,11 @@ private ScheduleAction UpdateCitizenState(ref TCitizen citizen, ref CitizenSched return ScheduleAction.ProcessState; case Citizen.Location.Visit: + if (CitizenProxy.GetWorkBuilding(ref citizen) == currentBuilding && schedule.WorkStatus == WorkStatus.Working) + { + goto case Citizen.Location.Work; + } + switch (buildingService) { case ItemClass.Service.Beautification: @@ -228,7 +234,7 @@ when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService. schedule.CurrentState = ResidentState.Shopping; return ScheduleAction.ProcessState; - case ItemClass.Service.Disaster: + case ItemClass.Service.Disaster when CitizenProxy.HasFlags(ref citizen, Citizen.Flags.Evacuating): schedule.CurrentState = ResidentState.InShelter; return ScheduleAction.ProcessState; } @@ -244,7 +250,7 @@ private bool UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, { // If the game changed the work building, we have to update the work shifts first ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); - if (schedule.WorkBuilding != workBuilding) + if (schedule.WorkBuilding != workBuilding || (workBuilding == 0 && schedule.WorkShift != WorkShift.Unemployed)) { schedule.WorkBuilding = workBuilding; workBehavior.UpdateWorkShift(ref schedule, CitizenProxy.GetAge(ref citizen)); @@ -397,7 +403,7 @@ private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, return; } - if (!executed && schedule.CurrentState == ResidentState.AtSchoolOrWork) + if (!executed && (schedule.CurrentState == ResidentState.AtSchoolOrWork || schedule.CurrentState == ResidentState.InShelter)) { schedule.Schedule(ResidentState.Unknown); DoScheduledHome(ref schedule, instance, citizenId, ref citizen); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index 3f41cd19..bb85a485 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -91,9 +91,11 @@ private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitiz { CitizenMgr.ModifyUnitGoods(citizenUnit, ShoppingGoodsAmount); } + + return foundBuilding; } - return foundBuilding; + return 0; } private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding) @@ -110,8 +112,9 @@ private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen return 0; } - StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding); - return leisureBuilding; + return StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding) + ? leisureBuilding + : (ushort)0; } private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding) @@ -129,13 +132,21 @@ private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitiz CitizenProxy.SetLocation(ref citizen, Citizen.Location.Visit); return true; } - else if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + if (CitizenProxy.GetVisitBuilding(ref citizen) == 0) { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); - return true; + // Building is full and doesn't accept visitors anymore + return false; } - return false; + if (!residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + return false; + } + + return true; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 57381950..004e427b 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -42,12 +42,13 @@ private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref private bool DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - // Relaxing was already scheduled last time, but the citizen is still at school/work. + // Relaxing was already scheduled last time, but the citizen is still at school/work or in shelter. // This can occur when the game's transfer manager can't find any activity for the citizen. // In that case, move back home. - if (schedule.CurrentState == ResidentState.AtSchoolOrWork && schedule.LastScheduledState == ResidentState.Relaxing) + if ((schedule.CurrentState == ResidentState.AtSchoolOrWork || schedule.CurrentState == ResidentState.InShelter) + && schedule.LastScheduledState == ResidentState.Relaxing) { - Log.Debug(LogCategory.Movement, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but is still at work. No relaxing activity found. Now going home."); + Log.Debug(LogCategory.Movement, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but is still at work or in shelter. No relaxing activity found. Now going home."); return false; } @@ -68,21 +69,23 @@ private bool DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin return true; case ScheduleHint.AttendingEvent: - DateTime returnTime = default; - ICityEvent cityEvent = EventMgr.GetCityEvent(schedule.EventBuilding); + ushort eventBuilding = schedule.EventBuilding; schedule.EventBuilding = 0; + ICityEvent cityEvent = EventMgr.GetCityEvent(eventBuilding); if (cityEvent == null) { - Log.Debug(LogCategory.Events, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{schedule.EventBuilding}', but there was no event there"); + Log.Debug(LogCategory.Events, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{eventBuilding}', but there was no event there"); } - else if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding)) + else if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, eventBuilding)) { - returnTime = cityEvent.EndTime; - Log.Debug(LogCategory.Events, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{schedule.EventBuilding}', will return at {returnTime}"); + schedule.Schedule(ResidentState.Unknown, cityEvent.EndTime); + Log.Debug(LogCategory.Events, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{eventBuilding}', will return at {cityEvent.EndTime}"); + return true; } - return returnTime != default; + schedule.Schedule(ResidentState.Unknown); + return false; } uint relaxChance = spareTimeBehavior.GetRelaxingChance( @@ -157,12 +160,13 @@ private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen private bool DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - // Shopping was already scheduled last time, but the citizen is still at school/work. + // Shopping was already scheduled last time, but the citizen is still at school/work or in shelter. // This can occur when the game's transfer manager can't find any activity for the citizen. // In that case, move back home. - if (schedule.CurrentState == ResidentState.AtSchoolOrWork && schedule.LastScheduledState == ResidentState.Shopping) + if ((schedule.CurrentState == ResidentState.AtSchoolOrWork || schedule.CurrentState == ResidentState.InShelter) + && schedule.LastScheduledState == ResidentState.Shopping) { - Log.Debug(LogCategory.Movement, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted go shopping but is still at work. No shopping activity found. Now going home."); + Log.Debug(LogCategory.Movement, TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted go shopping but is still at work or in shelter. No shopping activity found. Now going home."); return false; } diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index dc302565..f9e79a73 100644 --- a/src/RealTime/CustomAI/RealTimeTouristAI.cs +++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs @@ -111,8 +111,8 @@ private void ProcessMoving(TAI instance, uint citizenId, ref TCitizen citizen) if (vehicleId == 0 && CitizenMgr.IsAreaEvacuating(instanceId) && !CitizenProxy.HasFlags(ref citizen, Citizen.Flags.Evacuating)) { - Log.Debug(LogCategory.Movement, TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} was on the way, but the area evacuates. Leaving the city."); - touristAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetCurrentBuilding(ref citizen), touristAI.GetLeavingReason(instance, citizenId, ref citizen)); + Log.Debug(LogCategory.Movement, TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} was on the way, but the area evacuates. Searching for a shelter."); + touristAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetCurrentBuilding(ref citizen), touristAI.GetEvacuationReason(instance, 0)); return; } @@ -215,38 +215,29 @@ when BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.Co return; } - if (Random.ShouldOccur(TouristEventChance) && !IsBadWeather()) + ICityEvent currentEvent = EventMgr.GetCityEvent(visitBuilding); + if (currentEvent != null && currentEvent.StartTime < TimeInfo.Now) { - ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen); - if (cityEvent != null) + if (Random.ShouldOccur(TouristShoppingChance)) { - StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), cityEvent.BuildingId); - Log.Debug(LogCategory.Events, TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} attending an event at {cityEvent.BuildingId}"); - return; + BuildingMgr.ModifyMaterialBuffer(visitBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); } + + return; } - int doNothingChance; - switch (EventMgr.GetEventState(visitBuilding, DateTime.MaxValue)) + if (Random.ShouldOccur(TouristEventChance) && !IsBadWeather()) { - case CityEventState.Ongoing: - if (Random.ShouldOccur(TouristShoppingChance)) - { - BuildingMgr.ModifyMaterialBuffer(visitBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - } - + ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen); + if (cityEvent != null + && StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), cityEvent.BuildingId)) + { + Log.Debug(LogCategory.Events, TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} attending an event at {cityEvent.BuildingId}"); return; - - case CityEventState.Finished: - doNothingChance = 0; - break; - - default: - doNothingChance = TouristDoNothingProbability; - break; + } } - FindRandomVisitPlace(instance, citizenId, ref citizen, doNothingChance, visitBuilding); + FindRandomVisitPlace(instance, citizenId, ref citizen, 0, visitBuilding); } private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen citizen, int doNothingProbability, ushort currentBuilding) @@ -358,12 +349,22 @@ private ushort FindHotel(ushort currentBuilding) ItemClass.SubService.CommercialTourist); } - private void StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding, ushort visitBuilding) + private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding, ushort visitBuilding) { - if (touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + if (CitizenProxy.GetVisitBuilding(ref citizen) == 0) { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + // Building is full and doesn't accept visitors anymore + return false; } + + if (!touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + return false; + } + + return true; } private uint GetHotelLeaveChance() diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs index fc1f59e1..58e0445c 100644 --- a/src/RealTime/Events/RealTimeEventManager.cs +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -164,6 +164,11 @@ public CityEventState GetEventState(ushort buildingId, DateTime latestStart) /// An instance of the first matching city event, or null if none found. public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime latestStartTime) { + if (activeEvent != null && activeEvent.EndTime > latestStartTime) + { + return activeEvent; + } + if (upcomingEvents.Count == 0) { return null; @@ -319,20 +324,72 @@ private void Update() activeEvent = null; } - bool eventsChanged = false; - foreach (ushort eventId in eventManager.GetUpcomingEvents(timeInfo.Now, timeInfo.Now.AddDays(1))) + bool eventsChanged = SynchronizeWithVanillaEvents(); + + if (upcomingEvents.Count == 0) { - eventManager.TryGetEventInfo(eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice); - if (upcomingEvents.Concat(new[] { activeEvent }) - .OfType() - .Any(e => e.BuildingId == buildingId && e.StartTime.Date == startTime.Date)) + return; + } + + LinkedListNode upcomingEvent = upcomingEvents.First; + while (upcomingEvent != null && upcomingEvent.Value.StartTime <= timeInfo.Now) + { + if (activeEvent != null) + { + lastActiveEvent = activeEvent; + } + + activeEvent = upcomingEvent.Value; + upcomingEvents.RemoveFirst(); + eventsChanged = true; + upcomingEvent = upcomingEvent.Next; + Log.Debug(LogCategory.Events, timeInfo.Now, $"Event started! Building {activeEvent.BuildingId}, ends on {activeEvent.EndTime}"); + } + + if (eventsChanged) + { + OnEventsChanged(); + } + } + + private bool SynchronizeWithVanillaEvents() + { + bool result = false; + var oneMinuteInterval = TimeSpan.FromMinutes(1); + + foreach (ushort eventId in eventManager.GetUpcomingEvents(timeInfo.Now.Date, timeInfo.Now.AddDays(1))) + { + if (!eventManager.TryGetEventInfo(eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice)) { continue; } + VanillaEvent existingVanillaEvent = CityEvents + .OfType() + .FirstOrDefault(e => e.BuildingId == buildingId && e.EventId == eventId); + + if (existingVanillaEvent != null) + { + if (existingVanillaEvent.StartTime.RoundCeil(oneMinuteInterval) == startTime.RoundCeil(oneMinuteInterval)) + { + continue; + } + else + { + upcomingEvents.Remove(existingVanillaEvent); + } + } + + DateTime adjustedStartTime = AdjustEventStartTime(startTime, false); + if (adjustedStartTime != startTime) + { + startTime = adjustedStartTime; + eventManager.SetStartTime(eventId, startTime); + } + var newEvent = new VanillaEvent(eventId, duration, ticketPrice); newEvent.Configure(buildingId, buildingManager.GetBuildingName(buildingId), startTime); - eventsChanged = true; + result = true; Log.Debug(LogCategory.Events, timeInfo.Now, $"Vanilla event registered for {newEvent.BuildingId}, start time {newEvent.StartTime}, end time {newEvent.EndTime}"); LinkedListNode existingEvent = upcomingEvents.FirstOrDefaultNode(e => e.StartTime > startTime); @@ -343,27 +400,16 @@ private void Update() else { upcomingEvents.AddBefore(existingEvent, newEvent); + if (existingEvent.Value.StartTime < newEvent.EndTime) + { + // Avoid multiple events at the same time - vanilla events have priority + upcomingEvents.Remove(existingEvent); + earliestEvent = newEvent.EndTime.AddHours(12f); + } } } - if (upcomingEvents.Count == 0) - { - return; - } - - ICityEvent upcomingEvent = upcomingEvents.First.Value; - if (upcomingEvent.StartTime <= timeInfo.Now) - { - activeEvent = upcomingEvent; - upcomingEvents.RemoveFirst(); - eventsChanged = true; - Log.Debug(LogCategory.Events, timeInfo.Now, $"Event started! Building {activeEvent.BuildingId}, ends on {activeEvent.EndTime}"); - } - - if (eventsChanged) - { - OnEventsChanged(); - } + return result; } private bool RemoveCanceledEvents() @@ -424,8 +470,7 @@ private bool MustCancelEvent(ICityEvent cityEvent) if (cityEvent is VanillaEvent vanillaEvent) { EventData.Flags eventFlags = eventManager.GetEventFlags(vanillaEvent.EventId); - return eventFlags == 0 - || (eventFlags & (EventData.Flags.Cancelled | EventData.Flags.Deleted | EventData.Flags.Expired)) != 0; + return eventFlags == 0 || (eventFlags & (EventData.Flags.Cancelled | EventData.Flags.Deleted)) != 0; } return false; @@ -445,7 +490,11 @@ private void CreateRandomEvent(ushort buildingId) return; } - DateTime startTime = GetRandomEventStartTime(); + DateTime startTime = upcomingEvents.Count == 0 + ? timeInfo.Now + : upcomingEvents.Last.Value.EndTime.Add(MinimumIntervalBetweenEvents); + + startTime = AdjustEventStartTime(startTime, true); if (startTime < earliestEvent) { return; @@ -459,11 +508,9 @@ private void CreateRandomEvent(ushort buildingId) Log.Debug(LogCategory.Events, timeInfo.Now, $"New event created for building {newEvent.BuildingId}, starts on {newEvent.StartTime}, ends on {newEvent.EndTime}"); } - private DateTime GetRandomEventStartTime() + private DateTime AdjustEventStartTime(DateTime eventStartTime, bool randomize) { - DateTime result = upcomingEvents.Count == 0 - ? timeInfo.Now - : upcomingEvents.Last.Value.EndTime.Add(MinimumIntervalBetweenEvents); + DateTime result = eventStartTime; float earliestHour; float latestHour; @@ -478,7 +525,10 @@ private DateTime GetRandomEventStartTime() latestHour = config.LatestHourEventStartWeekday; } - float randomOffset = randomizer.GetRandomValue((uint)((latestHour - earliestHour) * 60f)) / 60f; + float randomOffset = randomize + ? randomizer.GetRandomValue((uint)((latestHour - earliestHour) * 60f)) / 60f + : 0; + result = result.AddHours(randomOffset).RoundCeil(EventStartTimeGranularity); if (result.Hour >= latestHour) diff --git a/src/RealTime/Events/VanillaEvents.cs b/src/RealTime/Events/VanillaEvents.cs new file mode 100644 index 00000000..d9beb374 --- /dev/null +++ b/src/RealTime/Events/VanillaEvents.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using System; + using System.Collections.Generic; + using SkyTools.Tools; + + /// + /// A class for handling the vanilla game events data. + /// + internal sealed class VanillaEvents + { + private Dictionary eventData = new Dictionary(); + + private VanillaEvents() + { + } + + /// Customizes the vanilla events in the game by changing their time behavior. + /// An instance of the class that can be used for reverting the events + /// to the original state. + public static VanillaEvents Customize() + { + var result = new VanillaEvents(); + + for (uint i = 0; i < PrefabCollection.PrefabCount(); ++i) + { + EventInfo eventInfo = PrefabCollection.GetPrefab(i); + if (eventInfo == null || eventInfo.m_eventAI == null) + { + continue; + } + + EventAI eventAI = eventInfo.m_eventAI; + var originalData = new EventAIData(eventAI.m_eventDuration, eventAI.m_prepareDuration, eventAI.m_disorganizeDuration); + result.eventData[eventAI] = originalData; + Customize(eventAI); + Log.Debug(LogCategory.Events, $"Customized event {eventAI.GetType().Name}"); + } + + return result; + } + + /// Updates all vanilla game events so that their start and end times correspond to the + /// currently active time flow speed. + /// A method that converts the frame-based time to a real time. + /// Thrown when the argument is null. + public static void ProcessUpdatedTimeSpeed(Func timeConverter) + { + if (timeConverter == null) + { + throw new ArgumentNullException(nameof(timeConverter)); + } + + for (int i = 0; i < EventManager.instance.m_events.m_size; ++i) + { + ref EventData eventData = ref EventManager.instance.m_events.m_buffer[i]; + if (eventData.m_startFrame == 0) + { + continue; + } + + Log.Debug(LogCategory.Events, $"Update event {i} time frames: original start {eventData.m_startFrame}, original end {eventData.m_expireFrame}"); + DateTime originalTime = timeConverter(eventData.m_startFrame); + eventData.m_startFrame = SimulationManager.instance.TimeToFrame(originalTime); + + originalTime = timeConverter(eventData.m_expireFrame); + eventData.m_expireFrame = SimulationManager.instance.TimeToFrame(originalTime); + Log.Debug(LogCategory.Events, $"Event updated: new start {eventData.m_startFrame}, new end {eventData.m_expireFrame}"); + } + } + + /// Reverts the vanilla events parameters to the original state. + public void Revert() + { + foreach (KeyValuePair item in eventData) + { + EventAI eventAI = item.Key; + eventAI.m_eventDuration = item.Value.EventDuration; + eventAI.m_prepareDuration = item.Value.PrepareDuration; + eventAI.m_disorganizeDuration = item.Value.DisorganizeDuration; + Log.Debug(LogCategory.Events, $"Reverted event {eventAI.GetType().Name}"); + } + + eventData.Clear(); + } + + private static void Customize(EventAI eventAI) + { + switch (eventAI) + { + case null: + return; + + case ConcertAI concertAI: + concertAI.m_eventDuration = 4f; + concertAI.m_prepareDuration = 4f; + concertAI.m_disorganizeDuration = 2f; + return; + + case SportMatchAI matchAI: + matchAI.m_eventDuration = 2f; + matchAI.m_prepareDuration = 4f; + matchAI.m_disorganizeDuration = 2f; + return; + + case RocketLaunchAI rocketLaunchAI: + rocketLaunchAI.m_eventDuration = 4f; + rocketLaunchAI.m_prepareDuration = 12f; + rocketLaunchAI.m_disorganizeDuration = 4f; + return; + + default: + eventAI.m_eventDuration = 2f; + eventAI.m_prepareDuration = 2f; + eventAI.m_disorganizeDuration = 2f; + return; + } + } + + private readonly struct EventAIData + { + public EventAIData(float eventDuration, float prepareDuration, float disorganizeDuration) + { + EventDuration = eventDuration; + PrepareDuration = prepareDuration; + DisorganizeDuration = disorganizeDuration; + } + + public float EventDuration { get; } + + public float PrepareDuration { get; } + + public float DisorganizeDuration { get; } + } + } +} diff --git a/src/RealTime/GameConnection/EventManagerConnection.cs b/src/RealTime/GameConnection/EventManagerConnection.cs index bda580d7..446e06f4 100644 --- a/src/RealTime/GameConnection/EventManagerConnection.cs +++ b/src/RealTime/GameConnection/EventManagerConnection.cs @@ -20,7 +20,7 @@ internal sealed class EventManagerConnection : IEventManagerConnection /// public EventData.Flags GetEventFlags(ushort eventId) { - return eventId == 0 + return eventId == 0 || eventId >= EventManager.instance.m_events.m_size ? EventData.Flags.None : EventManager.instance.m_events.m_buffer[eventId].m_flags; } @@ -34,21 +34,20 @@ public EventData.Flags GetEventFlags(ushort eventId) public IEnumerable GetUpcomingEvents(DateTime earliestTime, DateTime latestTime) { FastList events = EventManager.instance.m_events; - for (ushort i = 0; i < events.m_size && i < EventManager.MAX_EVENT_COUNT; ++i) + for (ushort i = 1; i < events.m_size; ++i) { - EventData eventData = events.m_buffer[i]; - if ((eventData.m_flags & (EventData.Flags.Preparing | EventData.Flags.Ready | EventData.Flags.Active)) == 0) + if ((events.m_buffer[i].m_flags & (EventData.Flags.Preparing | EventData.Flags.Ready | EventData.Flags.Active)) == 0) { continue; } - if ((eventData.m_flags + if ((events.m_buffer[i].m_flags & (EventData.Flags.Cancelled | EventData.Flags.Completed | EventData.Flags.Deleted | EventData.Flags.Expired)) != 0) { continue; } - if (eventData.StartTime >= earliestTime && eventData.StartTime < latestTime) + if (events.m_buffer[i].StartTime >= earliestTime && events.m_buffer[i].StartTime < latestTime) { yield return i; } @@ -77,12 +76,28 @@ public bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime return false; } - EventData eventData = EventManager.instance.m_events.m_buffer[eventId]; + ref EventData eventData = ref EventManager.instance.m_events.m_buffer[eventId]; buildingId = eventData.m_building; startTime = eventData.StartTime; duration = eventData.Info.m_eventAI.m_eventDuration; ticketPrice = eventData.m_ticketPrice / 100f; return true; } + + /// Sets the start time of the event to the specified value. + /// The ID of the event to change. + /// The new event start time. + public void SetStartTime(ushort eventId, DateTime startTime) + { + if (eventId == 0 || eventId >= EventManager.instance.m_events.m_size) + { + return; + } + + ref EventData eventData = ref EventManager.instance.m_events.m_buffer[eventId]; + uint oldStartTime = eventData.m_startFrame; + eventData.m_startFrame = SimulationManager.instance.TimeToFrame(startTime); + eventData.m_expireFrame = eventData.m_expireFrame + (eventData.m_startFrame - oldStartTime); + } } } diff --git a/src/RealTime/GameConnection/IEventManagerConnection.cs b/src/RealTime/GameConnection/IEventManagerConnection.cs index a64b1506..36997b84 100644 --- a/src/RealTime/GameConnection/IEventManagerConnection.cs +++ b/src/RealTime/GameConnection/IEventManagerConnection.cs @@ -31,5 +31,10 @@ internal interface IEventManagerConnection /// The city event's ticket price. /// true if the information was retrieved; otherwise, false. bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice); + + /// Sets the start time of the event to the specified value. + /// The ID of the event to change. + /// The new event start time. + void SetStartTime(ushort eventId, DateTime startTime); } } \ No newline at end of file diff --git a/src/RealTime/Localization/Translations/de.xml b/src/RealTime/Localization/Translations/de.xml index c59b2229..1a27248f 100644 --- a/src/RealTime/Localization/Translations/de.xml +++ b/src/RealTime/Localization/Translations/de.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/en.xml b/src/RealTime/Localization/Translations/en.xml index f23f07aa..9fade1ef 100644 --- a/src/RealTime/Localization/Translations/en.xml +++ b/src/RealTime/Localization/Translations/en.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/es.xml b/src/RealTime/Localization/Translations/es.xml index 2ddb5bbc..7041834b 100644 --- a/src/RealTime/Localization/Translations/es.xml +++ b/src/RealTime/Localization/Translations/es.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/fr.xml b/src/RealTime/Localization/Translations/fr.xml index e07799b2..70ccfd13 100644 --- a/src/RealTime/Localization/Translations/fr.xml +++ b/src/RealTime/Localization/Translations/fr.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/ko.xml b/src/RealTime/Localization/Translations/ko.xml index b59b6a63..26655c8a 100644 --- a/src/RealTime/Localization/Translations/ko.xml +++ b/src/RealTime/Localization/Translations/ko.xml @@ -82,8 +82,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/pl.xml b/src/RealTime/Localization/Translations/pl.xml index e8e61a97..bee4e2ea 100644 --- a/src/RealTime/Localization/Translations/pl.xml +++ b/src/RealTime/Localization/Translations/pl.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/pt.xml b/src/RealTime/Localization/Translations/pt.xml index e205b724..88b7144c 100644 --- a/src/RealTime/Localization/Translations/pt.xml +++ b/src/RealTime/Localization/Translations/pt.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/ru.xml b/src/RealTime/Localization/Translations/ru.xml index a8e18e0e..73dda6ef 100644 --- a/src/RealTime/Localization/Translations/ru.xml +++ b/src/RealTime/Localization/Translations/ru.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/Localization/Translations/zh.xml b/src/RealTime/Localization/Translations/zh.xml index bfaf4fa6..86372d06 100644 --- a/src/RealTime/Localization/Translations/zh.xml +++ b/src/RealTime/Localization/Translations/zh.xml @@ -83,8 +83,8 @@ - - + + diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index 9e20499f..8c04e00e 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -103,6 +103,7 @@ + diff --git a/src/RealTime/Simulation/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index ae327cfc..b6fe76cf 100644 --- a/src/RealTime/Simulation/SimulationHandler.cs +++ b/src/RealTime/Simulation/SimulationHandler.cs @@ -56,10 +56,8 @@ public sealed class SimulationHandler : ThreadingExtensionBase public override void OnBeforeSimulationTick() { WeatherInfo?.Update(); - EventManager?.ProcessEvents(); bool updateFrameLength = TimeAdjustment?.Update() ?? false; - if (CitizenProcessor != null) { if (updateFrameLength) @@ -74,8 +72,11 @@ public override void OnBeforeSimulationTick() { Buildings?.UpdateFrameDuration(); Statistics?.RefreshUnits(); + VanillaEvents.ProcessUpdatedTimeSpeed(TimeAdjustment.GetOriginalTime); } + EventManager?.ProcessEvents(); + if (DayTimeSimulation == null || CitizenProcessor == null) { return; diff --git a/src/RealTime/Simulation/TimeAdjustment.cs b/src/RealTime/Simulation/TimeAdjustment.cs index dcb3233f..06c2c9e5 100644 --- a/src/RealTime/Simulation/TimeAdjustment.cs +++ b/src/RealTime/Simulation/TimeAdjustment.cs @@ -18,6 +18,8 @@ internal sealed class TimeAdjustment private uint nightTimeSpeed; private bool isNightTime; private bool isNightEnabled; + private TimeSpan originalTimePerFrame; + private long originalTimeOffsetTicks; /// Initializes a new instance of the class. /// The configuration to run with. @@ -69,7 +71,16 @@ public void Disable() UpdateTimeSimulationValues(vanillaFramesPerDay); } - private static DateTime UpdateTimeSimulationValues(uint framesPerDay) + /// Gets the original time represented by the frame index. + /// This method can be used to convert frame-based times after time adjustments. + /// A frame index representing a time point. + /// A object for the specified . + public DateTime GetOriginalTime(uint frameIndex) + { + return new DateTime((frameIndex * originalTimePerFrame.Ticks) + originalTimeOffsetTicks); + } + + private DateTime UpdateTimeSimulationValues(uint framesPerDay) { SimulationManager sm = SimulationManager.instance; DateTime originalDate = sm.m_ThreadingWrapper.simulationTime; @@ -78,6 +89,8 @@ private static DateTime UpdateTimeSimulationValues(uint framesPerDay) SimulationManager.DAYTIME_FRAME_TO_HOUR = 24f / SimulationManager.DAYTIME_FRAMES; SimulationManager.DAYTIME_HOUR_TO_FRAME = SimulationManager.DAYTIME_FRAMES / 24f; + originalTimePerFrame = sm.m_timePerFrame; + originalTimeOffsetTicks = sm.m_timeOffsetTicks; sm.m_timePerFrame = new TimeSpan(24L * 3600L * 10_000_000L / framesPerDay); sm.m_timeOffsetTicks = originalDate.Ticks - (sm.m_currentFrameIndex * sm.m_timePerFrame.Ticks); sm.m_currentGameTime = originalDate; diff --git a/src/RealTime/UI/ConfigUI.cs b/src/RealTime/UI/ConfigUI.cs index 2b3cd90b..4c3207e4 100644 --- a/src/RealTime/UI/ConfigUI.cs +++ b/src/RealTime/UI/ConfigUI.cs @@ -37,7 +37,7 @@ private ConfigUI(ConfigurationProvider configProvider, IEnumerab /// The view item factory to use for creating the UI elements. /// A configured instance of the class. /// Thrown when any argument is null. - /// Thrown when the specified + /// Thrown when the specified /// is not initialized yet. public static ConfigUI Create(ConfigurationProvider configProvider, IViewItemFactory itemFactory) { diff --git a/src/SkyTools b/src/SkyTools index 3ea7ce1f..eadc9980 160000 --- a/src/SkyTools +++ b/src/SkyTools @@ -1 +1 @@ -Subproject commit 3ea7ce1f27eaad0fef516c1affdd802661dd0750 +Subproject commit eadc998094aee7a4ef5e37197537f5f1bfc0343c