From 070ede6a610b68c5571b7d6d17436c5379ca6d65 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 13 Jul 2018 19:51:32 +0200 Subject: [PATCH 01/42] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 85419162b0b0e933c74f4a7336b07e8fb8d2b441 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 15 Jul 2018 14:59:18 +0200 Subject: [PATCH 02/42] Remove the incomplete ride abandonment logic (will be implemented in #2) --- src/RealTime/CustomAI/Constants.cs | 3 -- .../CustomAI/RealTimeResidentAI.Moving.cs | 32 ++++++------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index f00bf551..bae2c3ab 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -17,9 +17,6 @@ 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 a citizen to go shopping. public const uint GoShoppingChance = 50; diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index 9737ac99..fe7adf45 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -5,7 +5,6 @@ namespace RealTime.CustomAI { using RealTime.Tools; - using static Constants; internal sealed partial class RealTimeResidentAI { @@ -27,7 +26,7 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit } else { - // TODO: check whether this makes sense and maybe remove/replace this logic + // 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); @@ -43,35 +42,22 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit return; } - 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; } - if (!returnHome && CitizenMgr.InstanceHasFlags(instanceId, CitizenInstance.Flags.WaitingTransport | CitizenInstance.Flags.WaitingTaxi)) + ItemClass.Service targetService = BuildingMgr.GetBuildingService(targetBuilding); + if (targetService != ItemClass.Service.Beautification || !IsBadWeather(citizenId)) { - if (mayCancel && CitizenMgr.GetInstanceWaitCounter(instanceId) == 255 && Random.ShouldOccur(AbandonTransportWaitChance)) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} goes back home"); - returnHome = true; - } + return; } - if (returnHome) + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} cancels the trip to a park due to bad weather"); + ushort home = CitizenProxy.GetHomeBuilding(ref citizen); + if (home != 0) { - ushort home = CitizenProxy.GetHomeBuilding(ref citizen); - if (home == 0) - { - return; - } - residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); } } From 747f7446fc13cbf595cc8956b2b6ce16255bf638 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 15 Jul 2018 16:57:11 +0200 Subject: [PATCH 03/42] Create a custom resident citizen state structure --- .../CustomAI/RealTimeResidentAI.Common.cs | 5 +- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 3 ++ .../CustomAI/RealTimeResidentAI.Visit.cs | 1 - src/RealTime/CustomAI/RealTimeResidentAI.cs | 7 ++- src/RealTime/CustomAI/ResidentState.cs | 2 +- .../CustomAI/ResidentStateDescriptor.cs | 49 +++++++++++++++++++ src/RealTime/CustomAI/WorkStatus.cs | 24 +++++++++ .../CitizenManagerConnection.cs | 7 +++ .../ICitizenManagerConnection.cs | 4 ++ src/RealTime/RealTime.csproj | 2 + 10 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/RealTime/CustomAI/ResidentStateDescriptor.cs create mode 100644 src/RealTime/CustomAI/WorkStatus.cs diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 9fc1412e..d01b60b4 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -137,7 +137,7 @@ private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitiz return false; } - private ResidentState GetResidentState(ref TCitizen citizen) + private ResidentState GetResidentState(uint citizenId, ref TCitizen citizen) { if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.DummyTraffic)) { @@ -185,8 +185,7 @@ private ResidentState GetResidentState(ref TCitizen citizen) switch (buildingService) { case ItemClass.Service.Commercial: - if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDay - && TimeInfo.CurrentHour > Config.LunchBegin && TimeInfo.CurrentHour < GetSpareTimeBeginHour(CitizenProxy.GetAge(ref citizen))) + if (residentStates[citizenId].WorkStatus == WorkStatus.AtLunch) { return ResidentState.AtLunch; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 64b4ae60..f88174c1 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -178,6 +178,7 @@ private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCit ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); if (lunchPlace != 0) { + residentStates[citizenId].WorkStatus = WorkStatus.AtLunch; Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} is going for lunch from {currentBuilding} to {lunchPlace}"); } else @@ -253,6 +254,7 @@ private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort workBuilding, ushor return false; } + // TODO: replace the day off logic. if ((citizenId & 0x7FF) == TimeInfo.Now.Day) { Log.Debug(TimeInfo.Now, $"Citizen {citizenId} has a day off work today"); @@ -416,6 +418,7 @@ private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); } + residentStates[citizenId].WorkStatus = WorkStatus.Default; return true; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 21788773..66db57ff 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -24,7 +24,6 @@ private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint { case ResidentState.AtLunch: CitizenReturnsFromLunch(instance, citizenId, ref citizen, isVirtual); - return; case ResidentState.AtLeisureArea: diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index dd60f12f..175cba6a 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -17,6 +17,7 @@ internal sealed partial class RealTimeResidentAI : RealTimeHumanA where TCitizen : struct { private readonly ResidentAIConnection residentAI; + private readonly ResidentStateDescriptor[] residentStates; /// Initializes a new instance of the class. /// Thrown when any argument is null. @@ -32,6 +33,7 @@ public RealTimeResidentAI( : base(config, connections, eventManager) { this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); + residentStates = new ResidentStateDescriptor[CitizenMgr.GetMaxCitizensCount()]; } /// The entry method of the custom AI. @@ -57,7 +59,7 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) return; } - ResidentState residentState = GetResidentState(ref citizen); + ResidentState residentState = GetResidentState(citizenId, ref citizen); bool isVirtual; switch (residentState) @@ -110,6 +112,9 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) break; } + + residentStates[citizenId].State = residentState; + } } private bool ShouldRealizeCitizen(TAI ai) diff --git a/src/RealTime/CustomAI/ResidentState.cs b/src/RealTime/CustomAI/ResidentState.cs index dee3cc68..345f47e9 100644 --- a/src/RealTime/CustomAI/ResidentState.cs +++ b/src/RealTime/CustomAI/ResidentState.cs @@ -3,7 +3,7 @@ namespace RealTime.CustomAI { /// Possible citizen's states. - internal enum ResidentState + internal enum ResidentState : byte { /// The state could not be determined. Unknown, diff --git a/src/RealTime/CustomAI/ResidentStateDescriptor.cs b/src/RealTime/CustomAI/ResidentStateDescriptor.cs new file mode 100644 index 00000000..9ef09542 --- /dev/null +++ b/src/RealTime/CustomAI/ResidentStateDescriptor.cs @@ -0,0 +1,49 @@ +// Copyright (c) dymanoid. All rights reserved. + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Tools; + using static Constants; + + /// A container struct that holds information about the detailed resident citizen state. + internal struct ResidentStateDescriptor + { + /// The citizen's last known state. + public ResidentState State; + + /// The citizen's current work status. + public WorkStatus WorkStatus; + + /// The time when citizen started their last movement in the city. + public DateTime DepartureTime; + + /// + /// Gets or sets the travel time (in hours) from citizen's home to the work building. The maximum value is + /// determined by the constant. + /// + public float TravelTimeToWork; + + /// 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 < DepartureTime) + { + return; + } + + float onTheWayHours = (float)(arrivalTime - DepartureTime).TotalHours; + if (onTheWayHours > MaxHoursOnTheWay) + { + onTheWayHours = MaxHoursOnTheWay; + } + + TravelTimeToWork = TravelTimeToWork == 0 + ? onTheWayHours + : (TravelTimeToWork + onTheWayHours) / 2; + } + } +} \ No newline at end of file diff --git a/src/RealTime/CustomAI/WorkStatus.cs b/src/RealTime/CustomAI/WorkStatus.cs new file mode 100644 index 00000000..13c6b08e --- /dev/null +++ b/src/RealTime/CustomAI/WorkStatus.cs @@ -0,0 +1,24 @@ +// +// 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. + Default, + + /// The citizen takes a break and goes out for lunch. + AtLunch, + + /// The citizen has a day off work today. + DayOff, + + /// The citizen is on vacation. + OnVacation + } +} diff --git a/src/RealTime/GameConnection/CitizenManagerConnection.cs b/src/RealTime/GameConnection/CitizenManagerConnection.cs index c4f9c86f..375bb95a 100644 --- a/src/RealTime/GameConnection/CitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/CitizenManagerConnection.cs @@ -105,5 +105,12 @@ 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; + } } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/ICitizenManagerConnection.cs b/src/RealTime/GameConnection/ICitizenManagerConnection.cs index bdc9f369..e888a799 100644 --- a/src/RealTime/GameConnection/ICitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/ICitizenManagerConnection.cs @@ -52,5 +52,9 @@ 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(); } } \ No newline at end of file diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index c44e9053..4dbe9926 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -64,7 +64,9 @@ + + From 06a66883f070adcdf02ec69e1ec233b7a994580b Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 15 Jul 2018 16:57:45 +0200 Subject: [PATCH 04/42] Implement citizen travel-to-work time tracking --- src/RealTime/Core/RealTimeCore.cs | 1 + .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 1 + src/RealTime/CustomAI/RealTimeResidentAI.cs | 22 +++++++++++++++++ .../CustomAI/ResidentStateDescriptor.cs | 2 +- .../CitizenManagerConnection.cs | 15 ++++++++++++ .../ICitizenManagerConnection.cs | 6 +++++ .../GameConnection/Patches/ResidentAIPatch.cs | 24 +++++++++++++++++++ 7 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index 5744c5a3..ac50412e 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, diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index f88174c1..80c30631 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -233,6 +233,7 @@ private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citiz } else { + residentStates[citizenId].DepartureTime = TimeInfo.Now; residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 175cba6a..9725555f 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -115,6 +115,28 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) residentStates[citizenId].State = residentState; } + + /// Notifies that a citizen has arrived their destination. + /// The citizen ID to process. + public void RegisterCitizenArrival(uint citizenId) + { + if (citizenId == 0 || citizenId >= residentStates.Length) + { + return; + } + + switch (CitizenMgr.GetCitizenLocation(citizenId)) + { + case Citizen.Location.Work: + ref ResidentStateDescriptor state = ref residentStates[citizenId]; + state.UpdateTravelTimeToWork(TimeInfo.Now); + state.DepartureTime = default; + Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {residentStates[citizenId].TravelTimeToWork} hours to get to work"); + break; + + default: + return; + } } private bool ShouldRealizeCitizen(TAI ai) diff --git a/src/RealTime/CustomAI/ResidentStateDescriptor.cs b/src/RealTime/CustomAI/ResidentStateDescriptor.cs index 9ef09542..d8eaebc7 100644 --- a/src/RealTime/CustomAI/ResidentStateDescriptor.cs +++ b/src/RealTime/CustomAI/ResidentStateDescriptor.cs @@ -30,7 +30,7 @@ internal struct ResidentStateDescriptor /// public void UpdateTravelTimeToWork(DateTime arrivalTime) { - if (arrivalTime < DepartureTime) + if (arrivalTime < DepartureTime || DepartureTime == default) { return; } diff --git a/src/RealTime/GameConnection/CitizenManagerConnection.cs b/src/RealTime/GameConnection/CitizenManagerConnection.cs index 375bb95a..c18fbea0 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. @@ -112,5 +113,19 @@ 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; + } } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/ICitizenManagerConnection.cs b/src/RealTime/GameConnection/ICitizenManagerConnection.cs index e888a799..afdf5a9b 100644 --- a/src/RealTime/GameConnection/ICitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/ICitizenManagerConnection.cs @@ -56,5 +56,11 @@ internal interface ICitizenManagerConnection /// 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); } } \ No newline at end of file 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 From 3b433ed41fbaeaeb10f68e24f3a3c9a30d42f98e Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Mon, 16 Jul 2018 20:11:26 +0200 Subject: [PATCH 05/42] Make citizens remember the travel time needed to reach the work building --- src/RealTime/CustomAI/Constants.cs | 4 +-- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 35 +++++++++++++------ src/RealTime/CustomAI/RealTimeResidentAI.cs | 9 ++--- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index bae2c3ab..9f0bfa7b 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -39,10 +39,10 @@ internal static class Constants public const int TouristDoNothingProbability = 5000; /// An assumed maximum on-the-way time to a target building. - public const float MaxHoursOnTheWay = 2.5f; + public const float MaxHoursOnTheWay = 4f; /// An assumed minimum on-the-way time to a target building. - public const float MinHoursOnTheWay = 0.5f; + public const float MinHoursOnTheWay = 0.1f; /// A minimum work shift duration in hours. public const float MinimumWorkShiftDuration = 2f; diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 80c30631..78011a9e 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -220,7 +220,7 @@ private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citiz ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if (!ShouldMoveToSchoolOrWork(citizenId, workBuilding, currentBuilding, CitizenProxy.GetAge(ref citizen))) + if (!ShouldMoveToSchoolOrWork(citizenId, homeBuilding, currentBuilding, workBuilding, CitizenProxy.GetAge(ref citizen))) { return false; } @@ -240,7 +240,7 @@ private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citiz return true; } - private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort workBuilding, ushort currentBuilding, Citizen.AgeGroup citizenAge) + private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort homeBuilding, ushort currentBuilding, ushort workBuilding, Citizen.AgeGroup citizenAge) { if (workBuilding == 0 || citizenAge == Citizen.AgeGroup.Senior) { @@ -262,9 +262,13 @@ private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort workBuilding, ushor return false; } + float travelTime = currentBuilding == homeBuilding + ? GetTravelTimeHomeToWork(citizenId, homeBuilding, workBuilding) + : 0; + if (citizenAge == Citizen.AgeGroup.Child || citizenAge == Citizen.AgeGroup.Teen) { - return ShouldMoveToSchoolOrWork(currentBuilding, workBuilding, Config.SchoolBegin, Config.SchoolEnd, 0); + return ShouldMoveToSchoolOrWork(workBuilding, Config.SchoolBegin, Config.SchoolEnd, 0, travelTime); } GetWorkShiftTimes(citizenId, buildingSevice, buildingSubService, out float workBeginHour, out float workEndHour); @@ -273,12 +277,14 @@ private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort workBuilding, ushor return false; } - float overtime = Random.ShouldOccur(Config.OnTimeQuota) ? 0 : Config.MaxOvertime * Random.GetRandomValue(100u) / 200f; + float overtime = currentBuilding != homeBuilding || Random.ShouldOccur(Config.OnTimeQuota) + ? 0 + : Config.MaxOvertime * Random.GetRandomValue(100u) / 200f; - return ShouldMoveToSchoolOrWork(currentBuilding, workBuilding, workBeginHour, workEndHour, overtime); + return ShouldMoveToSchoolOrWork(workBuilding, workBeginHour, workEndHour, overtime, travelTime); } - private bool ShouldMoveToSchoolOrWork(ushort currentBuilding, ushort workBuilding, float workBeginHour, float workEndHour, float overtime) + private bool ShouldMoveToSchoolOrWork(ushort workBuilding, float workBeginHour, float workEndHour, float overtime, float travelTime) { float gotoHour = workBeginHour - overtime - MaxHoursOnTheWay; if (gotoHour < 0) @@ -298,10 +304,7 @@ private bool ShouldMoveToSchoolOrWork(ushort currentBuilding, ushort workBuildin return false; } - float distance = BuildingMgr.GetDistanceBetweenBuildings(currentBuilding, workBuilding); - float onTheWay = Mathf.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); - - gotoHour = workBeginHour - overtime - onTheWay; + gotoHour = workBeginHour - overtime - travelTime; if (gotoHour < 0) { gotoHour += 24f; @@ -462,5 +465,17 @@ private void GetWorkShiftTimes(uint citizenId, ItemClass.Service sevice, ItemCla beginHour = begin; endHour = end; } + + private float GetTravelTimeHomeToWork(uint citizenId, ushort homeBuilding, ushort workBuilding) + { + float result = residentStates[citizenId].TravelTimeToWork; + if (result <= 0) + { + float distance = BuildingMgr.GetDistanceBetweenBuildings(homeBuilding, workBuilding); + result = Mathf.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); + } + + return result; + } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 9725555f..5aec3dec 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -125,18 +125,19 @@ public void RegisterCitizenArrival(uint citizenId) return; } + ref ResidentStateDescriptor citizenState = ref residentStates[citizenId]; switch (CitizenMgr.GetCitizenLocation(citizenId)) { case Citizen.Location.Work: - ref ResidentStateDescriptor state = ref residentStates[citizenId]; - state.UpdateTravelTimeToWork(TimeInfo.Now); - state.DepartureTime = default; + citizenState.UpdateTravelTimeToWork(TimeInfo.Now); Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {residentStates[citizenId].TravelTimeToWork} hours to get to work"); break; - default: + case Citizen.Location.Moving: return; } + + citizenState.DepartureTime = default; } private bool ShouldRealizeCitizen(TAI ai) From 7d555e3d737e57e01c2856846636ee58547e52f0 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 18:30:09 +0200 Subject: [PATCH 06/42] Disable teleporting virtual citizens - teleporting might cause issues with pathfinding and parked cars - virtual citizens stay at home - citizens that are not at home are always real - one exception from above rule: moving citizens without instances --- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 8 +- .../CustomAI/RealTimeResidentAI.Common.cs | 14 +-- .../CustomAI/RealTimeResidentAI.Home.cs | 12 +-- .../CustomAI/RealTimeResidentAI.Moving.cs | 6 +- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 49 ++++------- .../CustomAI/RealTimeResidentAI.Visit.cs | 85 +++++++++---------- src/RealTime/CustomAI/RealTimeResidentAI.cs | 18 ++-- src/RealTime/CustomAI/RealTimeTouristAI.cs | 70 +++++---------- 8 files changed, 104 insertions(+), 158 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 366babb6..b0e85f9f 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -266,7 +266,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; } @@ -323,18 +323,16 @@ protected void FindEvacuationPlace(uint citizenId, TransferManager.TransferReaso /// /// 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) + 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. diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index d01b60b4..4fca304f 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -15,7 +15,7 @@ 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"); CitizenMgr.ReleaseCitizen(citizenId); return; } @@ -47,7 +47,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 +77,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,7 +96,7 @@ 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; } @@ -106,12 +106,12 @@ private void ProcessCitizenEvacuation(TAI instance, uint citizenId, ref TCitizen ushort building = CitizenProxy.GetCurrentBuilding(ref citizen); if (building != 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} is trying to find an evacuation place"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is trying to find an evacuation place"); residentAI.FindEvacuationPlace(instance, citizenId, building, residentAI.GetEvacuationReason(instance, building)); } } - private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding, bool isVirtual) + private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding) { if (visitBuilding == 0) { @@ -120,7 +120,7 @@ private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitiz ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if (isVirtual || currentBuilding == visitBuilding) + if (currentBuilding == visitBuilding) { CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs index af3ab18f..26201c8a 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs @@ -9,11 +9,11 @@ namespace RealTime.CustomAI internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen citizen) { if (CitizenProxy.GetHomeBuilding(ref citizen) == 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: at home with no home building. Releasing the poor citizen."); CitizenMgr.ReleaseCitizen(citizenId); return; } @@ -21,11 +21,11 @@ private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen cit 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}"); + Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at home but vehicle = {vehicle}"); return; } - if (CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual)) + if (CitizenGoesWorking(instance, citizenId, ref citizen)) { return; } @@ -35,12 +35,12 @@ private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen cit return; } - if (CitizenGoesShopping(instance, citizenId, ref citizen, isVirtual) || CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) + if (CitizenGoesShopping(instance, citizenId, ref citizen) || CitizenGoesToEvent(instance, citizenId, ref citizen)) { return; } - CitizenGoesRelaxing(instance, citizenId, ref citizen, isVirtual); + CitizenGoesRelaxing(instance, citizenId, ref citizen); } private bool IsBusyAtHomeInTheMorning(Citizen.AgeGroup citizenAge) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index fe7adf45..9d47fd56 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -26,8 +26,6 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit } 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); } @@ -37,7 +35,7 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit 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."); TransferMgr.AddOutgoingOfferFromCurrentPosition(citizenId, residentAI.GetEvacuationReason(instance, 0)); return; } @@ -54,7 +52,7 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit return; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} cancels the trip to a park due to bad weather"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} cancels the trip to a park due to bad weather"); ushort home = CitizenProxy.GetHomeBuilding(ref citizen); if (home != 0) { diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 78011a9e..55b00908 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -162,12 +162,12 @@ private void CalculateWorkShiftValues() nightShiftValue = Config.NightShiftQuota - 1; } - private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCitizen citizen) { 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."); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: at school/work with no work building. Teleporting home."); CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); return; } @@ -175,15 +175,15 @@ private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCit ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); if (ShouldGoToLunch(CitizenProxy.GetAge(ref citizen), citizenId)) { - ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); + ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); if (lunchPlace != 0) { residentStates[citizenId].WorkStatus = WorkStatus.AtLunch; - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} is going for lunch from {currentBuilding} to {lunchPlace}"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going for lunch from {currentBuilding} to {lunchPlace}"); } 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(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); } return; @@ -194,27 +194,20 @@ private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCit return; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} leaves their workplace {workBuilding}"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} leaves their workplace {workBuilding}"); - if (CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) + if (CitizenGoesToEvent(instance, citizenId, ref citizen)) { return; } - if (!CitizenGoesShopping(instance, citizenId, ref citizen, isVirtual) && !CitizenGoesRelaxing(instance, citizenId, ref citizen, isVirtual)) + if (!CitizenGoesShopping(instance, citizenId, ref citizen) && !CitizenGoesRelaxing(instance, citizenId, ref citizen)) { - if (isVirtual) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - } - else - { - residentAI.StartMoving(instance, citizenId, ref citizen, workBuilding, CitizenProxy.GetHomeBuilding(ref citizen)); - } + residentAI.StartMoving(instance, citizenId, ref citizen, workBuilding, CitizenProxy.GetHomeBuilding(ref citizen)); } } - private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citizen) { ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); @@ -225,17 +218,11 @@ private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citiz return false; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} is going from {currentBuilding} to school/work {workBuilding}"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {workBuilding}"); - if (isVirtual) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Work); - } - else - { - residentStates[citizenId].DepartureTime = TimeInfo.Now; - residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); - } + ref ResidentStateDescriptor state = ref residentStates[citizenId]; + state.DepartureTime = TimeInfo.Now; + residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); return true; } @@ -403,7 +390,7 @@ private bool ShouldGoToLunch(Citizen.AgeGroup citizenAge, uint citizenId) return false; } - private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen citizen) { if (IsLunchHour) { @@ -413,12 +400,12 @@ private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); if (workBuilding != 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, $"{GetCitizenDesc(citizenId, ref citizen)} returning from lunch to {workBuilding}"); + ReturnFromVisit(instance, citizenId, ref citizen, workBuilding, Citizen.Location.Work); } else { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is at lunch but no work building. Teleporting home."); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at lunch but no work building. Teleporting home."); CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 66db57ff..03d5776c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -10,12 +10,12 @@ namespace RealTime.CustomAI internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint citizenId, ref TCitizen citizen, bool isVirtual) + private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (currentBuilding == 0) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} is in corrupt state: visiting with no visit building. Teleporting home."); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: visiting with no visit building. Teleporting home."); CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); return; } @@ -23,7 +23,7 @@ private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint switch (citizenState) { case ResidentState.AtLunch: - CitizenReturnsFromLunch(instance, citizenId, ref citizen, isVirtual); + CitizenReturnsFromLunch(instance, citizenId, ref citizen); return; case ResidentState.AtLeisureArea: @@ -37,9 +37,9 @@ private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint goto case ResidentState.Visiting; case ResidentState.Visiting: - if (!CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual)) + if (!CitizenGoesWorking(instance, citizenId, ref citizen)) { - CitizenReturnsHomeFromVisit(instance, citizenId, ref citizen, isVirtual); + CitizenReturnsHomeFromVisit(instance, citizenId, ref citizen); } return; @@ -51,16 +51,16 @@ private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - if (CitizenGoesWorking(instance, citizenId, ref citizen, isVirtual) - || CitizenGoesToEvent(instance, citizenId, ref citizen, isVirtual)) + if (CitizenGoesWorking(instance, citizenId, ref citizen) + || CitizenGoesToEvent(instance, citizenId, ref citizen)) { return; } if (Random.ShouldOccur(ReturnFromShoppingChance) || IsWorkDayMorning(CitizenProxy.GetAge(ref citizen))) { - 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); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from shopping at {currentBuilding} back home"); + ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen), Citizen.Location.Home); } return; @@ -77,12 +77,12 @@ private void ProcessCitizenOnTour(TAI instance, uint citizenId, ref TCitizen cit ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); if (homeBuilding != 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, false)} exits a guided tour and moves back home."); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} exits a guided tour and moves back home."); residentAI.StartMoving(instance, citizenId, ref citizen, 0, homeBuilding); } } - private bool CitizenReturnsFromShelter(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenReturnsFromShelter(TAI instance, uint citizenId, ref TCitizen citizen) { ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (BuildingMgr.GetBuildingService(visitBuilding) != ItemClass.Service.Disaster) @@ -98,17 +98,17 @@ private bool CitizenReturnsFromShelter(TAI instance, uint citizenId, ref TCitize ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); if (homeBuilding == 0) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen, isVirtual)} was in a shelter but seems to be homeless. Releasing the citizen."); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} was in a shelter but seems to be homeless. Releasing the citizen."); CitizenMgr.ReleaseCitizen(citizenId); return true; } - 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); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from evacuation place {visitBuilding} back home"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); return true; } - private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCitizen citizen) { ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); if (homeBuilding == 0 || CitizenProxy.GetVehicle(ref citizen) != 0) @@ -124,8 +124,8 @@ private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCiti 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); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from an event at {visitBuilding} back home to {homeBuilding}"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); return true; } @@ -133,8 +133,8 @@ private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCiti if (Random.ShouldOccur(ReturnFromVisitChance) || (visitedSubService == ItemClass.SubService.CommercialLeisure && TimeInfo.IsNightTime && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} returning from visit back home"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home, isVirtual); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from visit back home"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); return true; } @@ -146,8 +146,7 @@ private void ReturnFromVisit( uint citizenId, ref TCitizen citizen, ushort targetBuilding, - Citizen.Location targetLocation, - bool isVirtual) + Citizen.Location targetLocation) { if (targetBuilding == 0 || targetLocation == Citizen.Location.Visit || CitizenProxy.GetVehicle(ref citizen) != 0) { @@ -159,7 +158,7 @@ private void ReturnFromVisit( CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); - if (isVirtual || targetBuilding == currentBuilding) + if (targetBuilding == currentBuilding) { CitizenProxy.SetLocation(ref citizen, targetLocation); } @@ -169,7 +168,7 @@ private void ReturnFromVisit( } } - private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citizen) { if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather(citizenId)) { @@ -180,8 +179,8 @@ private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citi { if (Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping at night"); - ushort localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance, isVirtual); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping at night"); + ushort localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping at night to a local shop {localVisitPlace}"); return localVisitPlace > 0; } @@ -196,8 +195,8 @@ private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citi 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.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping"); + localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping to a local shop {localVisitPlace}"); } @@ -205,11 +204,11 @@ private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citi { if (localOnly) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna go shopping, but didn't find a local shop"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} 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"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, heading to a random shop"); residentAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetHomeBuilding(ref citizen), residentAI.GetShoppingReason(instance)); } @@ -219,7 +218,7 @@ private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citi return false; } - private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citizen) { if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather(citizenId)) { @@ -231,11 +230,11 @@ private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citiz return false; } - 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); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{buildingId}', on the way now."); + return StartMovingToVisitBuilding(instance, citizenId, ref citizen, buildingId); } - private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citizen, bool isVirtual) + private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citizen) { Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); if (!Random.ShouldOccur(GetGoOutChance(citizenAge)) || IsBadWeather(citizenId)) @@ -251,8 +250,8 @@ private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citi if (TimeInfo.IsNightTime) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna relax at night"); - ushort leisure = MoveToLeisure(instance, citizenId, ref citizen, buildingId, isVirtual); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax at night"); + ushort leisure = MoveToLeisure(instance, citizenId, ref citizen, buildingId); Log.DebugIf(leisure != 0, $"Citizen {citizenId} is heading to leisure building {leisure}"); return leisure != 0; } @@ -262,16 +261,12 @@ private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citi return false; } - if (!isVirtual) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen, isVirtual)} wanna relax, heading to an entertainment place"); - residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); - } - + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax, heading to an entertainment place"); + residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); return true; } - private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance, bool isVirtual) + private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) { ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); if (buildingId == 0) @@ -286,7 +281,7 @@ private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitiz return 0; } - if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding, isVirtual)) + if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding)) { ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); uint homeUnit = BuildingMgr.GetCitizenUnit(homeBuilding); @@ -300,7 +295,7 @@ private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitiz return foundBuilding; } - private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId, bool isVirtual) + private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId) { ushort leisureBuilding = BuildingMgr.FindActiveBuilding( buildingId, @@ -314,7 +309,7 @@ private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, return 0; } - StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding, isVirtual); + StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding); return leisureBuilding; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 5aec3dec..6504c6cf 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -60,17 +60,14 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) } ResidentState residentState = GetResidentState(citizenId, ref citizen); - bool isVirtual; - switch (residentState) { case ResidentState.MovingHome: ProcessCitizenMoving(instance, citizenId, ref citizen, false); break; - case ResidentState.AtHome: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - ProcessCitizenAtHome(instance, citizenId, ref citizen, isVirtual); + case ResidentState.AtHome when !IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen): + ProcessCitizenAtHome(instance, citizenId, ref citizen); break; case ResidentState.MovingToTarget: @@ -78,16 +75,14 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) break; case ResidentState.AtSchoolOrWork: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - ProcessCitizenAtSchoolOrWork(instance, citizenId, ref citizen, isVirtual); + ProcessCitizenAtSchoolOrWork(instance, citizenId, ref citizen); break; 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); + ProcessCitizenVisit(instance, residentState, citizenId, ref citizen); break; case ResidentState.OnTour: @@ -99,12 +94,11 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) break; case ResidentState.InShelter: - isVirtual = IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen); - CitizenReturnsFromShelter(instance, citizenId, ref citizen, isVirtual); + CitizenReturnsFromShelter(instance, citizenId, ref citizen); break; case ResidentState.Unknown: - Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen, null)} is in an UNKNOWN state! Teleporting back home"); + Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in an UNKNOWN state! Teleporting back home"); if (CitizenProxy.GetHomeBuilding(ref citizen) != 0) { CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index 0a792a7f..e5f00a4e 100644 --- a/src/RealTime/CustomAI/RealTimeTouristAI.cs +++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs @@ -71,7 +71,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,7 +97,7 @@ 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; } @@ -105,20 +105,20 @@ private void ProcessMoving(TAI instance, uint citizenId, ref TCitizen citizen) bool badWeather = IsBadWeather(citizenId); 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 +138,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; @@ -153,8 +153,8 @@ private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen, bo && !IsBadWeather(citizenId) && AttendUpcomingEvent(citizenId, ref citizen, out ushort eventBuilding)) { - 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}"); + StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), eventBuilding); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} attending an event at {eventBuilding}"); return; } @@ -178,34 +178,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(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 +201,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 +229,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); } } } From 170e7fe6627cfd3758a1c0d226f953b9e9f32981 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 19:02:51 +0200 Subject: [PATCH 07/42] Implement a math utility class to decouple from UnityEngine namespace --- src/RealTime/Config/RealTimeConfig.cs | 63 ++++++---------- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 3 +- src/RealTime/RealTime.csproj | 1 + src/RealTime/Tools/RealTimeMath.cs | 72 +++++++++++++++++++ 4 files changed, 97 insertions(+), 42 deletions(-) create mode 100644 src/RealTime/Tools/RealTimeMath.cs diff --git a/src/RealTime/Config/RealTimeConfig.cs b/src/RealTime/Config/RealTimeConfig.cs index 4d4359c0..041191da 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; /// @@ -216,42 +216,42 @@ public RealTimeConfig() /// 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, 8u); + NightShiftQuota = RealTimeMath.Clamp(NightShiftQuota, 1u, 8u); + 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; } @@ -293,22 +293,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/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 55b00908..a9ccc06c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -5,7 +5,6 @@ namespace RealTime.CustomAI { using RealTime.Tools; - using UnityEngine; using static Constants; internal sealed partial class RealTimeResidentAI @@ -459,7 +458,7 @@ private float GetTravelTimeHomeToWork(uint citizenId, ushort homeBuilding, ushor if (result <= 0) { float distance = BuildingMgr.GetDistanceBetweenBuildings(homeBuilding, workBuilding); - result = Mathf.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); + result = RealTimeMath.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); } return result; diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index 4dbe9926..74be79a5 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -134,6 +134,7 @@ + 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; + } + } +} From 0cb69f5bc883ff63a62bef28238ff9ce82d51e81 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 19:05:54 +0200 Subject: [PATCH 08/42] Improve work/lunch tracking and wakeup/extended first shift behavior --- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 17 +--------- .../CustomAI/RealTimeResidentAI.Home.cs | 9 +++--- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 32 +++++++++---------- src/RealTime/CustomAI/WorkStatus.cs | 8 ++--- 4 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index b0e85f9f..d579ca55 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -106,21 +106,6 @@ protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections c /// Gets the maximum count of the citizen instances. 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 . @@ -157,7 +142,7 @@ protected bool IsWorkDayMorning(Citizen.AgeGroup citizenAge) } float currentHour = TimeInfo.CurrentHour; - return currentHour >= TimeInfo.SunriseHour && currentHour <= workBeginHour; + return currentHour >= Config.WakeupHour && currentHour <= workBeginHour; } /// diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs index 26201c8a..a1f3b9fc 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs @@ -67,16 +67,17 @@ private bool IsBusyAtHomeInTheMorning(Citizen.AgeGroup citizenAge) } } + // TODO: make this method to a part of time simulation (no need to calculate for each citizen) private bool IsBusyAtHomeInTheMorning(float currentHour, float latestHour) { - if (currentHour >= latestHour || currentHour < EarliestWakeUp) + if (currentHour >= latestHour || currentHour < Config.WakeupHour) { return false; } - float sunriseHour = EarliestWakeUp; - float dx = latestHour - sunriseHour; - float x = currentHour - sunriseHour; + float wakeupHour = Config.WakeupHour; + float dx = latestHour - wakeupHour; + float x = currentHour - wakeupHour; // 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)); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index a9ccc06c..a767c29f 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -4,6 +4,7 @@ namespace RealTime.CustomAI { + using System; using RealTime.Tools; using static Constants; @@ -26,8 +27,6 @@ private enum WorkerShift Any } - private bool IsLunchHour => IsWorkDayAndBetweenHours(Config.LunchBegin, Config.LunchEnd); - private static bool IsBuildingActiveOnWeekend(ItemClass.Service service, ItemClass.SubService subService) { switch (service) @@ -84,7 +83,7 @@ private static int GetBuildingWorkShiftCount(ItemClass.Service service) } } - private static bool ShouldWorkAtDawn(ItemClass.Service service, ItemClass.SubService subService) + private static bool HasExtendedFirstWorkShift(ItemClass.Service service, ItemClass.SubService subService) { switch (service) { @@ -194,6 +193,7 @@ private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCit } Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} leaves their workplace {workBuilding}"); + residentStates[citizenId].WorkStatus = WorkStatus.Default; if (CitizenGoesToEvent(instance, citizenId, ref citizen)) { @@ -221,6 +221,7 @@ private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citiz ref ResidentStateDescriptor state = ref residentStates[citizenId]; state.DepartureTime = TimeInfo.Now; + state.WorkStatus = WorkStatus.AtWork; residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); return true; @@ -391,7 +392,13 @@ private bool ShouldGoToLunch(Citizen.AgeGroup citizenAge, uint citizenId) private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen citizen) { - if (IsLunchHour) + if (citizenId == 0) + { + return false; + } + + ref ResidentStateDescriptor state = ref residentStates[citizenId]; + if (state.WorkStatus != WorkStatus.AtLunch || TimeInfo.CurrentHour < Config.LunchEnd) { return false; } @@ -401,14 +408,15 @@ private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen { Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from lunch to {workBuilding}"); ReturnFromVisit(instance, citizenId, ref citizen, workBuilding, Citizen.Location.Work); + state.WorkStatus = WorkStatus.AtWork; } else { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at lunch but no work building. Teleporting home."); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at lunch but no work building."); + ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen), Citizen.Location.Home); + state.WorkStatus = WorkStatus.Default; } - residentStates[citizenId].WorkStatus = WorkStatus.Default; return true; } @@ -437,15 +445,7 @@ private void GetWorkShiftTimes(uint citizenId, ItemClass.Service sevice, ItemCla if (begin < 0 || end < 0) { end = Config.WorkEnd; - - if (ShouldWorkAtDawn(sevice, subService)) - { - begin = Mathf.Min(TimeInfo.SunriseHour, EarliestWakeUp); - } - else - { - begin = Config.WorkBegin; - } + begin = HasExtendedFirstWorkShift(sevice, subService) ? Math.Min(Config.WakeupHour, EarliestWakeUp) : Config.WorkBegin; } beginHour = begin; diff --git a/src/RealTime/CustomAI/WorkStatus.cs b/src/RealTime/CustomAI/WorkStatus.cs index 13c6b08e..0721549e 100644 --- a/src/RealTime/CustomAI/WorkStatus.cs +++ b/src/RealTime/CustomAI/WorkStatus.cs @@ -12,13 +12,13 @@ internal enum WorkStatus : byte /// No special handling. Default, + /// The citizen is at work or is heading to the work building. + AtWork, + /// The citizen takes a break and goes out for lunch. AtLunch, - /// The citizen has a day off work today. - DayOff, - - /// The citizen is on vacation. + /// The citizen is on vacation or has a day off work. OnVacation } } From ac8f3425f1f564fe60e7e1a638100966cbfbf052 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 19:26:00 +0200 Subject: [PATCH 09/42] Prepare the data structures for vacation tracking --- src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs | 6 ++++++ src/RealTime/CustomAI/ResidentStateDescriptor.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index a767c29f..83bd1b32 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -234,6 +234,12 @@ private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort homeBuilding, ushor return false; } + ref ResidentStateDescriptor state = ref residentStates[citizenId]; + if (state.WorkStatus == WorkStatus.OnVacation && state.VacationDaysLeft > 0) + { + return false; + } + ItemClass.Service buildingSevice = BuildingMgr.GetBuildingService(workBuilding); ItemClass.SubService buildingSubService = BuildingMgr.GetBuildingSubService(workBuilding); diff --git a/src/RealTime/CustomAI/ResidentStateDescriptor.cs b/src/RealTime/CustomAI/ResidentStateDescriptor.cs index d8eaebc7..44060e98 100644 --- a/src/RealTime/CustomAI/ResidentStateDescriptor.cs +++ b/src/RealTime/CustomAI/ResidentStateDescriptor.cs @@ -24,6 +24,9 @@ internal struct ResidentStateDescriptor /// public float TravelTimeToWork; + /// The number of days the citizen will be on vacation (as of current game date). + public byte VacationDaysLeft; + /// 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 . From 53632b95691d550f3dd5e14d67429f8543f80dce Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 19:43:40 +0200 Subject: [PATCH 10/42] Decouple remaining 'night' considerations from 'sunset' --- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 6 +++--- src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index d579ca55..60c3e9b4 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -160,7 +160,7 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) uint weekdayModifier; if (Config.IsWeekendEnabled) { - weekdayModifier = TimeInfo.Now.IsWeekendTime(GetSpareTimeBeginHour(citizenAge), TimeInfo.SunsetHour) + weekdayModifier = TimeInfo.Now.IsWeekendTime(GetSpareTimeBeginHour(citizenAge), Config.GoToSleepUpHour) ? 11u : 1u; } @@ -177,8 +177,8 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) } else { - float nightDuration = TimeInfo.NightDuration; - float relativeHour = currentHour - TimeInfo.SunsetHour; + float nightDuration = 24f - (Config.GoToSleepUpHour - Config.WakeupHour); + float relativeHour = currentHour - Config.GoToSleepUpHour; if (relativeHour < 0) { relativeHour += 24f; diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 03d5776c..49f6b61c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -316,8 +316,8 @@ private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, 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) + return (arriveHour >= Config.GoToSleepUpHour || TimeInfo.CurrentHour >= Config.GoToSleepUpHour + || arriveHour <= Config.WakeupHour || TimeInfo.CurrentHour <= Config.WakeupHour) && BuildingMgr.IsBuildingNoiseRestricted(building); } } From 2f17e46efac70a7dd2a912b944bb8204958647f7 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Wed, 18 Jul 2018 20:03:38 +0200 Subject: [PATCH 11/42] Revise the citizens relaxing chances --- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 60c3e9b4..0e85823b 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -153,6 +153,7 @@ protected bool IsWorkDayMorning(Citizen.AgeGroup citizenAge) /// /// A percentage value in range of 0..100 that describes the probability whether /// a citizen with provided age would go out on current time. + // TODO: make this method to a part of time simulation (no need to calculate for each citizen) protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) { float currentHour = TimeInfo.CurrentHour; @@ -169,22 +170,23 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) weekdayModifier = 1u; } - bool isDayTime = !TimeInfo.IsNightTime; + float latestGoOutHour = Config.GoToSleepUpHour - 2f; + bool isDayTime = currentHour >= Config.WakeupHour && currentHour < latestGoOutHour; float timeModifier; if (isDayTime) { - timeModifier = 5f; + timeModifier = 4f; } else { - float nightDuration = 24f - (Config.GoToSleepUpHour - Config.WakeupHour); - float relativeHour = currentHour - Config.GoToSleepUpHour; + float nightDuration = 24f - (latestGoOutHour - Config.WakeupHour); + float relativeHour = currentHour - latestGoOutHour; if (relativeHour < 0) { relativeHour += 24f; } - timeModifier = 5f / nightDuration * (nightDuration - relativeHour); + timeModifier = 3f / nightDuration * (nightDuration - relativeHour); } switch (citizenAge) @@ -196,7 +198,7 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) return (uint)((timeModifier + weekdayModifier) * timeModifier); case Citizen.AgeGroup.Senior when isDayTime: - return 80 + weekdayModifier; + return 30 + weekdayModifier; default: return 0; @@ -367,14 +369,14 @@ protected bool IsBadWeather(uint citizenId) { if (WeatherInfo.IsDisasterHazardActive) { - Log.Debug($"Citizen {citizenId} is uncomfortable because of a disaster"); + Log.Debug($"Citizen {citizenId} feels uncomfortable because of a disaster"); return true; } bool result = WeatherInfo.StayInsideChance != 0 && Random.ShouldOccur(WeatherInfo.StayInsideChance); if (result) { - Log.Debug($"Citizen {citizenId} is uncomfortable because of bad weather"); + Log.Debug($"Citizen {citizenId} feels uncomfortable because of bad weather"); } return result; From f0148e6f35546ad932ef06b85837006e822ba410 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 20 Jul 2018 18:44:05 +0200 Subject: [PATCH 12/42] Optimize current hour calculation performance (micro-optimization) --- src/RealTime/GameConnection/TimeInfo.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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; From 093a9c49c1ce3b6ec350965a7131bf0df0807c97 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 20 Jul 2018 19:03:31 +0200 Subject: [PATCH 13/42] Move frame processing logic into the designated class --- src/RealTime/CustomAI/RealTimePrivateBuildingAI.cs | 9 ++++++++- src/RealTime/Simulation/SimulationHandler.cs | 6 ++---- 2 files changed, 10 insertions(+), 5 deletions(-) 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/Simulation/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index 0ae2ddd6..1c68c27d 100644 --- a/src/RealTime/Simulation/SimulationHandler.cs +++ b/src/RealTime/Simulation/SimulationHandler.cs @@ -75,10 +75,8 @@ 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); } private static void OnNewDay(SimulationHandler sender) From da96ecacb4b47137a3e26665b9f03eb126cb10ff Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 12:46:09 +0200 Subject: [PATCH 14/42] Implement extension method for getting future time with particular hour --- src/RealTime/Tools/DateTimeExtensions.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/RealTime/Tools/DateTimeExtensions.cs b/src/RealTime/Tools/DateTimeExtensions.cs index 7a2ec904..bdaad519 100644 --- a/src/RealTime/Tools/DateTimeExtensions.cs +++ b/src/RealTime/Tools/DateTimeExtensions.cs @@ -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); + } } } From ea28f620d84ec6f2c16b046cab774f0c3efada4d Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 18:07:42 +0200 Subject: [PATCH 15/42] Implement the advanced simulation logic for #57 --- src/RealTime/Core/RealTimeCore.cs | 2 + src/RealTime/CustomAI/CitizenSchedule.cs | 100 ++++ src/RealTime/CustomAI/Constants.cs | 21 +- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 109 +---- .../CustomAI/RealTimeResidentAI.Common.cs | 279 ++++++++--- .../CustomAI/RealTimeResidentAI.Home.cs | 79 +-- .../CustomAI/RealTimeResidentAI.Moving.cs | 87 +++- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 457 +----------------- .../CustomAI/RealTimeResidentAI.Visit.cs | 326 ++++--------- src/RealTime/CustomAI/RealTimeResidentAI.cs | 105 ++-- src/RealTime/CustomAI/RealTimeTouristAI.cs | 18 +- src/RealTime/CustomAI/ResidentState.cs | 30 +- .../CustomAI/ResidentStateDescriptor.cs | 52 -- src/RealTime/CustomAI/ScheduleHint.cs | 25 + src/RealTime/CustomAI/WorkBehavior.cs | 329 +++++++++++++ src/RealTime/CustomAI/WorkShift.cs | 24 + src/RealTime/CustomAI/WorkStatus.cs | 12 +- src/RealTime/Events/RealTimeEventManager.cs | 27 ++ src/RealTime/RealTime.csproj | 6 +- src/RealTime/Simulation/CitizenProcessor.cs | 78 +++ src/RealTime/Simulation/SimulationHandler.cs | 18 +- src/RealTime/Simulation/TimeAdjustment.cs | 9 +- 22 files changed, 1149 insertions(+), 1044 deletions(-) create mode 100644 src/RealTime/CustomAI/CitizenSchedule.cs delete mode 100644 src/RealTime/CustomAI/ResidentStateDescriptor.cs create mode 100644 src/RealTime/CustomAI/ScheduleHint.cs create mode 100644 src/RealTime/CustomAI/WorkBehavior.cs create mode 100644 src/RealTime/CustomAI/WorkShift.cs create mode 100644 src/RealTime/Simulation/CitizenProcessor.cs diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index ac50412e..c7fc25b6 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -187,6 +187,7 @@ public void Stop() SimulationHandler.TimeAdjustment = null; SimulationHandler.WeatherInfo = null; SimulationHandler.Buildings = null; + SimulationHandler.CitizenProcessor = null; isEnabled = false; } @@ -229,6 +230,7 @@ private static bool SetupCustomAI( eventManager); ResidentAIPatch.RealTimeAI = realTimeResidentAI; + SimulationHandler.CitizenProcessor = new CitizenProcessor(realTimeResidentAI); TouristAIConnection touristAIConnection = TouristAIPatch.GetTouristAIConnection(); if (touristAIConnection == null) diff --git a/src/RealTime/CustomAI/CitizenSchedule.cs b/src/RealTime/CustomAI/CitizenSchedule.cs new file mode 100644 index 00000000..9d8d63e1 --- /dev/null +++ b/src/RealTime/CustomAI/CitizenSchedule.cs @@ -0,0 +1,100 @@ +// +// 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 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; + + /// 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; + } + } +} \ No newline at end of file diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index 9f0bfa7b..11b722fd 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -18,13 +18,16 @@ internal static class Constants public const float FullSearchDistance = BuildingManager.BUILDINGGRID_RESOLUTION * BuildingManager.BUILDINGGRID_CELL_SIZE / 2f; /// 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; + public const uint ReturnFromShoppingChance = 75; /// A chance in percent for a citizen to return from a visited building. - public const uint ReturnFromVisitChance = 40; + public const uint ReturnFromVisitChance = 50; + + /// 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; @@ -38,14 +41,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 = 4f; + /// 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.1f; + /// 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 0e85823b..530cf14c 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -106,45 +106,6 @@ protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections c /// Gets the maximum count of the citizen instances. protected uint CitizenInstancesMaxCount { get; } - /// - /// 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 >= Config.WakeupHour && currentHour <= workBeginHour; - } - /// /// Gets the probability whether a citizen with provided age would go out on current time. /// @@ -161,7 +122,7 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) uint weekdayModifier; if (Config.IsWeekendEnabled) { - weekdayModifier = TimeInfo.Now.IsWeekendTime(GetSpareTimeBeginHour(citizenAge), Config.GoToSleepUpHour) + weekdayModifier = TimeInfo.Now.IsWeekendTime(12f, Config.GoToSleepUpHour) ? 11u : 1u; } @@ -205,30 +166,6 @@ protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) } } - /// - /// 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. /// @@ -261,35 +198,32 @@ 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; } /// @@ -362,24 +296,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} feels 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) { - Log.Debug($"Citizen {citizenId} feels 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/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 4fca304f..e8e1f0a2 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -4,10 +4,19 @@ namespace RealTime.CustomAI { + using System; using RealTime.Tools; + using static Constants; internal sealed partial class RealTimeResidentAI { + private enum ScheduleAction + { + Ignore, + ProcessTransition, + ProcessState + } + private void ProcessCitizenDead(TAI instance, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); @@ -16,6 +25,7 @@ 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)} is released"); + residentSchedules[citizenId] = default; CitizenMgr.ReleaseCitizen(citizenId); return; } @@ -101,69 +111,89 @@ private bool ProcessCitizenSick(TAI instance, uint citizenId, ref TCitizen citiz 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) + { + schedule.Schedule(ResidentState.AtHome, default); + return; + } + + schedule.Schedule(ResidentState.InShelter, default); + if (schedule.CurrentState != ResidentState.InShelter) { Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is trying to find an evacuation place"); residentAI.FindEvacuationPlace(instance, citizenId, building, residentAI.GetEvacuationReason(instance, building)); } } - private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding) + private void ProcessCitizenInShelter(ref CitizenSchedule schedule, ref TCitizen citizen) { - if (visitBuilding == 0) + ushort shelter = CitizenProxy.GetVisitBuilding(ref citizen); + if (BuildingMgr.BuildingHasFlags(shelter, Building.Flags.Downgrading)) { - return false; + schedule.Schedule(ResidentState.Unknown, default); } + } - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - - if (currentBuilding == visitBuilding) + private ScheduleAction UpdateCitizenState(uint citizenId, ref TCitizen citizen, ref CitizenSchedule schedule) + { + if (schedule.CurrentState == ResidentState.Ignored) { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); - CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Visit); - return true; + return ScheduleAction.Ignore; } - else if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.DummyTraffic)) { - CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); - CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); - return true; + schedule.CurrentState = ResidentState.Ignored; + return ScheduleAction.Ignore; } - return false; - } - - private ResidentState GetResidentState(uint citizenId, ref TCitizen citizen) - { - if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.DummyTraffic)) + Citizen.Location location = CitizenProxy.GetLocation(ref citizen); + if (location == Citizen.Location.Moving) { - return ResidentState.Ignored; + 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,55 +202,178 @@ private ResidentState GetResidentState(uint citizenId, 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 (residentStates[citizenId].WorkStatus == WorkStatus.AtLunch) - { - return ResidentState.AtLunch; - } - - if (BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) - { - return ResidentState.AtLeisureArea; - } + case ItemClass.Service.Beautification: + 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; + break; + } - case Citizen.Location.Moving: - ushort instanceId = CitizenProxy.GetInstance(ref citizen); - if (CitizenMgr.InstanceHasFlags(instanceId, CitizenInstance.Flags.OnTour | CitizenInstance.Flags.TargetIsNode, true)) + return ScheduleAction.Ignore; + } + + 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.ScheduledState != ResidentState.Unknown) + { + return; + } + + Log.Debug($"Calculating schedule for citizen instance {CitizenProxy.GetInstance(ref citizen)}..."); + + if (schedule.WorkStatus == WorkStatus.Working) + { + schedule.WorkStatus = WorkStatus.None; + } + + DateTime nextActivityTime = TimeInfo.Now.FutureHour(Config.WakeupHour); + if (schedule.CurrentState != ResidentState.AtSchoolOrWork && workBuilding != 0) + { + DateTime workTime = ScheduleWork(ref schedule, CitizenProxy.GetCurrentBuilding(ref citizen)); + if (schedule.ScheduledState == ResidentState.AtSchoolOrWork) + { + Log.Debug($" - Schedule work at {workTime}"); + nextActivityTime = workTime; + + float timeLeft = (float)(nextActivityTime - TimeInfo.Now).TotalHours; + if (timeLeft <= PrepareToWorkHours) { - return ResidentState.OnTour; + // Just sit at home if the work time will come soon + Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + return; } - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - return homeBuilding != 0 && CitizenMgr.GetTargetBuilding(instanceId) == homeBuilding - ? ResidentState.MovingHome - : ResidentState.MovingToTarget; + if (timeLeft <= MaxTravelTime) + { + // If we have some time, try to shop locally. + Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); + ScheduleShopping(ref schedule, ref citizen, true); + return; + } + } + } + + 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) + { + Log.Debug($" - Scheduled 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) + { + ProcessCurrentState(ref schedule, ref citizen); + + if (TimeInfo.Now < schedule.ScheduledStateTime) + { + return; + } - default: - return ResidentState.Unknown; + 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 void ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citizen) + { + switch (schedule.CurrentState) + { + case ResidentState.Shopping: + ProcessCitizenShopping(ref citizen); + break; + + case ResidentState.Relaxing: + ProcessCitizenRelaxing(ref citizen); + break; + + case ResidentState.Visiting: + ProcessCitizenVisit(ref schedule, ref citizen); + break; + + case ResidentState.InShelter: + ProcessCitizenInShelter(ref schedule, ref citizen); + break; + } + } + + 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 a1f3b9fc..b3de2cbc 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs @@ -5,83 +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) + 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)} 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)} is at home but vehicle = {vehicle}"); - return; - } - - if (CitizenGoesWorking(instance, citizenId, ref citizen)) - { - return; - } - - if (IsBusyAtHomeInTheMorning(CitizenProxy.GetAge(ref citizen))) - { - return; - } - - if (CitizenGoesShopping(instance, citizenId, ref citizen) || CitizenGoesToEvent(instance, citizenId, ref citizen)) - { - return; - } - - CitizenGoesRelaxing(instance, citizenId, ref citizen); - } - - 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; - } - } - - // TODO: make this method to a part of time simulation (no need to calculate for each citizen) - private bool IsBusyAtHomeInTheMorning(float currentHour, float latestHour) - { - if (currentHour >= latestHour || currentHour < Config.WakeupHour) - { - return false; - } - - float wakeupHour = Config.WakeupHour; - float dx = latestHour - wakeupHour; - float x = currentHour - wakeupHour; - - // 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 9d47fd56..333718eb 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -5,10 +5,11 @@ namespace RealTime.CustomAI { using RealTime.Tools; + using static Constants; internal sealed partial class RealTimeResidentAI { - private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen citizen, bool mayCancel) + private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen citizen) { ushort instanceId = CitizenProxy.GetInstance(ref citizen); ushort vehicleId = CitizenProxy.GetVehicle(ref citizen); @@ -23,6 +24,7 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.MovingIn)) { CitizenMgr.ReleaseCitizen(citizenId); + residentSchedules[citizenId] = default; } else { @@ -47,17 +49,88 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit } ItemClass.Service targetService = BuildingMgr.GetBuildingService(targetBuilding); - if (targetService != ItemClass.Service.Beautification || !IsBadWeather(citizenId)) + if (targetService == ItemClass.Service.Beautification && IsBadWeather()) { - return; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} cancels the trip to a park due to bad weather"); + ushort home = CitizenProxy.GetHomeBuilding(ref citizen); + if (home != 0) + { + residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); + } } + } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} cancels the trip to a park due to bad weather"); - ushort home = CitizenProxy.GetHomeBuilding(ref citizen); - if (home != 0) + private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) + { + ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); + if (buildingId == 0) { - residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); + return 0; } + + ushort foundBuilding = BuildingMgr.FindActiveBuilding(buildingId, distance, ItemClass.Service.Commercial); + if (IsBuildingNoiseRestricted(foundBuilding)) + { + Log.Debug($"Citizen {citizenId} won't go to the commercial building {foundBuilding}, it has a NIMBY policy"); + return 0; + } + + if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding)) + { + 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); + } + } + + return foundBuilding; + } + + private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId) + { + ushort leisureBuilding = BuildingMgr.FindActiveBuilding( + buildingId, + LeisureSearchDistance, + ItemClass.Service.Commercial, + ItemClass.SubService.CommercialLeisure); + + if (IsBuildingNoiseRestricted(leisureBuilding)) + { + 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 83bd1b32..d58635b8 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -10,464 +10,47 @@ namespace RealTime.CustomAI 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 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) - { - switch (service) - { - 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; - } - } - - 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: - return true; - - default: - return false; - } - } - - private static bool CheckMinimumShiftDuration(float beginHour, float endHour) - { - if (beginHour < endHour) - { - return endHour - beginHour >= MinimumWorkShiftDuration; - } - else - { - return 24f - beginHour + endHour >= MinimumWorkShiftDuration; - } - } - - private static bool IsWorkHour(float currentHour, float gotoWorkHour, float leaveWorkHour) - { - if (gotoWorkHour < leaveWorkHour) - { - if (currentHour >= leaveWorkHour || currentHour < gotoWorkHour) - { - return false; - } - } - else - { - if (currentHour >= leaveWorkHour && currentHour < gotoWorkHour) - { - return false; - } - } - - 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() + private DateTime ScheduleWork(ref CitizenSchedule schedule, ushort currentBuilding) { - secondShiftValue = Config.SecondShiftQuota - 1; - nightShiftValue = Config.NightShiftQuota - 1; + return workBehavior.ScheduleGoToWork(ref schedule, currentBuilding, simulationCycle) + ? schedule.ScheduledStateTime + : default; } - private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCitizen citizen) + private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); - if (workBuilding == 0) - { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} 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); - if (lunchPlace != 0) - { - residentStates[citizenId].WorkStatus = WorkStatus.AtLunch; - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going for lunch from {currentBuilding} to {lunchPlace}"); - } - else - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); - } + schedule.WorkStatus = WorkStatus.Working; - return; - } - - if (!ShouldReturnFromSchoolOrWork(citizenId, currentBuilding, CitizenProxy.GetAge(ref citizen))) + if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, schedule.WorkBuilding) + && schedule.CurrentState == ResidentState.AtHome) { - return; + schedule.DepartureToWorkTime = TimeInfo.Now; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} leaves their workplace {workBuilding}"); - residentStates[citizenId].WorkStatus = WorkStatus.Default; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding}"); - if (CitizenGoesToEvent(instance, citizenId, ref citizen)) + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + if (!workBehavior.ScheduleLunch(ref schedule, citizenAge)) { - return; - } - - if (!CitizenGoesShopping(instance, citizenId, ref citizen) && !CitizenGoesRelaxing(instance, citizenId, ref citizen)) - { - residentAI.StartMoving(instance, citizenId, ref citizen, workBuilding, CitizenProxy.GetHomeBuilding(ref citizen)); + workBehavior.ScheduleReturnFromWork(ref schedule, citizenAge); } } - private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citizen) + private void DoScheduledLunch(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); - - if (!ShouldMoveToSchoolOrWork(citizenId, homeBuilding, currentBuilding, workBuilding, CitizenProxy.GetAge(ref citizen))) - { - return false; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {workBuilding}"); - - ref ResidentStateDescriptor state = ref residentStates[citizenId]; - state.DepartureTime = TimeInfo.Now; - state.WorkStatus = WorkStatus.AtWork; - residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); - - return true; - } - - private bool ShouldMoveToSchoolOrWork(uint citizenId, ushort homeBuilding, ushort currentBuilding, ushort workBuilding, Citizen.AgeGroup citizenAge) - { - if (workBuilding == 0 || citizenAge == Citizen.AgeGroup.Senior) - { - return false; - } - - ref ResidentStateDescriptor state = ref residentStates[citizenId]; - if (state.WorkStatus == WorkStatus.OnVacation && state.VacationDaysLeft > 0) - { - return false; - } - - ItemClass.Service buildingSevice = BuildingMgr.GetBuildingService(workBuilding); - ItemClass.SubService buildingSubService = BuildingMgr.GetBuildingSubService(workBuilding); - - if (IsWeekend && !IsBuildingActiveOnWeekend(buildingSevice, buildingSubService)) - { - return false; - } - - // TODO: replace the day off logic. - if ((citizenId & 0x7FF) == TimeInfo.Now.Day) - { - Log.Debug(TimeInfo.Now, $"Citizen {citizenId} has a day off work today"); - return false; - } - - float travelTime = currentBuilding == homeBuilding - ? GetTravelTimeHomeToWork(citizenId, homeBuilding, workBuilding) - : 0; - - if (citizenAge == Citizen.AgeGroup.Child || citizenAge == Citizen.AgeGroup.Teen) - { - return ShouldMoveToSchoolOrWork(workBuilding, Config.SchoolBegin, Config.SchoolEnd, 0, travelTime); - } - - GetWorkShiftTimes(citizenId, buildingSevice, buildingSubService, out float workBeginHour, out float workEndHour); - if (!CheckMinimumShiftDuration(workBeginHour, workEndHour)) - { - return false; - } - - float overtime = currentBuilding != homeBuilding || Random.ShouldOccur(Config.OnTimeQuota) - ? 0 - : Config.MaxOvertime * Random.GetRandomValue(100u) / 200f; - - return ShouldMoveToSchoolOrWork(workBuilding, workBeginHour, workEndHour, overtime, travelTime); - } - - private bool ShouldMoveToSchoolOrWork(ushort workBuilding, float workBeginHour, float workEndHour, float overtime, float travelTime) - { - float gotoHour = workBeginHour - overtime - MaxHoursOnTheWay; - if (gotoHour < 0) - { - gotoHour += 24f; - } - - float leaveHour = workEndHour + overtime; - if (leaveHour >= 24f) - { - leaveHour -= 24f; - } - - float currentHour = TimeInfo.CurrentHour; - if (!IsWorkHour(currentHour, gotoHour, leaveHour)) - { - return false; - } - - gotoHour = workBeginHour - overtime - travelTime; - 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); - } - } - else - { - if (currentHour >= latestLeaveHour && currentHour < earliestGotoHour) - { - return true; - } - else if (currentHour >= workEndHour && currentHour < earliestGotoHour) - { - return Random.ShouldOccur(Config.OnTimeQuota); - } - } - - 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) - { - if (citizenId == 0) - { - return false; - } - - ref ResidentStateDescriptor state = ref residentStates[citizenId]; - if (state.WorkStatus != WorkStatus.AtLunch || TimeInfo.CurrentHour < Config.LunchEnd) - { - return false; - } - - ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); - if (workBuilding != 0) + ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + if (lunchPlace != 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from lunch to {workBuilding}"); - ReturnFromVisit(instance, citizenId, ref citizen, workBuilding, Citizen.Location.Work); - state.WorkStatus = WorkStatus.AtWork; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going for lunch from {currentBuilding} to {lunchPlace}"); + workBehavior.ScheduleReturnFromLunch(ref schedule); } else { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at lunch but no work building."); - ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen), Citizen.Location.Home); - state.WorkStatus = WorkStatus.Default; - } - - 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; - begin = HasExtendedFirstWorkShift(sevice, subService) ? Math.Min(Config.WakeupHour, EarliestWakeUp) : Config.WorkBegin; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); + workBehavior.ScheduleReturnFromWork(ref schedule, CitizenProxy.GetAge(ref citizen)); } - - beginHour = begin; - endHour = end; - } - - private float GetTravelTimeHomeToWork(uint citizenId, ushort homeBuilding, ushort workBuilding) - { - float result = residentStates[citizenId].TravelTimeToWork; - if (result <= 0) - { - float distance = BuildingMgr.GetDistanceBetweenBuildings(homeBuilding, workBuilding); - result = RealTimeMath.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); - } - - return result; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 49f6b61c..a3142a93 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -4,173 +4,97 @@ 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) + private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { - ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - if (currentBuilding == 0) - { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: visiting with no visit building. Teleporting home."); - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - return; - } - - switch (citizenState) - { - case ResidentState.AtLunch: - CitizenReturnsFromLunch(instance, citizenId, ref citizen); - return; - - 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); - } - - goto case ResidentState.Visiting; - - case ResidentState.Visiting: - if (!CitizenGoesWorking(instance, citizenId, ref citizen)) - { - CitizenReturnsHomeFromVisit(instance, citizenId, ref citizen); - } - - return; - - case ResidentState.Shopping: - if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)) - { - BuildingMgr.ModifyMaterialBuffer(CitizenProxy.GetVisitBuilding(ref citizen), TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); - } - - if (CitizenGoesWorking(instance, citizenId, ref citizen) - || CitizenGoesToEvent(instance, citizenId, ref citizen)) - { - return; - } - - if (Random.ShouldOccur(ReturnFromShoppingChance) || IsWorkDayMorning(CitizenProxy.GetAge(ref citizen))) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from shopping at {currentBuilding} back home"); - ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen), Citizen.Location.Home); - } - - return; - } - } - - private void ProcessCitizenOnTour(TAI instance, uint citizenId, ref TCitizen citizen) - { - if (!CitizenMgr.InstanceHasFlags(CitizenProxy.GetInstance(ref citizen), CitizenInstance.Flags.TargetIsNode)) - { - return; - } - - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding != 0) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} exits a guided tour and moves back home."); - residentAI.StartMoving(instance, citizenId, ref citizen, 0, homeBuilding); - } - } - - private bool CitizenReturnsFromShelter(TAI instance, uint citizenId, 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)) + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + if (!Random.ShouldOccur(GetGoOutChance(citizenAge)) || IsBadWeather()) { return false; } - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding == 0) + ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen); + if (cityEvent != null) { - Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} was in a shelter but seems to be homeless. Releasing the citizen."); - CitizenMgr.ReleaseCitizen(citizenId); + 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; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from evacuation place {visitBuilding} back home"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); + schedule.Schedule(ResidentState.Relaxing, default); + schedule.Hint = TimeInfo.IsNightTime + ? ScheduleHint.RelaxAtLeisureBuilding + : ScheduleHint.None; + return true; } - private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCitizen citizen) + private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); - if (homeBuilding == 0 || CitizenProxy.GetVehicle(ref citizen) != 0) - { - return false; - } + ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - switch (EventMgr.GetEventState(visitBuilding, TimeInfo.Now.AddHours(MaxHoursOnTheWay))) + switch (schedule.Hint) { - case CityEventState.Upcoming: - case CityEventState.Ongoing: - return false; + case ScheduleHint.RelaxAtLeisureBuilding: + schedule.Schedule(ResidentState.Unknown, default); - case CityEventState.Finished: - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from an event at {visitBuilding} back home to {homeBuilding}"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); - return true; - } + ushort leisure = MoveToLeisureBuilding(instance, citizenId, ref citizen, buildingId); + if (leisure != 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} heading to a leisure building {leisure}"); + } - ItemClass.SubService visitedSubService = BuildingMgr.GetBuildingSubService(visitBuilding); - if (Random.ShouldOccur(ReturnFromVisitChance) || - (visitedSubService == ItemClass.SubService.CommercialLeisure && TimeInfo.IsNightTime && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from visit back home"); - ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding, Citizen.Location.Home); - return true; - } + return; - return false; - } + case ScheduleHint.AttendingEvent: + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{schedule.EventBuilding}', on the way now."); + StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding); + ICityEvent cityEvent = EventMgr.GetUpcomingCityEvent(schedule.EventBuilding); + DateTime returnTime = cityEvent == null + ? default + : cityEvent.EndTime; - private void ReturnFromVisit( - TAI instance, - uint citizenId, - ref TCitizen citizen, - ushort targetBuilding, - Citizen.Location targetLocation) - { - if (targetBuilding == 0 || targetLocation == Citizen.Location.Visit || CitizenProxy.GetVehicle(ref citizen) != 0) - { - return; + schedule.Schedule(ResidentState.Unknown, returnTime); + schedule.EventBuilding = 0; + return; } - ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + ResidentState nextState = Random.ShouldOccur(ReturnFromVisitChance) + ? ResidentState.Unknown + : ResidentState.Relaxing; - CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); - CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + schedule.Schedule(nextState, default); - if (targetBuilding == currentBuilding) + if (schedule.CurrentState != ResidentState.Relaxing) { - CitizenProxy.SetLocation(ref citizen, targetLocation); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax, heading to an entertainment building."); + residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); } - else + } + + private void ProcessCitizenRelaxing(ref TCitizen citizen) + { + ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) + && BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) { - residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, targetBuilding); + // 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); } } - private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citizen) + private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen, bool localOnly) { - if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather(citizenId)) + if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather()) { return false; } @@ -179,143 +103,97 @@ private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citi { if (Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping at night"); - ushort localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); - Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping at night to a local shop {localVisitPlace}"); - return localVisitPlace > 0; + schedule.Hint = ScheduleHint.LocalShoppingOnly; + schedule.Schedule(ResidentState.Shopping, default); + return true; } return false; } + schedule.Hint = ScheduleHint.None; if (Random.ShouldOccur(GoShoppingChance)) { - bool localOnly = CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(CitizenProxy.GetAge(ref citizen)); - ushort localVisitPlace = 0; - - if (Random.ShouldOccur(Config.LocalBuildingSearchQuota)) + if (localOnly || Random.ShouldOccur(Config.LocalBuildingSearchQuota)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping"); - localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); - Log.DebugIf(localVisitPlace != 0, $"Citizen {citizenId} is going shopping to a local shop {localVisitPlace}"); - } - - if (localVisitPlace == 0) - { - if (localOnly) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, but didn't find a local shop"); - return false; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, heading to a random shop"); - residentAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetHomeBuilding(ref citizen), residentAI.GetShoppingReason(instance)); + schedule.Hint = ScheduleHint.LocalShoppingOnly; } + schedule.Schedule(ResidentState.Shopping, default); return true; } return false; } - private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citizen) + private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather(citizenId)) - { - return false; - } + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if (!AttendUpcomingEvent(citizenId, ref citizen, out ushort buildingId)) + if ((schedule.Hint & ScheduleHint.LocalShoppingOnly) != 0) { - return false; - } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{buildingId}', on the way now."); - return StartMovingToVisitBuilding(instance, citizenId, ref citizen, buildingId); - } + schedule.Schedule(ResidentState.Unknown, default); - private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citizen) - { - Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); - if (!Random.ShouldOccur(GetGoOutChance(citizenAge)) || IsBadWeather(citizenId)) - { - return false; + ushort shop = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + if (shop == 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted go shopping, but didn't find a local shop"); + } } - - ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - if (buildingId == 0) + else { - return false; - } + ResidentState nextState = Random.ShouldOccur(ReturnFromShoppingChance) + ? ResidentState.Unknown + : ResidentState.Shopping; - if (TimeInfo.IsNightTime) - { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax at night"); - ushort leisure = MoveToLeisure(instance, citizenId, ref citizen, buildingId); - Log.DebugIf(leisure != 0, $"Citizen {citizenId} is heading to leisure building {leisure}"); - return leisure != 0; - } + schedule.Schedule(nextState, default); - if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(citizenAge)) - { - return false; + if (schedule.CurrentState != ResidentState.Shopping) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, heading to a random shop"); + residentAI.FindVisitPlace(instance, citizenId, currentBuilding, residentAI.GetShoppingReason(instance)); + } } - - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax, heading to an entertainment place"); - residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); - return true; } - private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) + private void ProcessCitizenShopping(ref TCitizen citizen) { - ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - if (buildingId == 0) - { - return 0; - } - - ushort foundBuilding = BuildingMgr.FindActiveBuilding(buildingId, distance, ItemClass.Service.Commercial); - if (IsBuildingNoiseRestricted(foundBuilding)) + if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)) { - Log.Debug($"Citizen {citizenId} won't go to the commercial building {foundBuilding}, it has a NIMBY policy"); - return 0; + return; } - if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding)) + ushort shop = CitizenProxy.GetVisitBuilding(ref citizen); + if (shop == 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) - { - CitizenMgr.ModifyUnitGoods(citizenUnit, ShoppingGoodsAmount); - } + return; } - return foundBuilding; + BuildingMgr.ModifyMaterialBuffer(shop, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId) + private void ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) { - ushort leisureBuilding = BuildingMgr.FindActiveBuilding( - buildingId, - LeisureSearchDistance, - ItemClass.Service.Commercial, - ItemClass.SubService.CommercialLeisure); + ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - if (IsBuildingNoiseRestricted(leisureBuilding)) + if (schedule.Hint == ScheduleHint.OnTour + || Random.ShouldOccur(ReturnFromVisitChance) + || (BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.CommercialLeisure + && TimeInfo.IsNightTime + && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) { - Log.Debug($"Citizen {citizenId} won't go to the leisure building {leisureBuilding}, it has a NIMBY policy"); - return 0; + schedule.Schedule(ResidentState.Unknown, default); + } + else + { + schedule.Schedule(ResidentState.Visiting, default); } - - StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding); - return leisureBuilding; } private bool IsBuildingNoiseRestricted(ushort building) { - float arriveHour = (float)TimeInfo.Now.AddHours(MaxHoursOnTheWay).TimeOfDay.TotalHours; + float arriveHour = (float)TimeInfo.Now.AddHours(MaxTravelTime).TimeOfDay.TotalHours; return (arriveHour >= Config.GoToSleepUpHour || TimeInfo.CurrentHour >= Config.GoToSleepUpHour || arriveHour <= Config.WakeupHour || TimeInfo.CurrentHour <= Config.WakeupHour) && BuildingMgr.IsBuildingNoiseRestricted(building); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 6504c6cf..a6ff2761 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -17,7 +17,9 @@ internal sealed partial class RealTimeResidentAI : RealTimeHumanA where TCitizen : struct { private readonly ResidentAIConnection residentAI; - private readonly ResidentStateDescriptor[] residentStates; + private readonly WorkBehavior workBehavior; + private readonly CitizenSchedule[] residentSchedules; + private float simulationCycle; /// Initializes a new instance of the class. /// Thrown when any argument is null. @@ -33,7 +35,8 @@ public RealTimeResidentAI( : base(config, connections, eventManager) { this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); - residentStates = new ResidentStateDescriptor[CitizenMgr.GetMaxCitizensCount()]; + residentSchedules = new CitizenSchedule[CitizenMgr.GetMaxCitizensCount()]; + workBehavior = new WorkBehavior(config, connections.Random, connections.BuildingManager, connections.TimeInfo, GetEstimatedTravelTime); } /// The entry method of the custom AI. @@ -44,99 +47,99 @@ 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(citizenId, ref citizen); - switch (residentState) + ScheduleAction actionType = UpdateCitizenState(citizenId, ref citizen, ref schedule); + switch (actionType) { - case ResidentState.MovingHome: - ProcessCitizenMoving(instance, citizenId, ref citizen, false); - break; - - case ResidentState.AtHome when !IsCitizenVirtual(instance, ref citizen, ShouldRealizeCitizen): - ProcessCitizenAtHome(instance, citizenId, ref citizen); - break; - - case ResidentState.MovingToTarget: - ProcessCitizenMoving(instance, citizenId, ref citizen, true); - break; - - case ResidentState.AtSchoolOrWork: - ProcessCitizenAtSchoolOrWork(instance, citizenId, ref citizen); - break; - - case ResidentState.AtLunch: - case ResidentState.Shopping: - case ResidentState.AtLeisureArea: - case ResidentState.Visiting: - ProcessCitizenVisit(instance, residentState, citizenId, ref citizen); - break; - - case ResidentState.OnTour: - ProcessCitizenOnTour(instance, citizenId, ref citizen); - break; - - case ResidentState.Evacuating: - ProcessCitizenEvacuation(instance, citizenId, ref citizen); - break; + case ScheduleAction.Ignore: + return; - case ResidentState.InShelter: - CitizenReturnsFromShelter(instance, citizenId, ref citizen); - break; + case ScheduleAction.ProcessTransition: + ProcessCitizenMoving(instance, citizenId, ref citizen); + return; + } - case ResidentState.Unknown: - Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in an UNKNOWN state! Teleporting back home"); - if (CitizenProxy.GetHomeBuilding(ref citizen) != 0) - { - CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); - } + 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; + } - break; + if (TimeInfo.Now < schedule.ScheduledStateTime) + { + return; } - residentStates[citizenId].State = residentState; + UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); + ExecuteCitizenSchedule(ref schedule, instance, citizenId, ref citizen); } /// Notifies that a citizen has arrived their destination. /// The citizen ID to process. public void RegisterCitizenArrival(uint citizenId) { - if (citizenId == 0 || citizenId >= residentStates.Length) + if (citizenId == 0 || citizenId >= residentSchedules.Length) { return; } - ref ResidentStateDescriptor citizenState = ref residentStates[citizenId]; + ref CitizenSchedule citizenState = ref residentSchedules[citizenId]; switch (CitizenMgr.GetCitizenLocation(citizenId)) { case Citizen.Location.Work: citizenState.UpdateTravelTimeToWork(TimeInfo.Now); - Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {residentStates[citizenId].TravelTimeToWork} hours to get to work"); + Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {residentSchedules[citizenId].TravelTimeToWork} hours to get to work"); break; case Citizen.Location.Moving: return; } - citizenState.DepartureTime = default; + citizenState.DepartureToWorkTime = default; + } + + /// Performs simulation for starting a new day for all citizens. + public void BeginNewDay() + { + workBehavior.UpdateLunchTime(); + } + + /// Performs simulation for starting a new day for a citizen with specified ID. + /// The citizen ID to process. + public void BeginNewDayForCitizen(uint citizenId) + { + 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) { - return residentAI.DoRandomMove(ai); + simulationCycle = cyclePeriod; + Log.Debug($"SIMULATION CYCLE PERIOD: {cyclePeriod} hours"); } } } \ No newline at end of file diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index e5f00a4e..8f95e801 100644 --- a/src/RealTime/CustomAI/RealTimeTouristAI.cs +++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs @@ -102,7 +102,7 @@ private void ProcessMoving(TAI instance, uint citizenId, ref TCitizen 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)} exits the guided tour."); @@ -149,13 +149,15 @@ private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen) 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); - Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} 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; @@ -191,7 +193,7 @@ private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen cit return; } - if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather(citizenId)) + if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather()) { FindHotel(instance, citizenId, ref citizen); return; diff --git a/src/RealTime/CustomAI/ResidentState.cs b/src/RealTime/CustomAI/ResidentState.cs index 345f47e9..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. + /// + /// 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/ResidentStateDescriptor.cs b/src/RealTime/CustomAI/ResidentStateDescriptor.cs deleted file mode 100644 index 44060e98..00000000 --- a/src/RealTime/CustomAI/ResidentStateDescriptor.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) dymanoid. All rights reserved. - -namespace RealTime.CustomAI -{ - using System; - using RealTime.Tools; - using static Constants; - - /// A container struct that holds information about the detailed resident citizen state. - internal struct ResidentStateDescriptor - { - /// The citizen's last known state. - public ResidentState State; - - /// The citizen's current work status. - public WorkStatus WorkStatus; - - /// The time when citizen started their last movement in the city. - public DateTime DepartureTime; - - /// - /// Gets or sets the travel time (in hours) from citizen's home to the work building. The maximum value is - /// determined by the constant. - /// - public float TravelTimeToWork; - - /// The number of days the citizen will be on vacation (as of current game date). - public byte VacationDaysLeft; - - /// 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 < DepartureTime || DepartureTime == default) - { - return; - } - - float onTheWayHours = (float)(arrivalTime - DepartureTime).TotalHours; - if (onTheWayHours > MaxHoursOnTheWay) - { - onTheWayHours = MaxHoursOnTheWay; - } - - TravelTimeToWork = TravelTimeToWork == 0 - ? onTheWayHours - : (TravelTimeToWork + onTheWayHours) / 2; - } - } -} \ No newline at end of file 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/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs new file mode 100644 index 00000000..b74549d1 --- /dev/null +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -0,0 +1,329 @@ +// +// 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); + float workBegin, workEnd; + WorkShift 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: + workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice)); + workBegin = config.WorkBegin; + workEnd = config.WorkEnd; + break; + + default: + return; + } + + ItemClass.SubService buildingSubService = buildingManager.GetBuildingSubService(schedule.WorkBuilding); + switch (schedule.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; + } + + if (config.IsWeekendEnabled && timeInfo.Now.IsWeekend() && !schedule.WorksOnWeekends) + { + return false; + } + + float travelTime = GetTravelTimeToWork(ref schedule, currentBuilding); + float departureHour = schedule.WorkShiftStartHour - travelTime - simulationCycle; + schedule.Schedule(ResidentState.AtSchoolOrWork, timeInfo.Now.FutureHour(departureHour)); + 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 (schedule.WorkStatus == WorkStatus.Working + && schedule.CurrentState == ResidentState.AtSchoolOrWork + && 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.CurrentState != ResidentState.Shopping) + { + return; + } + + 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.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) + { + switch (service) + { + 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; + } + } + + 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: + 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 index 0721549e..5c0ae73a 100644 --- a/src/RealTime/CustomAI/WorkStatus.cs +++ b/src/RealTime/CustomAI/WorkStatus.cs @@ -10,15 +10,9 @@ namespace RealTime.CustomAI internal enum WorkStatus : byte { /// No special handling. - Default, + None, - /// The citizen is at work or is heading to the work building. - AtWork, - - /// The citizen takes a break and goes out for lunch. - AtLunch, - - /// The citizen is on vacation or has a day off work. - OnVacation + /// The citizen has working hours. + Working } } diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs index f79ccdb6..5c109c72 100644 --- a/src/RealTime/Events/RealTimeEventManager.cs +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -172,6 +172,33 @@ public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime late : null; } + /// + /// Gets the instance of an 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 GetUpcomingCityEvent(ushort buildingId) + { + if (buildingId == 0 || 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 . diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index 74be79a5..f249d1d2 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -64,8 +64,11 @@ - + + + + @@ -124,6 +127,7 @@ + diff --git a/src/RealTime/Simulation/CitizenProcessor.cs b/src/RealTime/Simulation/CitizenProcessor.cs new file mode 100644 index 00000000..e19db6ed --- /dev/null +++ b/src/RealTime/Simulation/CitizenProcessor.cs @@ -0,0 +1,78 @@ +// +// 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 int dayStartFrame; + + /// Initializes a new instance of the class. + /// The custom resident AI implementation. + /// Thrown when the argument is null. + public CitizenProcessor(RealTimeResidentAI residentAI) + { + this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); + 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) + { + residentAI.SetSimulationCyclePeriod(frameDuration * (StepMask + 1)); + } + + /// 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/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index 1c68c27d..30eb8de1 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,18 @@ 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 (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); } } @@ -77,6 +76,7 @@ public override void OnBeforeSimulationFrame() { 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. From 43193c9a7229d6d32a477d59da2f933afdb4282f Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 19:12:20 +0200 Subject: [PATCH 16/42] Improve performance by consolidating frame-common calculations (#57) --- src/RealTime/Core/RealTimeCore.cs | 11 ++- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 60 ----------- .../CustomAI/RealTimeResidentAI.Visit.cs | 4 +- src/RealTime/CustomAI/RealTimeResidentAI.cs | 7 +- src/RealTime/CustomAI/RealTimeTouristAI.cs | 9 +- src/RealTime/CustomAI/SpareTimeBehavior.cs | 99 +++++++++++++++++++ src/RealTime/RealTime.csproj | 1 + src/RealTime/Simulation/CitizenProcessor.cs | 13 ++- 8 files changed, 133 insertions(+), 71 deletions(-) create mode 100644 src/RealTime/CustomAI/SpareTimeBehavior.cs diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index c7fc25b6..9908ccef 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -127,6 +127,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); @@ -223,14 +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); + SimulationHandler.CitizenProcessor = new CitizenProcessor(realTimeResidentAI, spareTimeBehavior); TouristAIConnection touristAIConnection = TouristAIPatch.GetTouristAIConnection(); if (touristAIConnection == null) @@ -242,7 +246,8 @@ private static bool SetupCustomAI( config, gameConnections, touristAIConnection, - eventManager); + eventManager, + spareTimeBehavior); TouristAIPatch.RealTimeAI = realTimeTouristAI; diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 530cf14c..9c42fba7 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -106,66 +106,6 @@ protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections c /// Gets the maximum count of the citizen instances. protected uint CitizenInstancesMaxCount { get; } - /// - /// 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. - // TODO: make this method to a part of time simulation (no need to calculate for each citizen) - protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) - { - float currentHour = TimeInfo.CurrentHour; - - uint weekdayModifier; - if (Config.IsWeekendEnabled) - { - weekdayModifier = TimeInfo.Now.IsWeekendTime(12f, Config.GoToSleepUpHour) - ? 11u - : 1u; - } - else - { - weekdayModifier = 1u; - } - - float latestGoOutHour = Config.GoToSleepUpHour - 2f; - bool isDayTime = currentHour >= Config.WakeupHour && currentHour < latestGoOutHour; - float timeModifier; - if (isDayTime) - { - timeModifier = 4f; - } - else - { - float nightDuration = 24f - (latestGoOutHour - Config.WakeupHour); - float relativeHour = currentHour - latestGoOutHour; - if (relativeHour < 0) - { - relativeHour += 24f; - } - - timeModifier = 3f / 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 30 + weekdayModifier; - - default: - return 0; - } - } - /// /// Ensures that the provided citizen is in a valid state and can be processed. /// diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index a3142a93..bdbd6d47 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -14,7 +14,7 @@ internal sealed partial class RealTimeResidentAI private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); - if (!Random.ShouldOccur(GetGoOutChance(citizenAge)) || IsBadWeather()) + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(citizenAge)) || IsBadWeather()) { return false; } @@ -101,7 +101,7 @@ private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen if (TimeInfo.IsNightTime) { - if (Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) + if (Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) { schedule.Hint = ScheduleHint.LocalShoppingOnly; schedule.Schedule(ResidentState.Shopping, default); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index a6ff2761..dafcb00c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -18,6 +18,7 @@ internal sealed partial class RealTimeResidentAI : RealTimeHumanA { private readonly ResidentAIConnection residentAI; private readonly WorkBehavior workBehavior; + private readonly SpareTimeBehavior spareTimeBehavior; private readonly CitizenSchedule[] residentSchedules; private float simulationCycle; @@ -27,14 +28,17 @@ 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); } @@ -127,6 +131,7 @@ public void BeginNewDay() /// The citizen ID to process. public void BeginNewDayForCitizen(uint citizenId) { + // TODO: use this method if (citizenId == 0) { return; diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index 8f95e801..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)); } /// @@ -193,7 +198,7 @@ private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen cit return; } - if (!Random.ShouldOccur(GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather()) + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen))) || IsBadWeather()) { FindHotel(instance, citizenId, ref citizen); return; diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs new file mode 100644 index 00000000..2ce11f1b --- /dev/null +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -0,0 +1,99 @@ +// +// 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 - simulationCycle, 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); + + 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] = defaultChance; + } + + /// + /// 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. + public uint GetGoOutChance(Citizen.AgeGroup citizenAge) + { + return chances[(int)citizenAge]; + } + } +} diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index f249d1d2..54594bdb 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -66,6 +66,7 @@ + diff --git a/src/RealTime/Simulation/CitizenProcessor.cs b/src/RealTime/Simulation/CitizenProcessor.cs index e19db6ed..c7fd79e2 100644 --- a/src/RealTime/Simulation/CitizenProcessor.cs +++ b/src/RealTime/Simulation/CitizenProcessor.cs @@ -16,14 +16,17 @@ internal sealed class CitizenProcessor 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. - /// Thrown when the argument is null. - public CitizenProcessor(RealTimeResidentAI residentAI) + /// 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; } @@ -38,13 +41,17 @@ public void StartNewDay() /// Duration of a simulation frame in hours. public void SetFrameDuration(float frameDuration) { - residentAI.SetSimulationCyclePeriod(frameDuration * (StepMask + 1)); + float cyclePeriod = frameDuration * (StepMask + 1); + residentAI.SetSimulationCyclePeriod(cyclePeriod); + spareTimeBehavior.SetSimulationCyclePeriod(cyclePeriod); } /// Processes the simulation frame. /// The index of the simulation frame to process. public void ProcessFrame(uint frameIndex) { + spareTimeBehavior.RefreshGoOutChances(); + if (dayStartFrame == -1) { return; From b6cd03602d7c5f5c99a0b45b145601ba3af2fe7b Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 19:12:40 +0200 Subject: [PATCH 17/42] Improve debug logging --- .../CustomAI/RealTimeResidentAI.Common.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index e8e1f0a2..1feaae49 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -234,12 +234,15 @@ when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService. private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { + Log.Debug(TimeInfo.Now, $"Calculating schedule for {GetCitizenDesc(citizenId, ref 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)); + Log.Debug($" - Updated work shifts: work shift {schedule.WorkShift}, {schedule.WorkShiftStartHour} - {schedule.WorkShiftEndHour}, weekends: {schedule.WorksOnWeekends}"); } if (schedule.ScheduledState != ResidentState.Unknown) @@ -247,8 +250,6 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, return; } - Log.Debug($"Calculating schedule for citizen instance {CitizenProxy.GetInstance(ref citizen)}..."); - if (schedule.WorkStatus == WorkStatus.Working) { schedule.WorkStatus = WorkStatus.None; @@ -274,8 +275,15 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, if (timeLeft <= MaxTravelTime) { // If we have some time, try to shop locally. - Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); - ScheduleShopping(ref schedule, ref citizen, true); + if (ScheduleShopping(ref schedule, ref citizen, true)) + { + Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); + } + else + { + Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + } + return; } } From 06a45441afa4d9fb457965a98ee31878a83b34b6 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 19:22:54 +0200 Subject: [PATCH 18/42] Change the work shift settings value format --- src/RealTime/Config/ConfigurationProvider.cs | 2 +- src/RealTime/Config/RealTimeConfig.cs | 37 ++++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/RealTime/Config/ConfigurationProvider.cs b/src/RealTime/Config/ConfigurationProvider.cs index 36ab31a1..9a12d122 100644 --- a/src/RealTime/Config/ConfigurationProvider.cs +++ b/src/RealTime/Config/ConfigurationProvider.cs @@ -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 041191da..dc902b1c 100644 --- a/src/RealTime/Config/RealTimeConfig.cs +++ b/src/RealTime/Config/RealTimeConfig.cs @@ -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,6 +215,20 @@ 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() @@ -225,8 +242,8 @@ public RealTimeConfig Validate() VirtualCitizens = (VirtualCitizensLevel)RealTimeMath.Clamp((int)VirtualCitizens, (int)VirtualCitizensLevel.None, (int)VirtualCitizensLevel.Many); ConstructionSpeed = RealTimeMath.Clamp(ConstructionSpeed, 0u, 100u); - SecondShiftQuota = RealTimeMath.Clamp(SecondShiftQuota, 1u, 8u); - NightShiftQuota = RealTimeMath.Clamp(NightShiftQuota, 1u, 8u); + 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); @@ -273,8 +290,8 @@ public void ResetToDefaults() StopConstructionAtNight = true; ConstructionSpeed = 50; - SecondShiftQuota = 4; - NightShiftQuota = 2; + SecondShiftQuota = 13; + NightShiftQuota = 6; LunchQuota = 80; LocalBuildingSearchQuota = 60; From 8a894d37ee67ec6713b5647bd604935260f88a8f Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:10:33 +0200 Subject: [PATCH 19/42] Improve performance by reducing method call frequency --- src/RealTime/Simulation/CitizenProcessor.cs | 8 ++++++-- src/RealTime/Simulation/SimulationHandler.cs | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/RealTime/Simulation/CitizenProcessor.cs b/src/RealTime/Simulation/CitizenProcessor.cs index c7fd79e2..ff5bf8e6 100644 --- a/src/RealTime/Simulation/CitizenProcessor.cs +++ b/src/RealTime/Simulation/CitizenProcessor.cs @@ -46,12 +46,16 @@ public void SetFrameDuration(float frameDuration) 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) { - spareTimeBehavior.RefreshGoOutChances(); - if (dayStartFrame == -1) { return; diff --git a/src/RealTime/Simulation/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index 30eb8de1..b56e408b 100644 --- a/src/RealTime/Simulation/SimulationHandler.cs +++ b/src/RealTime/Simulation/SimulationHandler.cs @@ -54,9 +54,14 @@ public override void OnBeforeSimulationTick() { WeatherInfo?.Update(); EventManager?.ProcessEvents(); - if (TimeAdjustment != null && TimeAdjustment.Update()) + + if (CitizenProcessor != null) { - CitizenProcessor?.SetFrameDuration(TimeAdjustment.HoursPerFrame); + CitizenProcessor.ProcessTick(); + if (TimeAdjustment != null && TimeAdjustment.Update()) + { + CitizenProcessor.SetFrameDuration(TimeAdjustment.HoursPerFrame); + } } DateTime currentDate = SimulationManager.instance.m_currentGameTime.Date; From 1fe62a6fb9010c8f6fe17f23f42c4d47213b9dea Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:10:51 +0200 Subject: [PATCH 20/42] Clean up code --- src/RealTime/Events/RealTimeEventManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs index 5c109c72..627a4feb 100644 --- a/src/RealTime/Events/RealTimeEventManager.cs +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -272,9 +272,10 @@ void IStorageData.ReadData(Stream source) 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); From 16b9e938e214ccc91611054196dfd38f9a281e1e Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:12:36 +0200 Subject: [PATCH 21/42] Improve visit and shopping logic --- .../CustomAI/RealTimeResidentAI.Visit.cs | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index bdbd6d47..3b905188 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -99,31 +99,23 @@ private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen return false; } - if (TimeInfo.IsNightTime) + if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen))) + || !Random.ShouldOccur(GoShoppingChance)) { - if (Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) - { - schedule.Hint = ScheduleHint.LocalShoppingOnly; - schedule.Schedule(ResidentState.Shopping, default); - return true; - } - return false; } - schedule.Hint = ScheduleHint.None; - if (Random.ShouldOccur(GoShoppingChance)) + if (TimeInfo.IsNightTime || localOnly || Random.ShouldOccur(Config.LocalBuildingSearchQuota)) { - if (localOnly || Random.ShouldOccur(Config.LocalBuildingSearchQuota)) - { - schedule.Hint = ScheduleHint.LocalShoppingOnly; - } - - schedule.Schedule(ResidentState.Shopping, default); - return true; + schedule.Hint = ScheduleHint.LocalShoppingOnly; + } + else + { + schedule.Hint = ScheduleHint.None; } - return false; + schedule.Schedule(ResidentState.Shopping, default); + return true; } private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) @@ -173,10 +165,14 @@ private void ProcessCitizenShopping(ref TCitizen citizen) CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - private void ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) { - ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (schedule.ScheduledState != ResidentState.Unknown) + { + return false; + } + ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (schedule.Hint == ScheduleHint.OnTour || Random.ShouldOccur(ReturnFromVisitChance) || (BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.CommercialLeisure @@ -184,11 +180,10 @@ private void ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citi && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) { schedule.Schedule(ResidentState.Unknown, default); + return true; } - else - { - schedule.Schedule(ResidentState.Visiting, default); - } + + return false; } private bool IsBuildingNoiseRestricted(ushort building) From 3967b2d28e8c882a7c488969502a805ac3b7e36b Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:13:37 +0200 Subject: [PATCH 22/42] Improve go to work logic --- src/RealTime/CustomAI/Constants.cs | 3 + .../CustomAI/RealTimeResidentAI.Common.cs | 68 +++++++++---------- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 45 ++++++++++-- src/RealTime/CustomAI/RealTimeResidentAI.cs | 9 +-- src/RealTime/Tools/DateTimeExtensions.cs | 2 +- 5 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index 11b722fd..60b24959 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -17,6 +17,9 @@ 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 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 = 80; diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 1feaae49..3374260d 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -10,6 +10,8 @@ namespace RealTime.CustomAI internal sealed partial class RealTimeResidentAI { + private DateTime todayWakeup; + private enum ScheduleAction { Ignore, @@ -128,13 +130,16 @@ private void DoScheduledEvacuation(ref CitizenSchedule schedule, TAI instance, u } } - private void ProcessCitizenInShelter(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCitizenInShelter(ref CitizenSchedule schedule, ref TCitizen citizen) { ushort shelter = CitizenProxy.GetVisitBuilding(ref citizen); if (BuildingMgr.BuildingHasFlags(shelter, Building.Flags.Downgrading)) { schedule.Schedule(ResidentState.Unknown, default); + return true; } + + return false; } private ScheduleAction UpdateCitizenState(uint citizenId, ref TCitizen citizen, ref CitizenSchedule schedule) @@ -255,37 +260,17 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, schedule.WorkStatus = WorkStatus.None; } - DateTime nextActivityTime = TimeInfo.Now.FutureHour(Config.WakeupHour); + DateTime nextActivityTime = todayWakeup; if (schedule.CurrentState != ResidentState.AtSchoolOrWork && workBuilding != 0) { - DateTime workTime = ScheduleWork(ref schedule, CitizenProxy.GetCurrentBuilding(ref citizen)); - if (schedule.ScheduledState == ResidentState.AtSchoolOrWork) + if (ScheduleWork(ref schedule, ref citizen)) { - Log.Debug($" - Schedule work at {workTime}"); - nextActivityTime = workTime; - - float timeLeft = (float)(nextActivityTime - TimeInfo.Now).TotalHours; - if (timeLeft <= PrepareToWorkHours) - { - // Just sit at home if the work time will come soon - Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); - return; - } + return; + } - if (timeLeft <= MaxTravelTime) - { - // If we have some time, try to shop locally. - if (ScheduleShopping(ref schedule, ref citizen, true)) - { - Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); - } - else - { - Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); - } - - return; - } + if (schedule.ScheduledStateTime > nextActivityTime) + { + nextActivityTime = schedule.ScheduledStateTime; } } @@ -303,7 +288,12 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, if (schedule.CurrentState == ResidentState.AtHome) { - Log.Debug($" - Scheduled sleeping at home until {nextActivityTime}"); + if (Random.ShouldOccur(StayHomeAllDayChance)) + { + nextActivityTime = todayWakeup.FutureHour(Config.WakeupHour); + } + + Log.Debug($" - Schedule sleeping at home until {nextActivityTime}"); schedule.Schedule(ResidentState.Unknown, nextActivityTime); } else @@ -315,7 +305,11 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { - ProcessCurrentState(ref schedule, ref citizen); + if (ProcessCurrentState(ref schedule, ref citizen) && schedule.ScheduledState == ResidentState.Unknown) + { + // If the state processing changed the schedule, we need to update it + UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); + } if (TimeInfo.Now < schedule.ScheduledStateTime) { @@ -357,26 +351,26 @@ private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, } } - private void ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citizen) { switch (schedule.CurrentState) { case ResidentState.Shopping: ProcessCitizenShopping(ref citizen); - break; + return false; case ResidentState.Relaxing: ProcessCitizenRelaxing(ref citizen); - break; + return false; case ResidentState.Visiting: - ProcessCitizenVisit(ref schedule, ref citizen); - break; + return ProcessCitizenVisit(ref schedule, ref citizen); case ResidentState.InShelter: - ProcessCitizenInShelter(ref schedule, ref citizen); - break; + return ProcessCitizenInShelter(ref schedule, ref citizen); } + + return false; } private bool ShouldRealizeCitizen(TAI ai) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index d58635b8..463c5d46 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -4,17 +4,52 @@ namespace RealTime.CustomAI { - using System; using RealTime.Tools; using static Constants; internal sealed partial class RealTimeResidentAI { - private DateTime ScheduleWork(ref CitizenSchedule schedule, ushort currentBuilding) + private bool ScheduleWork(ref CitizenSchedule schedule, ref TCitizen citizen) { - return workBehavior.ScheduleGoToWork(ref schedule, currentBuilding, simulationCycle) - ? schedule.ScheduledStateTime - : default; + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (!workBehavior.ScheduleGoToWork(ref schedule, currentBuilding, simulationCycle)) + { + return false; + } + + Log.Debug($" - Schedule work at {schedule.ScheduledStateTime}"); + + float timeLeft = (float)(schedule.ScheduledStateTime - TimeInfo.Now).TotalHours; + if (timeLeft <= PrepareToWorkHours) + { + // Just sit at home if the work time will come soon + Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + return true; + } + + if (timeLeft <= MaxTravelTime) + { + if (schedule.CurrentState != ResidentState.AtHome) + { + Log.Debug($" - Worktime in {timeLeft} hours, returning home"); + schedule.Schedule(ResidentState.AtHome, default); + return true; + } + + // If we have some time, try to shop locally. + if (ScheduleShopping(ref schedule, ref citizen, true)) + { + Log.Debug($" - Worktime in {timeLeft} hours, trying local shop"); + } + else + { + Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + } + + return true; + } + + return false; } private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index dafcb00c..047d560a 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -106,25 +106,26 @@ public void RegisterCitizenArrival(uint citizenId) return; } - ref CitizenSchedule citizenState = ref residentSchedules[citizenId]; + ref CitizenSchedule schedule = ref residentSchedules[citizenId]; switch (CitizenMgr.GetCitizenLocation(citizenId)) { case Citizen.Location.Work: - citizenState.UpdateTravelTimeToWork(TimeInfo.Now); - Log.Debug($"The citizen {citizenId} arrived at work at {TimeInfo.Now} and needs {residentSchedules[citizenId].TravelTimeToWork} hours to get to 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 Citizen.Location.Moving: return; } - citizenState.DepartureToWorkTime = default; + schedule.DepartureToWorkTime = default; } /// 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. diff --git a/src/RealTime/Tools/DateTimeExtensions.cs b/src/RealTime/Tools/DateTimeExtensions.cs index bdaad519..e4109c4c 100644 --- a/src/RealTime/Tools/DateTimeExtensions.cs +++ b/src/RealTime/Tools/DateTimeExtensions.cs @@ -78,7 +78,7 @@ public static DateTime FutureHour(this DateTime dateTime, float hour) } float delta = hour - (float)dateTime.TimeOfDay.TotalHours; - return delta >= 0 + return delta > 0 ? dateTime.AddHours(delta) : dateTime.AddHours(24f + delta); } From ff080c7cb9eb614910809b54adb1dec662127807 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:22:10 +0200 Subject: [PATCH 23/42] Fix issue caused wrong work shift times calculation --- src/RealTime/CustomAI/WorkBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index b74549d1..6e18dc4f 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -91,7 +91,7 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz } ItemClass.SubService buildingSubService = buildingManager.GetBuildingSubService(schedule.WorkBuilding); - switch (schedule.WorkShift) + switch (workShift) { case WorkShift.First when HasExtendedFirstWorkShift(buildingSevice, buildingSubService): workBegin = Math.Min(config.WakeupHour, EarliestWakeUp); From 750c14ef4ccb886b5a51d1d9adf151662dac107e Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:51:08 +0200 Subject: [PATCH 24/42] Allow citizens going to school/work if they missed the beginning hour --- src/RealTime/CustomAI/RealTimeResidentAI.Common.cs | 6 +++--- src/RealTime/CustomAI/WorkBehavior.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 3374260d..3cf59a40 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -239,15 +239,13 @@ when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService. private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { - Log.Debug(TimeInfo.Now, $"Calculating schedule for {GetCitizenDesc(citizenId, ref 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)); - Log.Debug($" - Updated work shifts: work shift {schedule.WorkShift}, {schedule.WorkShiftStartHour} - {schedule.WorkShiftEndHour}, weekends: {schedule.WorksOnWeekends}"); + Log.Debug($"Updated work shifts for citizen {citizenId}: work shift {schedule.WorkShift}, {schedule.WorkShiftStartHour} - {schedule.WorkShiftEndHour}, weekends: {schedule.WorksOnWeekends}"); } if (schedule.ScheduledState != ResidentState.Unknown) @@ -255,6 +253,8 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, return; } + Log.Debug(TimeInfo.Now, $"Calculating schedule for {GetCitizenDesc(citizenId, ref citizen)}..."); + if (schedule.WorkStatus == WorkStatus.Working) { schedule.WorkStatus = WorkStatus.None; diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index 6e18dc4f..46ae1255 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -123,14 +123,20 @@ public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuildin return false; } - if (config.IsWeekendEnabled && timeInfo.Now.IsWeekend() && !schedule.WorksOnWeekends) + DateTime now = timeInfo.Now; + if (config.IsWeekendEnabled && now.IsWeekend() && !schedule.WorksOnWeekends) { return false; } float travelTime = GetTravelTimeToWork(ref schedule, currentBuilding); - float departureHour = schedule.WorkShiftStartHour - travelTime - simulationCycle; - schedule.Schedule(ResidentState.AtSchoolOrWork, timeInfo.Now.FutureHour(departureHour)); + + DateTime workEndTime = now.FutureHour(schedule.WorkShiftEndHour); + DateTime departureTime = now.AddHours(travelTime + simulationCycle) < workEndTime + ? default + : now.FutureHour(schedule.WorkShiftStartHour - travelTime - simulationCycle); + + schedule.Schedule(ResidentState.AtSchoolOrWork, departureTime); return true; } From 52624ad0fe88b0f1f1a9921fd16eaca9fcf92aa5 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:51:42 +0200 Subject: [PATCH 25/42] Improve go out chance calculation --- src/RealTime/CustomAI/SpareTimeBehavior.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs index 2ce11f1b..060052ba 100644 --- a/src/RealTime/CustomAI/SpareTimeBehavior.cs +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -60,7 +60,7 @@ public void RefreshGoOutChances() float timeModifier; if (isDayTime) { - timeModifier = RealTimeMath.Clamp(currentHour - config.WakeupHour - simulationCycle, 0, 4f); + timeModifier = RealTimeMath.Clamp((currentHour - config.WakeupHour) * 2f, 0, 4f); } else { @@ -76,11 +76,18 @@ public void RefreshGoOutChances() 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] = defaultChance; + + 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]}"); + } } /// From 14ccb9dccdc483b82012d55769d6b5585cf3286a Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 21:54:52 +0200 Subject: [PATCH 26/42] Fix issue caused wrong departure to work time calculation --- src/RealTime/CustomAI/WorkBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index 46ae1255..1fc300bd 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -133,7 +133,7 @@ public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuildin DateTime workEndTime = now.FutureHour(schedule.WorkShiftEndHour); DateTime departureTime = now.AddHours(travelTime + simulationCycle) < workEndTime - ? default + ? now : now.FutureHour(schedule.WorkShiftStartHour - travelTime - simulationCycle); schedule.Schedule(ResidentState.AtSchoolOrWork, departureTime); From ecf0c2427f85e137862fb7430ae843c4c9461f7f Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 22:30:08 +0200 Subject: [PATCH 27/42] Improve work and lunch time selection --- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 11 +++++++---- src/RealTime/CustomAI/WorkBehavior.cs | 9 +++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 463c5d46..8779b9a6 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -23,7 +23,7 @@ private bool ScheduleWork(ref CitizenSchedule schedule, ref TCitizen citizen) if (timeLeft <= PrepareToWorkHours) { // Just sit at home if the work time will come soon - Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + Log.Debug($" - Worktime in {timeLeft} hours, preparing for departure"); return true; } @@ -63,11 +63,14 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci schedule.DepartureToWorkTime = TimeInfo.Now; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding}"); - Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); - if (!workBehavior.ScheduleLunch(ref schedule, citizenAge)) + if (workBehavior.ScheduleLunch(ref schedule, citizenAge)) + { + 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 { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding} and will leave work at {schedule.ScheduledStateTime}"); workBehavior.ScheduleReturnFromWork(ref schedule, citizenAge); } } diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index 1fc300bd..ae2d6b6a 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -132,9 +132,11 @@ public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuildin float travelTime = GetTravelTimeToWork(ref schedule, currentBuilding); DateTime workEndTime = now.FutureHour(schedule.WorkShiftEndHour); - DateTime departureTime = now.AddHours(travelTime + simulationCycle) < workEndTime - ? now - : now.FutureHour(schedule.WorkShiftStartHour - travelTime - simulationCycle); + 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; @@ -147,7 +149,6 @@ public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuildin public bool ScheduleLunch(ref CitizenSchedule schedule, Citizen.AgeGroup citizenAge) { if (schedule.WorkStatus == WorkStatus.Working - && schedule.CurrentState == ResidentState.AtSchoolOrWork && schedule.WorkShift == WorkShift.First && WillGoToLunch(citizenAge)) { From 9cddace4d2c9043104843b6d993fd38f2468dbfd Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 22:51:18 +0200 Subject: [PATCH 28/42] Improve debug logging --- src/RealTime/CustomAI/RealTimeResidentAI.Common.cs | 4 +++- src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 3cf59a40..9a48fd4c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -253,7 +253,7 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, return; } - Log.Debug(TimeInfo.Now, $"Calculating schedule for {GetCitizenDesc(citizenId, ref citizen)}..."); + Log.Debug(TimeInfo.Now, $"Scheduling for {GetCitizenDesc(citizenId, ref citizen)}..."); if (schedule.WorkStatus == WorkStatus.Working) { @@ -307,6 +307,8 @@ private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, { 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); } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 3b905188..b5951332 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -48,7 +48,11 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin schedule.Schedule(ResidentState.Unknown, default); ushort leisure = MoveToLeisureBuilding(instance, citizenId, ref citizen, buildingId); - if (leisure != 0) + if (leisure == 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but didn't found a leisure building"); + } + else { Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} heading to a leisure building {leisure}"); } @@ -131,6 +135,10 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin { 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}"); + } } else { @@ -179,6 +187,7 @@ private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citi && TimeInfo.IsNightTime && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit (see next line for citizen ID)"); schedule.Schedule(ResidentState.Unknown, default); return true; } From 81027b2323d23407df53e353ea2735ba0a8be68d Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 22:51:55 +0200 Subject: [PATCH 29/42] Fix issues with choosing of shopping and event target building --- .../CustomAI/RealTimeResidentAI.Visit.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index b5951332..f19ef9ae 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -60,12 +60,19 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin return; case ScheduleHint.AttendingEvent: - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{schedule.EventBuilding}', on the way now."); - StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding); + DateTime returnTime; ICityEvent cityEvent = EventMgr.GetUpcomingCityEvent(schedule.EventBuilding); - DateTime returnTime = cityEvent == null - ? default - : cityEvent.EndTime; + if (cityEvent == null) + { + returnTime = default; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{schedule.EventBuilding}', but there was no event there"); + } + else + { + StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding); + 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; @@ -126,7 +133,7 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin { ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); - if ((schedule.Hint & ScheduleHint.LocalShoppingOnly) != 0) + if (schedule.Hint == ScheduleHint.LocalShoppingOnly) { schedule.Schedule(ResidentState.Unknown, default); From 6b4bf094651e1fc46c7280ac3e32c28fc3c50425 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 23:09:00 +0200 Subject: [PATCH 30/42] Fix incorrect debug log value --- src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 8779b9a6..2a11b545 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -70,8 +70,8 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci } else { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} to school/work {schedule.WorkBuilding} and will leave work at {schedule.ScheduledStateTime}"); 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}"); } } From 93ff32d2e0e0bbbdc74eba3351011b927c53a949 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 21 Jul 2018 23:26:13 +0200 Subject: [PATCH 31/42] Update code documentation --- src/RealTime/Config/ConfigurationProvider.cs | 2 +- src/RealTime/Core/IStorageData.cs | 4 ++-- src/RealTime/Core/RealTimeCore.cs | 2 +- src/RealTime/Core/RealTimeStorage.cs | 4 ++-- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 10 +++++----- src/RealTime/CustomAI/SpareTimeBehavior.cs | 4 ++-- src/RealTime/Events/CityEventBase.cs | 6 +++--- src/RealTime/Events/ICityEvent.cs | 6 +++--- src/RealTime/Events/ICityEventsProvider.cs | 6 +++--- src/RealTime/Events/RealTimeCityEvent.cs | 4 ++-- src/RealTime/Events/RealTimeEventManager.cs | 6 +++--- src/RealTime/Events/Storage/CityEventsLoader.cs | 6 +++--- src/RealTime/Events/VanillaEvent.cs | 4 ++-- .../GameConnection/BuildingManagerConnection.cs | 8 ++++---- .../GameConnection/CitizenManagerConnection.cs | 4 ++-- src/RealTime/GameConnection/EventManagerConnection.cs | 2 +- .../GameConnection/IBuildingManagerConnection.cs | 8 ++++---- .../GameConnection/ICitizenManagerConnection.cs | 4 ++-- src/RealTime/GameConnection/IEventManagerConnection.cs | 2 +- src/RealTime/Patching/FastDelegate.cs | 4 ++-- src/RealTime/Simulation/DayTimeCalculator.cs | 2 +- src/RealTime/Tools/DateTimeExtensions.cs | 2 +- src/RealTime/Tools/GitVersion.cs | 4 ++-- src/RealTime/Tools/LinkedListExtensions.cs | 2 +- src/RealTime/UI/CitiesCheckBoxItem.cs | 2 +- src/RealTime/UI/CitiesComboBoxItem.cs | 2 +- src/RealTime/UI/CitiesSliderItem.cs | 2 +- src/RealTime/UI/CitiesViewItem.cs | 2 +- src/RealTime/UI/RealTimeUIDateTimeWrapper.cs | 2 +- 29 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/RealTime/Config/ConfigurationProvider.cs b/src/RealTime/Config/ConfigurationProvider.cs index 9a12d122..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. 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 9908ccef..2a78176a 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -195,7 +195,7 @@ public void Stop() /// /// Translates all the mod's component to a different language obtained from - /// the provided . + /// the specified . /// /// /// Thrown when the argument is null. 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/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 9c42fba7..01ca29fc 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -107,13 +107,13 @@ protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections c protected uint CitizenInstancesMaxCount { get; } /// - /// 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 @@ -167,7 +167,7 @@ protected ICityEvent GetUpcomingEventToAttend(uint citizenId, ref TCitizen citiz } /// - /// 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. @@ -179,13 +179,13 @@ 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. /// - /// A short string describing the provided citizen. + /// A short string describing the specified citizen. protected string GetCitizenDesc(uint citizenId, ref TCitizen citizen) { ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs index 060052ba..77a1be4c 100644 --- a/src/RealTime/CustomAI/SpareTimeBehavior.cs +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -91,13 +91,13 @@ public void RefreshGoOutChances() } /// - /// Gets the probability whether a citizen with provided age would go out on current time. + /// 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 provided age would go out on current time. + /// 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/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 627a4feb..af86e707 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. /// @@ -232,7 +232,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) { @@ -267,7 +267,7 @@ 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) { 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 c18fbea0..0393bab0 100644 --- a/src/RealTime/GameConnection/CitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/CitizenManagerConnection.cs @@ -32,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) 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 afdf5a9b..d5a43eae 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); 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/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/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/Tools/DateTimeExtensions.cs b/src/RealTime/Tools/DateTimeExtensions.cs index e4109c4c..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. 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/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. /// /// /// From b6dd250efba808e3d668f017cd0b86444588643a Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 01:46:44 +0200 Subject: [PATCH 32/42] Improve the lunch behavior --- .../CustomAI/RealTimeResidentAI.SchoolWork.cs | 16 +++++++++++++--- src/RealTime/CustomAI/WorkBehavior.cs | 9 ++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 2a11b545..6ccb09f9 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -57,7 +57,12 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); schedule.WorkStatus = WorkStatus.Working; - if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, schedule.WorkBuilding) + if (currentBuilding == schedule.WorkBuilding && schedule.CurrentState != ResidentState.AtSchoolOrWork) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Work); + } + else if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, schedule.WorkBuilding) && schedule.CurrentState == ResidentState.AtHome) { schedule.DepartureToWorkTime = TimeInfo.Now; @@ -78,15 +83,20 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci private void DoScheduledLunch(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) { 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)} is going for lunch from {currentBuilding} to {lunchPlace}"); + Log.Debug(TimeInfo.Now, $"{citizenDesc} is going for lunch from {currentBuilding} to {lunchPlace}"); workBehavior.ScheduleReturnFromLunch(ref schedule); } else { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go for lunch from {currentBuilding}, but there were no buildings close enough"); + 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)); } } diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index ae2d6b6a..c4d3b548 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -148,7 +148,8 @@ public bool ScheduleGoToWork(ref CitizenSchedule schedule, ushort currentBuildin /// true if a lunch time was scheduled; otherwise, false. public bool ScheduleLunch(ref CitizenSchedule schedule, Citizen.AgeGroup citizenAge) { - if (schedule.WorkStatus == WorkStatus.Working + if (timeInfo.Now < lunchBegin + && schedule.WorkStatus == WorkStatus.Working && schedule.WorkShift == WorkShift.First && WillGoToLunch(citizenAge)) { @@ -163,12 +164,10 @@ public bool ScheduleLunch(ref CitizenSchedule schedule, Citizen.AgeGroup citizen /// The citizen's schedule to update. public void ScheduleReturnFromLunch(ref CitizenSchedule schedule) { - if (schedule.WorkStatus != WorkStatus.Working || schedule.CurrentState != ResidentState.Shopping) + if (schedule.WorkStatus == WorkStatus.Working) { - return; + schedule.Schedule(ResidentState.AtSchoolOrWork, lunchEnd); } - - schedule.Schedule(ResidentState.AtSchoolOrWork, lunchEnd); } /// Updates the citizen's work schedule by determining the time for returning from work. From 9107613f057beaf5d50f9d3ad627825b23050788 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 01:47:02 +0200 Subject: [PATCH 33/42] Implement the custom schedule storage --- src/RealTime/Core/RealTimeCore.cs | 1 + src/RealTime/CustomAI/CitizenSchedule.cs | 38 ++++++++ .../CustomAI/CitizenScheduleStorage.cs | 88 +++++++++++++++++++ .../CustomAI/RealTimeResidentAI.Common.cs | 6 ++ src/RealTime/CustomAI/RealTimeResidentAI.cs | 9 ++ src/RealTime/CustomAI/WorkBehavior.cs | 8 +- .../CitizenManagerConnection.cs | 7 ++ .../ICitizenManagerConnection.cs | 4 + src/RealTime/RealTime.csproj | 1 + 9 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 src/RealTime/CustomAI/CitizenScheduleStorage.cs diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index 2a78176a..54354c4c 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -149,6 +149,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); diff --git a/src/RealTime/CustomAI/CitizenSchedule.cs b/src/RealTime/CustomAI/CitizenSchedule.cs index 9d8d63e1..6fbbdbaf 100644 --- a/src/RealTime/CustomAI/CitizenSchedule.cs +++ b/src/RealTime/CustomAI/CitizenSchedule.cs @@ -11,6 +11,9 @@ namespace RealTime.CustomAI /// 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; @@ -29,6 +32,8 @@ internal struct CitizenSchedule /// 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; } @@ -96,5 +101,38 @@ 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 = (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 = 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/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 9a48fd4c..5ad3ff01 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -245,6 +245,12 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, { 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)); + } + Log.Debug($"Updated work shifts for citizen {citizenId}: work shift {schedule.WorkShift}, {schedule.WorkShiftStartHour} - {schedule.WorkShiftEndHour}, weekends: {schedule.WorksOnWeekends}"); } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 047d560a..19a622ca 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; @@ -147,5 +149,12 @@ 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 new CitizenScheduleStorage(residentSchedules, CitizenMgr.GetCitizensArray(), TimeInfo); + } } } \ No newline at end of file diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index c4d3b548..bac4b3f2 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -68,7 +68,7 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz ItemClass.Service buildingSevice = buildingManager.GetBuildingService(schedule.WorkBuilding); float workBegin, workEnd; - WorkShift workShift; + WorkShift workShift = schedule.WorkShift; switch (citizenAge) { @@ -81,7 +81,11 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz case Citizen.AgeGroup.Young: case Citizen.AgeGroup.Adult: - workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice)); + if (workShift == WorkShift.Unemployed) + { + workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice)); + } + workBegin = config.WorkBegin; workEnd = config.WorkEnd; break; diff --git a/src/RealTime/GameConnection/CitizenManagerConnection.cs b/src/RealTime/GameConnection/CitizenManagerConnection.cs index 0393bab0..9a794868 100644 --- a/src/RealTime/GameConnection/CitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/CitizenManagerConnection.cs @@ -127,5 +127,12 @@ public Citizen.Location GetCitizenLocation(uint citizenId) 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/ICitizenManagerConnection.cs b/src/RealTime/GameConnection/ICitizenManagerConnection.cs index d5a43eae..37d78815 100644 --- a/src/RealTime/GameConnection/ICitizenManagerConnection.cs +++ b/src/RealTime/GameConnection/ICitizenManagerConnection.cs @@ -62,5 +62,9 @@ internal interface ICitizenManagerConnection /// 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/RealTime.csproj b/src/RealTime/RealTime.csproj index 54594bdb..0c5672fa 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -57,6 +57,7 @@ + From 84ed61dd5349e0ccc4e6899a2e3f4b4135dfb7bc Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 03:45:01 +0200 Subject: [PATCH 34/42] Avoid going out at night for senior citizens --- src/RealTime/CustomAI/SpareTimeBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs index 77a1be4c..261a6764 100644 --- a/src/RealTime/CustomAI/SpareTimeBehavior.cs +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -82,7 +82,7 @@ public void RefreshGoOutChances() 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] = defaultChance; + chances[(int)Citizen.AgeGroup.Senior] = isDayTime ? defaultChance : 0; if (dump) { From 5746c910c6c27a4ad13c211ba8e36525bf3e98de Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 03:45:58 +0200 Subject: [PATCH 35/42] Improve work scheduling (allow unemployment while at work) --- src/RealTime/CustomAI/RealTimeResidentAI.Common.cs | 5 +++++ src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 5ad3ff01..afbe642c 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -250,6 +250,11 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, // 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.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}"); } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 6ccb09f9..3f1f962d 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs @@ -56,6 +56,7 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci { ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); schedule.WorkStatus = WorkStatus.Working; + schedule.DepartureToWorkTime = default; if (currentBuilding == schedule.WorkBuilding && schedule.CurrentState != ResidentState.AtSchoolOrWork) { From 783e33b4df54bc82576091b0a73199902d2443dd Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 05:14:15 +0200 Subject: [PATCH 36/42] Fix issues with endless visiting and going to undefined work buildings --- src/RealTime/CustomAI/RealTimeResidentAI.Common.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index afbe642c..49d46a08 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -231,7 +231,7 @@ when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService. } schedule.CurrentState = ResidentState.Visiting; - break; + return ScheduleAction.ProcessState; } return ScheduleAction.Ignore; @@ -250,7 +250,8 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, // 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.WorkStatus == WorkStatus.Working) + 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); From 0809dc54f14dd64e919ca9a065f6a9974e3f510f Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 05:17:15 +0200 Subject: [PATCH 37/42] Improve event attending logic --- src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index f19ef9ae..fce55127 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -41,7 +41,6 @@ private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref 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: @@ -60,16 +59,14 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin return; case ScheduleHint.AttendingEvent: - DateTime returnTime; + DateTime returnTime = default; ICityEvent cityEvent = EventMgr.GetUpcomingCityEvent(schedule.EventBuilding); if (cityEvent == null) { - returnTime = default; Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{schedule.EventBuilding}', but there was no event there"); } - else + else if (StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding)) { - StartMovingToVisitBuilding(instance, citizenId, ref citizen, schedule.EventBuilding); returnTime = cityEvent.EndTime; Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{schedule.EventBuilding}', will return at {returnTime}"); } From f1a7935ea696c38d06d95a24e000d1e9c5e23f23 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 05:37:15 +0200 Subject: [PATCH 38/42] Allow citizens attend already ongoing events --- .../CustomAI/RealTimeResidentAI.Visit.cs | 2 +- src/RealTime/Events/RealTimeEventManager.cs | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index fce55127..40a4a25f 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -60,7 +60,7 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin case ScheduleHint.AttendingEvent: DateTime returnTime = default; - ICityEvent cityEvent = EventMgr.GetUpcomingCityEvent(schedule.EventBuilding); + ICityEvent cityEvent = EventMgr.GetCityEvent(schedule.EventBuilding); if (cityEvent == null) { Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted attend an event at '{schedule.EventBuilding}', but there was no event there"); diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs index af86e707..484f49bb 100644 --- a/src/RealTime/Events/RealTimeEventManager.cs +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -173,14 +173,24 @@ public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime late } /// - /// Gets the instance of an upcoming city event that takes place in a building + /// 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 GetUpcomingCityEvent(ushort buildingId) + public ICityEvent GetCityEvent(ushort buildingId) { - if (buildingId == 0 || upcomingEvents.Count == 0) + if (buildingId == 0) + { + return null; + } + + if (activeEvent != null && activeEvent.BuildingId == buildingId) + { + return activeEvent; + } + + if (upcomingEvents.Count == 0) { return null; } From 78ba4a9ab46e60714ea4c4a898dca8f768dce015 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 12:02:22 +0200 Subject: [PATCH 39/42] Fix issue caused wrong storage of the scheduled time --- src/RealTime/CustomAI/CitizenSchedule.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/RealTime/CustomAI/CitizenSchedule.cs b/src/RealTime/CustomAI/CitizenSchedule.cs index 6fbbdbaf..f75796d9 100644 --- a/src/RealTime/CustomAI/CitizenSchedule.cs +++ b/src/RealTime/CustomAI/CitizenSchedule.cs @@ -110,7 +110,10 @@ public void Write(byte[] target, long referenceTime) target[0] = (byte)(((int)WorkShift & 0xF) + ((int)WorkStatus << 4)); target[1] = (byte)ScheduledState; - ushort minutes = (ushort)((ScheduledStateTime.Ticks - referenceTime) / TimeSpan.TicksPerMinute); + ushort minutes = ScheduledStateTime == default + ? (ushort)0 + : (ushort)((ScheduledStateTime.Ticks - referenceTime) / TimeSpan.TicksPerMinute); + target[2] = (byte)(minutes & 0xFF); target[3] = (byte)(minutes >> 8); @@ -129,7 +132,9 @@ public void Read(byte[] source, long referenceTime) ScheduledState = (ResidentState)source[1]; int minutes = source[2] + (source[3] << 8); - ScheduledStateTime = new DateTime((minutes * TimeSpan.TicksPerMinute) + referenceTime); + ScheduledStateTime = minutes == 0 + ? default + : new DateTime((minutes * TimeSpan.TicksPerMinute) + referenceTime); int travelTime = source[4] + (source[5] << 8); TravelTimeToWork = travelTime / TravelTimeMultiplier; From ce0b8d077353c08229fb8d8bf20db0282014f6a3 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 13:35:25 +0200 Subject: [PATCH 40/42] Improve rescheduling of citizen's plans due to weather, time, policies --- src/RealTime/CustomAI/Constants.cs | 6 -- src/RealTime/CustomAI/RealTimeHumanAIBase.cs | 2 +- .../CustomAI/RealTimeResidentAI.Common.cs | 8 +- .../CustomAI/RealTimeResidentAI.Moving.cs | 35 +++--- .../CustomAI/RealTimeResidentAI.Visit.cs | 101 +++++++++++++----- src/RealTime/CustomAI/RealTimeResidentAI.cs | 3 +- 6 files changed, 96 insertions(+), 59 deletions(-) diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index 60b24959..cc006e68 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -23,12 +23,6 @@ internal static class Constants /// A chance in percent for a citizen to go shopping. public const uint GoShoppingChance = 80; - /// A chance in percent for a citizen to return shopping. - public const uint ReturnFromShoppingChance = 75; - - /// A chance in percent for a citizen to return from a visited building. - public const uint ReturnFromVisitChance = 50; - /// 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; diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs index 01ca29fc..945123ba 100644 --- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -254,7 +254,7 @@ protected bool IsBadWeather() /// An estimated travel time in hours. protected float GetEstimatedTravelTime(ushort building1, ushort building2) { - if (building1 == 0 || building2 == 0) + if (building1 == 0 || building2 == 0 || building1 == building2) { return 0; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 49d46a08..74d9c714 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -214,6 +214,8 @@ private ScheduleAction UpdateCitizenState(uint citizenId, ref TCitizen citizen, switch (buildingService) { 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: @@ -370,12 +372,10 @@ private bool ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citi switch (schedule.CurrentState) { case ResidentState.Shopping: - ProcessCitizenShopping(ref citizen); - return false; + return ProcessCitizenShopping(ref schedule, ref citizen); case ResidentState.Relaxing: - ProcessCitizenRelaxing(ref citizen); - return false; + return ProcessCitizenRelaxing(ref schedule, ref citizen); case ResidentState.Visiting: return ProcessCitizenVisit(ref schedule, ref citizen); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index 333718eb..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) + 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,52 +24,53 @@ private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen cit if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.MovingIn)) { CitizenMgr.ReleaseCitizen(citizenId); - residentSchedules[citizenId] = default; + schedule = default; } else { 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)} 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; } ushort targetBuilding = CitizenMgr.GetTargetBuilding(instanceId); if (targetBuilding == CitizenProxy.GetWorkBuilding(ref citizen)) { - return; + return true; } ItemClass.Service targetService = BuildingMgr.GetBuildingService(targetBuilding); if (targetService == ItemClass.Service.Beautification && IsBadWeather()) { Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} cancels the trip to a park due to bad weather"); - ushort home = CitizenProxy.GetHomeBuilding(ref citizen); - if (home != 0) - { - residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); - } + schedule.Schedule(ResidentState.AtHome, default); + return false; } + + return true; } private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) { - ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); - if (buildingId == 0) + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (currentBuilding == 0) { return 0; } - ushort foundBuilding = BuildingMgr.FindActiveBuilding(buildingId, distance, ItemClass.Service.Commercial); - if (IsBuildingNoiseRestricted(foundBuilding)) + 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; @@ -89,15 +90,15 @@ private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitiz return foundBuilding; } - private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId) + private ushort MoveToLeisureBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding) { ushort leisureBuilding = BuildingMgr.FindActiveBuilding( - buildingId, + currentBuilding, LeisureSearchDistance, ItemClass.Service.Commercial, ItemClass.SubService.CommercialLeisure); - if (IsBuildingNoiseRestricted(leisureBuilding)) + if (IsBuildingNoiseRestricted(leisureBuilding, currentBuilding)) { Log.Debug($"Citizen {citizenId} won't go to the leisure building {leisureBuilding}, it has a NIMBY policy"); return 0; diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index 40a4a25f..1433647b 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs @@ -76,7 +76,8 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin return; } - ResidentState nextState = Random.ShouldOccur(ReturnFromVisitChance) + uint relaxChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + ResidentState nextState = Random.ShouldOccur(relaxChance) ? ResidentState.Unknown : ResidentState.Relaxing; @@ -84,12 +85,12 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin if (schedule.CurrentState != ResidentState.Relaxing) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax, heading to an entertainment building."); + 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 void ProcessCitizenRelaxing(ref TCitizen citizen) + private bool ProcessCitizenRelaxing(ref CitizenSchedule schedule, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) @@ -98,6 +99,8 @@ private void ProcessCitizenRelaxing(ref TCitizen citizen) // 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); } + + return RescheduleVisit(ref schedule, ref citizen, currentBuilding); } private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen, bool localOnly) @@ -146,7 +149,8 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin } else { - ResidentState nextState = Random.ShouldOccur(ReturnFromShoppingChance) + uint moreShoppingChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + ResidentState nextState = Random.ShouldOccur(moreShoppingChance) ? ResidentState.Unknown : ResidentState.Shopping; @@ -154,57 +158,96 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin if (schedule.CurrentState != ResidentState.Shopping) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, heading to a random shop"); + 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)); } } } - private void ProcessCitizenShopping(ref TCitizen citizen) + private bool ProcessCitizenShopping(ref CitizenSchedule schedule, ref TCitizen citizen) { - if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)) + ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) && currentBuilding != 0) { - return; + BuildingMgr.ModifyMaterialBuffer(currentBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - ushort shop = CitizenProxy.GetVisitBuilding(ref citizen); - if (shop == 0) + return RescheduleVisit(ref schedule, ref citizen, currentBuilding); + } + + private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) + { + if (schedule.Hint == ScheduleHint.OnTour) { - return; + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a tour (see next line for citizen ID)"); + schedule.Schedule(ResidentState.Unknown, default); + return true; } - BuildingMgr.ModifyMaterialBuffer(shop, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); - CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); + return RescheduleVisit(ref schedule, ref citizen, CitizenProxy.GetVisitBuilding(ref citizen)); } - private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool IsBuildingNoiseRestricted(ushort targetBuilding, ushort currentBuilding) { - if (schedule.ScheduledState != ResidentState.Unknown) + if (BuildingMgr.GetBuildingSubService(targetBuilding) != ItemClass.SubService.CommercialLeisure) { return false; } - ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); - if (schedule.Hint == ScheduleHint.OnTour - || Random.ShouldOccur(ReturnFromVisitChance) - || (BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.CommercialLeisure - && TimeInfo.IsNightTime - && BuildingMgr.IsBuildingNoiseRestricted(visitBuilding))) + float currentHour = TimeInfo.CurrentHour; + if (currentHour >= Config.GoToSleepUpHour || currentHour <= Config.WakeupHour) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit (see next line for citizen ID)"); - schedule.Schedule(ResidentState.Unknown, default); - return true; + return BuildingMgr.IsBuildingNoiseRestricted(targetBuilding); + } + + float travelTime = GetEstimatedTravelTime(currentBuilding, targetBuilding); + if (travelTime == 0) + { + return false; + } + + float arriveHour = (float)TimeInfo.Now.AddHours(travelTime).TimeOfDay.TotalHours; + if (arriveHour >= Config.GoToSleepUpHour || arriveHour <= Config.WakeupHour) + { + return BuildingMgr.IsBuildingNoiseRestricted(targetBuilding); } return false; } - private bool IsBuildingNoiseRestricted(ushort building) + private bool RescheduleVisit(ref CitizenSchedule schedule, ref TCitizen citizen, ushort currentBuilding) { - float arriveHour = (float)TimeInfo.Now.AddHours(MaxTravelTime).TimeOfDay.TotalHours; - return (arriveHour >= Config.GoToSleepUpHour || TimeInfo.CurrentHour >= Config.GoToSleepUpHour - || arriveHour <= Config.WakeupHour || TimeInfo.CurrentHour <= Config.WakeupHour) - && BuildingMgr.IsBuildingNoiseRestricted(building); + if (schedule.ScheduledState != ResidentState.Relaxing + && schedule.ScheduledState != ResidentState.Shopping + && schedule.ScheduledState != ResidentState.Visiting) + { + return false; + } + + if (IsBadWeather()) + { + 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 (IsBuildingNoiseRestricted(currentBuilding, currentBuilding)) + { + 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; + } + + uint stayChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen)); + if (!Random.ShouldOccur(stayChance)) + { + 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; + } + + return false; } } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 19a622ca..f8c5e015 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -78,8 +78,7 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) case ScheduleAction.Ignore: return; - case ScheduleAction.ProcessTransition: - ProcessCitizenMoving(instance, citizenId, ref citizen); + case ScheduleAction.ProcessTransition when ProcessCitizenMoving(ref schedule, instance, citizenId, ref citizen): return; } From d932849f21a8c93f4fa5a8c3ab51dbe74e0a4101 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 13:51:10 +0200 Subject: [PATCH 41/42] Tweak the going out chances for citizens (closes #57) --- src/RealTime/CustomAI/SpareTimeBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs index 261a6764..f79ba8c4 100644 --- a/src/RealTime/CustomAI/SpareTimeBehavior.cs +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -60,7 +60,7 @@ public void RefreshGoOutChances() float timeModifier; if (isDayTime) { - timeModifier = RealTimeMath.Clamp((currentHour - config.WakeupHour) * 2f, 0, 4f); + timeModifier = RealTimeMath.Clamp(currentHour - config.WakeupHour, 0, 4f); } else { From 60a7b76f0f3acdec3104e7f3edfcb53b12b789ce Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 22 Jul 2018 14:03:05 +0200 Subject: [PATCH 42/42] Improve industry working hours (closes #68) - generic industry has 3 shifts with a 5-day week - forestry and agriculture industries have 1 extended shift 7/7 - oil and ore industries have 3 shifts 7/7 (365/24/7) --- src/RealTime/CustomAI/WorkBehavior.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/RealTime/CustomAI/WorkBehavior.cs b/src/RealTime/CustomAI/WorkBehavior.cs index bac4b3f2..8a2e5797 100644 --- a/src/RealTime/CustomAI/WorkBehavior.cs +++ b/src/RealTime/CustomAI/WorkBehavior.cs @@ -67,6 +67,8 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz } ItemClass.Service buildingSevice = buildingManager.GetBuildingService(schedule.WorkBuilding); + ItemClass.SubService buildingSubService = buildingManager.GetBuildingSubService(schedule.WorkBuilding); + float workBegin, workEnd; WorkShift workShift = schedule.WorkShift; @@ -83,7 +85,7 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz case Citizen.AgeGroup.Adult: if (workShift == WorkShift.Unemployed) { - workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice)); + workShift = GetWorkShift(GetBuildingWorkShiftCount(buildingSevice, buildingSubService)); } workBegin = config.WorkBegin; @@ -94,7 +96,6 @@ public void UpdateWorkShift(ref CitizenSchedule schedule, Citizen.AgeGroup citiz return; } - ItemClass.SubService buildingSubService = buildingManager.GetBuildingSubService(schedule.WorkBuilding); switch (workShift) { case WorkShift.First when HasExtendedFirstWorkShift(buildingSevice, buildingSubService): @@ -194,6 +195,7 @@ private static bool IsBuildingActiveOnWeekend(ItemClass.Service service, ItemCla { 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: @@ -211,13 +213,15 @@ private static bool IsBuildingActiveOnWeekend(ItemClass.Service service, ItemCla } } - private static int GetBuildingWorkShiftCount(ItemClass.Service service) + 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: @@ -252,6 +256,8 @@ private static bool HasExtendedFirstWorkShift(ItemClass.Service service, ItemCla 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: