diff --git a/src/RealTime/Config/RealTimeConfig.cs b/src/RealTime/Config/RealTimeConfig.cs
index 533e4ddb..c6735f13 100644
--- a/src/RealTime/Config/RealTimeConfig.cs
+++ b/src/RealTime/Config/RealTimeConfig.cs
@@ -17,7 +17,7 @@ public sealed class RealTimeConfig : IConfiguration
/// The storage ID for the configuration objects.
public const string StorageId = "RealTimeConfiguration";
- private const int LatestVersion = 2;
+ private const int LatestVersion = 3;
/// Initializes a new instance of the class.
public RealTimeConfig()
@@ -105,6 +105,14 @@ public RealTimeConfig(bool latestVersion)
[ConfigItemCheckBox]
public bool SwitchOffLightsAtNight { get; set; }
+ ///
+ /// Gets or sets the maximum height of a residential, commercial, or office building that will switch the lights off
+ /// at night. All buildings higher than this value will not switch the lights off.
+ ///
+ [ConfigItem("1General", "1Other", 4)]
+ [ConfigItemSlider(0, 100f, 5f, ValueType = SliderValueType.Default)]
+ public float SwitchOffLightsMaxHeight { get; set; }
+
/// Gets or sets a value indicating whether a citizen can abandon a journey when being too long in
/// a traffic congestion or waiting too long for public transport.
[ConfigItem("1General", "1Other", 5)]
@@ -314,6 +322,8 @@ public void Validate()
VirtualCitizens = (VirtualCitizensLevel)FastMath.Clamp((int)VirtualCitizens, (int)VirtualCitizensLevel.None, (int)VirtualCitizensLevel.Vanilla);
ConstructionSpeed = FastMath.Clamp(ConstructionSpeed, 1u, 100u);
+ SwitchOffLightsMaxHeight = FastMath.Clamp(SwitchOffLightsMaxHeight, 0f, 100f);
+
SecondShiftQuota = FastMath.Clamp(SecondShiftQuota, 1u, 25u);
NightShiftQuota = FastMath.Clamp(NightShiftQuota, 1u, 25u);
LunchQuota = FastMath.Clamp(LunchQuota, 0u, 100u);
@@ -363,6 +373,7 @@ public void ResetToDefaults()
StopConstructionAtNight = true;
ConstructionSpeed = 50;
SwitchOffLightsAtNight = true;
+ SwitchOffLightsMaxHeight = 40f;
CanAbandonJourney = true;
SecondShiftQuota = 13;
diff --git a/src/RealTime/Core/ModIds.cs b/src/RealTime/Core/ModIds.cs
index fee4dd15..c0fd2497 100644
--- a/src/RealTime/Core/ModIds.cs
+++ b/src/RealTime/Core/ModIds.cs
@@ -26,5 +26,8 @@ internal static class ModIds
/// The Workshop ID of the 'Force Level Up' mod.
public const ulong ForceLevelUp = 523818382ul;
+
+ /// The Workshop ID of the 'Realistic Walking Speed' mod.
+ public const ulong RealisticWalkingSpeed = 1412844620ul;
}
}
diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs
index b34338e3..2b61eb76 100644
--- a/src/RealTime/Core/RealTimeCore.cs
+++ b/src/RealTime/Core/RealTimeCore.cs
@@ -141,9 +141,10 @@ public static RealTimeCore Run(
new EventManagerConnection(),
buildingManager,
randomizer,
- timeInfo);
+ timeInfo,
+ Constants.MaxTravelTime);
- if (!SetupCustomAI(timeInfo, configProvider.Configuration, gameConnections, eventManager))
+ if (!SetupCustomAI(timeInfo, configProvider.Configuration, gameConnections, eventManager, compatibility))
{
Log.Error("The 'Real Time' mod failed to setup the customized AI and will now be deactivated.");
patcher.Revert();
@@ -184,6 +185,11 @@ public static RealTimeCore Run(
SimulationHandler.Buildings = BuildingAIPatches.RealTimeAI;
SimulationHandler.Buildings.UpdateFrameDuration();
+ if (appliedPatches.Contains(CitizenManagerPatch.CreateCitizenPatch1))
+ {
+ CitizenManagerPatch.NewCitizenBehavior = new NewCitizenBehavior(randomizer, configProvider.Configuration);
+ }
+
if (appliedPatches.Contains(BuildingAIPatches.GetColor))
{
SimulationHandler.Buildings.InitializeLightState();
@@ -191,11 +197,11 @@ public static RealTimeCore Run(
SimulationHandler.Statistics = statistics;
- if (appliedPatches.Contains(WorldInfoPanelPatches.UpdateBindings))
+ if (appliedPatches.Contains(WorldInfoPanelPatch.UpdateBindings))
{
- WorldInfoPanelPatches.CitizenInfoPanel = CustomCitizenInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider);
- WorldInfoPanelPatches.VehicleInfoPanel = CustomVehicleInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider);
- WorldInfoPanelPatches.CampusWorldInfoPanel = CustomCampusWorldInfoPanel.Enable(localizationProvider);
+ WorldInfoPanelPatch.CitizenInfoPanel = CustomCitizenInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider);
+ WorldInfoPanelPatch.VehicleInfoPanel = CustomVehicleInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider);
+ WorldInfoPanelPatch.CampusWorldInfoPanel = CustomCampusWorldInfoPanel.Enable(localizationProvider);
}
AwakeSleepSimulation.Install(configProvider.Configuration);
@@ -243,6 +249,7 @@ public void Stop()
SimulationHandler.Statistics = null;
ParkPatches.SpareTimeBehavior = null;
OutsideConnectionAIPatch.SpareTimeBehavior = null;
+ CitizenManagerPatch.NewCitizenBehavior = null;
Log.Info("The 'Real Time' mod reverts method patches.");
patcher.Revert();
@@ -261,14 +268,14 @@ public void Stop()
StorageBase.CurrentLevelStorage.GameSaving -= GameSaving;
- WorldInfoPanelPatches.CitizenInfoPanel?.Disable();
- WorldInfoPanelPatches.CitizenInfoPanel = null;
+ WorldInfoPanelPatch.CitizenInfoPanel?.Disable();
+ WorldInfoPanelPatch.CitizenInfoPanel = null;
- WorldInfoPanelPatches.VehicleInfoPanel?.Disable();
- WorldInfoPanelPatches.VehicleInfoPanel = null;
+ WorldInfoPanelPatch.VehicleInfoPanel?.Disable();
+ WorldInfoPanelPatch.VehicleInfoPanel = null;
- WorldInfoPanelPatches.CampusWorldInfoPanel?.Disable();
- WorldInfoPanelPatches.CampusWorldInfoPanel = null;
+ WorldInfoPanelPatch.CampusWorldInfoPanel?.Disable();
+ WorldInfoPanelPatch.CampusWorldInfoPanel = null;
isEnabled = false;
}
@@ -289,7 +296,7 @@ public void Translate(ILocalizationProvider localizationProvider)
}
timeBar.Translate(localizationProvider.CurrentCulture);
- UIGraphPatches.Translate(localizationProvider.CurrentCulture);
+ UIGraphPatch.Translate(localizationProvider.CurrentCulture);
}
private static List GetMethodPatches(Compatibility compatibility)
@@ -309,10 +316,10 @@ private static List GetMethodPatches(Compatibility compatibility)
ResidentAIPatch.InstanceSimulationStep,
TouristAIPatch.Location,
TransferManagerPatch.AddOutgoingOffer,
- WorldInfoPanelPatches.UpdateBindings,
- UIGraphPatches.MinDataPoints,
- UIGraphPatches.VisibleEndTime,
- UIGraphPatches.BuildLabels,
+ WorldInfoPanelPatch.UpdateBindings,
+ UIGraphPatch.MinDataPoints,
+ UIGraphPatch.VisibleEndTime,
+ UIGraphPatch.BuildLabels,
WeatherManagerPatch.SimulationStepImpl,
ParkPatches.DistrictParkSimulation,
OutsideConnectionAIPatch.DummyTrafficProbability,
@@ -326,6 +333,8 @@ private static List GetMethodPatches(Compatibility compatibility)
{
patches.Add(ResidentAIPatch.UpdateAge);
patches.Add(ResidentAIPatch.CanMakeBabies);
+ patches.Add(CitizenManagerPatch.CreateCitizenPatch1);
+ patches.Add(CitizenManagerPatch.CreateCitizenPatch2);
}
if (compatibility.IsAnyModActive(
@@ -367,7 +376,8 @@ private static bool SetupCustomAI(
TimeInfo timeInfo,
RealTimeConfig config,
GameConnections gameConnections,
- RealTimeEventManager eventManager)
+ RealTimeEventManager eventManager,
+ Compatibility compatibility)
{
ResidentAIConnection residentAIConnection = ResidentAIPatch.GetResidentAIConnection();
if (residentAIConnection == null)
@@ -375,8 +385,12 @@ private static bool SetupCustomAI(
return false;
}
+ float travelDistancePerCycle = compatibility.IsAnyModActive(ModIds.RealisticWalkingSpeed)
+ ? Constants.AverageTravelDistancePerCycle * 0.583f
+ : Constants.AverageTravelDistancePerCycle;
+
var spareTimeBehavior = new SpareTimeBehavior(config, timeInfo);
- var travelBehavior = new TravelBehavior(gameConnections.BuildingManager);
+ var travelBehavior = new TravelBehavior(gameConnections.BuildingManager, travelDistancePerCycle);
var workBehavior = new WorkBehavior(config, gameConnections.Random, gameConnections.BuildingManager, timeInfo, travelBehavior);
ParkPatches.SpareTimeBehavior = spareTimeBehavior;
@@ -437,7 +451,7 @@ private static void LoadStorageData(IEnumerable storageData, Stora
}
}
- private void CityEventsChanged(object sender, EventArgs e) => timeBar.UpdateEventsDisplay(eventManager.CityEvents);
+ private void CityEventsChanged(object sender, EventArgs e) => timeBar.UpdateEventsDisplay(eventManager.AllEvents);
private void GameSaving(object sender, EventArgs e)
{
diff --git a/src/RealTime/Core/RealTimeMod.cs b/src/RealTime/Core/RealTimeMod.cs
index db5bd0f1..a9a683fb 100644
--- a/src/RealTime/Core/RealTimeMod.cs
+++ b/src/RealTime/Core/RealTimeMod.cs
@@ -59,7 +59,7 @@ public void OnEnabled()
}
Log.Info("The 'Real Time' mod has been enabled, version: " + modVersion);
- configProvider = new ConfigurationProvider(RealTimeConfig.StorageId, Name, () => new RealTimeConfig(true));
+ configProvider = new ConfigurationProvider(RealTimeConfig.StorageId, Name, () => new RealTimeConfig(latestVersion: true));
configProvider.LoadDefaultConfiguration();
localizationProvider = new LocalizationProvider(Name, modPath);
}
diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs
index 0ed67ad2..b8cf04c5 100644
--- a/src/RealTime/CustomAI/Constants.cs
+++ b/src/RealTime/CustomAI/Constants.cs
@@ -48,7 +48,7 @@ internal static class Constants
public const float PrepareToWorkHours = 1f;
/// An assumed maximum travel time to a target building (in hours).
- public const float MaxTravelTime = 3.5f;
+ public const float MaxTravelTime = 4f;
/// An assumed minimum travel time to a target building (in hours).
public const float MinTravelTime = 0.5f;
@@ -74,10 +74,9 @@ internal static class Constants
/// The chance of a young female to get pregnant.
public const uint YoungFemalePregnancyChance = 50u;
- /// The average distance a citizen can move for (walking, by car, by public transport) during a full simulation
- /// cycle at maximum time speed (6).
+ /// The average distance a citizen can travel for (walking, by car, by public transport) during a single simulation cycle.
/// This value was determined empirically.
- public const float AverageDistancePerSimulationCycle = 600f;
+ public const float AverageTravelDistancePerCycle = 450f;
/// The maximum number of buildings (of one zone type) that are in construction or upgrading process.
public const int MaximumBuildingsInConstruction = 50;
diff --git a/src/RealTime/CustomAI/INewCitizenBehavior.cs b/src/RealTime/CustomAI/INewCitizenBehavior.cs
new file mode 100644
index 00000000..ac12a414
--- /dev/null
+++ b/src/RealTime/CustomAI/INewCitizenBehavior.cs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) dymanoid. All rights reserved.
+//
+
+namespace RealTime.CustomAI
+{
+ ///
+ /// An interface for a behavior that determines the creation of new citizens.
+ ///
+ internal interface INewCitizenBehavior
+ {
+ ///
+ /// Gets the education level of the new citizen based on their .
+ ///
+ ///
+ /// The citizen's age as raw value (0-255).
+ /// The current value of the citizen's education.
+ ///
+ /// The education level of the new citizen with the specified age.
+ Citizen.Education GetEducation(int age, Citizen.Education currentEducation);
+
+ ///
+ /// Adjusts the age of the new citizen based on their current .
+ ///
+ /// The citizen's age as raw value (0-255).
+ /// An adjusted raw value (0-255) for the citizen's age.
+ int AdjustCitizenAge(int age);
+ }
+}
diff --git a/src/RealTime/CustomAI/NewCitizenBehavior.cs b/src/RealTime/CustomAI/NewCitizenBehavior.cs
new file mode 100644
index 00000000..d0979252
--- /dev/null
+++ b/src/RealTime/CustomAI/NewCitizenBehavior.cs
@@ -0,0 +1,148 @@
+//
+// Copyright (c) dymanoid. All rights reserved.
+//
+
+namespace RealTime.CustomAI
+{
+ using System;
+ using RealTime.Config;
+ using RealTime.Simulation;
+
+ ///
+ /// A behavior that determines the creation of new citizens.
+ ///
+ internal sealed class NewCitizenBehavior : INewCitizenBehavior
+ {
+ private readonly IRandomizer randomizer;
+ private readonly RealTimeConfig config;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The randomizer to use for random decisions.
+ /// The configuration to run with.
+ /// Thrown when any argument is null.
+ public NewCitizenBehavior(IRandomizer randomizer, RealTimeConfig config)
+ {
+ this.randomizer = randomizer ?? throw new ArgumentNullException(nameof(randomizer));
+ this.config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ ///
+ /// Gets the education level of the new citizen based on their .
+ ///
+ /// The citizen's age as raw value (0-255).
+ /// The current value of the citizen's education.
+ /// The education level of the new citizen with the specified age.
+ public Citizen.Education GetEducation(int age, Citizen.Education currentEducation)
+ {
+ if (!config.UseSlowAging)
+ {
+ return currentEducation;
+ }
+
+ var randomValue = randomizer.GetRandomValue(100u);
+
+ // Age:
+ // 0-14 -> child
+ // 15-44 -> teen
+ // 45-89 -> young
+ // 90-179 -> adult
+ // 180-255 -> senior
+ if (age < 10)
+ {
+ // little children
+ return Citizen.Education.Uneducated;
+ }
+ else if (age < 40)
+ {
+ // children and most of the teens
+ return randomValue <= 25 ? Citizen.Education.Uneducated : Citizen.Education.OneSchool;
+ }
+ else if (age < 80)
+ {
+ // few teens and most of the young adults
+ if (randomValue < 10)
+ {
+ return Citizen.Education.Uneducated;
+ }
+ else if (randomValue < 50)
+ {
+ return Citizen.Education.OneSchool;
+ }
+ else
+ {
+ return Citizen.Education.TwoSchools;
+ }
+ }
+ else if (age < 120)
+ {
+ // few young adults and some adults
+ if (randomValue < 5)
+ {
+ return Citizen.Education.Uneducated;
+ }
+ else if (randomValue < 15)
+ {
+ return Citizen.Education.OneSchool;
+ }
+ else if (randomValue < 50)
+ {
+ return Citizen.Education.TwoSchools;
+ }
+ else
+ {
+ return Citizen.Education.ThreeSchools;
+ }
+ }
+ else
+ {
+ // mature adults and all seniors
+ if (randomValue < 10)
+ {
+ return Citizen.Education.Uneducated;
+ }
+ else if (randomValue < 20)
+ {
+ return Citizen.Education.OneSchool;
+ }
+ else if (randomValue < 40)
+ {
+ return Citizen.Education.TwoSchools;
+ }
+ else
+ {
+ return Citizen.Education.ThreeSchools;
+ }
+ }
+ }
+
+ ///
+ /// Adjusts the age of the new citizen based on their current .
+ ///
+ /// The citizen's age as raw value (0-255).
+ /// An adjusted raw value (0-255) for the citizen's age.
+ public int AdjustCitizenAge(int age)
+ {
+ // Age:
+ // 0-14 -> child
+ // 15-44 -> teen
+ // 45-89 -> young
+ // 90-179 -> adult
+ // 180-255 -> senior
+ if (!config.UseSlowAging || age <= 1)
+ {
+ return age;
+ }
+ else if (age <= 15)
+ {
+ // children may be teens too
+ return 4 + randomizer.GetRandomValue(40u);
+ }
+ else
+ {
+ return 75 + randomizer.GetRandomValue(115u);
+ }
+ }
+ }
+}
diff --git a/src/RealTime/CustomAI/RealTimeBuildingAI.cs b/src/RealTime/CustomAI/RealTimeBuildingAI.cs
index bf4af5c6..da031f53 100644
--- a/src/RealTime/CustomAI/RealTimeBuildingAI.cs
+++ b/src/RealTime/CustomAI/RealTimeBuildingAI.cs
@@ -509,18 +509,25 @@ private bool ShouldSwitchBuildingLightsOff(ushort buildingId, ItemClass.Service
return false;
case ItemClass.Service.Residential:
+ if (buildingManager.GetBuildingHeight(buildingId) > config.SwitchOffLightsMaxHeight)
+ {
+ return false;
+ }
+
float currentHour = timeInfo.CurrentHour;
return currentHour < Math.Min(config.WakeUpHour, EarliestWakeUp) || currentHour >= config.GoToSleepHour;
- case ItemClass.Service.Office when buildingManager.GetBuildingLevel(buildingId) != ItemClass.Level.Level1:
- return false;
-
case ItemClass.Service.Commercial when subService == ItemClass.SubService.CommercialLeisure:
return IsNoiseRestricted(buildingId);
- case ItemClass.Service.Commercial
- when subService == ItemClass.SubService.CommercialHigh && buildingManager.GetBuildingLevel(buildingId) != ItemClass.Level.Level1:
- return false;
+ case ItemClass.Service.Office:
+ case ItemClass.Service.Commercial:
+ if (buildingManager.GetBuildingHeight(buildingId) > config.SwitchOffLightsMaxHeight)
+ {
+ return false;
+ }
+
+ goto default;
case ItemClass.Service.Monument:
case ItemClass.Service.VarsitySports:
diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs
index 0483f98b..0cf7b855 100644
--- a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs
+++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs
@@ -10,7 +10,6 @@ namespace RealTime.CustomAI
using RealTime.GameConnection;
using RealTime.Simulation;
using SkyTools.Tools;
- using static Constants;
///
/// A base class for the custom logic of a human in the game.
@@ -141,7 +140,7 @@ protected bool EnsureCitizenCanBeProcessed(uint citizenId, ref TCitizen citizen)
/// The citizen data reference.
///
/// The city event or null if none found.
- protected ICityEvent GetUpcomingEventToAttend(uint citizenId, ref TCitizen citizen)
+ protected ICityEvent GetEventToAttend(uint citizenId, ref TCitizen citizen)
{
ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen);
if (EventMgr.GetEventState(currentBuilding, DateTime.MaxValue) == CityEventState.Ongoing)
@@ -149,13 +148,14 @@ protected ICityEvent GetUpcomingEventToAttend(uint citizenId, ref TCitizen citiz
return null;
}
- 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))
+ var cityEvents = EventMgr.EventsToAttend;
+ for (int i = 0; i < cityEvents.Count; ++i)
{
- return upcomingEvent;
+ var cityEvent = cityEvents[i];
+ if (CanAttendEvent(citizenId, ref citizen, cityEvent))
+ {
+ return cityEvent;
+ }
}
return null;
diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs
index 12d56f82..c0a77921 100644
--- a/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs
+++ b/src/RealTime/CustomAI/RealTimeResidentAI.Visit.cs
@@ -24,7 +24,7 @@ private bool ScheduleRelaxing(ref CitizenSchedule schedule, uint citizenId, ref
return false;
}
- ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen);
+ ICityEvent cityEvent = GetEventToAttend(citizenId, ref citizen);
if (cityEvent != null)
{
ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen);
diff --git a/src/RealTime/CustomAI/RealTimeTouristAI.cs b/src/RealTime/CustomAI/RealTimeTouristAI.cs
index 035546ee..333e5ed9 100644
--- a/src/RealTime/CustomAI/RealTimeTouristAI.cs
+++ b/src/RealTime/CustomAI/RealTimeTouristAI.cs
@@ -240,7 +240,7 @@ when BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.Co
if (Random.ShouldOccur(TouristEventChance) && !WeatherInfo.IsBadWeather)
{
- ICityEvent cityEvent = GetUpcomingEventToAttend(citizenId, ref citizen);
+ ICityEvent cityEvent = GetEventToAttend(citizenId, ref citizen);
if (cityEvent != null
&& StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), cityEvent.BuildingId))
{
diff --git a/src/RealTime/CustomAI/TravelBehavior.cs b/src/RealTime/CustomAI/TravelBehavior.cs
index 1e9a6fff..6cf35613 100644
--- a/src/RealTime/CustomAI/TravelBehavior.cs
+++ b/src/RealTime/CustomAI/TravelBehavior.cs
@@ -4,6 +4,7 @@
namespace RealTime.CustomAI
{
+ using System;
using RealTime.GameConnection;
using SkyTools.Tools;
using static Constants;
@@ -14,17 +15,26 @@ namespace RealTime.CustomAI
internal sealed class TravelBehavior : ITravelBehavior
{
private readonly IBuildingManagerConnection buildingManager;
- private float averageCitizenSpeed;
+ private readonly float travelDistancePerCycle;
+ private float averageTravelSpeedPerHour;
/// Initializes a new instance of the class.
///
/// A proxy object that provides a way to call the game-specific methods of the class.
///
- /// Thrown when the argument is null.
- public TravelBehavior(IBuildingManagerConnection buildingManager)
+ /// The average distance a citizen can travel during a single simulation cycle.
+ /// Thrown when is null.
+ /// Thrown when is negative or zero.
+ public TravelBehavior(IBuildingManagerConnection buildingManager, float travelDistancePerCycle)
{
- this.buildingManager = buildingManager ?? throw new System.ArgumentNullException(nameof(buildingManager));
- averageCitizenSpeed = AverageDistancePerSimulationCycle;
+ if (travelDistancePerCycle <= 0)
+ {
+ throw new ArgumentException("The travel distance per cycle cannot be negative or zero.");
+ }
+
+ this.buildingManager = buildingManager ?? throw new ArgumentNullException(nameof(buildingManager));
+ this.travelDistancePerCycle = travelDistancePerCycle;
+ averageTravelSpeedPerHour = travelDistancePerCycle;
}
/// Sets the duration (in hours) of a full simulation cycle for all citizens.
@@ -37,7 +47,7 @@ public void SetSimulationCyclePeriod(float cyclePeriod)
cyclePeriod = 1f;
}
- averageCitizenSpeed = AverageDistancePerSimulationCycle / cyclePeriod;
+ averageTravelSpeedPerHour = travelDistancePerCycle / cyclePeriod;
}
/// Gets an estimated travel time (in hours) between two specified buildings.
@@ -57,7 +67,7 @@ public float GetEstimatedTravelTime(ushort building1, ushort building2)
return MinTravelTime;
}
- return FastMath.Clamp(distance / averageCitizenSpeed, MinTravelTime, MaxTravelTime);
+ return FastMath.Clamp(distance / averageTravelSpeedPerHour, MinTravelTime, MaxTravelTime);
}
}
}
diff --git a/src/RealTime/Events/CityEventBase.cs b/src/RealTime/Events/CityEventBase.cs
index a9cfa3fe..3045c069 100644
--- a/src/RealTime/Events/CityEventBase.cs
+++ b/src/RealTime/Events/CityEventBase.cs
@@ -23,6 +23,11 @@ internal abstract class CityEventBase : ICityEvent
/// Gets the localized name of the building this city event takes place in.
public string BuildingName { get; private set; }
+ ///
+ /// Gets the event color.
+ ///
+ public abstract EventColor Color { get; }
+
/// Accepts an event attendee with specified properties.
/// The attendee age.
/// The attendee gender.
diff --git a/src/RealTime/Events/EventColor.cs b/src/RealTime/Events/EventColor.cs
new file mode 100644
index 00000000..e53b2d85
--- /dev/null
+++ b/src/RealTime/Events/EventColor.cs
@@ -0,0 +1,68 @@
+//
+// Copyright (c) dymanoid. All rights reserved.
+//
+
+namespace RealTime.Events
+{
+ using System;
+
+ ///
+ /// A struct representing the event color.
+ ///
+ internal struct EventColor : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The red component of the color.
+ /// The green component of the color.
+ /// The blue component of the color.
+ public EventColor(byte red, byte green, byte blue)
+ {
+ Red = red;
+ Green = green;
+ Blue = blue;
+ }
+
+ ///
+ /// Gets the red component of the color.
+ ///
+ public byte Red { get; }
+
+ ///
+ /// Gets the green component of the color.
+ ///
+ public byte Green { get; }
+
+ ///
+ /// Gets the blue component of the color.
+ ///
+ public byte Blue { get; }
+
+ public static bool operator ==(EventColor left, EventColor right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(EventColor left, EventColor right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ public override bool Equals(object obj) => obj is EventColor color && Equals(color);
+
+ ///
+ public bool Equals(EventColor other) => Red == other.Red && Green == other.Green && Blue == other.Blue;
+
+ ///
+ public override int GetHashCode()
+ {
+ var hashCode = -1058441243;
+ hashCode = hashCode * -1521134295 + Red.GetHashCode();
+ hashCode = hashCode * -1521134295 + Green.GetHashCode();
+ hashCode = hashCode * -1521134295 + Blue.GetHashCode();
+ return hashCode;
+ }
+ }
+}
diff --git a/src/RealTime/Events/ICityEvent.cs b/src/RealTime/Events/ICityEvent.cs
index 09537b55..cdb8d265 100644
--- a/src/RealTime/Events/ICityEvent.cs
+++ b/src/RealTime/Events/ICityEvent.cs
@@ -22,6 +22,11 @@ internal interface ICityEvent
/// Gets the localized name of the building this city event takes place in.
string BuildingName { get; }
+ ///
+ /// Gets the event color.
+ ///
+ EventColor Color { get; }
+
///
/// Configures this event to take place in the specified building and at the specified start time.
///
diff --git a/src/RealTime/Events/IRealTimeEventManager.cs b/src/RealTime/Events/IRealTimeEventManager.cs
index 21506a8c..50125b29 100644
--- a/src/RealTime/Events/IRealTimeEventManager.cs
+++ b/src/RealTime/Events/IRealTimeEventManager.cs
@@ -9,6 +9,12 @@ namespace RealTime.Events
/// An interface for the customized city events manager.
internal interface IRealTimeEventManager
{
+ ///
+ /// Gets the events that can be attended by the citizens when they start traveling to the event
+ /// at the current game time.
+ ///
+ IReadOnlyList EventsToAttend { get; }
+
///
/// Gets the instance of an ongoing or upcoming city event that takes place in a building
/// with specified ID.
@@ -17,14 +23,6 @@ internal interface IRealTimeEventManager
/// An instance of the first matching city event, or null if none found.
ICityEvent GetCityEvent(ushort buildingId);
- ///
- /// Gets the instance of an upcoming city event whose start time is between the specified values.
- ///
- /// The earliest city event start time to consider.
- /// The latest city event start time to consider.
- /// An instance of the first matching city event, or null if none found.
- ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime latestStartTime);
-
/// 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.
diff --git a/src/RealTime/Events/RealTimeCityEvent.cs b/src/RealTime/Events/RealTimeCityEvent.cs
index 97bb6dc1..62194fee 100644
--- a/src/RealTime/Events/RealTimeCityEvent.cs
+++ b/src/RealTime/Events/RealTimeCityEvent.cs
@@ -36,6 +36,11 @@ public RealTimeCityEvent(CityEventTemplate eventTemplate, int attendeesCount)
this.attendeesCount = attendeesCount;
}
+ ///
+ /// Gets the event color.
+ ///
+ public override EventColor Color { get; } = new EventColor(180, 0, 90);
+
/// Accepts an event attendee with specified properties.
/// The attendee age.
/// The attendee gender.
diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs
index 606e6f3c..7b4e2262 100644
--- a/src/RealTime/Events/RealTimeEventManager.cs
+++ b/src/RealTime/Events/RealTimeEventManager.cs
@@ -5,6 +5,7 @@ namespace RealTime.Events
using System;
using System.Collections.Generic;
using System.IO;
+ using System.Linq;
using System.Xml.Serialization;
using RealTime.Config;
using RealTime.Events.Storage;
@@ -34,11 +35,14 @@ internal sealed class RealTimeEventManager : IStorageData, IRealTimeEventManager
private readonly IBuildingManagerConnection buildingManager;
private readonly IRandomizer randomizer;
private readonly ITimeInfo timeInfo;
- private readonly List currentEvents;
- private readonly IReadOnlyList readonlyCurrentEvents;
+ private readonly List eventsCache;
+ private readonly IReadOnlyList readonlyEventsCache;
- private ICityEvent lastActiveEvent;
- private ICityEvent activeEvent;
+ private readonly float attendingTimeMargin;
+ private readonly List eventsToAttend;
+
+ private readonly List finishedEvents = new List();
+ private readonly List activeEvents = new List();
private DateTime lastProcessed;
private DateTime earliestEvent;
@@ -55,6 +59,8 @@ internal sealed class RealTimeEventManager : IStorageData, IRealTimeEventManager
/// An object that implements of the interface.
///
/// The time information source.
+ /// The time margin in hours specifying the maximum time before an event
+ /// can be attended by the citizen.
/// Thrown when any argument is null.
public RealTimeEventManager(
RealTimeConfig config,
@@ -62,7 +68,8 @@ public RealTimeEventManager(
IEventManagerConnection eventManager,
IBuildingManagerConnection buildingManager,
IRandomizer randomizer,
- ITimeInfo timeInfo)
+ ITimeInfo timeInfo,
+ float attendingTimeMargin)
{
this.config = config ?? throw new ArgumentNullException(nameof(config));
this.eventProvider = eventProvider ?? throw new ArgumentNullException(nameof(eventProvider));
@@ -70,36 +77,37 @@ public RealTimeEventManager(
this.buildingManager = buildingManager ?? throw new ArgumentNullException(nameof(buildingManager));
this.randomizer = randomizer ?? throw new ArgumentNullException(nameof(randomizer));
this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo));
+ this.attendingTimeMargin = attendingTimeMargin;
+
upcomingEvents = new LinkedList();
- currentEvents = new List();
- readonlyCurrentEvents = new ReadOnlyList(currentEvents);
+ eventsCache = new List();
+ readonlyEventsCache = new ReadOnlyList(eventsCache);
+ eventsToAttend = new List();
+ EventsToAttend = new ReadOnlyList(eventsToAttend);
}
/// Occurs when currently preparing, ready, ongoing, or recently finished events change.
public event EventHandler EventsChanged;
/// Gets the currently preparing, ready, ongoing, or recently finished city events.
- public IReadOnlyList CityEvents
+ public IReadOnlyList AllEvents
{
get
{
- currentEvents.Clear();
-
- if (lastActiveEvent != null)
- {
- currentEvents.Add(lastActiveEvent);
- }
-
- if (activeEvent != null)
- {
- currentEvents.Add(activeEvent);
- }
-
- upcomingEvents.CopyTo(currentEvents);
- return readonlyCurrentEvents;
+ eventsCache.Clear();
+ eventsCache.AddRange(finishedEvents);
+ eventsCache.AddRange(activeEvents);
+ upcomingEvents.CopyTo(eventsCache);
+ return readonlyEventsCache;
}
}
+ ///
+ /// Gets the events that can be attended by the citizens when they start traveling to the event
+ /// at the current game time.
+ ///
+ public IReadOnlyList EventsToAttend { get; }
+
/// Gets an unique ID of this storage data set.
string IStorageData.StorageDataId => StorageDataId;
@@ -122,7 +130,7 @@ public CityEventState GetEventState(ushort buildingId, DateTime latestStart)
EventData.Flags vanillaEventState = eventManager.GetEventFlags(eventId);
if ((vanillaEventState & (EventData.Flags.Preparing | EventData.Flags.Ready)) != 0)
{
- if (eventManager.TryGetEventInfo(eventId, out _, out DateTime startTime, out _, out _) && startTime <= latestStart)
+ if (eventManager.TryGetEventStartTime(eventId, out DateTime startTime) && startTime <= latestStart)
{
return CityEventState.Upcoming;
}
@@ -141,12 +149,12 @@ public CityEventState GetEventState(ushort buildingId, DateTime latestStart)
}
}
- if (activeEvent != null && activeEvent.BuildingId == buildingId)
+ if (FindEventByBuildingId(activeEvents, buildingId) != null)
{
return CityEventState.Ongoing;
}
- if (lastActiveEvent != null && lastActiveEvent.BuildingId == buildingId)
+ if (FindEventByBuildingId(finishedEvents, buildingId) != null)
{
return CityEventState.Finished;
}
@@ -159,30 +167,6 @@ public CityEventState GetEventState(ushort buildingId, DateTime latestStart)
return CityEventState.None;
}
- ///
- /// Gets the instance of an upcoming city event whose start time is between the specified values.
- ///
- /// The earliest city event start time to consider.
- /// The latest city event start time to consider.
- /// An instance of the first matching city event, or null if none found.
- public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime latestStartTime)
- {
- if (activeEvent != null && activeEvent.EndTime > latestStartTime)
- {
- return activeEvent;
- }
-
- if (upcomingEvents.Count == 0)
- {
- return null;
- }
-
- ICityEvent upcomingEvent = upcomingEvents.First.Value;
- return upcomingEvent.StartTime >= earliestStartTime && upcomingEvent.StartTime <= latestStartTime
- ? upcomingEvent
- : null;
- }
-
///
/// Gets the instance of an ongoing or upcoming city event that takes place in a building
/// with specified ID.
@@ -196,7 +180,8 @@ public ICityEvent GetCityEvent(ushort buildingId)
return null;
}
- if (activeEvent != null && activeEvent.BuildingId == buildingId)
+ var activeEvent = FindEventByBuildingId(activeEvents, buildingId);
+ if (activeEvent != null)
{
return activeEvent;
}
@@ -231,6 +216,8 @@ public void ProcessEvents()
OnEventsChanged();
}
+ UpdateEventsToAttend();
+
if ((timeInfo.Now - lastProcessed) < EventProcessInterval)
{
return;
@@ -257,8 +244,8 @@ public void ProcessEvents()
/// A to read the data set from.
void IStorageData.ReadData(Stream source)
{
- lastActiveEvent = null;
- activeEvent = null;
+ finishedEvents.Clear();
+ activeEvents.Clear();
upcomingEvents.Clear();
var serializer = new XmlSerializer(typeof(RealTimeEventStorageContainer));
@@ -279,7 +266,7 @@ void IStorageData.ReadData(Stream source)
if (realTimeEvent.EndTime < timeInfo.Now)
{
- lastActiveEvent = realTimeEvent;
+ finishedEvents.Add(realTimeEvent);
}
else
{
@@ -300,22 +287,33 @@ void IStorageData.StoreData(Stream target)
EarliestEvent = earliestEvent.Ticks,
};
- AddEventToStorage(lastActiveEvent);
- AddEventToStorage(activeEvent);
- foreach (var cityEvent in upcomingEvents)
- {
- AddEventToStorage(cityEvent);
- }
+ AddEventsToStorage(finishedEvents);
+ AddEventsToStorage(activeEvents);
+ AddEventsToStorage(upcomingEvents);
serializer.Serialize(target, data);
- void AddEventToStorage(ICityEvent cityEvent)
+ void AddEventsToStorage(IEnumerable cityEvents)
+ {
+ foreach (var cityEvent in cityEvents.OfType())
+ {
+ data.Events.Add(cityEvent.GetStorageData());
+ }
+ }
+ }
+
+ private static ICityEvent FindEventByBuildingId(List cityEvents, ushort buildingId)
+ {
+ for (int i = 0; i < cityEvents.Count; ++i)
{
- if (cityEvent != null && cityEvent is RealTimeCityEvent realTimeEvent)
+ var cityEvent = cityEvents[i];
+ if (cityEvent.BuildingId == buildingId)
{
- data.Events.Add(realTimeEvent.GetStorageData());
+ return cityEvent;
}
}
+
+ return null;
}
private static ICityEvent GetVanillaEvent(IReadOnlyList events, ushort eventId, ushort buildingId)
@@ -335,11 +333,15 @@ private static ICityEvent GetVanillaEvent(IReadOnlyList events, usho
private void Update()
{
- if (activeEvent != null && activeEvent.EndTime <= timeInfo.Now)
+ for (int i = activeEvents.Count - 1; i >= 0; --i)
{
- Log.Debug(LogCategory.Events, timeInfo.Now, $"Event finished in {activeEvent.BuildingId}, started at {activeEvent.StartTime}, end time {activeEvent.EndTime}");
- lastActiveEvent = activeEvent;
- activeEvent = null;
+ var cityEvent = activeEvents[i];
+ if (cityEvent.EndTime <= timeInfo.Now)
+ {
+ Log.Debug(LogCategory.Events, timeInfo.Now, $"Event finished in {cityEvent.BuildingId}, started at {cityEvent.StartTime}, end time {cityEvent.EndTime}");
+ finishedEvents.Add(cityEvent);
+ activeEvents.RemoveAt(i);
+ }
}
bool eventsChanged = SynchronizeWithVanillaEvents();
@@ -349,16 +351,11 @@ private void Update()
LinkedListNode upcomingEvent = upcomingEvents.First;
while (upcomingEvent != null && upcomingEvent.Value.StartTime <= timeInfo.Now)
{
- if (activeEvent != null)
- {
- lastActiveEvent = activeEvent;
- }
-
- activeEvent = upcomingEvent.Value;
+ activeEvents.Add(upcomingEvent.Value);
upcomingEvents.RemoveFirst();
+ Log.Debug(LogCategory.Events, timeInfo.Now, $"Event started! Building {upcomingEvent.Value.BuildingId}, ends on {upcomingEvent.Value.EndTime}");
eventsChanged = true;
upcomingEvent = upcomingEvent.Next;
- Log.Debug(LogCategory.Events, timeInfo.Now, $"Event started! Building {activeEvent.BuildingId}, ends on {activeEvent.EndTime}");
}
}
@@ -368,90 +365,129 @@ private void Update()
}
}
+ private void UpdateEventsToAttend()
+ {
+ DateTime latestAttendTime = timeInfo.Now.AddHours(attendingTimeMargin);
+
+ eventsToAttend.Clear();
+ for (int i = 0; i < activeEvents.Count; ++i)
+ {
+ var activeEvent = activeEvents[i];
+ if (activeEvent.EndTime > latestAttendTime)
+ {
+ eventsToAttend.Add(activeEvent);
+ }
+ }
+
+ if (upcomingEvents.Count == 0)
+ {
+ return;
+ }
+
+ var upcomingEventNode = upcomingEvents.First;
+ while (upcomingEventNode != null)
+ {
+ var upcomingEvent = upcomingEventNode.Value;
+ if (upcomingEvent.StartTime <= latestAttendTime)
+ {
+ eventsToAttend.Add(upcomingEvent);
+ }
+
+ upcomingEventNode = upcomingEventNode.Next;
+ }
+ }
+
private bool SynchronizeWithVanillaEvents()
{
- bool result = false;
+ bool eventsChanged = false;
DateTime today = timeInfo.Now.Date;
var upcomingEventIds = eventManager.GetUpcomingEvents(today, today.AddDays(1));
for (int i = 0; i < upcomingEventIds.Count; ++i)
{
- ushort eventId = upcomingEventIds[i];
+ // The evaluation order is important here - avoid short-circuit, we need to call the method on each iteration
+ eventsChanged = SynchronizeWithVanillaEvent(upcomingEventIds[i]) || eventsChanged;
+ }
- if (!eventManager.TryGetEventInfo(eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice))
- {
- continue;
- }
+ return eventsChanged;
+ }
- if (startTime.AddHours(duration) < timeInfo.Now)
- {
- continue;
- }
+ private bool SynchronizeWithVanillaEvent(ushort eventId)
+ {
+ if (!eventManager.TryGetEventInfo(eventId, out var eventInfo))
+ {
+ return false;
+ }
- var existingVanillaEvent = GetVanillaEvent(CityEvents, eventId, buildingId);
+ var startTime = eventInfo.StartTime;
- if (existingVanillaEvent != null)
- {
- if (Math.Abs((startTime - existingVanillaEvent.StartTime).TotalMinutes) <= 5d)
- {
- continue;
- }
- else if (existingVanillaEvent == activeEvent)
- {
- activeEvent = null;
- }
- else
- {
- upcomingEvents.Remove(existingVanillaEvent);
- }
- }
+ if (startTime.AddHours(eventInfo.Duration) < timeInfo.Now)
+ {
+ return false;
+ }
+
+ var existingVanillaEvent = GetVanillaEvent(AllEvents, eventId, eventInfo.BuildingId);
- DateTime adjustedStartTime = AdjustEventStartTime(startTime, randomize: false);
- if (adjustedStartTime != startTime)
+ if (existingVanillaEvent != null)
+ {
+ if (Math.Abs((startTime - existingVanillaEvent.StartTime).TotalMinutes) <= 5d)
{
- startTime = adjustedStartTime;
- eventManager.SetStartTime(eventId, startTime);
+ return false;
}
-
- var newEvent = new VanillaEvent(eventId, duration, ticketPrice);
- newEvent.Configure(buildingId, buildingManager.GetBuildingName(buildingId), startTime);
- result = true;
- Log.Debug(LogCategory.Events, timeInfo.Now, $"Vanilla event registered for {newEvent.BuildingId}, start time {newEvent.StartTime}, end time {newEvent.EndTime}");
-
- LinkedListNode existingEvent = upcomingEvents.FirstOrDefaultNode(e => e.StartTime >= startTime);
- if (existingEvent == null)
+ else if (activeEvents.Contains(existingVanillaEvent))
{
- upcomingEvents.AddLast(newEvent);
+ activeEvents.Remove(existingVanillaEvent);
}
else
{
- upcomingEvents.AddBefore(existingEvent, newEvent);
- if (existingEvent.Value.StartTime < newEvent.EndTime && existingEvent.Value is RealTimeCityEvent)
- {
- // Avoid multiple events at the same time - vanilla events have priority
- upcomingEvents.Remove(existingEvent);
- earliestEvent = newEvent.EndTime.AddHours(12f);
- }
+ upcomingEvents.Remove(existingVanillaEvent);
}
}
- return result;
+ DateTime adjustedStartTime = AdjustEventStartTime(startTime, randomize: false);
+ if (adjustedStartTime != startTime)
+ {
+ startTime = adjustedStartTime;
+ eventManager.SetStartTime(eventId, startTime);
+ }
+
+ var newEvent = new VanillaEvent(eventId, eventInfo.Duration, eventInfo.TicketPrice, eventManager);
+ newEvent.Configure(eventInfo.BuildingId, buildingManager.GetBuildingName(eventInfo.BuildingId), startTime);
+ Log.Debug(LogCategory.Events, timeInfo.Now, $"Vanilla event registered for {newEvent.BuildingId}, start time {newEvent.StartTime}, end time {newEvent.EndTime}");
+
+ LinkedListNode existingEvent = upcomingEvents.FirstOrDefaultNode(e => e.StartTime >= startTime);
+ if (existingEvent == null)
+ {
+ upcomingEvents.AddLast(newEvent);
+ }
+ else
+ {
+ upcomingEvents.AddBefore(existingEvent, newEvent);
+ }
+
+ return true;
}
private bool RemoveCanceledEvents()
{
- if (lastActiveEvent != null && MustCancelEvent(lastActiveEvent))
+ for (int i = finishedEvents.Count - 1; i >= 0; --i)
{
- lastActiveEvent = null;
+ if (MustCancelEvent(finishedEvents[i]))
+ {
+ finishedEvents.RemoveAt(i);
+ }
}
bool eventsChanged = false;
- if (activeEvent != null && MustCancelEvent(activeEvent))
+ for (int i = activeEvents.Count - 1; i >= 0; --i)
{
- Log.Debug(LogCategory.Events, $"The active event in building {activeEvent.BuildingId} must be canceled");
- activeEvent = null;
- eventsChanged = true;
+ if (MustCancelEvent(activeEvents[i]))
+ {
+ Log.Debug(LogCategory.Events, $"The active event in building {activeEvents[i].BuildingId} must be canceled");
+ activeEvents.RemoveAt(i);
+ eventsChanged = true;
+ }
}
if (upcomingEvents.Count == 0)
diff --git a/src/RealTime/Events/VanillaEvent.cs b/src/RealTime/Events/VanillaEvent.cs
index 37685aba..ae97577a 100644
--- a/src/RealTime/Events/VanillaEvent.cs
+++ b/src/RealTime/Events/VanillaEvent.cs
@@ -4,6 +4,7 @@
namespace RealTime.Events
{
+ using RealTime.GameConnection;
using RealTime.Simulation;
/// A class for the default game city event.
@@ -12,21 +13,29 @@ internal sealed class VanillaEvent : CityEventBase
{
private readonly float duration;
private readonly float ticketPrice;
+ private readonly IEventManagerConnection eventManager;
/// Initializes a new instance of the class.
/// The event ID.
/// The city event duration in hours.
/// The event ticket price.
- public VanillaEvent(ushort id, float duration, float ticketPrice)
+ /// An reference.
+ public VanillaEvent(ushort id, float duration, float ticketPrice, IEventManagerConnection eventManager)
{
this.duration = duration;
this.ticketPrice = ticketPrice;
EventId = id;
+ this.eventManager = eventManager ?? throw new System.ArgumentNullException(nameof(eventManager));
}
/// Gets the vanilla event ID.
public ushort EventId { get; }
+ ///
+ /// Gets the event color.
+ ///
+ public override EventColor Color => eventManager.GetEventColor(EventId);
+
/// Accepts an event attendee with specified properties.
/// The attendee age.
/// The attendee gender.
diff --git a/src/RealTime/GameConnection/BuildingManagerConnection.cs b/src/RealTime/GameConnection/BuildingManagerConnection.cs
index 491042c1..728217be 100644
--- a/src/RealTime/GameConnection/BuildingManagerConnection.cs
+++ b/src/RealTime/GameConnection/BuildingManagerConnection.cs
@@ -361,14 +361,14 @@ public void UpdateBuildingColors(ushort buildingId)
}
}
- /// Gets the building's level.
+ /// Gets the building's height in game units.
/// The ID of the building.
- /// The level of the building with the specified ID.
- public ItemClass.Level GetBuildingLevel(ushort buildingId)
+ /// The height of the building with the specified ID.
+ public float GetBuildingHeight(ushort buildingId)
{
return buildingId == 0
- ? ItemClass.Level.None
- : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info?.m_class?.m_level ?? ItemClass.Level.None;
+ ? 0f
+ : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info?.m_size.y ?? 0f;
}
///
diff --git a/src/RealTime/GameConnection/EventManagerConnection.cs b/src/RealTime/GameConnection/EventManagerConnection.cs
index 06145aa9..3fea4202 100644
--- a/src/RealTime/GameConnection/EventManagerConnection.cs
+++ b/src/RealTime/GameConnection/EventManagerConnection.cs
@@ -6,6 +6,7 @@ namespace RealTime.GameConnection
{
using System;
using System.Collections.Generic;
+ using RealTime.Events;
///
/// The default implementation of the interface.
@@ -81,41 +82,75 @@ public IReadOnlyList GetUpcomingEvents(DateTime earliestTime, DateTime l
return readonlyUpcomingEvents;
}
+ /// Gets the start time of a city event with specified ID.
+ /// The ID of the city event to get start time of.
+ /// The start time of the event with the specified ID.
+ /// true if the start time was retrieved; otherwise, false.
+ public bool TryGetEventStartTime(ushort eventId, out DateTime startTime)
+ {
+ if (eventId == 0 || eventId >= EventManager.instance.m_events.m_size)
+ {
+ startTime = default;
+ return false;
+ }
+
+ ref EventData eventData = ref EventManager.instance.m_events.m_buffer[eventId];
+ if (eventData.Info?.m_type == EventManager.EventType.AcademicYear)
+ {
+ startTime = default;
+ return false;
+ }
+
+ startTime = eventData.StartTime;
+ return true;
+ }
+
///
/// Gets various information about a city event with specified ID.
///
/// The ID of the city event to get information for.
- /// The ID of a building where the city event takes place.
- /// The start time of the city event.
- /// The duration in hours of the city event.
- /// The city event's ticket price.
+ /// A ref-struct containing the event information.
///
/// true if the information was retrieved; otherwise, false.
///
- public bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice)
+ public bool TryGetEventInfo(ushort eventId, out VanillaEventInfo eventInfo)
{
- buildingId = default;
- duration = default;
- startTime = default;
- ticketPrice = default;
if (eventId == 0 || eventId >= EventManager.instance.m_events.m_size)
{
+ eventInfo = default;
return false;
}
ref EventData eventData = ref EventManager.instance.m_events.m_buffer[eventId];
if (eventData.Info?.m_type == EventManager.EventType.AcademicYear)
{
+ eventInfo = default;
return false;
}
- buildingId = eventData.m_building;
- startTime = eventData.StartTime;
- duration = eventData.Info.m_eventAI.m_eventDuration;
- ticketPrice = eventData.m_ticketPrice / 100f;
+ eventInfo = new VanillaEventInfo(
+ eventData.m_building,
+ eventData.StartTime,
+ eventData.Info.m_eventAI.m_eventDuration,
+ eventData.m_ticketPrice / 100f);
return true;
}
+ /// Gets the color of a city event with specified ID.
+ /// The ID of the city event to get the color of.
+ /// The color of the event.
+ public EventColor GetEventColor(ushort eventId)
+ {
+ if (eventId == 0 || eventId >= EventManager.instance.m_events.m_size)
+ {
+ return default;
+ }
+
+ ref EventData eventData = ref EventManager.instance.m_events.m_buffer[eventId];
+ var color = eventData.m_color;
+ return new EventColor(color.r, color.g, color.b);
+ }
+
/// Sets the start time of the event to the specified value.
/// The ID of the event to change.
/// The new event start time.
diff --git a/src/RealTime/GameConnection/IBuildingManagerConnection.cs b/src/RealTime/GameConnection/IBuildingManagerConnection.cs
index baa11dca..de78f754 100644
--- a/src/RealTime/GameConnection/IBuildingManagerConnection.cs
+++ b/src/RealTime/GameConnection/IBuildingManagerConnection.cs
@@ -158,10 +158,10 @@ ushort FindActiveBuilding(
/// The ID of the building to update.
void UpdateBuildingColors(ushort buildingId);
- /// Gets the building's level.
+ /// Gets the building's height in game units.
/// The ID of the building.
- /// The level of the building with the specified ID.
- ItemClass.Level GetBuildingLevel(ushort buildingId);
+ /// The height of the building with the specified ID.
+ float GetBuildingHeight(ushort buildingId);
/// Visually deactivates the building with specified ID without affecting its production or coverage.
/// The building ID.
diff --git a/src/RealTime/GameConnection/IEventManagerConnection.cs b/src/RealTime/GameConnection/IEventManagerConnection.cs
index 17be5c8e..0dd2b74f 100644
--- a/src/RealTime/GameConnection/IEventManagerConnection.cs
+++ b/src/RealTime/GameConnection/IEventManagerConnection.cs
@@ -5,6 +5,7 @@
namespace RealTime.GameConnection
{
using System;
+ using RealTime.Events;
/// An interface for the game specific logic related to the event management.
internal interface IEventManagerConnection
@@ -24,12 +25,20 @@ internal interface IEventManagerConnection
/// Gets various information about a city event with specified ID.
/// The ID of the city event to get information for.
- /// The ID of a building where the city event takes place.
- /// The start time of the city event.
- /// The duration in hours of the city event.
- /// The city event's ticket price.
+ /// A ref-struct containing the event information.
/// true if the information was retrieved; otherwise, false.
- bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice);
+ bool TryGetEventInfo(ushort eventId, out VanillaEventInfo eventInfo);
+
+ /// Gets the start time of a city event with specified ID.
+ /// The ID of the city event to get start time of.
+ /// The start time of the event with the specified ID.
+ /// true if the start time was retrieved; otherwise, false.
+ bool TryGetEventStartTime(ushort eventId, out DateTime startTime);
+
+ /// Gets the color of a city event with specified ID.
+ /// The ID of the city event to get the color of.
+ /// The color of the event.
+ EventColor GetEventColor(ushort eventId);
/// Sets the start time of the event to the specified value.
/// The ID of the event to change.
diff --git a/src/RealTime/GameConnection/Patches/CitizenManagerPatch.cs b/src/RealTime/GameConnection/Patches/CitizenManagerPatch.cs
new file mode 100644
index 00000000..93d87b17
--- /dev/null
+++ b/src/RealTime/GameConnection/Patches/CitizenManagerPatch.cs
@@ -0,0 +1,92 @@
+//
+// Copyright (c) dymanoid. All rights reserved.
+//
+
+namespace RealTime.GameConnection.Patches
+{
+ using System.Reflection;
+ using ColossalFramework.Math;
+ using RealTime.CustomAI;
+ using SkyTools.Patching;
+
+ ///
+ /// A static class that provides the patch objects for the game's citizen manager.
+ ///
+ internal static class CitizenManagerPatch
+ {
+ ///
+ /// Gets or sets the implementation of the interface.
+ ///
+ public static INewCitizenBehavior NewCitizenBehavior { get; set; }
+
+ /// Gets the patch object for the method that creates a new citizen.
+ public static IPatch CreateCitizenPatch1 { get; } = new CitizenManager_CreateCitizen1();
+
+ /// Gets the patch object for the method that creates a new citizen (with specified gender).
+ public static IPatch CreateCitizenPatch2 { get; } = new CitizenManager_CreateCitizen2();
+
+ private static void UpdateCizizenAge(uint citizenId)
+ {
+ ref var citizen = ref CitizenManager.instance.m_citizens.m_buffer[citizenId];
+ citizen.Age = NewCitizenBehavior.AdjustCitizenAge(citizen.Age);
+ }
+
+ private static void UpdateCitizenEducation(uint citizenId)
+ {
+ ref var citizen = ref CitizenManager.instance.m_citizens.m_buffer[citizenId];
+ var newEducation = NewCitizenBehavior.GetEducation(citizen.Age, citizen.EducationLevel);
+ citizen.Education3 = newEducation == Citizen.Education.ThreeSchools;
+ citizen.Education2 = newEducation == Citizen.Education.TwoSchools || newEducation == Citizen.Education.ThreeSchools;
+ citizen.Education1 = newEducation != Citizen.Education.Uneducated;
+ }
+
+ private sealed class CitizenManager_CreateCitizen1 : PatchBase
+ {
+ protected override MethodInfo GetMethod()
+ {
+ return typeof(CitizenManager).GetMethod(
+ "CreateCitizen",
+ BindingFlags.Instance | BindingFlags.Public,
+ null,
+ new[] { typeof(uint).MakeByRefType(), typeof(int), typeof(int), typeof(Randomizer).MakeByRefType() },
+ new ParameterModifier[0]);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1213", Justification = "Harmony patch")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming Rules", "SA1313", Justification = "Harmony patch")]
+ private static void Postfix(ref uint citizen, bool __result)
+ {
+ if (__result)
+ {
+ // This method is called by the game in two cases only: a new child is born or a citizen joins the city.
+ // So we tailor the age here.
+ UpdateCizizenAge(citizen);
+ UpdateCitizenEducation(citizen);
+ }
+ }
+ }
+
+ private sealed class CitizenManager_CreateCitizen2 : PatchBase
+ {
+ protected override MethodInfo GetMethod()
+ {
+ return typeof(CitizenManager).GetMethod(
+ "CreateCitizen",
+ BindingFlags.Instance | BindingFlags.Public,
+ null,
+ new[] { typeof(uint).MakeByRefType(), typeof(int), typeof(int), typeof(Randomizer).MakeByRefType(), typeof(Citizen.Gender) },
+ new ParameterModifier[0]);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1213", Justification = "Harmony patch")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming Rules", "SA1313", Justification = "Harmony patch")]
+ private static void Postfix(ref uint citizen, bool __result)
+ {
+ if (__result)
+ {
+ UpdateCitizenEducation(citizen);
+ }
+ }
+ }
+ }
+}
diff --git a/src/RealTime/GameConnection/Patches/UIGraphPatches.cs b/src/RealTime/GameConnection/Patches/UIGraphPatch.cs
similarity index 99%
rename from src/RealTime/GameConnection/Patches/UIGraphPatches.cs
rename to src/RealTime/GameConnection/Patches/UIGraphPatch.cs
index 17c7a08a..8730d111 100644
--- a/src/RealTime/GameConnection/Patches/UIGraphPatches.cs
+++ b/src/RealTime/GameConnection/Patches/UIGraphPatch.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) dymanoid. All rights reserved.
//
@@ -18,7 +18,7 @@ namespace RealTime.GameConnection.Patches
///
/// A static class that provides the patch objects for the statistics graph.
///
- internal static class UIGraphPatches
+ internal static class UIGraphPatch
{
private const int MinRangeInDays = 7;
private const int MinPointsCount = 32;
diff --git a/src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs b/src/RealTime/GameConnection/Patches/WorldInfoPanelPatch.cs
similarity index 95%
rename from src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs
rename to src/RealTime/GameConnection/Patches/WorldInfoPanelPatch.cs
index 989ac6b5..992ecb2a 100644
--- a/src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs
+++ b/src/RealTime/GameConnection/Patches/WorldInfoPanelPatch.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright (c) dymanoid. All rights reserved.
//
@@ -12,7 +12,7 @@ namespace RealTime.GameConnection.Patches
///
/// A static class that provides the patch objects for the world info panel game methods.
///
- internal static class WorldInfoPanelPatches
+ internal static class WorldInfoPanelPatch
{
/// Gets or sets the customized citizen information panel.
public static CustomCitizenInfoPanel CitizenInfoPanel { get; set; }
diff --git a/src/RealTime/GameConnection/VanillaEventInfo.cs b/src/RealTime/GameConnection/VanillaEventInfo.cs
new file mode 100644
index 00000000..91cae176
--- /dev/null
+++ b/src/RealTime/GameConnection/VanillaEventInfo.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (c) dymanoid. All rights reserved.
+//
+
+namespace RealTime.GameConnection
+{
+ using System;
+
+ ///
+ /// A ref-struct consolidating the information about a vanilla game event.
+ ///
+ internal readonly ref struct VanillaEventInfo
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The ID of the building where the event takes place.
+ /// The event start date and time.
+ /// The event duration in hours.
+ /// The ticket price for the event.
+ public VanillaEventInfo(ushort buildingId, DateTime startTime, float duration, float ticketPrice)
+ {
+ BuildingId = buildingId;
+ StartTime = startTime;
+ Duration = duration;
+ TicketPrice = ticketPrice;
+ }
+
+ ///
+ /// Gets the ID of the building where the event takes place.
+ ///
+ public ushort BuildingId { get; }
+
+ ///
+ /// Gets the event start date and time.
+ ///
+ public DateTime StartTime { get; }
+
+ ///
+ /// Gets the event duration in hours.
+ ///
+ public float Duration { get; }
+
+ ///
+ /// Gets the ticket price for the event.
+ ///
+ public float TicketPrice { get; }
+ }
+}
diff --git a/src/RealTime/Localization/Translations/de.xml b/src/RealTime/Localization/Translations/de.xml
index 8435ccdd..429b0361 100644
--- a/src/RealTime/Localization/Translations/de.xml
+++ b/src/RealTime/Localization/Translations/de.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -223,5 +226,5 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/en.xml b/src/RealTime/Localization/Translations/en.xml
index 6025563a..0b754b3f 100644
--- a/src/RealTime/Localization/Translations/en.xml
+++ b/src/RealTime/Localization/Translations/en.xml
@@ -2,7 +2,7 @@
-
+
@@ -10,6 +10,7 @@
+
@@ -23,7 +24,7 @@
-
+
@@ -42,10 +43,12 @@
+
+
-
+
@@ -59,7 +62,7 @@
-
+
@@ -70,7 +73,7 @@
-
+
@@ -192,7 +195,7 @@
-
+
@@ -223,5 +226,5 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/es.xml b/src/RealTime/Localization/Translations/es.xml
index 2bac5dfb..ebec0746 100644
--- a/src/RealTime/Localization/Translations/es.xml
+++ b/src/RealTime/Localization/Translations/es.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/fr.xml b/src/RealTime/Localization/Translations/fr.xml
index ac062121..25fd82e9 100644
--- a/src/RealTime/Localization/Translations/fr.xml
+++ b/src/RealTime/Localization/Translations/fr.xml
@@ -10,6 +10,7 @@
+
@@ -42,9 +43,11 @@
+
+
-
+
@@ -58,7 +61,7 @@
-
+
@@ -70,7 +73,7 @@
-
+
@@ -88,7 +91,7 @@
-
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/it.xml b/src/RealTime/Localization/Translations/it.xml
index 005a756f..7643fb72 100644
--- a/src/RealTime/Localization/Translations/it.xml
+++ b/src/RealTime/Localization/Translations/it.xml
@@ -2,26 +2,33 @@
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
@@ -36,8 +43,12 @@
+
+
+
+
-
+
@@ -46,10 +57,12 @@
+
+
-
+
@@ -60,7 +73,7 @@
-
+
@@ -76,6 +89,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -167,7 +195,7 @@
-
+
@@ -197,14 +225,6 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/ja.xml b/src/RealTime/Localization/Translations/ja.xml
index fb124e36..5ac478fa 100644
--- a/src/RealTime/Localization/Translations/ja.xml
+++ b/src/RealTime/Localization/Translations/ja.xml
@@ -2,7 +2,7 @@
-
+
@@ -10,6 +10,7 @@
+
@@ -23,7 +24,7 @@
-
+
@@ -42,10 +43,12 @@
+
+
-
+
@@ -59,7 +62,7 @@
-
+
@@ -70,7 +73,7 @@
-
+
@@ -192,7 +195,7 @@
-
+
@@ -223,5 +226,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/ko.xml b/src/RealTime/Localization/Translations/ko.xml
index 346a3fb5..235e03b4 100644
--- a/src/RealTime/Localization/Translations/ko.xml
+++ b/src/RealTime/Localization/Translations/ko.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/pl.xml b/src/RealTime/Localization/Translations/pl.xml
index 8f6e6780..da396136 100644
--- a/src/RealTime/Localization/Translations/pl.xml
+++ b/src/RealTime/Localization/Translations/pl.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/pt.xml b/src/RealTime/Localization/Translations/pt.xml
index e09735e1..2602fa8c 100644
--- a/src/RealTime/Localization/Translations/pt.xml
+++ b/src/RealTime/Localization/Translations/pt.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/ru.xml b/src/RealTime/Localization/Translations/ru.xml
index f5408de2..1f7994de 100644
--- a/src/RealTime/Localization/Translations/ru.xml
+++ b/src/RealTime/Localization/Translations/ru.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -223,5 +226,5 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/Localization/Translations/zh.xml b/src/RealTime/Localization/Translations/zh.xml
index d07995f1..b9f003bd 100644
--- a/src/RealTime/Localization/Translations/zh.xml
+++ b/src/RealTime/Localization/Translations/zh.xml
@@ -10,6 +10,7 @@
+
@@ -42,6 +43,8 @@
+
+
@@ -70,7 +73,7 @@
-
+
@@ -224,4 +227,4 @@
-
+
\ No newline at end of file
diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj
index 9c753208..8fe8a8c2 100644
--- a/src/RealTime/RealTime.csproj
+++ b/src/RealTime/RealTime.csproj
@@ -73,10 +73,12 @@
+
+
@@ -92,6 +94,7 @@
+
@@ -115,12 +118,13 @@
+
-
+
-
+
@@ -139,6 +143,7 @@
+
@@ -186,6 +191,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
PreserveNewest
diff --git a/src/RealTime/UI/CustomTimeBar.cs b/src/RealTime/UI/CustomTimeBar.cs
index 16252e4d..e9bf68c6 100644
--- a/src/RealTime/UI/CustomTimeBar.cs
+++ b/src/RealTime/UI/CustomTimeBar.cs
@@ -27,9 +27,13 @@ internal sealed class CustomTimeBar
private const string UILabelTime = "Time";
private const string UISpriteEvent = "Event";
- private static readonly Color32 EventColor = new Color32(180, 0, 90, 160);
+ private const byte EventSpriteOpacity = 128;
+
+ private static readonly Color32 TimeLabelShadowColor = new Color32(32, 32, 32, 255);
+ private static readonly Vector2 TimeLabelShadowOffset = new Vector2(1f, -1f);
private readonly List displayedEvents = new List();
+ private readonly List displayedEventSprites = new List();
private readonly List eventsToDisplay = new List();
private CultureInfo currentCulture = CultureInfo.CurrentCulture;
@@ -53,7 +57,8 @@ public void Enable(DateTime currentDate)
}
customDateTimeWrapper = new RealTimeUIDateTimeWrapper(currentDate);
- originalWrapper = SetUIDateTimeWrapper(customDateTimeWrapper, customize: true);
+ originalWrapper = SetCustomUIDateTimeWrapper(customDateTimeWrapper, enableWrapper: true);
+ SetEventColorUpdater(progressSprite, enableUpdater: true);
}
///
@@ -68,7 +73,8 @@ public void Disable()
}
RemoveAllCityEvents();
- SetUIDateTimeWrapper(originalWrapper, customize: false);
+ SetCustomUIDateTimeWrapper(originalWrapper, enableWrapper: false);
+ SetEventColorUpdater(progressSprite, enableUpdater: false);
originalWrapper = null;
progressSprite = null;
customDateTimeWrapper = null;
@@ -132,6 +138,25 @@ public void UpdateEventsDisplay(IReadOnlyList availableEvents)
}
}
+ ///
+ /// Updates the colors of currently displayed event bars.
+ ///
+ public void UpdateEventsColors()
+ {
+ if (progressSprite == null)
+ {
+ return;
+ }
+
+ foreach (var sprite in displayedEventSprites)
+ {
+ var cityEvent = (ICityEvent)sprite.objectUserData;
+ sprite.color = GetColor(cityEvent.Color, EventSpriteOpacity);
+ }
+ }
+
+ private static Color32 GetColor(EventColor color, byte alpha) => new Color32(color.Red, color.Green, color.Blue, alpha);
+
private static bool EventListsEqual(List first, List second)
{
if (first.Count != second.Count)
@@ -195,7 +220,7 @@ private static UISprite GetProgressSprite(UIPanel infoPanel)
return progressSprite;
}
- private static void CustomizeTimePanel(UISprite progressSprite)
+ private static void SetCustomTimePanelLayout(UISprite progressSprite, bool enableCustomization)
{
UILabel dateLabel = progressSprite.Find(UILabelTime);
if (dateLabel == null)
@@ -204,22 +229,29 @@ private static void CustomizeTimePanel(UISprite progressSprite)
return;
}
- dateLabel.autoSize = false;
- dateLabel.size = progressSprite.size;
- dateLabel.textAlignment = UIHorizontalAlignment.Center;
- dateLabel.relativePosition = new Vector3(0, 0, 0);
+ dateLabel.autoSize = !enableCustomization;
+ dateLabel.isInteractive = !enableCustomization;
+ dateLabel.useDropShadow = enableCustomization;
+ if (enableCustomization)
+ {
+ dateLabel.size = progressSprite.size;
+ dateLabel.textAlignment = UIHorizontalAlignment.Center;
+ dateLabel.relativePosition = new Vector3(0, 0, 0);
+ dateLabel.dropShadowColor = TimeLabelShadowColor;
+ dateLabel.dropShadowOffset = TimeLabelShadowOffset;
+ }
}
- private static void SetTooltip(UIComponent component, CultureInfo cultureInfo, bool customize)
+ private static void SetCustomTooltip(UIComponent component, CultureInfo cultureInfo, bool enableTooltip)
{
DateTooltipBehavior tooltipBehavior = component.gameObject.GetComponent();
- if (tooltipBehavior == null && customize)
+ if (enableTooltip && tooltipBehavior == null)
{
tooltipBehavior = component.gameObject.AddComponent();
tooltipBehavior.IgnoredComponentNamePrefix = UISpriteEvent;
tooltipBehavior.Translate(cultureInfo);
}
- else if (tooltipBehavior != null && !customize)
+ else if (!enableTooltip && tooltipBehavior != null)
{
UnityEngine.Object.Destroy(tooltipBehavior);
}
@@ -231,7 +263,21 @@ private static void TranslateTooltip(UIComponent tooltipParent, CultureInfo cult
tooltipBehavior?.Translate(cultureInfo);
}
- private UIDateTimeWrapper SetUIDateTimeWrapper(UIDateTimeWrapper wrapper, bool customize)
+ private void SetEventColorUpdater(UIComponent parent, bool enableUpdater)
+ {
+ var updateBehavior = parent.gameObject.GetComponent();
+ if (enableUpdater && updateBehavior == null)
+ {
+ updateBehavior = parent.gameObject.AddComponent();
+ updateBehavior.TimeBar = this;
+ }
+ else if (!enableUpdater && updateBehavior != null)
+ {
+ UnityEngine.Object.Destroy(updateBehavior);
+ }
+ }
+
+ private UIDateTimeWrapper SetCustomUIDateTimeWrapper(UIDateTimeWrapper wrapper, bool enableWrapper)
{
UIPanel infoPanel = UIView.Find(UIInfoPanel);
if (infoPanel == null)
@@ -243,12 +289,8 @@ private UIDateTimeWrapper SetUIDateTimeWrapper(UIDateTimeWrapper wrapper, bool c
progressSprite = GetProgressSprite(infoPanel);
if (progressSprite != null)
{
- SetTooltip(progressSprite, currentCulture, customize);
-
- if (customize)
- {
- CustomizeTimePanel(progressSprite);
- }
+ SetCustomTooltip(progressSprite, currentCulture, enableWrapper);
+ SetCustomTimePanelLayout(progressSprite, enableWrapper);
}
return ReplaceUIDateTimeWrapperInPanel(infoPanel, wrapper);
@@ -275,11 +317,32 @@ private void DisplayCityEvent(ICityEvent cityEvent, DateTime todayStart, DateTim
eventSprite.height = progressSprite.height;
eventSprite.width = endPosition - startPosition;
eventSprite.fillDirection = UIFillDirection.Horizontal;
- eventSprite.color = EventColor;
+ eventSprite.color = GetColor(cityEvent.Color, EventSpriteOpacity);
eventSprite.fillAmount = 1f;
+ eventSprite.SendToBack();
eventSprite.objectUserData = cityEvent;
eventSprite.eventClicked += EventSprite_Clicked;
SetEventTooltip(eventSprite, todayStart, todayEnd);
+
+ var spriteBounds = eventSprite.GetBounds();
+ var overlappingEvents = displayedEventSprites
+ .Where(e => e.GetBounds().Intersects(spriteBounds))
+ .ToList();
+
+ if (overlappingEvents.Count > 0)
+ {
+ overlappingEvents.Add(eventSprite);
+ var itemCount = overlappingEvents.Count;
+ var newSpriteHeight = progressSprite.height / itemCount;
+ for (int i = 0; i < itemCount; ++i)
+ {
+ var sprite = overlappingEvents[i];
+ sprite.height = newSpriteHeight;
+ sprite.relativePosition = new Vector3(sprite.relativePosition.x, i * newSpriteHeight);
+ }
+ }
+
+ displayedEventSprites.Add(eventSprite);
}
private void SetEventTooltip(UISprite eventSprite, DateTime todayStart, DateTime todayEnd)
@@ -305,16 +368,13 @@ private void RemoveAllCityEvents()
return;
}
- foreach (var cityEvent in displayedEvents)
+ foreach (var sprite in displayedEventSprites)
{
- UISprite sprite = progressSprite.Find(UISpriteEvent + cityEvent.BuildingId);
- if (sprite != null)
- {
- sprite.eventClicked -= EventSprite_Clicked;
- UnityEngine.Object.Destroy(sprite);
- }
+ sprite.eventClicked -= EventSprite_Clicked;
+ UnityEngine.Object.Destroy(sprite);
}
+ displayedEventSprites.Clear();
displayedEvents.Clear();
}
@@ -327,5 +387,13 @@ private void EventSprite_Clicked(UIComponent component, UIMouseEventParameter ev
}
private void OnCityEventClick(ushort buildingId) => CityEventClick?.Invoke(this, new CustomTimeBarClickEventArgs(buildingId));
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Used as a Unity3D component")]
+ private sealed class EventColorsUpdateBehavior : MonoBehaviour
+ {
+ public CustomTimeBar TimeBar { get; set; }
+
+ public void Update() => TimeBar?.UpdateEventsColors();
+ }
}
}
\ No newline at end of file
diff --git a/src/SkyTools b/src/SkyTools
index 49a0fd4c..0c6f45a2 160000
--- a/src/SkyTools
+++ b/src/SkyTools
@@ -1 +1 @@
-Subproject commit 49a0fd4c8285d89f44cd60dc466635ccb16acd07
+Subproject commit 0c6f45a2c5c093088e331c523ae3e91214f3cdc0