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.
///
/// ///