diff --git a/.gitignore b/.gitignore index 25b7a3f8..42dbee36 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ _ReSharper*/ #Nuget packages folder packages/ + +#MacOS stuff .DS_Store diff --git a/src/RealTime/Config/ConfigurationProvider.cs b/src/RealTime/Config/ConfigurationProvider.cs index 36ab31a1..025c8cba 100644 --- a/src/RealTime/Config/ConfigurationProvider.cs +++ b/src/RealTime/Config/ConfigurationProvider.cs @@ -42,7 +42,7 @@ public static RealTimeConfig LoadConfiguration() } /// - /// Stores the provided object to the storage. + /// Stores the specified object to the storage. /// /// /// Thrown when the argument is null. @@ -70,7 +70,7 @@ private static RealTimeConfig Deserialize() var serializer = new XmlSerializer(typeof(RealTimeConfig)); using (var sr = new StreamReader(SettingsFileName)) { - return ((RealTimeConfig)serializer.Deserialize(sr)).Validate(); + return ((RealTimeConfig)serializer.Deserialize(sr)).MigrateWhenNecessary().Validate(); } } diff --git a/src/RealTime/Config/RealTimeConfig.cs b/src/RealTime/Config/RealTimeConfig.cs index 4d4359c0..dc902b1c 100644 --- a/src/RealTime/Config/RealTimeConfig.cs +++ b/src/RealTime/Config/RealTimeConfig.cs @@ -4,7 +4,7 @@ namespace RealTime.Config { - using System.Collections.Generic; + using RealTime.Tools; using RealTime.UI; /// @@ -18,6 +18,9 @@ public RealTimeConfig() ResetToDefaults(); } + /// Gets or sets the version number of this configuration. + public int Version { get; set; } + /// /// Gets or sets the daytime hour when the city wakes up. /// @@ -96,7 +99,7 @@ public RealTimeConfig() /// Valid values are 1..8. /// [ConfigItem("2Quotas", 0)] - [ConfigItemSlider(1, 8, DisplayMultiplier = 3.125f)] + [ConfigItemSlider(1, 25)] public uint SecondShiftQuota { get; set; } /// @@ -104,15 +107,15 @@ public RealTimeConfig() /// Valid values are 1..8. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "NightShift", Justification = "Reviewed")] - [ConfigItem("2Quotas", 0)] - [ConfigItemSlider(1, 8, DisplayMultiplier = 3.125f)] + [ConfigItem("2Quotas", 1)] + [ConfigItemSlider(1, 25)] public uint NightShiftQuota { get; set; } /// /// Gets or sets the percentage of the Cims that will go out for lunch. /// Valid values are 0..100. /// - [ConfigItem("2Quotas", 0)] + [ConfigItem("2Quotas", 2)] [ConfigItemSlider(0, 100)] public uint LunchQuota { get; set; } @@ -120,7 +123,7 @@ public RealTimeConfig() /// Gets or sets the percentage of the population that will search locally for buildings. /// Valid values are 0..100. /// - [ConfigItem("2Quotas", 1)] + [ConfigItem("2Quotas", 3)] [ConfigItemSlider(0, 100)] public uint LocalBuildingSearchQuota { get; set; } @@ -129,7 +132,7 @@ public RealTimeConfig() /// on time (no overtime!). /// Valid values are 0..100. /// - [ConfigItem("2Quotas", 2)] + [ConfigItem("2Quotas", 4)] [ConfigItemSlider(0, 100)] public uint OnTimeQuota { get; set; } @@ -212,46 +215,60 @@ public RealTimeConfig() [ConfigItemSlider(11, 16, 0.25f, SliderValueType.Time)] public float SchoolEnd { get; set; } + /// Checks the version of the deserialized object and migrates it to the latest version when necessary. + /// This instance. + public RealTimeConfig MigrateWhenNecessary() + { + if (Version == 0) + { + SecondShiftQuota = (uint)(SecondShiftQuota * 3.125f); + NightShiftQuota = (uint)(NightShiftQuota * 3.125f); + } + + Version = 1; + return this; + } + /// Validates this instance and corrects possible invalid property values. /// This instance. public RealTimeConfig Validate() { - WakeupHour = Clamp(WakeupHour, 4f, 8f); - GoToSleepUpHour = Clamp(GoToSleepUpHour, 20f, 23.75f); + WakeupHour = RealTimeMath.Clamp(WakeupHour, 4f, 8f); + GoToSleepUpHour = RealTimeMath.Clamp(GoToSleepUpHour, 20f, 23.75f); - DayTimeSpeed = Clamp(DayTimeSpeed, 1u, 7u); - NightTimeSpeed = Clamp(NightTimeSpeed, 1u, 7u); + DayTimeSpeed = RealTimeMath.Clamp(DayTimeSpeed, 1u, 7u); + NightTimeSpeed = RealTimeMath.Clamp(NightTimeSpeed, 1u, 7u); - VirtualCitizens = (VirtualCitizensLevel)Clamp((int)VirtualCitizens, (int)VirtualCitizensLevel.None, (int)VirtualCitizensLevel.Many); - ConstructionSpeed = Clamp(ConstructionSpeed, 0u, 100u); + VirtualCitizens = (VirtualCitizensLevel)RealTimeMath.Clamp((int)VirtualCitizens, (int)VirtualCitizensLevel.None, (int)VirtualCitizensLevel.Many); + ConstructionSpeed = RealTimeMath.Clamp(ConstructionSpeed, 0u, 100u); - SecondShiftQuota = Clamp(SecondShiftQuota, 1u, 8u); - NightShiftQuota = Clamp(NightShiftQuota, 1u, 8u); - LunchQuota = Clamp(LunchQuota, 0u, 100u); - LocalBuildingSearchQuota = Clamp(LocalBuildingSearchQuota, 0u, 100u); - OnTimeQuota = Clamp(OnTimeQuota, 0u, 100u); + SecondShiftQuota = RealTimeMath.Clamp(SecondShiftQuota, 1u, 25u); + NightShiftQuota = RealTimeMath.Clamp(NightShiftQuota, 1u, 25u); + LunchQuota = RealTimeMath.Clamp(LunchQuota, 0u, 100u); + LocalBuildingSearchQuota = RealTimeMath.Clamp(LocalBuildingSearchQuota, 0u, 100u); + OnTimeQuota = RealTimeMath.Clamp(OnTimeQuota, 0u, 100u); - EarliestHourEventStartWeekday = Clamp(EarliestHourEventStartWeekday, 0f, 23.75f); - LatestHourEventStartWeekday = Clamp(LatestHourEventStartWeekday, 0f, 23.75f); + EarliestHourEventStartWeekday = RealTimeMath.Clamp(EarliestHourEventStartWeekday, 0f, 23.75f); + LatestHourEventStartWeekday = RealTimeMath.Clamp(LatestHourEventStartWeekday, 0f, 23.75f); if (LatestHourEventStartWeekday < EarliestHourEventStartWeekday) { LatestHourEventStartWeekday = EarliestHourEventStartWeekday; } - EarliestHourEventStartWeekend = Clamp(EarliestHourEventStartWeekend, 0f, 23.75f); - LatestHourEventStartWeekend = Clamp(LatestHourEventStartWeekend, 0f, 23.75f); + EarliestHourEventStartWeekend = RealTimeMath.Clamp(EarliestHourEventStartWeekend, 0f, 23.75f); + LatestHourEventStartWeekend = RealTimeMath.Clamp(LatestHourEventStartWeekend, 0f, 23.75f); if (LatestHourEventStartWeekend < EarliestHourEventStartWeekend) { LatestHourEventStartWeekend = EarliestHourEventStartWeekend; } - WorkBegin = Clamp(WorkBegin, 4f, 11f); - WorkEnd = Clamp(WorkEnd, 12f, 20f); - LunchBegin = Clamp(LunchBegin, 11f, 13f); - LunchEnd = Clamp(LunchEnd, 13f, 15f); - SchoolBegin = Clamp(SchoolBegin, 4f, 10f); - SchoolEnd = Clamp(SchoolEnd, 11f, 16f); - MaxOvertime = Clamp(MaxOvertime, 0f, 4f); + WorkBegin = RealTimeMath.Clamp(WorkBegin, 4f, 11f); + WorkEnd = RealTimeMath.Clamp(WorkEnd, 12f, 20f); + LunchBegin = RealTimeMath.Clamp(LunchBegin, 11f, 13f); + LunchEnd = RealTimeMath.Clamp(LunchEnd, 13f, 15f); + SchoolBegin = RealTimeMath.Clamp(SchoolBegin, 4f, 10f); + SchoolEnd = RealTimeMath.Clamp(SchoolEnd, 11f, 16f); + MaxOvertime = RealTimeMath.Clamp(MaxOvertime, 0f, 4f); return this; } @@ -273,8 +290,8 @@ public void ResetToDefaults() StopConstructionAtNight = true; ConstructionSpeed = 50; - SecondShiftQuota = 4; - NightShiftQuota = 2; + SecondShiftQuota = 13; + NightShiftQuota = 6; LunchQuota = 80; LocalBuildingSearchQuota = 60; @@ -293,22 +310,5 @@ public void ResetToDefaults() SchoolBegin = 8f; SchoolEnd = 14f; } - - private static T Clamp(T value, T min, T max) - where T : struct - { - Comparer comparer = Comparer.Default; - if (comparer.Compare(value, min) < 0) - { - return min; - } - - if (comparer.Compare(value, max) > 0) - { - return max; - } - - return value; - } } } diff --git a/src/RealTime/Core/IStorageData.cs b/src/RealTime/Core/IStorageData.cs index f44591b5..7d49ab41 100644 --- a/src/RealTime/Core/IStorageData.cs +++ b/src/RealTime/Core/IStorageData.cs @@ -17,7 +17,7 @@ internal interface IStorageData string StorageDataId { get; } /// - /// Reads the data set from the provided . + /// Reads the data set from the specified . /// /// /// Thrown when the argument is null. @@ -26,7 +26,7 @@ internal interface IStorageData void ReadData(Stream source); /// - /// Reads the data set to the provided . + /// Stores the data set to the specified . /// /// /// Thrown when the argument is null. diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index 4dba5421..f0135c82 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -76,6 +76,7 @@ public static RealTimeCore Run(RealTimeConfig config, string rootPath, ILocaliza BuildingAIPatches.PrivateHandleWorkers, BuildingAIPatches.CommercialSimulation, ResidentAIPatch.Location, + ResidentAIPatch.ArriveAtDestination, TouristAIPatch.Location, UIGraphPatches.MinDataPoints, UIGraphPatches.VisibleEndTime, @@ -125,6 +126,7 @@ public static RealTimeCore Run(RealTimeConfig config, string rootPath, ILocaliza var timeAdjustment = new TimeAdjustment(config); DateTime gameDate = timeAdjustment.Enable(); + SimulationHandler.CitizenProcessor.SetFrameDuration(timeAdjustment.HoursPerFrame); CityEventsLoader.Instance.ReloadEvents(rootPath); @@ -146,6 +148,7 @@ public static RealTimeCore Run(RealTimeConfig config, string rootPath, ILocaliza RealTimeStorage.CurrentLevelStorage.GameSaving += result.GameSaving; result.storageData.Add(eventManager); + result.storageData.Add(ResidentAIPatch.RealTimeAI.GetStorageService()); result.LoadStorageData(); result.Translate(localizationProvider); @@ -185,13 +188,14 @@ public void Stop() SimulationHandler.TimeAdjustment = null; SimulationHandler.WeatherInfo = null; SimulationHandler.Buildings = null; + SimulationHandler.CitizenProcessor = null; isEnabled = false; } /// /// Translates all the mod's component to a different language obtained from - /// the provided . + /// the specified . /// /// /// Thrown when the argument is null. @@ -220,13 +224,17 @@ private static bool SetupCustomAI( return false; } + var spareTimeBehavior = new SpareTimeBehavior(config, timeInfo); + var realTimeResidentAI = new RealTimeResidentAI( config, gameConnections, residentAIConnection, - eventManager); + eventManager, + spareTimeBehavior); ResidentAIPatch.RealTimeAI = realTimeResidentAI; + SimulationHandler.CitizenProcessor = new CitizenProcessor(realTimeResidentAI, spareTimeBehavior); TouristAIConnection touristAIConnection = TouristAIPatch.GetTouristAIConnection(); if (touristAIConnection == null) @@ -238,7 +246,8 @@ private static bool SetupCustomAI( config, gameConnections, touristAIConnection, - eventManager); + eventManager, + spareTimeBehavior); TouristAIPatch.RealTimeAI = realTimeTouristAI; diff --git a/src/RealTime/Core/RealTimeStorage.cs b/src/RealTime/Core/RealTimeStorage.cs index 780565c3..0bb7e8cf 100644 --- a/src/RealTime/Core/RealTimeStorage.cs +++ b/src/RealTime/Core/RealTimeStorage.cs @@ -56,7 +56,7 @@ public override void OnReleased() } /// - /// Serializes the data described by the provided to this level's storage. + /// Serializes the data described by the specified to this level's storage. /// /// /// Thrown when the argument is null. @@ -86,7 +86,7 @@ internal void Serialize(IStorageData data) } /// - /// Deserializes the data described by the provided from this level's storage. + /// Deserializes the data described by the specified from this level's storage. /// /// /// Thrown when the argument is null. diff --git a/src/RealTime/CustomAI/CitizenSchedule.cs b/src/RealTime/CustomAI/CitizenSchedule.cs new file mode 100644 index 00000000..f75796d9 --- /dev/null +++ b/src/RealTime/CustomAI/CitizenSchedule.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using static Constants; + + /// A container struct that holds information about the detailed resident citizen state. + /// Note that this struct is intentionally made mutable to increase performance. + internal struct CitizenSchedule + { + /// The size of the buffer in bytes to store the data. + public const int DataRecordSize = 6; + + /// The citizen's current state. + public ResidentState CurrentState; + + /// The citizen's schedule hint. + public ScheduleHint Hint; + + /// The ID of the building where an event takes place, if the citizen schedules to attend one. + public ushort EventBuilding; + + /// The citizen's work status. + public WorkStatus WorkStatus; + + /// The ID of the citizen's work building. If it doesn't equal the game's value, the work shift data needs to be updated. + public ushort WorkBuilding; + + /// The time when citizen started their last ride to the work building. + public DateTime DepartureToWorkTime; + + private const float TravelTimeMultiplier = ushort.MaxValue / MaxTravelTime; + + /// Gets the citizen's next scheduled state. + public ResidentState ScheduledState { get; private set; } + + /// Gets the time when the citizen will perform the next state change. + public DateTime ScheduledStateTime { get; private set; } + + /// + /// Gets the travel time (in hours) from citizen's home to the work building. The maximum value is + /// determined by the constant. + /// + public float TravelTimeToWork { get; private set; } + + /// Gets the citizen's work shift. + public WorkShift WorkShift { get; private set; } + + /// Gets the daytime hour when the citizen's work shift starts. + public float WorkShiftStartHour { get; private set; } + + /// Gets the daytime hour when the citizen's work shift ends. + public float WorkShiftEndHour { get; private set; } + + /// Gets a value indicating whether this citizen works on weekends. + public bool WorksOnWeekends { get; private set; } + + /// Updates the travel time that the citizen needs to read the work building or school/university. + /// + /// The arrival time at the work building or school/university. Must be great than . + /// + public void UpdateTravelTimeToWork(DateTime arrivalTime) + { + if (arrivalTime < DepartureToWorkTime || DepartureToWorkTime == default) + { + return; + } + + float onTheWayHours = (float)(arrivalTime - DepartureToWorkTime).TotalHours; + if (onTheWayHours > MaxTravelTime) + { + onTheWayHours = MaxTravelTime; + } + + TravelTimeToWork = TravelTimeToWork == 0 + ? onTheWayHours + : (TravelTimeToWork + onTheWayHours) / 2; + } + + /// Updates the work shift data for this citizen's schedule. + /// The citizen's work shift. + /// The work shift start hour. + /// The work shift end hour. + /// if true, the citizen works on weekends. + public void UpdateWorkShift(WorkShift workShift, float startHour, float endHour, bool worksOnWeekends) + { + WorkShift = workShift; + WorkShiftStartHour = startHour; + WorkShiftEndHour = endHour; + WorksOnWeekends = worksOnWeekends; + } + + /// Schedules next actions for the citizen. + /// The next scheduled citizen's state. + /// The time when the scheduled state must change. + public void Schedule(ResidentState nextState, DateTime nextStateTime) + { + ScheduledState = nextState; + ScheduledStateTime = nextStateTime; + } + + /// Writes this instance to the specified target buffer. + /// The target buffer. Must have length of elements. + /// The reference time (in ticks) to use for time serialization. + public void Write(byte[] target, long referenceTime) + { + target[0] = (byte)(((int)WorkShift & 0xF) + ((int)WorkStatus << 4)); + target[1] = (byte)ScheduledState; + + ushort minutes = ScheduledStateTime == default + ? (ushort)0 + : (ushort)((ScheduledStateTime.Ticks - referenceTime) / TimeSpan.TicksPerMinute); + + target[2] = (byte)(minutes & 0xFF); + target[3] = (byte)(minutes >> 8); + + ushort travelTime = (ushort)(TravelTimeToWork * TravelTimeMultiplier); + target[4] = (byte)(travelTime & 0xFF); + target[5] = (byte)(travelTime >> 8); + } + + /// Reads this instance from the specified source buffer. + /// The source buffer. Must have length of elements. + /// The reference time (in ticks) to use for time deserialization. + public void Read(byte[] source, long referenceTime) + { + WorkShift = (WorkShift)(source[0] & 0xF); + WorkStatus = (WorkStatus)(source[0] >> 4); + ScheduledState = (ResidentState)source[1]; + + int minutes = source[2] + (source[3] << 8); + ScheduledStateTime = minutes == 0 + ? default + : new DateTime((minutes * TimeSpan.TicksPerMinute) + referenceTime); + + int travelTime = source[4] + (source[5] << 8); + TravelTimeToWork = travelTime / TravelTimeMultiplier; + } + } +} \ No newline at end of file diff --git a/src/RealTime/CustomAI/CitizenScheduleStorage.cs b/src/RealTime/CustomAI/CitizenScheduleStorage.cs new file mode 100644 index 00000000..227902de --- /dev/null +++ b/src/RealTime/CustomAI/CitizenScheduleStorage.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using System.IO; + using RealTime.Core; + using RealTime.Simulation; + + /// + /// A helper class that enables loading and saving of the custom citizen schedules. + /// This class accesses the directly for better performance. + /// + /// + internal sealed class CitizenScheduleStorage : IStorageData + { + private const string StorageDataId = "RealTimeCitizenSchedule"; + + private readonly CitizenSchedule[] residentSchedules; + private readonly Citizen[] citizens; + private readonly ITimeInfo timeInfo; + + /// Initializes a new instance of the class. + /// The resident schedules to store or load. + /// The game's citizens array. + /// An object that provides the game time information. + /// Thrown when any argument is null. + /// Thrown when and + /// have different length. + public CitizenScheduleStorage(CitizenSchedule[] residentSchedules, Citizen[] citizens, ITimeInfo timeInfo) + { + this.residentSchedules = residentSchedules ?? throw new System.ArgumentNullException(nameof(residentSchedules)); + this.citizens = citizens ?? throw new ArgumentNullException(nameof(citizens)); + this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + if (residentSchedules.Length != citizens.Length) + { + throw new ArgumentException($"{nameof(residentSchedules)} and {nameof(citizens)} arrays must have equal length"); + } + } + + /// Gets an unique ID of this storage data set. + string IStorageData.StorageDataId => StorageDataId; + + /// Reads the data set from the specified . + /// A to read the data set from. + void IStorageData.ReadData(Stream source) + { + byte[] buffer = new byte[CitizenSchedule.DataRecordSize]; + long referenceTime = timeInfo.Now.Date.Ticks; + + for (int i = 0; i < citizens.Length; ++i) + { + Citizen.Flags flags = citizens[i].m_flags; + if ((flags & Citizen.Flags.Created) == 0 + || (flags & Citizen.Flags.DummyTraffic) != 0) + { + continue; + } + + source.Read(buffer, 0, buffer.Length); + residentSchedules[i].Read(buffer, referenceTime); + } + } + + /// Reads the data set to the specified . + /// A to write the data set to. + void IStorageData.StoreData(Stream target) + { + byte[] buffer = new byte[CitizenSchedule.DataRecordSize]; + long referenceTime = timeInfo.Now.Date.Ticks; + + for (int i = 0; i < citizens.Length; ++i) + { + Citizen.Flags flags = citizens[i].m_flags; + if ((flags & Citizen.Flags.Created) == 0 + || (flags & Citizen.Flags.DummyTraffic) != 0) + { + continue; + } + + residentSchedules[i].Write(buffer, referenceTime); + target.Write(buffer, 0, buffer.Length); + } + } + } +} diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index f00bf551..cc006e68 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -17,17 +17,14 @@ internal static class Constants /// A distance in game units that corresponds to the complete map. public const float FullSearchDistance = BuildingManager.BUILDINGGRID_RESOLUTION * BuildingManager.BUILDINGGRID_CELL_SIZE / 2f; - /// A chance in percent for a citizen to abandon the transport waiting if it lasts too long. - public const uint AbandonTransportWaitChance = 80; + /// A chance in percent for an unemployed citizen to stay home until next morning. + public const uint StayHomeAllDayChance = 15; /// A chance in percent for a citizen to go shopping. - public const uint GoShoppingChance = 50; + public const uint GoShoppingChance = 80; - /// A chance in percent for a citizen to return shopping. - public const uint ReturnFromShoppingChance = 60; - - /// A chance in percent for a citizen to return from a visited building. - public const uint ReturnFromVisitChance = 40; + /// A chance in percent for a citizen to go to sleep when he or she is at home and doesn't go out. + public const uint GoSleepingChance = 75; /// A chance in percent for a tourist to find a hotel for sleepover. public const uint FindHotelChance = 80; @@ -41,14 +38,14 @@ internal static class Constants /// A hard coded game value describing a 'do nothing' probability for a tourist. public const int TouristDoNothingProbability = 5000; - /// An assumed maximum on-the-way time to a target building. - public const float MaxHoursOnTheWay = 2.5f; + /// The amount of hours the citizen will spend preparing to work and not going out. + public const float PrepareToWorkHours = 1f; - /// An assumed minimum on-the-way time to a target building. - public const float MinHoursOnTheWay = 0.5f; + /// An assumed maximum travel time to a target building. + public const float MaxTravelTime = 4f; - /// A minimum work shift duration in hours. - public const float MinimumWorkShiftDuration = 2f; + /// An assumed minimum travel to a target building. + public const float MinTravelTime = 0.25f; /// An earliest hour when citizens wake up at home. public const float EarliestWakeUp = 5.5f; diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 366babb6..945123ba 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -107,149 +107,13 @@ protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections c protected uint CitizenInstancesMaxCount { get; } /// - /// Determines whether the current date and time represent the specified time interval on a work day. - /// - /// - /// The hour representing the interval start to check (inclusive). - /// The hour representing the interval end to check (exclusive). - /// - /// true if the current date and time represent the specified time interval on a work day; otherwise, false. - /// - protected bool IsWorkDayAndBetweenHours(float fromInclusive, float toExclusive) - { - float currentHour = TimeInfo.CurrentHour; - return IsWorkDay && (currentHour >= fromInclusive && currentHour < toExclusive); - } - - /// - /// Determines whether the current time represents a morning hour of a work day - /// for a citizen with the provided . - /// - /// - /// The citizen age to check. - /// - /// - /// true if the current time represents a morning hour of a work day - /// for a citizen with the provided age; otherwise, false. - /// - protected bool IsWorkDayMorning(Citizen.AgeGroup citizenAge) - { - if (!IsWorkDay) - { - return false; - } - - float workBeginHour; - switch (citizenAge) - { - case Citizen.AgeGroup.Child: - case Citizen.AgeGroup.Teen: - workBeginHour = Config.SchoolBegin; - break; - - case Citizen.AgeGroup.Young: - case Citizen.AgeGroup.Adult: - workBeginHour = Config.WorkBegin; - break; - - default: - return false; - } - - float currentHour = TimeInfo.CurrentHour; - return currentHour >= TimeInfo.SunriseHour && currentHour <= workBeginHour; - } - - /// - /// Gets the probability whether a citizen with provided age would go out on current time. - /// - /// - /// The citizen age to check. - /// - /// A percentage value in range of 0..100 that describes the probability whether - /// a citizen with provided age would go out on current time. - protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) - { - float currentHour = TimeInfo.CurrentHour; - - uint weekdayModifier; - if (Config.IsWeekendEnabled) - { - weekdayModifier = TimeInfo.Now.IsWeekendTime(GetSpareTimeBeginHour(citizenAge), TimeInfo.SunsetHour) - ? 11u - : 1u; - } - else - { - weekdayModifier = 1u; - } - - bool isDayTime = !TimeInfo.IsNightTime; - float timeModifier; - if (isDayTime) - { - timeModifier = 5f; - } - else - { - float nightDuration = TimeInfo.NightDuration; - float relativeHour = currentHour - TimeInfo.SunsetHour; - if (relativeHour < 0) - { - relativeHour += 24f; - } - - timeModifier = 5f / nightDuration * (nightDuration - relativeHour); - } - - switch (citizenAge) - { - case Citizen.AgeGroup.Child when isDayTime: - case Citizen.AgeGroup.Teen when isDayTime: - case Citizen.AgeGroup.Young: - case Citizen.AgeGroup.Adult: - return (uint)((timeModifier + weekdayModifier) * timeModifier); - - case Citizen.AgeGroup.Senior when isDayTime: - return 80 + weekdayModifier; - - default: - return 0; - } - } - - /// - /// Gets the spare time begin hour for a citizen with provided age. - /// - /// - /// The citizen age to check. - /// - /// A value representing the hour of the day when the citizen's spare time begins. - protected float GetSpareTimeBeginHour(Citizen.AgeGroup citizenAge) - { - switch (citizenAge) - { - case Citizen.AgeGroup.Child: - case Citizen.AgeGroup.Teen: - return Config.SchoolEnd; - - case Citizen.AgeGroup.Young: - case Citizen.AgeGroup.Adult: - return Config.WorkEnd; - - default: - return 0; - } - } - - /// - /// Ensures that the provided citizen is in a valid state and can be processed. + /// Ensures that the specified citizen is in a valid state and can be processed. /// /// /// The citizen ID to check. /// The citizen data reference. /// - /// true if the provided citizen is in a valid state; otherwise, false. + /// true if the specified citizen is in a valid state; otherwise, false. protected bool EnsureCitizenCanBeProcessed(uint citizenId, ref TCitizen citizen) { if ((CitizenProxy.GetHomeBuilding(ref citizen) == 0 @@ -266,7 +130,7 @@ protected bool EnsureCitizenCanBeProcessed(uint citizenId, ref TCitizen citizen) if (CitizenProxy.IsCollapsed(ref citizen)) { - Log.Debug($"{GetCitizenDesc(citizenId, ref citizen, false)} is collapsed, doing nothing..."); + Log.Debug($"{GetCitizenDesc(citizenId, ref citizen)} is collapsed, doing nothing..."); return false; } @@ -274,39 +138,36 @@ protected bool EnsureCitizenCanBeProcessed(uint citizenId, ref TCitizen citizen) } /// - /// Lets the provided citizen try attending the next upcoming event. + /// Searches for an upcoming event and checks whether the specified citizen ca attend it. + /// Returns null if no matching events found. /// /// - /// The citizen ID. + /// The ID of the citizen to check. /// The citizen data reference. - /// The building ID where the upcoming event will take place. /// - /// true if the provided citizen will attend the next event; otherwise, false. - protected bool AttendUpcomingEvent(uint citizenId, ref TCitizen citizen, out ushort eventBuildingId) + /// The city event or null if none found. + protected ICityEvent GetUpcomingEventToAttend(uint citizenId, ref TCitizen citizen) { - eventBuildingId = default; - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); if (EventMgr.GetEventState(currentBuilding, DateTime.MaxValue) == CityEventState.Ongoing) { - return false; + return null; } - DateTime earliestStart = TimeInfo.Now.AddHours(MinHoursOnTheWay); - DateTime latestStart = TimeInfo.Now.AddHours(MaxHoursOnTheWay); + DateTime earliestStart = TimeInfo.Now.AddHours(MinTravelTime); + DateTime latestStart = TimeInfo.Now.AddHours(MaxTravelTime); ICityEvent upcomingEvent = EventMgr.GetUpcomingCityEvent(earliestStart, latestStart); if (upcomingEvent != null && CanAttendEvent(citizenId, ref citizen, upcomingEvent)) { - eventBuildingId = upcomingEvent.BuildingId; - return true; + return upcomingEvent; } - return false; + return null; } /// - /// Finds an evacuation place for the provided citizen. + /// Finds an evacuation place for the specified citizen. /// /// /// The citizen ID to find an evacuation place for. @@ -318,23 +179,21 @@ protected void FindEvacuationPlace(uint citizenId, TransferManager.TransferReaso } /// - /// Gets a string that describes the provided citizen. + /// Gets a string that describes the specified citizen. /// /// /// The citizen ID. /// The citizen data reference. - /// true if the citizen is in a virtual mode; otherwise, false. /// - /// A short string describing the provided citizen. - protected string GetCitizenDesc(uint citizenId, ref TCitizen citizen, bool? isVirtual) + /// A short string describing the specified citizen. + protected string GetCitizenDesc(uint citizenId, ref TCitizen citizen) { ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); string home = homeBuilding == 0 ? "homeless" : "lives at " + homeBuilding; ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); string employment = workBuilding == 0 ? "unemployed" : "works at " + workBuilding; Citizen.Location location = CitizenProxy.GetLocation(ref citizen); - string virt = isVirtual.HasValue ? (isVirtual.Value ? " (virtual)" : " (real)") : null; - return $"Citizen {citizenId} ({CitizenProxy.GetAge(ref citizen)}, {home}, {employment}, currently {location} at {CitizenProxy.GetCurrentBuilding(ref citizen)}) / instance {CitizenProxy.GetInstance(ref citizen)}{virt}"; + return $"Citizen {citizenId} ({CitizenProxy.GetAge(ref citizen)}, {home}, {employment}, currently {location} at {CitizenProxy.GetCurrentBuilding(ref citizen)}) / instance {CitizenProxy.GetInstance(ref citizen)}"; } /// Determines whether the specified citizen must be processed as a virtual citizen. @@ -377,24 +236,31 @@ protected bool IsCitizenVirtual(TAI humanAI, ref TCitizen citizen, FuncDetermines whether the weather is currently so bad that the citizen would like to stay inside a building. - /// The ID of the citizen to check the weather for. /// /// true if the weather is bad; otherwise, false. - protected bool IsBadWeather(uint citizenId) + protected bool IsBadWeather() { if (WeatherInfo.IsDisasterHazardActive) { - Log.Debug($"Citizen {citizenId} is uncomfortable because of a disaster"); return true; } - bool result = WeatherInfo.StayInsideChance != 0 && Random.ShouldOccur(WeatherInfo.StayInsideChance); - if (result) + return WeatherInfo.StayInsideChance != 0 && Random.ShouldOccur(WeatherInfo.StayInsideChance); + } + + /// Gets an estimated travel time (in hours) between two specified buildings. + /// The ID of the first building. + /// The ID of the second building. + /// An estimated travel time in hours. + protected float GetEstimatedTravelTime(ushort building1, ushort building2) + { + if (building1 == 0 || building2 == 0 || building1 == building2) { - Log.Debug($"Citizen {citizenId} is uncomfortable because of bad weather"); + return 0; } - return result; + float distance = BuildingMgr.GetDistanceBetweenBuildings(building1, building2); + return RealTimeMath.Clamp(distance / OnTheWayDistancePerHour, MinTravelTime, MaxTravelTime); } private bool CanAttendEvent(uint citizenId, ref TCitizen citizen, ICityEvent cityEvent) diff --git a/src/RealTime/CustomAI/RealTimePrivateBuildingAI.cs b/src/RealTime/CustomAI/RealTimePrivateBuildingAI.cs index 86a49142..c9d2ad52 100644 --- a/src/RealTime/CustomAI/RealTimePrivateBuildingAI.cs +++ b/src/RealTime/CustomAI/RealTimePrivateBuildingAI.cs @@ -16,6 +16,7 @@ internal sealed class RealTimePrivateBuildingAI { private const int ConstructionSpeedPaused = 10880; private const int ConstructionSpeedMinimum = 1088; + private const int StartFrameMask = 0xFF; private readonly RealTimeConfig config; private readonly ITimeInfo timeInfo; @@ -108,8 +109,14 @@ public void ProcessWorkerProblems(ushort buildingId, byte oldValue, byte newValu /// Notifies this simulation object that a new simulation frame is started. /// The buildings will be processed again from the beginning of the list. - public void StartBuildingProcessingFrame() + /// The simulation frame index to process. + public void ProcessFrame(uint frameIndex) { + if ((frameIndex & StartFrameMask) != 0) + { + return; + } + int currentMinute = timeInfo.Now.Minute; if (lastProcessedMinute != currentMinute) { diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 9fc1412e..74d9c714 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -4,10 +4,21 @@ namespace RealTime.CustomAI { + using System; using RealTime.Tools; + using static Constants; internal sealed partial class RealTimeResidentAI { + private DateTime todayWakeup; + + private enum ScheduleAction + { + Ignore, + ProcessTransition, + ProcessState + } + private void ProcessCitizenDead(TAI instance, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); @@ -15,7 +26,8 @@ private void ProcessCitizenDead(TAI instance, uint citizenId, ref TCitizen citiz if (currentBuilding == 0 || (currentLocation == Citizen.Location.Moving && CitizenProxy.GetVehicle(ref citizen) == 0)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} is released"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is released"); + residentSchedules[citizenId] = default; CitizenMgr.ReleaseCitizen(citizenId); return; } @@ -47,7 +59,7 @@ private void ProcessCitizenDead(TAI instance, uint citizenId, ref TCitizen citiz } residentAI.FindHospital(instance, citizenId, currentBuilding, TransferManager.TransferReason.Dead); - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} is dead, body should get serviced"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is dead, body should get serviced"); } private bool ProcessCitizenArrested(ref TCitizen citizen) @@ -77,7 +89,7 @@ private bool ProcessCitizenSick(TAI instance, uint citizenId, ref TCitizen citiz if (currentLocation != Citizen.Location.Home && currentBuilding == 0) { - Log.Debug($"Teleporting {GetCitizenDesc(citizenId, ref citizen, false)} back home because they are sick but no building is specified"); + Log.Debug($"Teleporting {GetCitizenDesc(citizenId, ref citizen)} back home because they are sick but no building is specified"); CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); return true; } @@ -96,74 +108,97 @@ private bool ProcessCitizenSick(TAI instance, uint citizenId, ref TCitizen citiz } } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} is sick, trying to get to a hospital"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is sick, trying to get to a hospital"); residentAI.FindHospital(instance, citizenId, currentBuilding, TransferManager.TransferReason.Sick); return true; } - private void ProcessCitizenEvacuation(TAI instance, uint citizenId, ref TCitizen citizen) + private void DoScheduledEvacuation(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { ushort building = CitizenProxy.GetCurrentBuilding(ref citizen); - if (building != 0) + if (building == 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} is trying to find an evacuation place"); - residentAI.FindEvacuationPlace(instance, citizenId, building, residentAI.GetEvacuationReason(instance, building)); + schedule.Schedule(ResidentState.AtHome, default); + return; } - } - private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding, bool isVirtual) - { - if (visitBuilding == 0) + schedule.Schedule(ResidentState.InShelter, default); + if (schedule.CurrentState != ResidentState.InShelter) { - return false; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is trying to find an evacuation place"); + residentAI.FindEvacuationPlace(instance, citizenId, building, residentAI.GetEvacuationReason(instance, building)); } + } - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - - if (isVirtual || currentBuilding == visitBuilding) - { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); - CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Visit); - return true; - } - else if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + private bool ProcessCitizenInShelter(ref CitizenSchedule schedule, ref TCitizen citizen) + { + ushort shelter = CitizenProxy.GetVisitBuilding(ref citizen); + if (BuildingMgr.BuildingHasFlags(shelter, Building.Flags.Downgrading)) { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); - CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); + schedule.Schedule(ResidentState.Unknown, default); return true; } return false; } - private ResidentState GetResidentState(ref TCitizen citizen) + private ScheduleAction UpdateCitizenState(uint citizenId, ref TCitizen citizen, ref CitizenSchedule schedule) { + if (schedule.CurrentState == ResidentState.Ignored) + { + return ScheduleAction.Ignore; + } + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.DummyTraffic)) { - return ResidentState.Ignored; + schedule.CurrentState = ResidentState.Ignored; + return ScheduleAction.Ignore; + } + + Citizen.Location location = CitizenProxy.GetLocation(ref citizen); + if (location == Citizen.Location.Moving) + { + if (CitizenMgr.InstanceHasFlags( + CitizenProxy.GetInstance(ref citizen), + CitizenInstance.Flags.OnTour | CitizenInstance.Flags.TargetIsNode, + true)) + { + // Guided tours are treated as visits + schedule.CurrentState = ResidentState.Visiting; + schedule.Hint = ScheduleHint.OnTour; + return ScheduleAction.ProcessState; + } + + return ScheduleAction.ProcessTransition; } ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - ItemClass.Service buildingService = BuildingMgr.GetBuildingService(currentBuilding); + if (currentBuilding == 0) + { + schedule.CurrentState = ResidentState.Unknown; + return ScheduleAction.ProcessState; + } + ItemClass.Service buildingService = BuildingMgr.GetBuildingService(currentBuilding); if (BuildingMgr.BuildingHasFlags(currentBuilding, Building.Flags.Evacuating) && buildingService != ItemClass.Service.Disaster) { - return ResidentState.Evacuating; + schedule.CurrentState = ResidentState.Evacuation; + schedule.Schedule(ResidentState.InShelter, default); + return ScheduleAction.ProcessState; } - switch (CitizenProxy.GetLocation(ref citizen)) + switch (location) { case Citizen.Location.Home: - return currentBuilding != 0 - ? ResidentState.AtHome - : ResidentState.Unknown; + schedule.CurrentState = ResidentState.AtHome; + return ScheduleAction.ProcessState; case Citizen.Location.Work: if (buildingService == ItemClass.Service.Disaster && CitizenProxy.HasFlags(ref citizen, Citizen.Flags.Evacuating)) { - return ResidentState.InShelter; + schedule.CurrentState = ResidentState.InShelter; + return ScheduleAction.ProcessState; } if (CitizenProxy.GetVisitBuilding(ref citizen) == currentBuilding) @@ -172,56 +207,189 @@ private ResidentState GetResidentState(ref TCitizen citizen) goto case Citizen.Location.Visit; } - return currentBuilding != 0 - ? ResidentState.AtSchoolOrWork - : ResidentState.Unknown; + schedule.CurrentState = ResidentState.AtSchoolOrWork; + return ScheduleAction.ProcessState; case Citizen.Location.Visit: - if (currentBuilding == 0) - { - return ResidentState.Unknown; - } - switch (buildingService) { - case ItemClass.Service.Commercial: - if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDay - && TimeInfo.CurrentHour > Config.LunchBegin && TimeInfo.CurrentHour < GetSpareTimeBeginHour(CitizenProxy.GetAge(ref citizen))) - { - return ResidentState.AtLunch; - } - - if (BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) - { - return ResidentState.AtLeisureArea; - } + case ItemClass.Service.Beautification: + case ItemClass.Service.Monument: + case ItemClass.Service.Tourism: + case ItemClass.Service.Commercial + when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure + && schedule.WorkStatus != WorkStatus.Working: - return ResidentState.Shopping; + schedule.CurrentState = ResidentState.Relaxing; + return ScheduleAction.ProcessState; - case ItemClass.Service.Beautification: - return ResidentState.AtLeisureArea; + case ItemClass.Service.Commercial: + schedule.CurrentState = ResidentState.Shopping; + return ScheduleAction.ProcessState; case ItemClass.Service.Disaster: - return ResidentState.InShelter; + schedule.CurrentState = ResidentState.InShelter; + return ScheduleAction.ProcessState; } - return ResidentState.Visiting; + schedule.CurrentState = ResidentState.Visiting; + return ScheduleAction.ProcessState; + } - case Citizen.Location.Moving: - ushort instanceId = CitizenProxy.GetInstance(ref citizen); - if (CitizenMgr.InstanceHasFlags(instanceId, CitizenInstance.Flags.OnTour | CitizenInstance.Flags.TargetIsNode, true)) - { - return ResidentState.OnTour; - } + return ScheduleAction.Ignore; + } - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - return homeBuilding != 0 && CitizenMgr.GetTargetBuilding(instanceId) == homeBuilding - ? ResidentState.MovingHome - : ResidentState.MovingToTarget; + private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) + { + // 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) + { + schedule.WorkBuilding = workBuilding; + workBehavior.UpdateWorkShift(ref schedule, CitizenProxy.GetAge(ref citizen)); + if (schedule.CurrentState == ResidentState.AtSchoolOrWork && schedule.ScheduledStateTime == default) + { + // When enabling for an existing game, the citizens that are working have no schedule yet + schedule.Schedule(ResidentState.Unknown, TimeInfo.Now.FutureHour(schedule.WorkShiftEndHour)); + } + else if (schedule.WorkBuilding == 0 + && (schedule.ScheduledState == ResidentState.AtSchoolOrWork || schedule.WorkStatus == WorkStatus.Working)) + { + // This is for the case when the citizen becomes unemployed while at work + schedule.Schedule(ResidentState.Unknown, default); + } + + Log.Debug($"Updated work shifts for citizen {citizenId}: work shift {schedule.WorkShift}, {schedule.WorkShiftStartHour} - {schedule.WorkShiftEndHour}, weekends: {schedule.WorksOnWeekends}"); + } - default: - return ResidentState.Unknown; + if (schedule.ScheduledState != ResidentState.Unknown) + { + return; } + + Log.Debug(TimeInfo.Now, $"Scheduling for {GetCitizenDesc(citizenId, ref citizen)}..."); + + if (schedule.WorkStatus == WorkStatus.Working) + { + schedule.WorkStatus = WorkStatus.None; + } + + DateTime nextActivityTime = todayWakeup; + if (schedule.CurrentState != ResidentState.AtSchoolOrWork && workBuilding != 0) + { + if (ScheduleWork(ref schedule, ref citizen)) + { + return; + } + + if (schedule.ScheduledStateTime > nextActivityTime) + { + nextActivityTime = schedule.ScheduledStateTime; + } + } + + if (ScheduleShopping(ref schedule, ref citizen, false)) + { + Log.Debug($" - Schedule shopping"); + return; + } + + if (ScheduleRelaxing(ref schedule, citizenId, ref citizen)) + { + Log.Debug($" - Schedule relaxing"); + return; + } + + if (schedule.CurrentState == ResidentState.AtHome) + { + if (Random.ShouldOccur(StayHomeAllDayChance)) + { + nextActivityTime = todayWakeup.FutureHour(Config.WakeupHour); + } + + Log.Debug($" - Schedule sleeping at home until {nextActivityTime}"); + schedule.Schedule(ResidentState.Unknown, nextActivityTime); + } + else + { + Log.Debug($" - Schedule moving home"); + schedule.Schedule(ResidentState.AtHome, default); + } + } + + private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) + { + if (ProcessCurrentState(ref schedule, ref citizen) && schedule.ScheduledState == ResidentState.Unknown) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} will be rescheduled now"); + + // If the state processing changed the schedule, we need to update it + UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); + } + + if (TimeInfo.Now < schedule.ScheduledStateTime) + { + return; + } + + if (schedule.CurrentState == ResidentState.AtHome && IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen)) + { + Log.Debug($" *** Citizen {citizenId} is virtual this time"); + schedule.Schedule(ResidentState.Unknown, default); + return; + } + + switch (schedule.ScheduledState) + { + case ResidentState.AtHome: + DoScheduledHome(ref schedule, instance, citizenId, ref citizen); + break; + + case ResidentState.AtSchoolOrWork: + DoScheduledWork(ref schedule, instance, citizenId, ref citizen); + break; + + case ResidentState.Shopping when schedule.WorkStatus == WorkStatus.Working: + DoScheduledLunch(ref schedule, instance, citizenId, ref citizen); + break; + + case ResidentState.Shopping: + DoScheduledShopping(ref schedule, instance, citizenId, ref citizen); + break; + + case ResidentState.Relaxing: + DoScheduledRelaxing(ref schedule, instance, citizenId, ref citizen); + break; + + case ResidentState.InShelter: + DoScheduledEvacuation(ref schedule, instance, citizenId, ref citizen); + break; + } + } + + private bool ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citizen) + { + switch (schedule.CurrentState) + { + case ResidentState.Shopping: + return ProcessCitizenShopping(ref schedule, ref citizen); + + case ResidentState.Relaxing: + return ProcessCitizenRelaxing(ref schedule, ref citizen); + + case ResidentState.Visiting: + return ProcessCitizenVisit(ref schedule, ref citizen); + + case ResidentState.InShelter: + return ProcessCitizenInShelter(ref schedule, ref citizen); + } + + return false; + } + + private bool ShouldRealizeCitizen(TAI ai) + { + return residentAI.DoRandomMove(ai); } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs index af3ab18f..b3de2cbc 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs @@ -5,82 +5,26 @@ namespace RealTime.CustomAI { using RealTime.Tools; - using static Constants; internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void DoScheduledHome(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - if (CitizenProxy.GetHomeBuilding(ref citizen) == 0) + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + if (homeBuilding == 0) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is in corrupt state: at home with no home building. Releasing the poor citizen."); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: want to go home with no home building. Releasing the poor citizen."); CitizenMgr.ReleaseCitizen(citizenId); + schedule = default; return; } - ushort vehicle = CitizenProxy.GetVehicle(ref citizen); - if (vehicle != 0) - { - Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is at home but vehicle = {vehicle}"); - return; - } - - if (CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual)) - { - return; - } - - if (IsBusyAtHomeInTheMorning(CitizenProxy.GetAge(ref citizen))) - { - return; - } - - if (CitizenGoesShopping(instance, citizenId, ref citizen, isVirtual) || CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) - { - return; - } - - CitizenGoesRelaxing(instance, citizenId, ref citizen, isVirtual); - } - - private bool IsBusyAtHomeInTheMorning(Citizen.AgeGroup citizenAge) - { - float currentHour = TimeInfo.CurrentHour; - float offset = IsWeekend ? 2 : 0; - switch (citizenAge) - { - case Citizen.AgeGroup.Child: - return IsBusyAtHomeInTheMorning(currentHour, 8 + offset); - - case Citizen.AgeGroup.Teen: - case Citizen.AgeGroup.Young: - return IsBusyAtHomeInTheMorning(currentHour, 9 + offset); - - case Citizen.AgeGroup.Adult: - return IsBusyAtHomeInTheMorning(currentHour, 8 + (offset / 2f)); - - case Citizen.AgeGroup.Senior: - return IsBusyAtHomeInTheMorning(currentHour, 7); - - default: - return true; - } - } - - private bool IsBusyAtHomeInTheMorning(float currentHour, float latestHour) - { - if (currentHour >= latestHour || currentHour < EarliestWakeUp) - { - return false; - } - - float sunriseHour = EarliestWakeUp; - float dx = latestHour - sunriseHour; - float x = currentHour - sunriseHour; - - // A cubic probability curve from the earliest wake up hour (0%) to latest hour (100%) - uint chance = (uint)((100f / dx * x) - ((dx - x) * (dx - x) * x)); - return !Random.ShouldOccur(chance); + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, homeBuilding); + schedule.Schedule(ResidentState.Unknown, default); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} back home"); } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index 9737ac99..cdfe4e5e 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -9,7 +9,7 @@ namespace RealTime.CustomAI internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen citizen, bool mayCancel) + private bool ProcessCitizenMoving(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { ushort instanceId = CitizenProxy.GetInstance(ref citizen); ushort vehicleId = CitizenProxy.GetVehicle(ref citizen); @@ -24,56 +24,114 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.MovingIn)) { CitizenMgr.ReleaseCitizen(citizenId); + schedule = default; } else { - // TODO: check whether this makes sense and maybe remove/replace this logic - // Don't know why the original game does this... CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); CitizenProxy.SetArrested(ref citizen, false); + schedule.Schedule(ResidentState.Unknown, default); } - return; + return true; } if (vehicleId == 0 && CitizenMgr.IsAreaEvacuating(instanceId) && !CitizenProxy.HasFlags(ref citizen, Citizen.Flags.Evacuating)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} was on the way, but the area evacuates. Finding an evacuation place."); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} was on the way, but the area evacuates. Finding an evacuation place."); + schedule.Schedule(ResidentState.Unknown, default); TransferMgr.AddOutgoingOfferFromCurrentPosition(citizenId, residentAI.GetEvacuationReason(instance, 0)); - return; + return true; } - bool returnHome = false; ushort targetBuilding = CitizenMgr.GetTargetBuilding(instanceId); - if (targetBuilding != CitizenProxy.GetWorkBuilding(ref citizen)) + if (targetBuilding == CitizenProxy.GetWorkBuilding(ref citizen)) { - ItemClass.Service targetService = BuildingMgr.GetBuildingService(targetBuilding); - if (targetService == ItemClass.Service.Beautification && IsBadWeather(citizenId)) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} cancels the trip to a park due to bad weather"); - returnHome = true; - } + return true; } - if (!returnHome && CitizenMgr.InstanceHasFlags(instanceId, CitizenInstance.Flags.WaitingTransport | CitizenInstance.Flags.WaitingTaxi)) + ItemClass.Service targetService = BuildingMgr.GetBuildingService(targetBuilding); + if (targetService == ItemClass.Service.Beautification && IsBadWeather()) { - if (mayCancel && CitizenMgr.GetInstanceWaitCounter(instanceId) == 255 && Random.ShouldOccur(AbandonTransportWaitChance)) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} goes back home"); - returnHome = true; - } + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} cancels the trip to a park due to bad weather"); + schedule.Schedule(ResidentState.AtHome, default); + return false; + } + + return true; + } + + private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) + { + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (currentBuilding == 0) + { + return 0; + } + + ushort foundBuilding = BuildingMgr.FindActiveBuilding(currentBuilding, distance, ItemClass.Service.Commercial); + if (IsBuildingNoiseRestricted(foundBuilding, currentBuilding)) + { + Log.Debug($"Citizen {citizenId} won't go to the commercial building {foundBuilding}, it has a NIMBY policy"); + return 0; } - if (returnHome) + if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding)) { - ushort home = CitizenProxy.GetHomeBuilding(ref citizen); - if (home == 0) + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + uint homeUnit = BuildingMgr.GetCitizenUnit(homeBuilding); + uint citizenUnit = CitizenProxy.GetContainingUnit(ref citizen, citizenId, homeUnit, CitizenUnit.Flags.Home); + if (citizenUnit != 0) { - return; + CitizenMgr.ModifyUnitGoods(citizenUnit, ShoppingGoodsAmount); } + } + + return foundBuilding; + } - residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); + private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding) + { + ushort leisureBuilding = BuildingMgr.FindActiveBuilding( + currentBuilding, + LeisureSearchDistance, + ItemClass.Service.Commercial, + ItemClass.SubService.CommercialLeisure); + + if (IsBuildingNoiseRestricted(leisureBuilding, currentBuilding)) + { + Log.Debug($"Citizen {citizenId} won't go to the leisure building {leisureBuilding}, it has a NIMBY policy"); + return 0; + } + + StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding); + return leisureBuilding; + } + + private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding) + { + if (visitBuilding == 0) + { + return false; + } + + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + + if (currentBuilding == visitBuilding) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); + 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); + CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); + return true; } + + return false; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 64b4ae60..3f1f962d 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -5,458 +5,101 @@ namespace RealTime.CustomAI { using RealTime.Tools; - using UnityEngine; using static Constants; internal sealed partial class RealTimeResidentAI { - private const int ShiftBitsCount = 5; - private const uint WorkShiftsMask = (1u << ShiftBitsCount) - 1; - - private uint secondShiftQuota; - private uint nightShiftQuota; - private uint secondShiftValue; - private uint nightShiftValue; - - private enum WorkerShift - { - None, - First, - Second, - Night, - Any - } - - private bool IsLunchHour => IsWorkDayAndBetweenHours(Config.LunchBegin, Config.LunchEnd); - - private static bool IsBuildingActiveOnWeekend(ItemClass.Service service, ItemClass.SubService subService) - { - switch (service) - { - case ItemClass.Service.Commercial - when subService != ItemClass.SubService.CommercialHigh && subService != ItemClass.SubService.CommercialEco: - case ItemClass.Service.Tourism: - case ItemClass.Service.Electricity: - case ItemClass.Service.Water: - case ItemClass.Service.Beautification: - case ItemClass.Service.HealthCare: - case ItemClass.Service.PoliceDepartment: - case ItemClass.Service.FireDepartment: - case ItemClass.Service.PublicTransport: - case ItemClass.Service.Disaster: - case ItemClass.Service.Monument: - return true; - - default: - return false; - } - } - - private static int GetBuildingWorkShiftCount(ItemClass.Service service) + private bool ScheduleWork(ref CitizenSchedule schedule, ref TCitizen citizen) { - switch (service) + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (!workBehavior.ScheduleGoToWork(ref schedule, currentBuilding, simulationCycle)) { - case ItemClass.Service.Office: - case ItemClass.Service.Garbage: - case ItemClass.Service.Education: - return 1; - - case ItemClass.Service.Road: - case ItemClass.Service.Beautification: - case ItemClass.Service.Monument: - case ItemClass.Service.Citizen: - return 2; - - case ItemClass.Service.Commercial: - case ItemClass.Service.Industrial: - case ItemClass.Service.Tourism: - case ItemClass.Service.Electricity: - case ItemClass.Service.Water: - case ItemClass.Service.HealthCare: - case ItemClass.Service.PoliceDepartment: - case ItemClass.Service.FireDepartment: - case ItemClass.Service.PublicTransport: - case ItemClass.Service.Disaster: - case ItemClass.Service.Natural: - return 3; - - default: - return 1; + return false; } - } - private static bool ShouldWorkAtDawn(ItemClass.Service service, ItemClass.SubService subService) - { - switch (service) - { - case ItemClass.Service.Commercial when subService == ItemClass.SubService.CommercialLow: - case ItemClass.Service.Beautification: - case ItemClass.Service.Garbage: - case ItemClass.Service.Road: - return true; - - default: - return false; - } - } + Log.Debug($" - Schedule work at {schedule.ScheduledStateTime}"); - private static bool CheckMinimumShiftDuration(float beginHour, float endHour) - { - if (beginHour < endHour) + float timeLeft = (float)(schedule.ScheduledStateTime - TimeInfo.Now).TotalHours; + if (timeLeft <= PrepareToWorkHours) { - return endHour - beginHour >= MinimumWorkShiftDuration; - } - else - { - return 24f - beginHour + endHour >= MinimumWorkShiftDuration; + // Just sit at home if the work time will come soon + Log.Debug($" - Worktime in {timeLeft} hours, preparing for departure"); + return true; } - } - private static bool IsWorkHour(float currentHour, float gotoWorkHour, float leaveWorkHour) - { - if (gotoWorkHour < leaveWorkHour) + if (timeLeft <= MaxTravelTime) { - if (currentHour >= leaveWorkHour || currentHour < gotoWorkHour) + if (schedule.CurrentState != ResidentState.AtHome) { - return false; - } - } - else - { - if (currentHour >= leaveWorkHour && currentHour < gotoWorkHour) - { - return false; + Log.Debug($" - Worktime in {timeLeft} hours, returning home"); + schedule.Schedule(ResidentState.AtHome, default); + return true; } - } - - return true; - } - - private WorkerShift GetWorkerShift(uint citizenId) - { - if (secondShiftQuota != Config.SecondShiftQuota || nightShiftQuota != Config.NightShiftQuota) - { - secondShiftQuota = Config.SecondShiftQuota; - nightShiftQuota = Config.NightShiftQuota; - CalculateWorkShiftValues(); - } - - uint value = citizenId & WorkShiftsMask; - if (value <= secondShiftValue) - { - return WorkerShift.Second; - } - - value = (citizenId >> ShiftBitsCount) & WorkShiftsMask; - if (value <= nightShiftValue) - { - return WorkerShift.Night; - } - return WorkerShift.First; - } - - private void CalculateWorkShiftValues() - { - secondShiftValue = Config.SecondShiftQuota - 1; - nightShiftValue = Config.NightShiftQuota - 1; - } - - private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) - { - ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); - if (workBuilding == 0) - { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is in corrupt state: at school/work with no work building. Teleporting home."); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - return; - } - - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if (ShouldGoToLunch(CitizenProxy.GetAge(ref citizen), citizenId)) - { - ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); - if (lunchPlace != 0) + // If we have some time, try to shop locally. + if (ScheduleShopping(ref schedule, ref citizen, true)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} is going for lunch from {currentBuilding} to {lunchPlace}"); + Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); } else { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); + Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); } - return; - } - - if (!ShouldReturnFromSchoolOrWork(citizenId, currentBuilding, CitizenProxy.GetAge(ref citizen))) - { - return; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} leaves their workplace {workBuilding}"); - - if (CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) - { - return; + return true; } - if (!CitizenGoesShopping(instance, citizenId, ref citizen, isVirtual) && !CitizenGoesRelaxing(instance, citizenId, ref citizen, isVirtual)) - { - if (isVirtual) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - } - else - { - residentAI.StartMoving(instance, citizenId, ref citizen, workBuilding, CitizenProxy.GetHomeBuilding(ref citizen)); - } - } + return false; } - private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + schedule.WorkStatus = WorkStatus.Working; + schedule.DepartureToWorkTime = default; - if (!ShouldMoveToSchoolOrWork(citizenId, workBuilding, currentBuilding, CitizenProxy.GetAge(ref citizen))) - { - return false; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} is going from {currentBuilding} to school/work {workBuilding}"); - - if (isVirtual) + if (currentBuilding == schedule.WorkBuilding && schedule.CurrentState != ResidentState.AtSchoolOrWork) { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); CitizenProxy.SetLocation(ref citizen, Citizen.Location.Work); } - else - { - residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); - } - - return true; - } - - private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort workBuilding, ushort currentBuilding, Citizen.AgeGroup citizenAge) - { - if (workBuilding == 0 || citizenAge == Citizen.AgeGroup.Senior) - { - return false; - } - - ItemClass.Service buildingSevice = BuildingMgr.GetBuildingService(workBuilding); - ItemClass.SubService buildingSubService = BuildingMgr.GetBuildingSubService(workBuilding); - - if (IsWeekend && !IsBuildingActiveOnWeekend(buildingSevice, buildingSubService)) - { - return false; - } - - if ((citizenId & 0x7FF) == TimeInfo.Now.Day) - { - Log.Debug(TimeInfo.Now, $"Citizen {citizenId} has a day off work today"); - return false; - } - - if (citizenAge == Citizen.AgeGroup.Child || citizenAge == Citizen.AgeGroup.Teen) - { - return ShouldMoveToSchoolOrWork(currentBuilding, workBuilding, Config.SchoolBegin, Config.SchoolEnd, 0); - } - - GetWorkShiftTimes(citizenId, buildingSevice, buildingSubService, out float workBeginHour, out float workEndHour); - if (!CheckMinimumShiftDuration(workBeginHour, workEndHour)) - { - return false; - } - - float overtime = Random.ShouldOccur(Config.OnTimeQuota) ? 0 : Config.MaxOvertime * Random.GetRandomValue(100u) / 200f; - - return ShouldMoveToSchoolOrWork(currentBuilding, workBuilding, workBeginHour, workEndHour, overtime); - } - - private bool ShouldMoveToSchoolOrWork(ushort currentBuilding, ushort workBuilding, float workBeginHour, float workEndHour, float overtime) - { - float gotoHour = workBeginHour - overtime - MaxHoursOnTheWay; - if (gotoHour < 0) - { - gotoHour += 24f; - } - - float leaveHour = workEndHour + overtime; - if (leaveHour >= 24f) + else if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, schedule.WorkBuilding) + && schedule.CurrentState == ResidentState.AtHome) { - leaveHour -= 24f; + schedule.DepartureToWorkTime = TimeInfo.Now; } - float currentHour = TimeInfo.CurrentHour; - if (!IsWorkHour(currentHour, gotoHour, leaveHour)) + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + if (workBehavior.ScheduleLunch(ref schedule, citizenAge)) { - return false; - } - - float distance = BuildingMgr.GetDistanceBetweenBuildings(currentBuilding, workBuilding); - float onTheWay = Mathf.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); - - gotoHour = workBeginHour - overtime - onTheWay; - if (gotoHour < 0) - { - gotoHour += 24f; - } - - return IsWorkHour(currentHour, gotoHour, leaveHour); - } - - private bool ShouldReturnFromSchoolOrWork(uint citizenId, ushort buildingId, Citizen.AgeGroup citizenAge) - { - if (citizenAge == Citizen.AgeGroup.Senior) - { - return true; - } - - ItemClass.Service buildingSevice = BuildingMgr.GetBuildingService(buildingId); - ItemClass.SubService buildingSubService = BuildingMgr.GetBuildingSubService(buildingId); - - if (IsWeekend && !IsBuildingActiveOnWeekend(buildingSevice, buildingSubService)) - { - return true; - } - - float currentHour = TimeInfo.CurrentHour; - - if (citizenAge == Citizen.AgeGroup.Child || citizenAge == Citizen.AgeGroup.Teen) - { - return currentHour >= Config.SchoolEnd || currentHour < Config.SchoolBegin - MaxHoursOnTheWay; - } - - GetWorkShiftTimes(citizenId, buildingSevice, buildingSubService, out float workBeginHour, out float workEndHour); - if (!CheckMinimumShiftDuration(workBeginHour, workEndHour)) - { - return true; - } - - float earliestGotoHour = workBeginHour - MaxHoursOnTheWay - Config.MaxOvertime; - if (earliestGotoHour < 0) - { - earliestGotoHour += 24f; - } - - float latestLeaveHour = workEndHour + Config.MaxOvertime; - if (latestLeaveHour >= 24f) - { - latestLeaveHour -= 24f; - } - - if (earliestGotoHour < latestLeaveHour) - { - if (currentHour >= latestLeaveHour || currentHour < earliestGotoHour) - { - return true; - } - else if (currentHour >= workEndHour) - { - return Random.ShouldOccur(Config.OnTimeQuota); - } + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding} and will go to lunch at {schedule.ScheduledStateTime}"); } else { - if (currentHour >= latestLeaveHour && currentHour < earliestGotoHour) - { - return true; - } - else if (currentHour >= workEndHour && currentHour < earliestGotoHour) - { - return Random.ShouldOccur(Config.OnTimeQuota); - } + workBehavior.ScheduleReturnFromWork(ref schedule, citizenAge); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding} and will leave work at {schedule.ScheduledStateTime}"); } - - return false; - } - - private bool ShouldGoToLunch(Citizen.AgeGroup citizenAge, uint citizenId) - { - if (!Config.IsLunchtimeEnabled) - { - return false; - } - - switch (citizenAge) - { - case Citizen.AgeGroup.Child: - case Citizen.AgeGroup.Teen: - case Citizen.AgeGroup.Senior: - return false; - } - - float currentHour = TimeInfo.CurrentHour; - if (!IsBadWeather(citizenId) && currentHour >= Config.LunchBegin && currentHour <= Config.LunchEnd) - { - return Random.ShouldOccur(Config.LunchQuota); - } - - return false; } - private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void DoScheduledLunch(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - if (IsLunchHour) - { - return false; - } - - ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); - if (workBuilding != 0) + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); +#if DEBUG + string citizenDesc = GetCitizenDesc(citizenId, ref citizen); +#else + string citizenDesc = null; +#endif + ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + if (lunchPlace != 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from lunch to {workBuilding}"); - ReturnFromVisit(instance, citizenId, ref citizen, workBuilding, Citizen.Location.Work, isVirtual); + Log.Debug(TimeInfo.Now, $"{citizenDesc} is going for lunch from {currentBuilding} to {lunchPlace}"); + workBehavior.ScheduleReturnFromLunch(ref schedule); } else { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is at lunch but no work building. Teleporting home."); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + Log.Debug(TimeInfo.Now, $"{citizenDesc} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); + workBehavior.ScheduleReturnFromWork(ref schedule, CitizenProxy.GetAge(ref citizen)); } - - return true; - } - - private void GetWorkShiftTimes(uint citizenId, ItemClass.Service sevice, ItemClass.SubService subService, out float beginHour, out float endHour) - { - float begin = -1; - float end = -1; - - int shiftCount = GetBuildingWorkShiftCount(sevice); - if (shiftCount > 1) - { - switch (GetWorkerShift(citizenId)) - { - case WorkerShift.Second: - begin = Config.WorkEnd; - end = 0; - break; - - case WorkerShift.Night when shiftCount == 3: - begin = 0; - end = Config.WorkBegin; - break; - } - } - - if (begin < 0 || end < 0) - { - end = Config.WorkEnd; - - if (ShouldWorkAtDawn(sevice, subService)) - { - begin = Mathf.Min(TimeInfo.SunriseHour, EarliestWakeUp); - } - else - { - begin = Config.WorkBegin; - } - } - - beginHour = begin; - endHour = end; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 21788773..1433647b 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -4,327 +4,250 @@ namespace RealTime.CustomAI { + using System; using RealTime.Events; using RealTime.Tools; using static Constants; internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { - ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - if (currentBuilding == 0) + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(citizenAge)) || IsBadWeather()) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is in corrupt state: visiting with no visit building. Teleporting home."); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - return; + return false; } - switch (citizenState) + ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen); + if (cityEvent != null) { - case ResidentState.AtLunch: - CitizenReturnsFromLunch(instance, citizenId, ref citizen, isVirtual); + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + DateTime departureTime = cityEvent.StartTime.AddHours(-GetEstimatedTravelTime(currentBuilding, cityEvent.BuildingId)); + schedule.Schedule(ResidentState.Relaxing, departureTime); + schedule.EventBuilding = cityEvent.BuildingId; + schedule.Hint = ScheduleHint.AttendingEvent; + return true; + } - return; + schedule.Schedule(ResidentState.Relaxing, default); + schedule.Hint = TimeInfo.IsNightTime + ? ScheduleHint.RelaxAtLeisureBuilding + : ScheduleHint.None; - case ResidentState.AtLeisureArea: - if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) - && BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) - { - // No Citizen.Flags.NeedGoods flag reset here, because we only bought 'beer' or 'champagne' in a leisure building. - BuildingMgr.ModifyMaterialBuffer(CitizenProxy.GetVisitBuilding(ref citizen), TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - } + return true; + } - goto case ResidentState.Visiting; + private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); + switch (schedule.Hint) + { + case ScheduleHint.RelaxAtLeisureBuilding: + schedule.Schedule(ResidentState.Unknown, default); - case ResidentState.Visiting: - if (!CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual)) + ushort leisure = MoveToLeisureBuilding(instance, citizenId, ref citizen, buildingId); + if (leisure == 0) { - CitizenReturnsHomeFromVisit(instance, citizenId, ref citizen, isVirtual); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but didn't found a leisure building"); } - - return; - - case ResidentState.Shopping: - if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)) + else { - BuildingMgr.ModifyMaterialBuffer(CitizenProxy.GetVisitBuilding(ref citizen), TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} heading to a leisure building {leisure}"); } - if (CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual) - || CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) + return; + + case ScheduleHint.AttendingEvent: + DateTime returnTime = default; + ICityEvent cityEvent = EventMgr.GetCityEvent(schedule.EventBuilding); + if (cityEvent == null) { - return; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{schedule.EventBuilding}', but there was no event there"); } - - if (Random.ShouldOccur(ReturnFromShoppingChance) || IsWorkDayMorning(CitizenProxy.GetAge(ref citizen))) + else if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from shopping at {currentBuilding} back home"); - ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen), Citizen.Location.Home, isVirtual); + returnTime = cityEvent.EndTime; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{schedule.EventBuilding}', will return at {returnTime}"); } + schedule.Schedule(ResidentState.Unknown, returnTime); + schedule.EventBuilding = 0; return; } - } - private void ProcessCitizenOnTour(TAI instance, uint citizenId, ref TCitizen citizen) - { - if (!CitizenMgr.InstanceHasFlags(CitizenProxy.GetInstance(ref citizen), CitizenInstance.Flags.TargetIsNode)) - { - return; - } + uint relaxChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + ResidentState nextState = Random.ShouldOccur(relaxChance) + ? ResidentState.Unknown + : ResidentState.Relaxing; - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding != 0) + schedule.Schedule(nextState, default); + + if (schedule.CurrentState != ResidentState.Relaxing) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} exits a guided tour and moves back home."); - residentAI.StartMoving(instance, citizenId, ref citizen, 0, homeBuilding); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} in state {schedule.CurrentState} wanna relax and then schedules {nextState}, heading to an entertainment building."); + residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); } } - private bool CitizenReturnsFromShelter(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool ProcessCitizenRelaxing(ref CitizenSchedule schedule, ref TCitizen citizen) { - ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - if (BuildingMgr.GetBuildingService(visitBuilding) != ItemClass.Service.Disaster) - { - return true; - } - - if (!BuildingMgr.BuildingHasFlags(visitBuilding, Building.Flags.Downgrading)) - { - return false; - } - - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding == 0) + ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) + && BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} was in a shelter but seems to be homeless. Releasing the citizen."); - CitizenMgr.ReleaseCitizen(citizenId); - return true; + // No Citizen.Flags.NeedGoods flag reset here, because we only bought 'beer' or 'champagne' in a leisure building. + BuildingMgr.ModifyMaterialBuffer(currentBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from evacuation place {visitBuilding} back home"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home, isVirtual); - return true; + return RescheduleVisit(ref schedule, ref citizen, currentBuilding); } - private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen, bool localOnly) { - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding == 0 || CitizenProxy.GetVehicle(ref citizen) != 0) + if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather()) { return false; } - ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - switch (EventMgr.GetEventState(visitBuilding, TimeInfo.Now.AddHours(MaxHoursOnTheWay))) - { - case CityEventState.Upcoming: - case CityEventState.Ongoing: - return false; - - case CityEventState.Finished: - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from an event at {visitBuilding} back home to {homeBuilding}"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home, isVirtual); - return true; - } - - ItemClass.SubService visitedSubService = BuildingMgr.GetBuildingSubService(visitBuilding); - if (Random.ShouldOccur(ReturnFromVisitChance) || - (visitedSubService == ItemClass.SubService.CommercialLeisure && TimeInfo.IsNightTime && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen))) + || !Random.ShouldOccur(GoShoppingChance)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from visit back home"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home, isVirtual); - return true; - } - - return false; - } - - private void ReturnFromVisit( - TAI instance, - uint citizenId, - ref TCitizen citizen, - ushort targetBuilding, - Citizen.Location targetLocation, - bool isVirtual) - { - if (targetBuilding == 0 || targetLocation == Citizen.Location.Visit || CitizenProxy.GetVehicle(ref citizen) != 0) - { - return; + return false; } - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - - CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); - CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); - - if (isVirtual || targetBuilding == currentBuilding) + if (TimeInfo.IsNightTime || localOnly || Random.ShouldOccur(Config.LocalBuildingSearchQuota)) { - CitizenProxy.SetLocation(ref citizen, targetLocation); + schedule.Hint = ScheduleHint.LocalShoppingOnly; } else { - residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, targetBuilding); + schedule.Hint = ScheduleHint.None; } + + schedule.Schedule(ResidentState.Shopping, default); + return true; } - private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather(citizenId)) - { - return false; - } + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if (TimeInfo.IsNightTime) + if (schedule.Hint == ScheduleHint.LocalShoppingOnly) { - if (Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) + schedule.Schedule(ResidentState.Unknown, default); + + ushort shop = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + if (shop == 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping at night"); - ushort localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); - Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping at night to a local shop {localVisitPlace}"); - return localVisitPlace > 0; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted go shopping, but didn't find a local shop"); + } + else + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} goes shopping at a local shop {shop}"); } - - return false; } - - if (Random.ShouldOccur(GoShoppingChance)) + else { - bool localOnly = CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(CitizenProxy.GetAge(ref citizen)); - ushort localVisitPlace = 0; + uint moreShoppingChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + ResidentState nextState = Random.ShouldOccur(moreShoppingChance) + ? ResidentState.Unknown + : ResidentState.Shopping; - if (Random.ShouldOccur(Config.LocalBuildingSearchQuota)) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping"); - localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); - Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping to a local shop {localVisitPlace}"); - } + schedule.Schedule(nextState, default); - if (localVisitPlace == 0) + if (schedule.CurrentState != ResidentState.Shopping) { - if (localOnly) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping, but didn't find a local shop"); - return false; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping, heading to a random shop"); - residentAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetHomeBuilding(ref citizen), residentAI.GetShoppingReason(instance)); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} in state {schedule.CurrentState} wanna go shopping and schedules {nextState}, heading to a random shop"); + residentAI.FindVisitPlace(instance, citizenId, currentBuilding, residentAI.GetShoppingReason(instance)); } - - return true; } - - return false; } - private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool ProcessCitizenShopping(ref CitizenSchedule schedule, ref TCitizen citizen) { - if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather(citizenId)) - { - return false; - } - - if (!AttendUpcomingEvent(citizenId, ref citizen, out ushort buildingId)) + ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) && currentBuilding != 0) { - return false; + BuildingMgr.ModifyMaterialBuffer(currentBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna attend an event at '{buildingId}', on the way now."); - return StartMovingToVisitBuilding(instance, citizenId, ref citizen, buildingId, isVirtual); + return RescheduleVisit(ref schedule, ref citizen, currentBuilding); } - private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) { - Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); - if (!Random.ShouldOccur(GetGoOutChance(citizenAge)) || IsBadWeather(citizenId)) + if (schedule.Hint == ScheduleHint.OnTour) { - return false; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a tour (see next line for citizen ID)"); + schedule.Schedule(ResidentState.Unknown, default); + return true; } - ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - if (buildingId == 0) + return RescheduleVisit(ref schedule, ref citizen, CitizenProxy.GetVisitBuilding(ref citizen)); + } + + private bool IsBuildingNoiseRestricted(ushort targetBuilding, ushort currentBuilding) + { + if (BuildingMgr.GetBuildingSubService(targetBuilding) != ItemClass.SubService.CommercialLeisure) { return false; } - if (TimeInfo.IsNightTime) + float currentHour = TimeInfo.CurrentHour; + if (currentHour >= Config.GoToSleepUpHour || currentHour <= Config.WakeupHour) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna relax at night"); - ushort leisure = MoveToLeisure(instance, citizenId, ref citizen, buildingId, isVirtual); - Log.DebugIf(leisure != 0, $"Citizen {citizenId} is heading to leisure building {leisure}"); - return leisure != 0; + return BuildingMgr.IsBuildingNoiseRestricted(targetBuilding); } - if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(citizenAge)) + float travelTime = GetEstimatedTravelTime(currentBuilding, targetBuilding); + if (travelTime == 0) { return false; } - if (!isVirtual) + float arriveHour = (float)TimeInfo.Now.AddHours(travelTime).TimeOfDay.TotalHours; + if (arriveHour >= Config.GoToSleepUpHour || arriveHour <= Config.WakeupHour) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna relax, heading to an entertainment place"); - residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); + return BuildingMgr.IsBuildingNoiseRestricted(targetBuilding); } - return true; + return false; } - private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance, bool isVirtual) + private bool RescheduleVisit(ref CitizenSchedule schedule, ref TCitizen citizen, ushort currentBuilding) { - ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - if (buildingId == 0) + if (schedule.ScheduledState != ResidentState.Relaxing + && schedule.ScheduledState != ResidentState.Shopping + && schedule.ScheduledState != ResidentState.Visiting) { - return 0; + return false; } - ushort foundBuilding = BuildingMgr.FindActiveBuilding(buildingId, distance, ItemClass.Service.Commercial); - if (IsBuildingNoiseRestricted(foundBuilding)) + if (IsBadWeather()) { - Log.Debug($"Citizen {citizenId} won't go to the commercial building {foundBuilding}, it has a NIMBY policy"); - return 0; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of bad weather (see next line for citizen ID)"); + schedule.Schedule(ResidentState.AtHome, default); + return true; } - if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding, isVirtual)) + if (IsBuildingNoiseRestricted(currentBuilding, currentBuilding)) { - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - uint homeUnit = BuildingMgr.GetCitizenUnit(homeBuilding); - uint citizenUnit = CitizenProxy.GetContainingUnit(ref citizen, citizenId, homeUnit, CitizenUnit.Flags.Home); - if (citizenUnit != 0) - { - CitizenMgr.ModifyUnitGoods(citizenUnit, ShoppingGoodsAmount); - } + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of NIMBY policy (see next line for citizen ID)"); + schedule.Schedule(ResidentState.Unknown, default); + return true; } - return foundBuilding; - } - - private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId, bool isVirtual) - { - ushort leisureBuilding = BuildingMgr.FindActiveBuilding( - buildingId, - LeisureSearchDistance, - ItemClass.Service.Commercial, - ItemClass.SubService.CommercialLeisure); - - if (IsBuildingNoiseRestricted(leisureBuilding)) + uint stayChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + if (!Random.ShouldOccur(stayChance)) { - Log.Debug($"Citizen {citizenId} won't go to the leisure building {leisureBuilding}, it has a NIMBY policy"); - return 0; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of time (see next line for citizen ID)"); + schedule.Schedule(ResidentState.AtHome, default); + return true; } - StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding, isVirtual); - return leisureBuilding; - } - - private bool IsBuildingNoiseRestricted(ushort building) - { - float arriveHour = (float)TimeInfo.Now.AddHours(MaxHoursOnTheWay).TimeOfDay.TotalHours; - return (arriveHour >= TimeInfo.SunsetHour || TimeInfo.CurrentHour >= TimeInfo.SunsetHour - || arriveHour <= TimeInfo.SunriseHour || TimeInfo.CurrentHour <= TimeInfo.SunriseHour) - && BuildingMgr.IsBuildingNoiseRestricted(building); + return false; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index dd60f12f..f8c5e015 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -3,7 +3,9 @@ namespace RealTime.CustomAI { using System; + using System.IO; using RealTime.Config; + using RealTime.Core; using RealTime.Events; using RealTime.GameConnection; using RealTime.Tools; @@ -17,6 +19,10 @@ internal sealed partial class RealTimeResidentAI : RealTimeHumanA where TCitizen : struct { private readonly ResidentAIConnection residentAI; + private readonly WorkBehavior workBehavior; + private readonly SpareTimeBehavior spareTimeBehavior; + private readonly CitizenSchedule[] residentSchedules; + private float simulationCycle; /// Initializes a new instance of the class. /// Thrown when any argument is null. @@ -24,14 +30,19 @@ internal sealed partial class RealTimeResidentAI : RealTimeHumanA /// A instance that provides the game connection implementation. /// A connection to the game's resident AI. /// A instance. + /// A behavior that provides simulation info for the citizens spare time. public RealTimeResidentAI( RealTimeConfig config, GameConnections connections, ResidentAIConnection residentAI, - RealTimeEventManager eventManager) + RealTimeEventManager eventManager, + SpareTimeBehavior spareTimeBehavior) : base(config, connections, eventManager) { this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); + this.spareTimeBehavior = spareTimeBehavior ?? throw new ArgumentNullException(nameof(spareTimeBehavior)); + residentSchedules = new CitizenSchedule[CitizenMgr.GetMaxCitizensCount()]; + workBehavior = new WorkBehavior(config, connections.Random, connections.BuildingManager, connections.TimeInfo, GetEstimatedTravelTime); } /// The entry method of the custom AI. @@ -42,79 +53,107 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) { if (!EnsureCitizenCanBeProcessed(citizenId, ref citizen)) { + residentSchedules[citizenId] = default; return; } + ref CitizenSchedule schedule = ref residentSchedules[citizenId]; if (CitizenProxy.IsDead(ref citizen)) { ProcessCitizenDead(instance, citizenId, ref citizen); + schedule.Schedule(ResidentState.Unknown, default); return; } if ((CitizenProxy.IsSick(ref citizen) && ProcessCitizenSick(instance, citizenId, ref citizen)) || (CitizenProxy.IsArrested(ref citizen) && ProcessCitizenArrested(ref citizen))) { + schedule.Schedule(ResidentState.Unknown, default); return; } - ResidentState residentState = GetResidentState(ref citizen); - bool isVirtual; - - switch (residentState) + ScheduleAction actionType = UpdateCitizenState(citizenId, ref citizen, ref schedule); + switch (actionType) { - case ResidentState.MovingHome: - ProcessCitizenMoving(instance, citizenId, ref citizen, false); - break; + case ScheduleAction.Ignore: + return; - case ResidentState.AtHome: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - ProcessCitizenAtHome(instance, citizenId, ref citizen, isVirtual); - break; + case ScheduleAction.ProcessTransition when ProcessCitizenMoving(ref schedule, instance, citizenId, ref citizen): + return; + } - case ResidentState.MovingToTarget: - ProcessCitizenMoving(instance, citizenId, ref citizen, true); - break; + if (schedule.CurrentState == ResidentState.Unknown) + { + Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in an UNKNOWN state! Changing to 'moving'"); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Moving); + return; + } - case ResidentState.AtSchoolOrWork: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - ProcessCitizenAtSchoolOrWork(instance, citizenId, ref citizen, isVirtual); - break; + if (TimeInfo.Now < schedule.ScheduledStateTime) + { + return; + } - case ResidentState.AtLunch: - case ResidentState.Shopping: - case ResidentState.AtLeisureArea: - case ResidentState.Visiting: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - ProcessCitizenVisit(instance, residentState, citizenId, ref citizen, isVirtual); - break; + UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); + ExecuteCitizenSchedule(ref schedule, instance, citizenId, ref citizen); + } - case ResidentState.OnTour: - ProcessCitizenOnTour(instance, citizenId, ref citizen); - break; + /// Notifies that a citizen has arrived their destination. + /// The citizen ID to process. + public void RegisterCitizenArrival(uint citizenId) + { + if (citizenId == 0 || citizenId >= residentSchedules.Length) + { + return; + } - case ResidentState.Evacuating: - ProcessCitizenEvacuation(instance, citizenId, ref citizen); + ref CitizenSchedule schedule = ref residentSchedules[citizenId]; + switch (CitizenMgr.GetCitizenLocation(citizenId)) + { + case Citizen.Location.Work: + schedule.UpdateTravelTimeToWork(TimeInfo.Now); + Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {schedule.TravelTimeToWork} hours to get to work"); break; - case ResidentState.InShelter: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - CitizenReturnsFromShelter(instance, citizenId, ref citizen, isVirtual); - break; + case Citizen.Location.Moving: + return; + } - case ResidentState.Unknown: - Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen, null)} is in an UNKNOWN state! Teleporting back home"); - if (CitizenProxy.GetHomeBuilding(ref citizen) != 0) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - } + schedule.DepartureToWorkTime = default; + } - break; + /// Performs simulation for starting a new day for all citizens. + public void BeginNewDay() + { + workBehavior.UpdateLunchTime(); + todayWakeup = TimeInfo.Now.Date.AddHours(Config.WakeupHour); + } + + /// Performs simulation for starting a new day for a citizen with specified ID. + /// The citizen ID to process. + public void BeginNewDayForCitizen(uint citizenId) + { + // TODO: use this method + if (citizenId == 0) + { + return; } } - private bool ShouldRealizeCitizen(TAI ai) + /// Sets the duration (in hours) of a full simulation cycle for all citizens. + /// The game calls the simulation methods for a particular citizen with this period. + /// The citizens simulation cycle period, in game hours. + public void SetSimulationCyclePeriod(float cyclePeriod) + { + simulationCycle = cyclePeriod; + Log.Debug($"SIMULATION CYCLE PERIOD: {cyclePeriod} hours"); + } + + /// Gets an instance of the storage service that can read and write the custom schedule data. + /// An object that implements the interface. + public IStorageData GetStorageService() { - return residentAI.DoRandomMove(ai); + return new CitizenScheduleStorage(residentSchedules, CitizenMgr.GetCitizensArray(), TimeInfo); } } } \ No newline at end of file diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index 0a792a7f..36aa72ad 100644 --- a/src/RealTime/CustomAI/RealTimeTouristAI.cs +++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs @@ -17,11 +17,13 @@ namespace RealTime.CustomAI /// The type of the tourist AI. /// The type of the citizen objects. /// + // TODO: tourist AI should be unified with resident AI where possible internal sealed class RealTimeTouristAI : RealTimeHumanAIBase where TAI : class where TCitizen : struct { private readonly TouristAIConnection touristAI; + private readonly SpareTimeBehavior spareTimeBehavior; /// /// Initializes a new instance of the class. @@ -31,16 +33,19 @@ internal sealed class RealTimeTouristAI : RealTimeHumanAIBaseA instance that provides the game connection implementation. /// A connection to game's tourist AI. /// The custom event manager. + /// A behavior that provides simulation info for the citizens spare time. /// /// Thrown when any argument is null. public RealTimeTouristAI( RealTimeConfig config, GameConnections connections, TouristAIConnection touristAI, - RealTimeEventManager eventManager) + RealTimeEventManager eventManager, + SpareTimeBehavior spareTimeBehavior) : base(config, connections, eventManager) { this.touristAI = touristAI ?? throw new ArgumentNullException(nameof(touristAI)); + this.spareTimeBehavior = spareTimeBehavior ?? throw new ArgumentNullException(nameof(spareTimeBehavior)); } /// @@ -71,7 +76,7 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) break; case Citizen.Location.Visit: - ProcessVisit(instance, citizenId, ref citizen, IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen)); + ProcessVisit(instance, citizenId, ref citizen); break; case Citizen.Location.Moving: @@ -97,28 +102,28 @@ 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(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, false)} was on the way, but the area evacuates. Leaving the city."); + Log.Debug(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)); return; } - bool badWeather = IsBadWeather(citizenId); + bool badWeather = IsBadWeather(); if (CitizenMgr.InstanceHasFlags(instanceId, CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour, true)) { - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, false)} exits the guided tour."); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} exits the guided tour."); if (!badWeather) { - FindRandomVisitPlace(instance, citizenId, ref citizen, TouristDoNothingProbability, 0, false); + FindRandomVisitPlace(instance, citizenId, ref citizen, TouristDoNothingProbability, 0); } } if (badWeather) { - FindHotel(instance, citizenId, ref citizen, false); + FindHotel(instance, citizenId, ref citizen); } } - private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen) { ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (visitBuilding == 0) @@ -138,7 +143,7 @@ private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen, bo case ItemClass.Service.Disaster: if (BuildingMgr.BuildingHasFlags(visitBuilding, Building.Flags.Downgrading)) { - FindRandomVisitPlace(instance, citizenId, ref citizen, 0, visitBuilding, false); + FindRandomVisitPlace(instance, citizenId, ref citizen, 0, visitBuilding); } return; @@ -149,13 +154,15 @@ private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen, bo return; } - if (Random.ShouldOccur(TouristEventChance) - && !IsBadWeather(citizenId) - && AttendUpcomingEvent(citizenId, ref citizen, out ushort eventBuilding)) + if (Random.ShouldOccur(TouristEventChance) && !IsBadWeather()) { - StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), eventBuilding, isVirtual); - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} attending an event at {eventBuilding}"); - return; + ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen); + if (cityEvent != null) + { + StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), cityEvent.BuildingId); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} attending an event at {cityEvent.BuildingId}"); + return; + } } int doNothingChance; @@ -178,34 +185,22 @@ private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen, bo break; } - FindRandomVisitPlace(instance, citizenId, ref citizen, doNothingChance, visitBuilding, isVirtual); + FindRandomVisitPlace(instance, citizenId, ref citizen, doNothingChance, visitBuilding); } - private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen citizen, int doNothingProbability, ushort currentBuilding, bool isVirtual) + private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen citizen, int doNothingProbability, ushort currentBuilding) { int targetType = touristAI.GetRandomTargetType(instance, doNothingProbability); if (targetType == 1) { - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} decides to leave the city"); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} decides to leave the city"); touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); return; } - if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather(citizenId)) - { - FindHotel(instance, citizenId, ref citizen, isVirtual); - return; - } - - if (isVirtual) + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather()) { - if (Random.ShouldOccur(TouristShoppingChance) && BuildingMgr.GetBuildingService(currentBuilding) == ItemClass.Service.Commercial) - { - BuildingMgr.ModifyMaterialBuffer(currentBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - } - - touristAI.AddTouristVisit(instance, citizenId, currentBuilding); - + FindHotel(instance, citizenId, ref citizen); return; } @@ -213,22 +208,22 @@ private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen cit { case 2: touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetShoppingReason(instance)); - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} stays in the city, goes shopping"); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in the city, goes shopping"); break; case 3: - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} stays in the city, goes relaxing"); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in the city, goes relaxing"); touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetEntertainmentReason(instance)); break; } } - private void FindHotel(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void FindHotel(TAI instance, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); if (!Random.ShouldOccur(FindHotelChance)) { - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} didn't want to stay in a hotel, leaving the city"); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} didn't want to stay in a hotel, leaving the city"); touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); return; } @@ -241,34 +236,20 @@ private void FindHotel(TAI instance, uint citizenId, ref TCitizen citizen, bool if (hotel == 0) { - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} didn't find a hotel, leaving the city"); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} didn't find a hotel, leaving the city"); touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); return; } - StartMovingToVisitBuilding(instance, citizenId, ref citizen, currentBuilding, hotel, isVirtual); - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen, isVirtual)} stays in a hotel {hotel}"); + StartMovingToVisitBuilding(instance, citizenId, ref citizen, currentBuilding, hotel); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in a hotel {hotel}"); } - private void StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding, ushort visitBuilding, bool isVirtual) + private void StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding, ushort visitBuilding) { CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); - - if (isVirtual) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Visit); - touristAI.AddTouristVisit(instance, citizenId, visitBuilding); - } - else - { - touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding); - } - } - - private bool ShouldRealizeCitizen(TAI ai) - { - return touristAI.DoRandomMove(ai); + touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding); } } } diff --git a/src/RealTime/CustomAI/ResidentState.cs b/src/RealTime/CustomAI/ResidentState.cs index dee3cc68..3613a135 100644 --- a/src/RealTime/CustomAI/ResidentState.cs +++ b/src/RealTime/CustomAI/ResidentState.cs @@ -2,44 +2,34 @@ namespace RealTime.CustomAI { - /// Possible citizen's states. - internal enum ResidentState + /// + /// Possible citizen's target states. While moving to the target building, citizen will already have the target state. + /// + internal enum ResidentState : byte { - /// The state could not be determined. + /// The state is not defined. A good time to make a decision. Unknown, - /// The citizen should be ignored, just a dummy traffic. + /// The citizen should be ignored, just dummy traffic. Ignored, - /// The citizen is moving to the home building. - MovingHome, - /// The citizen is in the home building. AtHome, - /// The citizen is moving to a target that is not their home building. - MovingToTarget, - /// The citizen is in the school or work building. AtSchoolOrWork, - /// The citizen has lunch time. - AtLunch, - - /// The citizen is shopping in a commercial building. + /// The citizen is shopping or having lunch time in a commercial building. Shopping, - /// The citizen is in a commercial leisure building or in a beautification building. - AtLeisureArea, + /// The citizen is in a leisure building or in a beautification building. + Relaxing, /// The citizen visits a building. Visiting, - /// The citizen is on a guided tour. - OnTour, - - /// The citizen is evacuating. - Evacuating, + /// The citizen has to evacuate the current building (or area). + Evacuation, /// The citizen is in a shelter building. InShelter diff --git a/src/RealTime/CustomAI/ScheduleHint.cs b/src/RealTime/CustomAI/ScheduleHint.cs new file mode 100644 index 00000000..15aea66b --- /dev/null +++ b/src/RealTime/CustomAI/ScheduleHint.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + /// Describes various citizen schedule hints. + internal enum ScheduleHint : byte + { + /// No hint. + None, + + /// The citizen can shop only locally. + LocalShoppingOnly, + + /// The citizen should find a leisure building. + RelaxAtLeisureBuilding, + + /// The citizen is on a guided tour. + OnTour, + + /// The citizen is attending an event. + AttendingEvent + } +} \ No newline at end of file diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs new file mode 100644 index 00000000..f79ba8c4 --- /dev/null +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -0,0 +1,106 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Config; + using RealTime.Simulation; + using RealTime.Tools; + + /// + /// A class that provides custom logic for the spare time simulation. + /// + internal sealed class SpareTimeBehavior + { + private readonly RealTimeConfig config; + private readonly ITimeInfo timeInfo; + private readonly uint[] chances; + private float simulationCycle; + + /// Initializes a new instance of the class. + /// The configuration to run with. + /// The object providing the game time information. + /// Thrown when any argument is null. + public SpareTimeBehavior(RealTimeConfig config, ITimeInfo timeInfo) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + chances = new uint[Enum.GetValues(typeof(Citizen.AgeGroup)).Length]; + } + + /// Sets the duration (in hours) of a full simulation cycle for all citizens. + /// The game calls the simulation methods for a particular citizen with this period. + /// The citizens simulation cycle period, in game hours. + public void SetSimulationCyclePeriod(float cyclePeriod) + { + simulationCycle = cyclePeriod; + } + + /// Calculates the chances for the citizens to go out based on the current game time. + public void RefreshGoOutChances() + { + uint weekdayModifier; + if (config.IsWeekendEnabled) + { + weekdayModifier = timeInfo.Now.IsWeekendTime(12f, config.GoToSleepUpHour) + ? 11u + : 1u; + } + else + { + weekdayModifier = 1u; + } + + float currentHour = timeInfo.CurrentHour; + + float latestGoOutHour = config.GoToSleepUpHour - simulationCycle; + bool isDayTime = currentHour >= config.WakeupHour && currentHour < latestGoOutHour; + float timeModifier; + if (isDayTime) + { + timeModifier = RealTimeMath.Clamp(currentHour - config.WakeupHour, 0, 4f); + } + else + { + float nightDuration = 24f - (latestGoOutHour - config.WakeupHour); + float relativeHour = currentHour - latestGoOutHour; + if (relativeHour < 0) + { + relativeHour += 24f; + } + + timeModifier = 3f / nightDuration * (nightDuration - relativeHour); + } + + uint defaultChance = (uint)((timeModifier + weekdayModifier) * timeModifier); + + bool dump = chances[(int)Citizen.AgeGroup.Young] != defaultChance; + + chances[(int)Citizen.AgeGroup.Child] = isDayTime ? defaultChance : 0; + chances[(int)Citizen.AgeGroup.Teen] = isDayTime ? defaultChance : 0; + chances[(int)Citizen.AgeGroup.Young] = defaultChance; + chances[(int)Citizen.AgeGroup.Adult] = defaultChance; + chances[(int)Citizen.AgeGroup.Senior] = isDayTime ? defaultChance : 0; + + if (dump) + { + Log.Debug($"GO OUT CHANCES for {timeInfo.Now}: child = {chances[0]}, teen = {chances[1]}, young = {chances[2]}, adult = {chances[3]}, senior = {chances[4]}"); + } + } + + /// + /// Gets the probability whether a citizen with specified age would go out on current time. + /// + /// + /// The citizen age to check. + /// + /// A percentage value in range of 0..100 that describes the probability whether + /// a citizen with specified age would go out on current time. + public uint GetGoOutChance(Citizen.AgeGroup citizenAge) + { + return chances[(int)citizenAge]; + } + } +} diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs new file mode 100644 index 00000000..8a2e5797 --- /dev/null +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -0,0 +1,345 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Config; + using RealTime.GameConnection; + using RealTime.Simulation; + using RealTime.Tools; + using static Constants; + + /// + /// A class containing methods for managing the citizens' work behavior. + /// + internal sealed class WorkBehavior + { + private readonly RealTimeConfig config; + private readonly IRandomizer randomizer; + private readonly IBuildingManagerConnection buildingManager; + private readonly ITimeInfo timeInfo; + private readonly Func travelTimeCalculator; + + private DateTime lunchBegin; + private DateTime lunchEnd; + + /// Initializes a new instance of the class. + /// The configuration to run with. + /// The randomizer implementation. + /// The building manager implementation. + /// The time information source. + /// A method accepting two building IDs and returning the estimated travel time + /// between those buildings (in hours). + /// Thrown when any argument is null. + public WorkBehavior( + RealTimeConfig config, + IRandomizer randomizer, + IBuildingManagerConnection buildingManager, + ITimeInfo timeInfo, + Func travelTimeCalculator) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + this.randomizer = randomizer ?? throw new ArgumentNullException(nameof(randomizer)); + this.buildingManager = buildingManager ?? throw new ArgumentNullException(nameof(buildingManager)); + this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + this.travelTimeCalculator = travelTimeCalculator ?? throw new ArgumentNullException(nameof(travelTimeCalculator)); + } + + /// Updates the lunch time according to current date and configuration. + public void UpdateLunchTime() + { + DateTime today = timeInfo.Now.Date; + lunchBegin = today.AddHours(config.LunchBegin); + lunchEnd = today.AddHours(config.LunchEnd); + } + + /// Updates the citizen's work shift parameters in the specified citizen's . + /// The citizen's schedule to update the work shift in. + /// The age of the citizen. + public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citizenAge) + { + if (schedule.WorkBuilding == 0 || citizenAge == Citizen.AgeGroup.Senior) + { + schedule.UpdateWorkShift(WorkShift.Unemployed, 0, 0, false); + return; + } + + ItemClass.Service buildingSevice = buildingManager.GetBuildingService(schedule.WorkBuilding); + ItemClass.SubService buildingSubService = buildingManager.GetBuildingSubService(schedule.WorkBuilding); + + float workBegin, workEnd; + WorkShift workShift = schedule.WorkShift; + + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + workShift = WorkShift.First; + workBegin = config.SchoolBegin; + workEnd = config.SchoolEnd; + break; + + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + if (workShift == WorkShift.Unemployed) + { + workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice, buildingSubService)); + } + + workBegin = config.WorkBegin; + workEnd = config.WorkEnd; + break; + + default: + return; + } + + switch (workShift) + { + case WorkShift.First when HasExtendedFirstWorkShift(buildingSevice, buildingSubService): + workBegin = Math.Min(config.WakeupHour, EarliestWakeUp); + break; + + case WorkShift.Second: + workBegin = workEnd; + workEnd = 0; + break; + + case WorkShift.Night: + workEnd = workBegin; + workBegin = 0; + break; + } + + schedule.UpdateWorkShift(workShift, workBegin, workEnd, IsBuildingActiveOnWeekend(buildingSevice, buildingSubService)); + } + + /// Updates the citizen's work schedule by determining the time for going to work. + /// The citizen's schedule to update. + /// The ID of the building where the citizen is currently located. + /// The duration (in hours) of a full citizens simulation cycle. + /// true if work was scheduled; otherwise, false. + public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuilding, float simulationCycle) + { + if (schedule.CurrentState == ResidentState.AtSchoolOrWork) + { + return false; + } + + DateTime now = timeInfo.Now; + if (config.IsWeekendEnabled && now.IsWeekend() && !schedule.WorksOnWeekends) + { + return false; + } + + float travelTime = GetTravelTimeToWork(ref schedule, currentBuilding); + + DateTime workEndTime = now.FutureHour(schedule.WorkShiftEndHour); + DateTime departureTime = now.FutureHour(schedule.WorkShiftStartHour - travelTime - simulationCycle); + if (departureTime > workEndTime && now.AddHours(travelTime + simulationCycle) < workEndTime) + { + departureTime = now; + } + + schedule.Schedule(ResidentState.AtSchoolOrWork, departureTime); + return true; + } + + /// Updates the citizen's work schedule by determining the lunch time. + /// The citizen's schedule to update. + /// The citizen's age. + /// true if a lunch time was scheduled; otherwise, false. + public bool ScheduleLunch(ref CitizenSchedule schedule, Citizen.AgeGroup citizenAge) + { + if (timeInfo.Now < lunchBegin + && schedule.WorkStatus == WorkStatus.Working + && schedule.WorkShift == WorkShift.First + && WillGoToLunch(citizenAge)) + { + schedule.Schedule(ResidentState.Shopping, lunchBegin); + return true; + } + + return false; + } + + /// Updates the citizen's work schedule by determining the returning from lunch time. + /// The citizen's schedule to update. + public void ScheduleReturnFromLunch(ref CitizenSchedule schedule) + { + if (schedule.WorkStatus == WorkStatus.Working) + { + schedule.Schedule(ResidentState.AtSchoolOrWork, lunchEnd); + } + } + + /// Updates the citizen's work schedule by determining the time for returning from work. + /// The citizen's schedule to update. + /// The age of the citizen. + public void ScheduleReturnFromWork(ref CitizenSchedule schedule, Citizen.AgeGroup citizenAge) + { + if (schedule.WorkStatus != WorkStatus.Working) + { + return; + } + + float departureHour = schedule.WorkShiftEndHour + GetOvertime(citizenAge); + schedule.Schedule(ResidentState.Unknown, timeInfo.Now.FutureHour(departureHour)); + } + + private static bool IsBuildingActiveOnWeekend(ItemClass.Service service, ItemClass.SubService subService) + { + switch (service) + { + case ItemClass.Service.Commercial + when subService != ItemClass.SubService.CommercialHigh && subService != ItemClass.SubService.CommercialEco: + case ItemClass.Service.Industrial when subService != ItemClass.SubService.IndustrialGeneric: + case ItemClass.Service.Tourism: + case ItemClass.Service.Electricity: + case ItemClass.Service.Water: + case ItemClass.Service.Beautification: + case ItemClass.Service.HealthCare: + case ItemClass.Service.PoliceDepartment: + case ItemClass.Service.FireDepartment: + case ItemClass.Service.PublicTransport: + case ItemClass.Service.Disaster: + case ItemClass.Service.Monument: + return true; + + default: + return false; + } + } + + private static int GetBuildingWorkShiftCount(ItemClass.Service service, ItemClass.SubService subService) + { + switch (service) + { + case ItemClass.Service.Office: + case ItemClass.Service.Garbage: + case ItemClass.Service.Education: + case ItemClass.Service.Industrial + when subService == ItemClass.SubService.IndustrialForestry || subService == ItemClass.SubService.IndustrialFarming: + return 1; + + case ItemClass.Service.Road: + case ItemClass.Service.Beautification: + case ItemClass.Service.Monument: + case ItemClass.Service.Citizen: + return 2; + + case ItemClass.Service.Commercial: + case ItemClass.Service.Industrial: + case ItemClass.Service.Tourism: + case ItemClass.Service.Electricity: + case ItemClass.Service.Water: + case ItemClass.Service.HealthCare: + case ItemClass.Service.PoliceDepartment: + case ItemClass.Service.FireDepartment: + case ItemClass.Service.PublicTransport: + case ItemClass.Service.Disaster: + case ItemClass.Service.Natural: + return 3; + + default: + return 1; + } + } + + private static bool HasExtendedFirstWorkShift(ItemClass.Service service, ItemClass.SubService subService) + { + switch (service) + { + case ItemClass.Service.Commercial when subService == ItemClass.SubService.CommercialLow: + case ItemClass.Service.Beautification: + case ItemClass.Service.Garbage: + case ItemClass.Service.Road: + case ItemClass.Service.Industrial + when subService == ItemClass.SubService.IndustrialFarming || subService == ItemClass.SubService.IndustrialForestry: + return true; + + default: + return false; + } + } + + private WorkShift GetWorkShift(int workShiftCount) + { + switch (workShiftCount) + { + case 1: + return WorkShift.First; + + case 2: + return randomizer.ShouldOccur(config.SecondShiftQuota) + ? WorkShift.Second + : WorkShift.First; + + case 3: + int random = randomizer.GetRandomValue(100u); + if (random < config.NightShiftQuota) + { + return WorkShift.Night; + } + else if (random < config.SecondShiftQuota + config.NightShiftQuota) + { + return WorkShift.Second; + } + + return WorkShift.First; + + default: + return WorkShift.Unemployed; + } + } + + private float GetTravelTimeToWork(ref CitizenSchedule schedule, ushort buildingId) + { + float result = schedule.CurrentState == ResidentState.AtHome + ? schedule.TravelTimeToWork + : 0; + + if (result <= 0) + { + result = travelTimeCalculator(buildingId, schedule.WorkBuilding); + } + + return result; + } + + private bool WillGoToLunch(Citizen.AgeGroup citizenAge) + { + if (!config.IsLunchtimeEnabled) + { + return false; + } + + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + case Citizen.AgeGroup.Senior: + return false; + } + + return randomizer.ShouldOccur(config.LunchQuota); + } + + private float GetOvertime(Citizen.AgeGroup citizenAge) + { + switch (citizenAge) + { + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + return randomizer.ShouldOccur(config.OnTimeQuota) + ? 0 + : config.MaxOvertime * randomizer.GetRandomValue(100u) / 100f; + + default: + return 0; + } + } + } +} diff --git a/src/RealTime/CustomAI/WorkShift.cs b/src/RealTime/CustomAI/WorkShift.cs new file mode 100644 index 00000000..c22795fd --- /dev/null +++ b/src/RealTime/CustomAI/WorkShift.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + /// + /// An enumeration that describes the citizen's work shift. + /// + internal enum WorkShift : byte + { + /// The citizen will not go to work or school. + Unemployed, + + /// The citizen will not work first (or default) shift. + First, + + /// The citizen will not work second shift. + Second, + + /// The citizen will not work night shift. + Night + } +} diff --git a/src/RealTime/CustomAI/WorkStatus.cs b/src/RealTime/CustomAI/WorkStatus.cs new file mode 100644 index 00000000..5c0ae73a --- /dev/null +++ b/src/RealTime/CustomAI/WorkStatus.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + /// + /// Describes the work (or school/university) status of a citizen. + /// + internal enum WorkStatus : byte + { + /// No special handling. + None, + + /// The citizen has working hours. + Working + } +} diff --git a/src/RealTime/Events/CityEventBase.cs b/src/RealTime/Events/CityEventBase.cs index d2adec38..6f5f746d 100644 --- a/src/RealTime/Events/CityEventBase.cs +++ b/src/RealTime/Events/CityEventBase.cs @@ -23,7 +23,7 @@ internal abstract class CityEventBase : ICityEvent /// Gets the localized name of the building this city event takes place in. public string BuildingName { get; private set; } - /// Accepts an event attendee with provided properties. + /// Accepts an event attendee with specified properties. /// The attendee age. /// The attendee gender. /// The attendee education. @@ -32,7 +32,7 @@ internal abstract class CityEventBase : ICityEvent /// The attendee happiness. /// A reference to the game's randomizer. /// - /// true if the event attendee with provided properties is accepted and can attend + /// true if the event attendee with specified properties is accepted and can attend /// this city event; otherwise, false. /// public virtual bool TryAcceptAttendee( @@ -48,7 +48,7 @@ public virtual bool TryAcceptAttendee( } /// - /// Configures this event to take place in the provided building and at the provided start time. + /// Configures this event to take place in the specified building and at the specified start time. /// /// The building ID this city event should take place in. /// diff --git a/src/RealTime/Events/ICityEvent.cs b/src/RealTime/Events/ICityEvent.cs index 249683f0..09537b55 100644 --- a/src/RealTime/Events/ICityEvent.cs +++ b/src/RealTime/Events/ICityEvent.cs @@ -23,7 +23,7 @@ internal interface ICityEvent string BuildingName { get; } /// - /// Configures this event to take place in the provided building and at the provided start time. + /// Configures this event to take place in the specified building and at the specified start time. /// /// /// /// The building ID this city event should take place in. @@ -33,7 +33,7 @@ internal interface ICityEvent /// The city event start time. void Configure(ushort buildingId, string buildingName, DateTime startTime); - /// Accepts an event attendee with provided properties. + /// Accepts an event attendee with specified properties. /// The attendee age. /// The attendee gender. /// The attendee education. @@ -42,7 +42,7 @@ internal interface ICityEvent /// The attendee happiness. /// A reference to the game's randomizer. /// - /// true if the event attendee with provided properties is accepted and can attend + /// true if the event attendee with specified properties is accepted and can attend /// this city event; otherwise, false. /// bool TryAcceptAttendee( diff --git a/src/RealTime/Events/ICityEventsProvider.cs b/src/RealTime/Events/ICityEventsProvider.cs index ed0cb0da..094ead1b 100644 --- a/src/RealTime/Events/ICityEventsProvider.cs +++ b/src/RealTime/Events/ICityEventsProvider.cs @@ -7,12 +7,12 @@ namespace RealTime.Events using RealTime.Events.Storage; /// - /// An interface for a type that can create city event instances for provided building classes. + /// An interface for a type that can create city event instances for specified building classes. /// internal interface ICityEventsProvider { /// - /// Gets a randomly created city event for a building of provided class. If no city event + /// Gets a randomly created city event for a building of specified class. If no city event /// could be created, returns null. /// /// The building class to create a city event for. @@ -23,7 +23,7 @@ internal interface ICityEventsProvider ICityEvent GetRandomEvent(string buildingClass); /// - /// Gets the event template that has the provided name and is configured for the provided + /// Gets the event template that has the specified name and is configured for the specified /// building class. /// /// The unique name of the city event template. diff --git a/src/RealTime/Events/RealTimeCityEvent.cs b/src/RealTime/Events/RealTimeCityEvent.cs index 220ee7ec..0c8fc44e 100644 --- a/src/RealTime/Events/RealTimeCityEvent.cs +++ b/src/RealTime/Events/RealTimeCityEvent.cs @@ -44,7 +44,7 @@ public RealTimeCityEvent(CityEventTemplate eventTemplate, int attendeesCount) this.attendeesCount = attendeesCount; } - /// Accepts an event attendee with provided properties. + /// Accepts an event attendee with specified properties. /// The attendee age. /// The attendee gender. /// The attendee education. @@ -53,7 +53,7 @@ public RealTimeCityEvent(CityEventTemplate eventTemplate, int attendeesCount) /// The attendee happiness. /// A reference to the game's randomizer. /// - /// true if the event attendee with provided properties is accepted and can attend this city event; + /// true if the event attendee with specified properties is accepted and can attend this city event; /// otherwise, false. /// public override bool TryAcceptAttendee( diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs index f79ccdb6..484f49bb 100644 --- a/src/RealTime/Events/RealTimeEventManager.cs +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -100,7 +100,7 @@ public IEnumerable CityEvents /// Gets an unique ID of this storage data set. string IStorageData.StorageDataId => StorageDataId; - /// Gets the state of a city event in the provided building. + /// Gets the state of a city event in the specified building. /// The building ID to check events in. /// The latest start time of events to consider. /// @@ -172,6 +172,43 @@ public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime late : null; } + /// + /// Gets the instance of an ongoing or upcoming city event that takes place in a building + /// with specified ID. + /// + /// The ID of a building to search events for. + /// An instance of the first matching city event, or null if none found. + public ICityEvent GetCityEvent(ushort buildingId) + { + if (buildingId == 0) + { + return null; + } + + if (activeEvent != null && activeEvent.BuildingId == buildingId) + { + return activeEvent; + } + + if (upcomingEvents.Count == 0) + { + return null; + } + + LinkedListNode upcomingEvent = upcomingEvents.First; + while (upcomingEvent != null) + { + if (upcomingEvent.Value.BuildingId == buildingId) + { + return upcomingEvent.Value; + } + + upcomingEvent = upcomingEvent.Next; + } + + return null; + } + /// /// Processes the city events simulation step. The method can be called frequently, but the processing occurs periodically /// at an interval specified by . @@ -205,7 +242,7 @@ public void ProcessEvents() CreateRandomEvent(building); } - /// Reads the data set from the provided . + /// Reads the data set from the specified . /// A to read the data set from. void IStorageData.ReadData(Stream source) { @@ -240,14 +277,15 @@ void IStorageData.ReadData(Stream source) OnEventsChanged(); } - /// Reads the data set to the provided . + /// Reads the data set to the specified . /// A to write the data set to. void IStorageData.StoreData(Stream target) { var serializer = new XmlSerializer(typeof(RealTimeEventStorageContainer)); - var data = new RealTimeEventStorageContainer(); - - data.EarliestEvent = earliestEvent.Ticks; + var data = new RealTimeEventStorageContainer + { + EarliestEvent = earliestEvent.Ticks + }; AddEventToStorage(lastActiveEvent); AddEventToStorage(activeEvent); diff --git a/src/RealTime/Events/Storage/CityEventsLoader.cs b/src/RealTime/Events/Storage/CityEventsLoader.cs index 32db2340..872e241f 100644 --- a/src/RealTime/Events/Storage/CityEventsLoader.cs +++ b/src/RealTime/Events/Storage/CityEventsLoader.cs @@ -33,7 +33,7 @@ private CityEventsLoader() /// /// Reloads the event templates from the storage file that is located in a subdirectory of - /// the provided path. + /// the specified path. /// /// The path where the mod's custom data files are stored. /// @@ -64,7 +64,7 @@ public void Clear() } /// - /// Gets a randomly created city event for a building of provided class. If no city event + /// Gets a randomly created city event for a building of specified class. If no city event /// could be created, returns null. /// /// The building class to create a city event for. @@ -90,7 +90,7 @@ ICityEvent ICityEventsProvider.GetRandomEvent(string buildingClass) } /// - /// Gets the event template that has the provided name and is configured for the provided + /// Gets the event template that has the specified name and is configured for the specified /// building class. /// /// The unique name of the city event template. diff --git a/src/RealTime/Events/VanillaEvent.cs b/src/RealTime/Events/VanillaEvent.cs index 3c965702..487119b5 100644 --- a/src/RealTime/Events/VanillaEvent.cs +++ b/src/RealTime/Events/VanillaEvent.cs @@ -23,7 +23,7 @@ public VanillaEvent(float duration, float ticketPrice) this.ticketPrice = ticketPrice; } - /// Accepts an event attendee with provided properties. + /// Accepts an event attendee with specified properties. /// The attendee age. /// The attendee gender. /// The attendee education. @@ -32,7 +32,7 @@ public VanillaEvent(float duration, float ticketPrice) /// The attendee happiness. /// A reference to the game's randomizer. /// - /// true if the event attendee with provided properties is accepted and can attend + /// true if the event attendee with specified properties is accepted and can attend /// this city event; otherwise, false. /// public override bool TryAcceptAttendee( diff --git a/src/RealTime/GameConnection/BuildingManagerConnection.cs b/src/RealTime/GameConnection/BuildingManagerConnection.cs index d017fe0a..1eb561b9 100644 --- a/src/RealTime/GameConnection/BuildingManagerConnection.cs +++ b/src/RealTime/GameConnection/BuildingManagerConnection.cs @@ -58,8 +58,8 @@ public uint GetCitizenUnit(ushort buildingId) /// The building flags to check. /// true if a building without any flags can also be considered. /// - /// true if the building with the specified ID has the - /// provided; otherwise, false. + /// true if the building with the specified ID has the specified ; + /// otherwise, false. /// public bool BuildingHasFlags(ushort buildingId, Building.Flags flags, bool includeZero = false) { @@ -137,7 +137,7 @@ public ushort FindActiveBuilding( restrictedFlags); } - /// Gets the ID of an event that takes place in the building with provided ID. + /// Gets the ID of an event that takes place in the building with specified ID. /// The building ID to check. /// An ID of an event that takes place in the building, or 0 if none. public ushort GetEvent(ushort buildingId) @@ -148,7 +148,7 @@ public ushort GetEvent(ushort buildingId) } /// - /// Gets an ID of a random building in the city that belongs to any of the provided . + /// Gets an ID of a random building in the city that belongs to any of the specified . /// /// A collection of that specifies in which services to /// search the random building in. diff --git a/src/RealTime/GameConnection/CitizenManagerConnection.cs b/src/RealTime/GameConnection/CitizenManagerConnection.cs index c4f9c86f..9a794868 100644 --- a/src/RealTime/GameConnection/CitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/CitizenManagerConnection.cs @@ -2,6 +2,7 @@ namespace RealTime.GameConnection { + using System; using UnityEngine; /// The default implementation of the interface. @@ -31,11 +32,11 @@ public ushort GetTargetBuilding(ushort instanceId) : (ushort)0; } - /// Determines whether the citizen's instance with provided ID has particular flags. + /// Determines whether the citizen's instance with specified ID has particular flags. /// The instance ID to check. /// The flags to check. /// - /// true to check all flags from the provided , false to check any flags. + /// true to check all flags from the specified , false to check any flags. /// /// true if the citizen instance has the specified flags; otherwise, false. public bool InstanceHasFlags(ushort instanceId, CitizenInstance.Flags flags, bool all = false) @@ -105,5 +106,33 @@ public uint GetMaxInstancesCount() { return CitizenManager.instance.m_instances.m_size; } + + /// Gets the maximum count of the citizens. + /// The maximum number of the citizens. + public uint GetMaxCitizensCount() + { + return CitizenManager.instance.m_citizens.m_size; + } + + /// Gets the location of the citizen with specified ID. + /// The ID of the citizen to query location of. + /// A value that describes the citizen's current location. + /// Thrown when the argument is 0. + public Citizen.Location GetCitizenLocation(uint citizenId) + { + if (citizenId == 0) + { + throw new ArgumentOutOfRangeException(nameof(citizenId), "The citizen ID cannot be 0"); + } + + return CitizenManager.instance.m_citizens.m_buffer[citizenId].CurrentLocation; + } + + /// Gets the game's citizens array (direct reference). + /// The reference to the game's array containing the items. + public Citizen[] GetCitizensArray() + { + return CitizenManager.instance.m_citizens.m_buffer; + } } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/EventManagerConnection.cs b/src/RealTime/GameConnection/EventManagerConnection.cs index 78e09cba..6b3b06aa 100644 --- a/src/RealTime/GameConnection/EventManagerConnection.cs +++ b/src/RealTime/GameConnection/EventManagerConnection.cs @@ -13,7 +13,7 @@ namespace RealTime.GameConnection /// internal sealed class EventManagerConnection : IEventManagerConnection { - /// Gets the flags of an event with provided ID. + /// Gets the flags of an event with specified ID. /// The ID of the event to get flags of. /// /// The event flags or if none found. diff --git a/src/RealTime/GameConnection/IBuildingManagerConnection.cs b/src/RealTime/GameConnection/IBuildingManagerConnection.cs index e3c3ca56..d3dea3f6 100644 --- a/src/RealTime/GameConnection/IBuildingManagerConnection.cs +++ b/src/RealTime/GameConnection/IBuildingManagerConnection.cs @@ -39,8 +39,8 @@ internal interface IBuildingManagerConnection /// The building flags to check. /// true if a building without any flags can also be considered. /// - /// true if the building with the specified ID has the - /// provided; otherwise, false. + /// true if the building with the specified ID has the specified ; + /// otherwise, false. /// bool BuildingHasFlags(ushort buildingId, Building.Flags flags, bool includeZero = false); @@ -72,13 +72,13 @@ ushort FindActiveBuilding( ItemClass.Service service, ItemClass.SubService subService = ItemClass.SubService.None); - /// Gets the ID of an event that takes place in the building with provided ID. + /// Gets the ID of an event that takes place in the building with specified ID. /// The building ID to check. /// An ID of an event that takes place in the building, or 0 if none. ushort GetEvent(ushort buildingId); /// - /// Gets an ID of a random building in the city that belongs to any of the provided . + /// Gets an ID of a random building in the city that belongs to any of the specified . /// /// Thrown when the argument is null. /// diff --git a/src/RealTime/GameConnection/ICitizenManagerConnection.cs b/src/RealTime/GameConnection/ICitizenManagerConnection.cs index bdc9f369..37d78815 100644 --- a/src/RealTime/GameConnection/ICitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/ICitizenManagerConnection.cs @@ -16,11 +16,11 @@ internal interface ICitizenManagerConnection /// The ID of the building the citizen is moving to, or 0 if none. ushort GetTargetBuilding(ushort instanceId); - /// Determines whether the citizen's instance with provided ID has particular flags. + /// Determines whether the citizen's instance with specified ID has particular flags. /// The instance ID to check. /// The flags to check. /// - /// true to check all flags from the provided , false to check any flags. + /// true to check all flags from the specified , false to check any flags. /// /// true if the citizen instance has the specified flags; otherwise, false. bool InstanceHasFlags(ushort instanceId, CitizenInstance.Flags flags, bool all = false); @@ -52,5 +52,19 @@ internal interface ICitizenManagerConnection /// Gets the maximum count of the active citizens instances. /// The maximum number of active citizens instances. uint GetMaxInstancesCount(); + + /// Gets the maximum count of the citizens. + /// The maximum number of the citizens. + uint GetMaxCitizensCount(); + + /// Gets the location of the citizen with specified ID. + /// The ID of the citizen to query location of. + /// A value that describes the citizen's current location. + /// Thrown when the argument is 0. + Citizen.Location GetCitizenLocation(uint citizenId); + + /// Gets the game's citizens array (direct reference). + /// The reference to the game's array containing the items. + Citizen[] GetCitizensArray(); } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/IEventManagerConnection.cs b/src/RealTime/GameConnection/IEventManagerConnection.cs index d7963bf1..a64b1506 100644 --- a/src/RealTime/GameConnection/IEventManagerConnection.cs +++ b/src/RealTime/GameConnection/IEventManagerConnection.cs @@ -10,7 +10,7 @@ namespace RealTime.GameConnection /// An interface for the game specific logic related to the event management. internal interface IEventManagerConnection { - /// Gets the flags of an event with provided ID. + /// Gets the flags of an event with specified ID. /// The ID of the event to get flags of. /// The event flags or if none found. EventData.Flags GetEventFlags(ushort eventId); diff --git a/src/RealTime/GameConnection/Patches/ResidentAIPatch.cs b/src/RealTime/GameConnection/Patches/ResidentAIPatch.cs index 670f27ad..34fe20e9 100644 --- a/src/RealTime/GameConnection/Patches/ResidentAIPatch.cs +++ b/src/RealTime/GameConnection/Patches/ResidentAIPatch.cs @@ -21,6 +21,9 @@ internal static class ResidentAIPatch /// Gets the patch object for the location method. public static IPatch Location { get; } = new ResidentAI_UpdateLocation(); + /// Gets the patch object for the arrive at destination method. + public static IPatch ArriveAtDestination { get; } = new HumanAI_ArriveAtDestination(); + /// Creates a game connection object for the resident AI class. /// A new object. public static ResidentAIConnection GetResidentAIConnection() @@ -92,5 +95,26 @@ private static bool Prefix(ResidentAI __instance, uint citizenID, ref Citizen da } #pragma warning restore SA1313 // Parameter names must begin with lower-case letter } + + private sealed class HumanAI_ArriveAtDestination : PatchBase + { + protected override MethodInfo GetMethod() + { + return typeof(HumanAI).GetMethod( + "ArriveAtDestination", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new[] { typeof(ushort), typeof(CitizenInstance).MakeByRefType(), typeof(bool) }, + new ParameterModifier[0]); + } + + private static void Postfix(ref CitizenInstance citizenData, bool success) + { + if (success && citizenData.Info.m_citizenAI is ResidentAI) + { + RealTimeAI?.RegisterCitizenArrival(citizenData.m_citizen); + } + } + } } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/TimeInfo.cs b/src/RealTime/GameConnection/TimeInfo.cs index e58bec39..9623db3f 100644 --- a/src/RealTime/GameConnection/TimeInfo.cs +++ b/src/RealTime/GameConnection/TimeInfo.cs @@ -15,6 +15,8 @@ namespace RealTime.GameConnection internal sealed class TimeInfo : ITimeInfo { private readonly RealTimeConfig config; + private DateTime currentTime; + private float currentHour; /// Initializes a new instance of the class. /// The configuration to run with. @@ -28,7 +30,19 @@ public TimeInfo(RealTimeConfig config) public DateTime Now => SimulationManager.instance.m_currentGameTime; /// Gets the current daytime hour. - public float CurrentHour => (float)Now.TimeOfDay.TotalHours; + public float CurrentHour + { + get + { + if (SimulationManager.instance.m_currentGameTime != currentTime) + { + currentTime = SimulationManager.instance.m_currentGameTime; + currentHour = (float)Now.TimeOfDay.TotalHours; + } + + return currentHour; + } + } /// Gets the sunrise hour of the current day. public float SunriseHour => SimulationManager.SUNRISE_HOUR; diff --git a/src/RealTime/Patching/FastDelegate.cs b/src/RealTime/Patching/FastDelegate.cs index fe0a0ca4..f9bcee28 100644 --- a/src/RealTime/Patching/FastDelegate.cs +++ b/src/RealTime/Patching/FastDelegate.cs @@ -17,7 +17,7 @@ namespace RealTime.Patching internal static class FastDelegate { /// - /// Creates a delegate instance of the provided type that represents a method + /// Creates a delegate instance of the specified type that represents a method /// of the class. If the target method is a 's instance /// method, the first parameter of the signature must be a reference to a /// instance. @@ -67,7 +67,7 @@ private static MethodInfo GetMethodInfo(string name, bool inst if (methodInfo == null) { - throw new MissingMethodException($"The method '{typeof(TType).Name}.{name}' matching the provided signature doesn't exist: {typeof(TDelegate)}"); + throw new MissingMethodException($"The method '{typeof(TType).Name}.{name}' matching the specified signature doesn't exist: {typeof(TDelegate)}"); } return methodInfo; diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index c44e9053..0c5672fa 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -57,6 +57,7 @@ + @@ -64,7 +65,13 @@ + + + + + + @@ -122,6 +129,7 @@ + @@ -132,6 +140,7 @@ + diff --git a/src/RealTime/Simulation/CitizenProcessor.cs b/src/RealTime/Simulation/CitizenProcessor.cs new file mode 100644 index 00000000..ff5bf8e6 --- /dev/null +++ b/src/RealTime/Simulation/CitizenProcessor.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + using RealTime.CustomAI; + + /// + /// A class that executes various citizen simulation logic that is not related to movement. + /// + internal sealed class CitizenProcessor + { + private const uint StepSize = 256; + private const uint StepMask = 0xFFF; + + private readonly RealTimeResidentAI residentAI; + private readonly SpareTimeBehavior spareTimeBehavior; + private int dayStartFrame; + + /// Initializes a new instance of the class. + /// The custom resident AI implementation. + /// A behavior that provides simulation info for the citizens spare time. + /// Thrown when any argument is null. + public CitizenProcessor(RealTimeResidentAI residentAI, SpareTimeBehavior spareTimeBehavior) + { + this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); + this.spareTimeBehavior = spareTimeBehavior ?? throw new ArgumentNullException(nameof(spareTimeBehavior)); + dayStartFrame = int.MinValue; + } + + /// Notifies this simulation object that a new game day begins. + public void StartNewDay() + { + dayStartFrame = int.MinValue; + residentAI.BeginNewDay(); + } + + /// Applies the duration of a simulation frame to this simulation object. + /// Duration of a simulation frame in hours. + public void SetFrameDuration(float frameDuration) + { + float cyclePeriod = frameDuration * (StepMask + 1); + residentAI.SetSimulationCyclePeriod(cyclePeriod); + spareTimeBehavior.SetSimulationCyclePeriod(cyclePeriod); + } + + /// Processes the simulation tick. + public void ProcessTick() + { + spareTimeBehavior.RefreshGoOutChances(); + } + + /// Processes the simulation frame. + /// The index of the simulation frame to process. + public void ProcessFrame(uint frameIndex) + { + if (dayStartFrame == -1) + { + return; + } + + uint step = frameIndex & StepMask; + if (dayStartFrame == int.MinValue) + { + dayStartFrame = (int)step; + } + else if (step == dayStartFrame) + { + dayStartFrame = -1; + return; + } + + uint idFrom = step * StepSize; + uint idTo = ((step + 1) * StepSize) - 1; + + for (uint i = idFrom; i <= idTo; i++) + { + if ((CitizenManager.instance.m_citizens.m_buffer[i].m_flags & Citizen.Flags.Created) == 0) + { + continue; + } + + residentAI.BeginNewDayForCitizen(i); + } + } + } +} diff --git a/src/RealTime/Simulation/DayTimeCalculator.cs b/src/RealTime/Simulation/DayTimeCalculator.cs index 1588a084..8e7137f9 100644 --- a/src/RealTime/Simulation/DayTimeCalculator.cs +++ b/src/RealTime/Simulation/DayTimeCalculator.cs @@ -35,7 +35,7 @@ public DayTimeCalculator(float latitude) } /// - /// Calculates the sunrise and sunset hours for the provided . + /// Calculates the sunrise and sunset hours for the specified . /// /// /// The game date to calculate the sunrise and sunset times for. diff --git a/src/RealTime/Simulation/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index 0ae2ddd6..b56e408b 100644 --- a/src/RealTime/Simulation/SimulationHandler.cs +++ b/src/RealTime/Simulation/SimulationHandler.cs @@ -43,6 +43,9 @@ public sealed class SimulationHandler : ThreadingExtensionBase /// Gets or sets the weather information class instance. internal static WeatherInfo WeatherInfo { get; set; } + /// Gets or sets the citizen processing class instance. + internal static CitizenProcessor CitizenProcessor { get; set; } + /// /// Called before each game simulation tick. A tick contains multiple frames. /// Performs the dispatching for this simulation phase. @@ -50,22 +53,23 @@ public sealed class SimulationHandler : ThreadingExtensionBase public override void OnBeforeSimulationTick() { WeatherInfo?.Update(); - } - - /// - /// Called after each game simulation tick. A tick contains multiple frames. - /// Performs the dispatching for this simulation phase. - /// - public override void OnAfterSimulationTick() - { EventManager?.ProcessEvents(); - TimeAdjustment?.Update(); + + if (CitizenProcessor != null) + { + CitizenProcessor.ProcessTick(); + if (TimeAdjustment != null && TimeAdjustment.Update()) + { + CitizenProcessor.SetFrameDuration(TimeAdjustment.HoursPerFrame); + } + } DateTime currentDate = SimulationManager.instance.m_currentGameTime.Date; if (currentDate != lastHandledDate) { lastHandledDate = currentDate; DayTimeSimulation?.Process(currentDate); + CitizenProcessor?.StartNewDay(); OnNewDay(this); } } @@ -75,10 +79,9 @@ public override void OnAfterSimulationTick() /// public override void OnBeforeSimulationFrame() { - if ((SimulationManager.instance.m_currentFrameIndex & 0xFF) == 0) - { - Buildings?.StartBuildingProcessingFrame(); - } + uint currentFrame = SimulationManager.instance.m_currentFrameIndex; + Buildings?.ProcessFrame(currentFrame); + CitizenProcessor?.ProcessFrame(currentFrame); } private static void OnNewDay(SimulationHandler sender) diff --git a/src/RealTime/Simulation/TimeAdjustment.cs b/src/RealTime/Simulation/TimeAdjustment.cs index a14d3446..f46c8e39 100644 --- a/src/RealTime/Simulation/TimeAdjustment.cs +++ b/src/RealTime/Simulation/TimeAdjustment.cs @@ -28,6 +28,9 @@ public TimeAdjustment(RealTimeConfig config) vanillaFramesPerDay = SimulationManager.DAYTIME_FRAMES; } + /// Gets the number of hours that fit into one simulation frame. + public float HoursPerFrame => SimulationManager.DAYTIME_FRAME_TO_HOUR; + /// Enables the customized time adjustment. /// The current game date and time. public DateTime Enable() @@ -40,7 +43,8 @@ public DateTime Enable() } /// Updates the time adjustment to be synchronized with the configuration and the daytime. - public void Update() + /// true if the time adjustment was updated; otherwise, false. + public bool Update() { if (SimulationManager.instance.m_enableDayNight != isNightEnabled) { @@ -51,7 +55,7 @@ public void Update() && dayTimeSpeed == config.DayTimeSpeed && nightTimeSpeed == config.NightTimeSpeed)) { - return; + return false; } isNightTime = SimulationManager.instance.m_isNightTime; @@ -59,6 +63,7 @@ public void Update() nightTimeSpeed = config.NightTimeSpeed; UpdateTimeSimulationValues(CalculateFramesPerDay()); + return true; } /// Disables the customized time adjustment restoring the default vanilla values. diff --git a/src/RealTime/Tools/DateTimeExtensions.cs b/src/RealTime/Tools/DateTimeExtensions.cs index 7a2ec904..9586e310 100644 --- a/src/RealTime/Tools/DateTimeExtensions.cs +++ b/src/RealTime/Tools/DateTimeExtensions.cs @@ -50,7 +50,7 @@ public static bool IsWeekendTime(this DateTime dateTime, float fridayStartHour, } /// - /// Rounds this to the provided (ceiling). + /// Rounds this to the specified (ceiling). /// /// /// The to round. @@ -62,5 +62,25 @@ public static DateTime RoundCeil(this DateTime dateTime, TimeSpan interval) long overflow = dateTime.Ticks % interval.Ticks; return overflow == 0 ? dateTime : dateTime.AddTicks(interval.Ticks - overflow); } + + /// Gets a new value that is greater than this + /// and whose daytime hour equals to the specified one. If is negative, + /// it will be shifted in the next day to become positive. + /// The to get the future value for. + /// The daytime hour for the result value. Can be negative. + /// A new value that is greater than this + /// and whose daytime hour is set to the specified . + public static DateTime FutureHour(this DateTime dateTime, float hour) + { + if (hour < 0) + { + hour += 24f; + } + + float delta = hour - (float)dateTime.TimeOfDay.TotalHours; + return delta > 0 + ? dateTime.AddHours(delta) + : dateTime.AddHours(24f + delta); + } } } diff --git a/src/RealTime/Tools/GitVersion.cs b/src/RealTime/Tools/GitVersion.cs index 0bd092cd..211442fc 100644 --- a/src/RealTime/Tools/GitVersion.cs +++ b/src/RealTime/Tools/GitVersion.cs @@ -16,7 +16,7 @@ internal static class GitVersion private const string VersionFieldName = "FullSemVer"; /// - /// Gets a string representation of the full semantic assembly version of the provided . + /// Gets a string representation of the full semantic assembly version of the specified . /// This assembly should be built using the GitVersion toolset; otherwise, a "?" version string will /// be returned. /// @@ -25,7 +25,7 @@ internal static class GitVersion /// /// An to get the version of. Should be built using the GitVersion toolset. /// - /// A string representation of the full semantic version of the provided , + /// A string representation of the full semantic version of the specified , /// or "?" if the version could not be determined. public static string GetAssemblyVersion(Assembly assembly) { diff --git a/src/RealTime/Tools/LinkedListExtensions.cs b/src/RealTime/Tools/LinkedListExtensions.cs index 74f677ad..c922d552 100644 --- a/src/RealTime/Tools/LinkedListExtensions.cs +++ b/src/RealTime/Tools/LinkedListExtensions.cs @@ -13,7 +13,7 @@ namespace RealTime.Tools internal static class LinkedListExtensions { /// - /// Gets the linked list's firsts node that matches the provided , + /// Gets the linked list's firsts node that matches the specified , /// or null if no matching node was found. /// /// diff --git a/src/RealTime/Tools/RealTimeMath.cs b/src/RealTime/Tools/RealTimeMath.cs new file mode 100644 index 00000000..8bbe5d07 --- /dev/null +++ b/src/RealTime/Tools/RealTimeMath.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + /// + /// A static class containing various math methods. + /// + internal static class RealTimeMath + { + /// Ensures that a value is constrained by the specified range. + /// The value to check. + /// The minimum constraint. + /// The maximum constraint. + /// A value that is guaranteed to be in the specified range. + public static float Clamp(float value, float min, float max) + { + if (value < min) + { + return min; + } + + if (value > max) + { + return max; + } + + return value; + } + + /// Ensures that a value is constrained by the specified range. + /// The value to check. + /// The minimum constraint. + /// The maximum constraint. + /// A value that is guaranteed to be in the specified range. + public static uint Clamp(uint value, uint min, uint max) + { + if (value < min) + { + return min; + } + + if (value > max) + { + return max; + } + + return value; + } + + /// Ensures that a value is constrained by the specified range. + /// The value to check. + /// The minimum constraint. + /// The maximum constraint. + /// A value that is guaranteed to be in the specified range. + public static int Clamp(int value, int min, int max) + { + if (value < min) + { + return min; + } + + if (value > max) + { + return max; + } + + return value; + } + } +} diff --git a/src/RealTime/UI/CitiesCheckBoxItem.cs b/src/RealTime/UI/CitiesCheckBoxItem.cs index 08dc1dbc..55a22270 100644 --- a/src/RealTime/UI/CitiesCheckBoxItem.cs +++ b/src/RealTime/UI/CitiesCheckBoxItem.cs @@ -50,7 +50,7 @@ public override void Refresh() UIComponent.isChecked = Value; } - /// Creates the view item using the provided . + /// Creates the view item using the specified . /// The UI helper to use for item creation. /// The item's default value. /// A newly created view item. diff --git a/src/RealTime/UI/CitiesComboBoxItem.cs b/src/RealTime/UI/CitiesComboBoxItem.cs index 7893248a..d9f8777d 100644 --- a/src/RealTime/UI/CitiesComboBoxItem.cs +++ b/src/RealTime/UI/CitiesComboBoxItem.cs @@ -80,7 +80,7 @@ public override void Refresh() UIComponent.selectedIndex = Value; } - /// Creates the view item using the provided . + /// Creates the view item using the specified . /// The UI helper to use for item creation. /// The item's default value. /// A newly created view item. diff --git a/src/RealTime/UI/CitiesSliderItem.cs b/src/RealTime/UI/CitiesSliderItem.cs index fc8fcf49..6de687aa 100644 --- a/src/RealTime/UI/CitiesSliderItem.cs +++ b/src/RealTime/UI/CitiesSliderItem.cs @@ -107,7 +107,7 @@ public override void Refresh() UIComponent.value = Value; } - /// Creates the view item using the provided . + /// Creates the view item using the specified . /// The UI helper to use for item creation. /// The item's default value. /// A newly created view item. diff --git a/src/RealTime/UI/CitiesViewItem.cs b/src/RealTime/UI/CitiesViewItem.cs index 6c3bebc8..9916b406 100644 --- a/src/RealTime/UI/CitiesViewItem.cs +++ b/src/RealTime/UI/CitiesViewItem.cs @@ -87,7 +87,7 @@ private set /// Thrown when the argument is null. public abstract void Translate(ILocalizationProvider localizationProvider); - /// Creates the view item using the provided . + /// Creates the view item using the specified . /// The UI helper to use for item creation. /// The item's default value. /// A newly created view item. diff --git a/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs b/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs index e5570f00..6bdccded 100644 --- a/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs +++ b/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs @@ -31,7 +31,7 @@ internal RealTimeUIDateTimeWrapper(DateTime initial) public DateTime CurrentValue => m_Value; /// - /// Checks the provided value whether it should be converted to a + /// Checks the specified value whether it should be converted to a /// string representation. Converts the value when necessary. /// /// ///