diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs index 01df13a6..50e7395b 100644 --- a/src/RealTime/CustomAI/Constants.cs +++ b/src/RealTime/CustomAI/Constants.cs @@ -18,11 +18,14 @@ internal static class Constants public const float FullSearchDistance = BuildingManager.BUILDINGGRID_RESOLUTION * BuildingManager.BUILDINGGRID_CELL_SIZE / 2f; /// A chance in percent for a citizen to stay home until next scheduled action. - public const uint StayHomeAllDayChance = 15; + public const uint StayHomeAllDayChance = 2; /// A chance in percent for a citizen to go shopping in the night. public const uint NightShoppingChance = 20u; + /// A chance in percent for a citizen to go shopping just for fun. + public const uint FunShoppingChance = 35u; + /// A chance in percent for a tourist to find a hotel for sleepover. public const uint FindHotelChance = 80; diff --git a/src/RealTime/CustomAI/RealTimeBuildingAI.cs b/src/RealTime/CustomAI/RealTimeBuildingAI.cs index 40d6ded3..87f4896d 100644 --- a/src/RealTime/CustomAI/RealTimeBuildingAI.cs +++ b/src/RealTime/CustomAI/RealTimeBuildingAI.cs @@ -94,9 +94,8 @@ public int GetConstructionTime() /// Performs the custom processing of the outgoing problem timer. /// /// The ID of the building to process. - /// The old value of the outgoing problem timer. - /// The new value of the outgoing problem timer. - public void ProcessOutgoingProblems(ushort buildingId, byte oldValue, byte newValue) + /// The previous value of the outgoing problem timer. + public void ProcessBuildingProblems(ushort buildingId, byte outgoingProblemTimer) { // We have only few customers at night - that's an intended behavior. // To avoid commercial buildings from collapsing due to lack of customers, @@ -104,7 +103,7 @@ public void ProcessOutgoingProblems(ushort buildingId, byte oldValue, byte newVa // In the daytime, the timer is running slower. if (timeInfo.IsNightTime || timeInfo.Now.Minute % ProblemTimersInterval != 0 || freezeProblemTimers) { - buildingManager.SetOutgoingProblemTimer(buildingId, oldValue); + buildingManager.SetOutgoingProblemTimer(buildingId, outgoingProblemTimer); } } @@ -113,8 +112,7 @@ public void ProcessOutgoingProblems(ushort buildingId, byte oldValue, byte newVa /// /// The ID of the building to process. /// The old value of the worker problem timer. - /// The new value of the worker problem timer. - public void ProcessWorkerProblems(ushort buildingId, byte oldValue, byte newValue) + public void ProcessWorkerProblems(ushort buildingId, byte oldValue) { // We force the problem timer to pause at night time. // In the daytime, the timer is running slower. @@ -176,9 +174,7 @@ public void ProcessFrame(uint frameIndex) /// public bool ShouldSwitchBuildingLightsOff(ushort buildingId) { - return config.SwitchOffLightsAtNight - ? !lightStates[buildingId] - : false; + return config.SwitchOffLightsAtNight && !lightStates[buildingId]; } private void UpdateLightState(uint frameIndex) diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs index 5290d75d..d234704d 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Common.cs @@ -239,7 +239,7 @@ when BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService. return ScheduleAction.Ignore; } - private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) + private bool 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); @@ -264,7 +264,7 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, if (schedule.ScheduledState != ResidentState.Unknown) { - return; + return false; } Log.Debug(TimeInfo.Now, $"Scheduling for {GetCitizenDesc(citizenId, ref citizen)}..."); @@ -279,7 +279,7 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, { if (ScheduleWork(ref schedule, ref citizen)) { - return; + return true; } if (schedule.ScheduledStateTime > nextActivityTime) @@ -291,23 +291,39 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, if (ScheduleShopping(ref schedule, ref citizen, false)) { Log.Debug($" - Schedule shopping"); - return; + return true; } if (ScheduleRelaxing(ref schedule, citizenId, ref citizen)) { Log.Debug($" - Schedule relaxing"); - return; + return true; } if (schedule.CurrentState == ResidentState.AtHome) { if (Random.ShouldOccur(StayHomeAllDayChance)) { - nextActivityTime = todayWakeup.FutureHour(Config.WakeupHour); + if (nextActivityTime < TimeInfo.Now) + { + nextActivityTime = todayWakeup.FutureHour(Config.WakeupHour); + } + } + else + { + nextActivityTime = default; } - Log.Debug($" - Schedule sleeping at home until {nextActivityTime}"); +#if DEBUG + if (nextActivityTime <= TimeInfo.Now) + { + Log.Debug($" - Schedule idle until next scheduling run"); + } + else + { + Log.Debug($" - Schedule idle until {nextActivityTime}"); + } +#endif schedule.Schedule(ResidentState.Unknown, nextActivityTime); } else @@ -315,13 +331,16 @@ private void UpdateCitizenSchedule(ref CitizenSchedule schedule, uint citizenId, Log.Debug($" - Schedule moving home"); schedule.Schedule(ResidentState.AtHome, default); } + + return true; } - private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen) + private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, uint citizenId, ref TCitizen citizen, bool noReschedule) { - if (ProcessCurrentState(ref schedule, ref citizen) && schedule.ScheduledState == ResidentState.Unknown) + if (ProcessCurrentState(ref schedule, citizenId, ref citizen) + && schedule.ScheduledState == ResidentState.Unknown && !noReschedule) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} will be re-scheduled now"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} will re-schedule now"); // If the state processing changed the schedule, we need to update it UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); @@ -367,21 +386,21 @@ private void ExecuteCitizenSchedule(ref CitizenSchedule schedule, TAI instance, } } - private bool ProcessCurrentState(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCurrentState(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { switch (schedule.CurrentState) { case ResidentState.AtHome: - return RescheduleAtHome(ref schedule, ref citizen); + return RescheduleAtHome(ref schedule, citizenId, ref citizen); case ResidentState.Shopping: - return ProcessCitizenShopping(ref schedule, ref citizen); + return ProcessCitizenShopping(ref schedule, citizenId, ref citizen); case ResidentState.Relaxing: - return ProcessCitizenRelaxing(ref schedule, ref citizen); + return ProcessCitizenRelaxing(ref schedule, citizenId, ref citizen); case ResidentState.Visiting: - return ProcessCitizenVisit(ref schedule, ref citizen); + return ProcessCitizenVisit(ref schedule, citizenId, ref citizen); case ResidentState.InShelter: return ProcessCitizenInShelter(ref schedule, ref citizen); diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs index 869cdb28..71eff2fb 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Home.cs @@ -21,13 +21,20 @@ private void DoScheduledHome(ref CitizenSchedule schedule, TAI instance, uint ci 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"); + + if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, homeBuilding)) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + schedule.Schedule(ResidentState.Unknown, default); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} back home"); + } + else + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go home from {currentBuilding} but can't, waiting for the next opportunity"); + } } - private bool RescheduleAtHome(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool RescheduleAtHome(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { if (schedule.CurrentState != ResidentState.AtHome || TimeInfo.Now < schedule.ScheduledStateTime) { @@ -39,24 +46,24 @@ private bool RescheduleAtHome(ref CitizenSchedule schedule, ref TCitizen citizen return false; } - if (IsBadWeather()) + if (schedule.ScheduledState != ResidentState.Shopping && IsBadWeather()) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} re-schedules an activity because of bad weather (see next line for citizen ID)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} re-schedules an activity because of bad weather"); schedule.Schedule(ResidentState.Unknown, default); return true; } - uint goOutChance = spareTimeBehavior.GetGoOutChance( - CitizenProxy.GetAge(ref citizen), - schedule.WorkShift, - schedule.ScheduledState == ResidentState.Shopping); + var age = CitizenProxy.GetAge(ref citizen); + uint goOutChance = schedule.ScheduledState == ResidentState.Shopping + ? spareTimeBehavior.GetShoppingChance(age) + : spareTimeBehavior.GetRelaxingChance(age, schedule.WorkShift); - if (Random.ShouldOccur(goOutChance)) + if (goOutChance > 0) { return false; } - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} re-schedules an activity because of time (see next line for citizen ID)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} re-schedules an activity because of time"); schedule.Schedule(ResidentState.Unknown, default); return true; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs index cdfe4e5e..8722855d 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.Moving.cs @@ -70,6 +70,12 @@ private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitiz } ushort foundBuilding = BuildingMgr.FindActiveBuilding(currentBuilding, distance, ItemClass.Service.Commercial); + if (foundBuilding == 0) + { + Log.Debug($"Citizen {citizenId} didn't find any visitable commercial buildings nearby"); + return 0; + } + if (IsBuildingNoiseRestricted(foundBuilding, currentBuilding)) { Log.Debug($"Citizen {citizenId} won't go to the commercial building {foundBuilding}, it has a NIMBY policy"); @@ -120,14 +126,12 @@ private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitiz 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; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomAI/RealTimeResidentAI.SchoolWork.cs index 3f1f962d..3abd5ef7 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, preparing for departure"); + Log.Debug($" - Work time in {timeLeft} hours, preparing for departure"); return true; } @@ -31,7 +31,7 @@ private bool ScheduleWork(ref CitizenSchedule schedule, ref TCitizen citizen) { if (schedule.CurrentState != ResidentState.AtHome) { - Log.Debug($" - Worktime in {timeLeft} hours, returning home"); + Log.Debug($" - Work time in {timeLeft} hours, returning home"); schedule.Schedule(ResidentState.AtHome, default); return true; } @@ -39,11 +39,11 @@ private bool ScheduleWork(ref CitizenSchedule schedule, ref TCitizen citizen) // 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"); + Log.Debug($" - Work time in {timeLeft} hours, trying local shop"); } else { - Log.Debug($" - Worktime in {timeLeft} hours, doing nothing"); + Log.Debug($" - Work time in {timeLeft} hours, doing nothing"); } return true; @@ -62,22 +62,31 @@ private void DoScheduledWork(ref CitizenSchedule schedule, TAI instance, uint ci { 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; + return; } - Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); - if (workBehavior.ScheduleLunch(ref schedule, citizenAge)) + if (residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, schedule.WorkBuilding)) { - 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}"); + if (schedule.CurrentState == ResidentState.AtHome) + { + schedule.DepartureToWorkTime = TimeInfo.Now; + } + + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + 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 + { + 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}"); + } } else { - 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}"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go to work from {currentBuilding} but can't, will try once again next time"); + schedule.Schedule(ResidentState.Unknown, default); } } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs index db78579a..3b993ac0 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(spareTimeBehavior.GetGoOutChance(citizenAge, schedule.WorkShift, false)) || IsBadWeather()) + if (!Random.ShouldOccur(spareTimeBehavior.GetRelaxingChance(citizenAge, schedule.WorkShift)) || IsBadWeather()) { return false; } @@ -49,7 +49,7 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin ushort leisure = MoveToLeisureBuilding(instance, citizenId, ref citizen, buildingId); if (leisure == 0) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but didn't found a leisure building"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted relax but didn't find a leisure building"); } else { @@ -76,10 +76,10 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin return; } - uint relaxChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, false); + uint relaxChance = spareTimeBehavior.GetRelaxingChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift); ResidentState nextState = Random.ShouldOccur(relaxChance) - ? ResidentState.Unknown - : ResidentState.Relaxing; + ? ResidentState.Relaxing + : ResidentState.Unknown; schedule.Schedule(nextState, default); @@ -90,7 +90,7 @@ private void DoScheduledRelaxing(ref CitizenSchedule schedule, TAI instance, uin } } - private bool ProcessCitizenRelaxing(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCitizenRelaxing(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) @@ -100,17 +100,28 @@ private bool ProcessCitizenRelaxing(ref CitizenSchedule schedule, ref TCitizen c BuildingMgr.ModifyMaterialBuffer(currentBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); } - return RescheduleVisit(ref schedule, ref citizen, currentBuilding); + return RescheduleVisit(ref schedule, citizenId, ref citizen, currentBuilding); } private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen, bool localOnly) { - if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) || IsBadWeather()) + // If the citizen doesn't need any good, he/she still can go shopping just for fun + if (!CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)) { - return false; + if (schedule.Hint == ScheduleHint.NoShoppingAnyMore || !Random.ShouldOccur(FunShoppingChance)) + { + schedule.Hint = ScheduleHint.None; + return false; + } + + schedule.Hint = ScheduleHint.NoShoppingAnyMore; + } + else + { + schedule.Hint = ScheduleHint.None; } - if (!Random.ShouldOccur(spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, true))) + if (!Random.ShouldOccur(spareTimeBehavior.GetShoppingChance(CitizenProxy.GetAge(ref citizen)))) { return false; } @@ -119,10 +130,6 @@ private bool ScheduleShopping(ref CitizenSchedule schedule, ref TCitizen citizen { schedule.Hint = ScheduleHint.LocalShoppingOnly; } - else - { - schedule.Hint = ScheduleHint.None; - } schedule.Schedule(ResidentState.Shopping, default); return true; @@ -143,27 +150,32 @@ private void DoScheduledShopping(ref CitizenSchedule schedule, TAI instance, uin } else { + if (TimeInfo.IsNightTime) + { + schedule.Hint = ScheduleHint.NoShoppingAnyMore; + } + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} goes shopping at a local shop {shop}"); } } else { - uint moreShoppingChance = spareTimeBehavior.GetGoOutChance(CitizenProxy.GetAge(ref citizen), schedule.WorkShift, true); - ResidentState nextState = Random.ShouldOccur(moreShoppingChance) - ? ResidentState.Unknown - : ResidentState.Shopping; + uint moreShoppingChance = spareTimeBehavior.GetShoppingChance(CitizenProxy.GetAge(ref citizen)); + ResidentState nextState = schedule.Hint != ScheduleHint.NoShoppingAnyMore && Random.ShouldOccur(moreShoppingChance) + ? ResidentState.Shopping + : ResidentState.Unknown; schedule.Schedule(nextState, default); if (schedule.CurrentState != ResidentState.Shopping) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} in state {schedule.CurrentState} wanna go shopping and schedules {nextState}, 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, hint = {schedule.Hint}"); residentAI.FindVisitPlace(instance, citizenId, currentBuilding, residentAI.GetShoppingReason(instance)); } } } - private bool ProcessCitizenShopping(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCitizenShopping(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { ushort currentBuilding = CitizenProxy.GetVisitBuilding(ref citizen); if (CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) && currentBuilding != 0) @@ -172,19 +184,19 @@ private bool ProcessCitizenShopping(ref CitizenSchedule schedule, ref TCitizen c CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); } - return RescheduleVisit(ref schedule, ref citizen, currentBuilding); + return RescheduleVisit(ref schedule, citizenId, ref citizen, currentBuilding); } - private bool ProcessCitizenVisit(ref CitizenSchedule schedule, ref TCitizen citizen) + private bool ProcessCitizenVisit(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen) { if (schedule.Hint == ScheduleHint.OnTour) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a tour (see next line for citizen ID)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} quits a tour"); schedule.Schedule(ResidentState.Unknown, default); return true; } - return RescheduleVisit(ref schedule, ref citizen, CitizenProxy.GetVisitBuilding(ref citizen)); + return RescheduleVisit(ref schedule, citizenId, ref citizen, CitizenProxy.GetVisitBuilding(ref citizen)); } private bool IsBuildingNoiseRestricted(ushort targetBuilding, ushort currentBuilding) @@ -215,7 +227,7 @@ private bool IsBuildingNoiseRestricted(ushort targetBuilding, ushort currentBuil return false; } - private bool RescheduleVisit(ref CitizenSchedule schedule, ref TCitizen citizen, ushort currentBuilding) + private bool RescheduleVisit(ref CitizenSchedule schedule, uint citizenId, ref TCitizen citizen, ushort currentBuilding) { switch (schedule.ScheduledState) { @@ -228,28 +240,28 @@ private bool RescheduleVisit(ref CitizenSchedule schedule, ref TCitizen citizen, return false; } - if (IsBadWeather()) + if (schedule.CurrentState != ResidentState.Shopping && IsBadWeather()) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of bad weather (see next line for citizen ID)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} quits a visit because of bad weather"); 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)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} quits a visit because of NIMBY policy"); schedule.Schedule(ResidentState.Unknown, default); return true; } - uint stayChance = spareTimeBehavior.GetGoOutChance( - CitizenProxy.GetAge(ref citizen), - schedule.WorkShift, - schedule.CurrentState == ResidentState.Shopping); + Citizen.AgeGroup age = CitizenProxy.GetAge(ref citizen); + uint stayChance = schedule.CurrentState == ResidentState.Shopping + ? spareTimeBehavior.GetShoppingChance(age) + : spareTimeBehavior.GetRelaxingChance(age, schedule.WorkShift); if (!Random.ShouldOccur(stayChance)) { - Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(0, ref citizen)} quits a visit because of time (see next line for citizen ID)"); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} quits a visit because of time"); schedule.Schedule(ResidentState.AtHome, default); return true; } diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 6358e2f8..0fdf6b95 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -101,8 +101,8 @@ public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) return; } - UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); - ExecuteCitizenSchedule(ref schedule, instance, citizenId, ref citizen); + bool updated = UpdateCitizenSchedule(ref schedule, citizenId, ref citizen); + ExecuteCitizenSchedule(ref schedule, instance, citizenId, ref citizen, updated); } /// Notifies that a citizen has arrived their destination. diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs index e3cdbb6a..d6f70055 100644 --- a/src/RealTime/CustomAI/RealTimeTouristAI.cs +++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs @@ -198,10 +198,10 @@ private void FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen cit return; } - uint goOutChance = spareTimeBehavior.GetGoOutChance( - CitizenProxy.GetAge(ref citizen), - WorkShift.Unemployed, - CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods)); + Citizen.AgeGroup age = CitizenProxy.GetAge(ref citizen); + uint goOutChance = CitizenProxy.HasFlags(ref citizen, Citizen.Flags.NeedGoods) + ? spareTimeBehavior.GetShoppingChance(age) + : spareTimeBehavior.GetRelaxingChance(age, WorkShift.Unemployed); if (!Random.ShouldOccur(goOutChance) || IsBadWeather()) { @@ -252,9 +252,10 @@ private void FindHotel(TAI instance, uint citizenId, ref TCitizen citizen) 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); - touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding); + if (touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding)) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + } } } } diff --git a/src/RealTime/CustomAI/ScheduleHint.cs b/src/RealTime/CustomAI/ScheduleHint.cs index 15aea66b..0f3c8755 100644 --- a/src/RealTime/CustomAI/ScheduleHint.cs +++ b/src/RealTime/CustomAI/ScheduleHint.cs @@ -13,6 +13,9 @@ internal enum ScheduleHint : byte /// The citizen can shop only locally. LocalShoppingOnly, + /// The citizen will not go shopping one more time right away. + NoShoppingAnyMore, + /// The citizen should find a leisure building. RelaxAtLeisureBuilding, diff --git a/src/RealTime/CustomAI/SpareTimeBehavior.cs b/src/RealTime/CustomAI/SpareTimeBehavior.cs index f00209a9..eaab42ee 100644 --- a/src/RealTime/CustomAI/SpareTimeBehavior.cs +++ b/src/RealTime/CustomAI/SpareTimeBehavior.cs @@ -49,7 +49,7 @@ public void SetSimulationCyclePeriod(float cyclePeriod) } /// Calculates the chances for the citizens to go out based on the current game time. - public void RefreshGoOutChances() + public void RefreshChances() { uint weekdayModifier; if (config.IsWeekendEnabled) @@ -73,22 +73,29 @@ public void RefreshGoOutChances() } /// - /// Gets the probability whether a citizen with specified age would go out on current time. + /// Gets the probability whether a citizen with specified age would go shopping on current time. /// /// /// The age of the citizen to check. - /// The citizen's assigned work shift (or ). - /// true when the citizen needs to buy something; otherwise, false. /// /// A percentage value in range of 0..100 that describes the probability whether - /// a citizen with specified age would go out on current time. - public uint GetGoOutChance(Citizen.AgeGroup citizenAge, WorkShift workShift, bool needsShopping) + /// a citizen with specified age would go shopping on current time. + public uint GetShoppingChance(Citizen.AgeGroup citizenAge) { - if (needsShopping) - { - return shoppingChances[(int)citizenAge]; - } + return shoppingChances[(int)citizenAge]; + } + /// + /// Gets the probability whether a citizen with specified age would go relaxing on current time. + /// + /// + /// The age of the citizen to check. + /// The citizen's assigned work shift (or ). + /// + /// A percentage value in range of 0..100 that describes the probability whether + /// a citizen with specified age would go relaxing on current time. + public uint GetRelaxingChance(Citizen.AgeGroup citizenAge, WorkShift workShift) + { int age = (int)citizenAge; switch (citizenAge) { @@ -246,11 +253,11 @@ private void CalculateShoppingChance(float currentHour) uint roundedChance = (uint)Math.Round(chance); - shoppingChances[(int)Citizen.AgeGroup.Child] = isNight ? 0u : (uint)Math.Round(chance * 0.6f); + shoppingChances[(int)Citizen.AgeGroup.Child] = isNight ? 0u : roundedChance; shoppingChances[(int)Citizen.AgeGroup.Teen] = isNight ? 0u : roundedChance; shoppingChances[(int)Citizen.AgeGroup.Young] = roundedChance; shoppingChances[(int)Citizen.AgeGroup.Adult] = roundedChance; - shoppingChances[(int)Citizen.AgeGroup.Senior] = isNight ? 0u : (uint)Math.Round(chance * 0.8f); + shoppingChances[(int)Citizen.AgeGroup.Senior] = isNight ? (uint)Math.Round(chance * 0.1f) : roundedChance; #if DEBUG if (oldChance != roundedChance) diff --git a/src/RealTime/GameConnection/BuildingManagerConnection.cs b/src/RealTime/GameConnection/BuildingManagerConnection.cs index 9af5276d..fc2c76bf 100644 --- a/src/RealTime/GameConnection/BuildingManagerConnection.cs +++ b/src/RealTime/GameConnection/BuildingManagerConnection.cs @@ -14,6 +14,9 @@ namespace RealTime.GameConnection /// internal sealed class BuildingManagerConnection : IBuildingManagerConnection { + private const int MaxBuildingGridIndex = BuildingManager.BUILDINGGRID_RESOLUTION - 1; + private const int BuildingGridMiddle = BuildingManager.BUILDINGGRID_RESOLUTION / 2; + /// Gets the service type of the building with specified ID. /// The ID of the building to get the service type of. /// @@ -24,7 +27,7 @@ public ItemClass.Service GetBuildingService(ushort buildingId) { return buildingId == 0 ? ItemClass.Service.None - : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.m_class.m_service; + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info?.m_class?.m_service ?? ItemClass.Service.None; } /// Gets the sub-service type of the building with specified ID. @@ -37,7 +40,7 @@ public ItemClass.SubService GetBuildingSubService(ushort buildingId) { return buildingId == 0 ? ItemClass.SubService.None - : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.m_class.m_subService; + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info?.m_class?.m_subService ?? ItemClass.SubService.None; } /// Gets the service and sub-service types of the building with specified ID. @@ -130,10 +133,10 @@ public void ModifyMaterialBuffer(ushort buildingId, TransferManager.TransferReas } ref Building building = ref BuildingManager.instance.m_buildings.m_buffer[buildingId]; - building.Info.m_buildingAI.ModifyMaterialBuffer(buildingId, ref building, reason, ref delta); + building.Info?.m_buildingAI.ModifyMaterialBuffer(buildingId, ref building, reason, ref delta); } - /// Finds an active building that matches the specified criteria. + /// Finds an active building that matches the specified criteria and can accept visitors. /// The building ID that represents the search area center point. /// The maximum distance for search, the search area radius. /// The building service type to find. @@ -155,13 +158,47 @@ public ushort FindActiveBuilding( Building.Flags restrictedFlags = Building.Flags.Deleted | Building.Flags.Evacuating | Building.Flags.Flooded | Building.Flags.Collapsed | Building.Flags.BurnedDown | Building.Flags.RoadAccessFailed; - return BuildingManager.instance.FindBuilding( - currentPosition, - maxDistance, - service, - subService, - Building.Flags.Created | Building.Flags.Completed | Building.Flags.Active, - restrictedFlags); + Building.Flags requiredFlags = Building.Flags.Created | Building.Flags.Completed | Building.Flags.Active; + Building.Flags combinedFlags = requiredFlags | restrictedFlags; + + int gridXFrom = Mathf.Max((int)(((currentPosition.x - maxDistance) / BuildingManager.BUILDINGGRID_CELL_SIZE) + BuildingGridMiddle), 0); + int gridZFrom = Mathf.Max((int)(((currentPosition.z - maxDistance) / BuildingManager.BUILDINGGRID_CELL_SIZE) + BuildingGridMiddle), 0); + int gridXTo = Mathf.Min((int)(((currentPosition.x + maxDistance) / BuildingManager.BUILDINGGRID_CELL_SIZE) + BuildingGridMiddle), MaxBuildingGridIndex); + int gridZTo = Mathf.Min((int)(((currentPosition.z + maxDistance) / BuildingManager.BUILDINGGRID_CELL_SIZE) + BuildingGridMiddle), MaxBuildingGridIndex); + + float sqrMaxDistance = maxDistance * maxDistance; + for (int z = gridZFrom; z <= gridZTo; ++z) + { + for (int x = gridXFrom; x <= gridXTo; ++x) + { + ushort buildingId = BuildingManager.instance.m_buildingGrid[(z * BuildingManager.BUILDINGGRID_RESOLUTION) + x]; + uint counter = 0; + while (buildingId != 0) + { + ref Building building = ref BuildingManager.instance.m_buildings.m_buffer[buildingId]; + if (building.Info != null + && building.Info.m_class != null + && (building.Info.m_class.m_service == service) + && (subService == ItemClass.SubService.None || building.Info.m_class.m_subService == subService) + && (building.m_flags & combinedFlags) == requiredFlags) + { + float sqrDistance = Vector3.SqrMagnitude(currentPosition - building.m_position); + if (sqrDistance < sqrMaxDistance && BuildingCanBeVisited(buildingId)) + { + return buildingId; + } + } + + buildingId = building.m_nextGridBuilding; + if (++counter >= BuildingManager.MAX_BUILDING_COUNT) + { + break; + } + } + } + } + + return 0; } /// Gets the ID of an event that takes place in the building with specified ID. @@ -253,7 +290,7 @@ public string GetBuildingClassName(ushort buildingId) { return buildingId == 0 ? string.Empty - : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.name; + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info?.name ?? string.Empty; } /// Gets the localized name of a building with specified ID. @@ -304,5 +341,33 @@ public void UpdateBuildingColors(ushort buildingId) BuildingManager.instance.UpdateBuildingColors(buildingId); } } + + private static bool BuildingCanBeVisited(ushort buildingId) + { + uint currentUnitId = BuildingManager.instance.m_buildings.m_buffer[buildingId].m_citizenUnits; + + uint counter = 0; + while (currentUnitId != 0) + { + ref CitizenUnit currentUnit = ref CitizenManager.instance.m_units.m_buffer[currentUnitId]; + if ((currentUnit.m_flags & CitizenUnit.Flags.Visit) != 0 + && (currentUnit.m_citizen0 == 0 + || currentUnit.m_citizen1 == 0 + || currentUnit.m_citizen2 == 0 + || currentUnit.m_citizen3 == 0 + || currentUnit.m_citizen4 == 0)) + { + return true; + } + + currentUnitId = currentUnit.m_nextUnit; + if (++counter >= CitizenManager.MAX_UNIT_COUNT) + { + break; + } + } + + return false; + } } } diff --git a/src/RealTime/GameConnection/CitizenConnection.cs b/src/RealTime/GameConnection/CitizenConnection.cs index 608c1b53..c21b25f1 100644 --- a/src/RealTime/GameConnection/CitizenConnection.cs +++ b/src/RealTime/GameConnection/CitizenConnection.cs @@ -224,16 +224,6 @@ public void SetLocation(ref Citizen citizen, Citizen.Location location) citizen.CurrentLocation = location; } - /// - /// Sets the ID of the building that is currently visited by the specified citizen. - /// - /// The citizen to set the visited building ID for. - /// The ID of the currently visited building. - public void SetVisitBuilding(ref Citizen citizen, ushort visitBuilding) - { - citizen.m_visitBuilding = visitBuilding; - } - /// /// Sets the ID of the building the specified citizen is currently visiting. /// diff --git a/src/RealTime/GameConnection/IBuildingManagerConnection.cs b/src/RealTime/GameConnection/IBuildingManagerConnection.cs index 0de218ed..0b80cc81 100644 --- a/src/RealTime/GameConnection/IBuildingManagerConnection.cs +++ b/src/RealTime/GameConnection/IBuildingManagerConnection.cs @@ -66,7 +66,7 @@ internal interface IBuildingManagerConnection /// The amount to modify the buffer by. void ModifyMaterialBuffer(ushort buildingId, TransferManager.TransferReason reason, int delta); - /// Finds an active building that matches the specified criteria. + /// Finds an active building that matches the specified criteria and can accept visitors. /// /// The building ID that represents the search area center point. /// diff --git a/src/RealTime/GameConnection/ICitizenConnection.cs b/src/RealTime/GameConnection/ICitizenConnection.cs index 8538e80e..b068e22d 100644 --- a/src/RealTime/GameConnection/ICitizenConnection.cs +++ b/src/RealTime/GameConnection/ICitizenConnection.cs @@ -26,13 +26,6 @@ internal interface ICitizenConnection /// The ID of the building currently visited by the citizen, or 0 if none found. ushort GetVisitBuilding(ref T citizen); - /// - /// Sets the ID of the building that is currently visited by the specified citizen. - /// - /// The citizen to set the visited building ID for. - /// The ID of the currently visited building. - void SetVisitBuilding(ref T citizen, ushort visitBuilding); - /// Gets the instance ID of the specified citizen. /// The citizen to get the instance ID of. /// The ID of the citizen's instance, or 0 if none found. diff --git a/src/RealTime/GameConnection/Patches/BuildingAIPatches.cs b/src/RealTime/GameConnection/Patches/BuildingAIPatches.cs index 09fb9ab1..c080271c 100644 --- a/src/RealTime/GameConnection/Patches/BuildingAIPatches.cs +++ b/src/RealTime/GameConnection/Patches/BuildingAIPatches.cs @@ -48,6 +48,13 @@ protected override MethodInfo GetMethod() private static bool Prefix(ref Building buildingData, ref byte __state) { __state = buildingData.m_outgoingProblemTimer; + if (buildingData.m_customBuffer2 > 0) + { + // Simulate some goods become spoiled; additionally, this will cause the buildings to never reach the 'stock full' state. + // In that state, no visits are possible anymore, so the building gets stuck + --buildingData.m_customBuffer2; + } + return true; } @@ -55,7 +62,7 @@ private static void Postfix(ushort buildingID, ref Building buildingData, byte _ { if (__state != buildingData.m_outgoingProblemTimer) { - RealTimeAI?.ProcessOutgoingProblems(buildingID, __state, buildingData.m_outgoingProblemTimer); + RealTimeAI?.ProcessBuildingProblems(buildingID, __state); } } #pragma warning restore SA1313 // Parameter names must begin with lower-case letter @@ -86,7 +93,7 @@ private static void Postfix(ushort buildingID, ref Building buildingData, byte _ { if (__state != buildingData.m_workerProblemTimer) { - RealTimeAI?.ProcessWorkerProblems(buildingID, __state, buildingData.m_workerProblemTimer); + RealTimeAI?.ProcessWorkerProblems(buildingID, __state); } } #pragma warning restore SA1313 // Parameter names must begin with lower-case letter diff --git a/src/RealTime/Localization/Translations/de.xml b/src/RealTime/Localization/Translations/de.xml index 819ca15b..134d888b 100644 --- a/src/RealTime/Localization/Translations/de.xml +++ b/src/RealTime/Localization/Translations/de.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/en.xml b/src/RealTime/Localization/Translations/en.xml index 76bb20ff..19e9081e 100644 --- a/src/RealTime/Localization/Translations/en.xml +++ b/src/RealTime/Localization/Translations/en.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/es.xml b/src/RealTime/Localization/Translations/es.xml index 4a10c751..ef7059bf 100644 --- a/src/RealTime/Localization/Translations/es.xml +++ b/src/RealTime/Localization/Translations/es.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/fr.xml b/src/RealTime/Localization/Translations/fr.xml index 38a7732d..7b8ec65f 100644 --- a/src/RealTime/Localization/Translations/fr.xml +++ b/src/RealTime/Localization/Translations/fr.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/ko.xml b/src/RealTime/Localization/Translations/ko.xml index 2de086e1..5aa00faf 100644 --- a/src/RealTime/Localization/Translations/ko.xml +++ b/src/RealTime/Localization/Translations/ko.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/pl.xml b/src/RealTime/Localization/Translations/pl.xml index 18c37f04..dd79c59e 100644 --- a/src/RealTime/Localization/Translations/pl.xml +++ b/src/RealTime/Localization/Translations/pl.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/pt.xml b/src/RealTime/Localization/Translations/pt.xml index 65259669..89a0b125 100644 --- a/src/RealTime/Localization/Translations/pt.xml +++ b/src/RealTime/Localization/Translations/pt.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/ru.xml b/src/RealTime/Localization/Translations/ru.xml index 7c300256..c08b8141 100644 --- a/src/RealTime/Localization/Translations/ru.xml +++ b/src/RealTime/Localization/Translations/ru.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Localization/Translations/zh.xml b/src/RealTime/Localization/Translations/zh.xml index f2ac101b..09e67f9c 100644 --- a/src/RealTime/Localization/Translations/zh.xml +++ b/src/RealTime/Localization/Translations/zh.xml @@ -1,4 +1,4 @@ - + diff --git a/src/RealTime/Simulation/CitizenProcessor.cs b/src/RealTime/Simulation/CitizenProcessor.cs index 977eb401..86b5e842 100644 --- a/src/RealTime/Simulation/CitizenProcessor.cs +++ b/src/RealTime/Simulation/CitizenProcessor.cs @@ -51,7 +51,7 @@ public void UpdateFrameDuration() /// Processes the simulation tick. public void ProcessTick() { - spareTimeBehavior.RefreshGoOutChances(); + spareTimeBehavior.RefreshChances(); } /// Processes the simulation frame. diff --git a/src/RealTime/UI/CustomTimeBar.cs b/src/RealTime/UI/CustomTimeBar.cs index 56409950..587fe26b 100644 --- a/src/RealTime/UI/CustomTimeBar.cs +++ b/src/RealTime/UI/CustomTimeBar.cs @@ -231,11 +231,11 @@ private UIDateTimeWrapper SetUIDateTimeWrapper(UIDateTimeWrapper wrapper, bool c private void DisplayCityEvent(ICityEvent cityEvent, DateTime todayStart, DateTime todayEnd) { float startPercent = cityEvent.StartTime <= todayStart - ? startPercent = 0 + ? 0 : (float)cityEvent.StartTime.TimeOfDay.TotalHours / 24f; float endPercent = cityEvent.EndTime >= todayEnd - ? endPercent = 1f + ? 1f : (float)cityEvent.EndTime.TimeOfDay.TotalHours / 24f; float startPosition = progressSprite.width * startPercent;