diff --git a/RealTime/RealTime.csproj b/RealTime/RealTime.csproj deleted file mode 100644 index 41a9c532..00000000 --- a/RealTime/RealTime.csproj +++ /dev/null @@ -1,89 +0,0 @@ - - - - - Debug - AnyCPU - {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52} - Library - Properties - RealTime - RealTime - v3.5 - 512 - - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\RealTime.xml - true - RealTime.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\RealTime.xml - true - RealTime.ruleset - - - - ..\..\..\..\..\Data\SteamLibrary\steamapps\common\Cities_Skylines\Cities_Data\Managed\Assembly-CSharp.dll - False - - - ..\..\..\..\..\Data\SteamLibrary\steamapps\common\Cities_Skylines\Cities_Data\Managed\ColossalManaged.dll - False - - - ..\..\..\..\..\Data\SteamLibrary\steamapps\common\Cities_Skylines\Cities_Data\Managed\ICities.dll - False - - - - - - - - ..\..\..\..\..\Data\SteamLibrary\steamapps\common\Cities_Skylines\Cities_Data\Managed\UnityEngine.dll - False - - - - - - - - - - - - - - - - - mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(ProjectName)" -del "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(ProjectName)\$(TargetFileName)" -xcopy /y "$(TargetDir)*.dll" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(ProjectName)" - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - \ No newline at end of file diff --git a/RealTime/RealTime.ruleset b/RealTime/RealTime.ruleset deleted file mode 100644 index 4d362ff7..00000000 --- a/RealTime/RealTime.ruleset +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/RealTime/RealTime.sln b/RealTime/RealTime.sln deleted file mode 100644 index f9e91c4b..00000000 --- a/RealTime/RealTime.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealTime", "RealTime.csproj", "{7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {99EFBE80-0D13-4351-896D-96D0F6D5FEFB} - EndGlobalSection -EndGlobal diff --git a/src/BuildEnvironment/RealTime.ruleset b/src/BuildEnvironment/RealTime.ruleset new file mode 100644 index 00000000..9ee68edc --- /dev/null +++ b/src/BuildEnvironment/RealTime.ruleset @@ -0,0 +1,661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RealTime/stylecop.json b/src/BuildEnvironment/stylecop.json similarity index 100% rename from RealTime/stylecop.json rename to src/BuildEnvironment/stylecop.json diff --git a/src/RealTime.sln b/src/RealTime.sln new file mode 100644 index 00000000..47c20d33 --- /dev/null +++ b/src/RealTime.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2036 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealTime", "RealTime\RealTime.csproj", "{7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Redirection", "Redirection\Redirection.csproj", "{7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RealTimeTests", "RealTimeTests\RealTimeTests.csproj", "{687CE9B5-2E2E-454E-83FC-512250CACF3E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Debug|x64.ActiveCfg = Debug|x64 + {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Debug|x64.Build.0 = Debug|x64 + {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Release|x64.ActiveCfg = Release|x64 + {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52}.Release|x64.Build.0 = Release|x64 + {7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13}.Debug|x64.ActiveCfg = Debug|x64 + {7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13}.Debug|x64.Build.0 = Debug|x64 + {7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13}.Release|x64.ActiveCfg = Release|x64 + {7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13}.Release|x64.Build.0 = Release|x64 + {687CE9B5-2E2E-454E-83FC-512250CACF3E}.Debug|x64.ActiveCfg = Debug|x64 + {687CE9B5-2E2E-454E-83FC-512250CACF3E}.Debug|x64.Build.0 = Debug|x64 + {687CE9B5-2E2E-454E-83FC-512250CACF3E}.Release|x64.ActiveCfg = Release|x64 + {687CE9B5-2E2E-454E-83FC-512250CACF3E}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {99EFBE80-0D13-4351-896D-96D0F6D5FEFB} + EndGlobalSection +EndGlobal diff --git a/src/RealTime/Config/ConfigurationProvider.cs b/src/RealTime/Config/ConfigurationProvider.cs new file mode 100644 index 00000000..ad485273 --- /dev/null +++ b/src/RealTime/Config/ConfigurationProvider.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Config +{ + using System; + using System.IO; + using System.Xml.Serialization; + using RealTime.Tools; + + internal static class ConfigurationProvider + { + private static readonly string SettingsFileName = typeof(ConfigurationProvider).Assembly.GetName().Name + ".xml"; + + public static RealTimeConfig LoadConfiguration() + { + try + { + if (!File.Exists(SettingsFileName)) + { + return new RealTimeConfig(); + } + + return Deserialize(); + } + catch + { + return new RealTimeConfig(); + } + } + + public static void SaveConfiguration(RealTimeConfig config) + { + try + { + Serialize(config); + } + catch (Exception ex) + { + Log.Error("The 'Real Time' mod cannot save its configuration, error message: " + ex.Message); + } + } + + private static RealTimeConfig Deserialize() + { + var serializer = new XmlSerializer(typeof(RealTimeConfig)); + using (var sr = new StreamReader(SettingsFileName)) + { + return (RealTimeConfig)serializer.Deserialize(sr); + } + } + + private static void Serialize(RealTimeConfig config) + { + var serializer = new XmlSerializer(typeof(RealTimeConfig)); + using (var sw = new StreamWriter(SettingsFileName)) + { + serializer.Serialize(sw, config); + } + } + } +} diff --git a/src/RealTime/Config/RealTimeConfig.cs b/src/RealTime/Config/RealTimeConfig.cs new file mode 100644 index 00000000..2edce81b --- /dev/null +++ b/src/RealTime/Config/RealTimeConfig.cs @@ -0,0 +1,128 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Config +{ + using RealTime.UI; + + /// + /// The mod's configuration. + /// + public sealed class RealTimeConfig + { + /// + /// Gets or sets a value indicating whether the weekends are enabled. Cims don't go to work on weekends. + /// + [ConfigItem("1General", 0)] + [ConfigItemCheckBox] + public bool IsWeekendEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating whether Cims should go out at lunch for food. + /// + [ConfigItem("1General", 1)] + [ConfigItemCheckBox] + public bool IsLunchtimeEnabled { get; set; } = true; + + [ConfigItem("1General", 2)] + [ConfigItemCheckBox] + public bool StopConstructionAtNight { get; set; } = true; + + [ConfigItem("1General", 3)] + [ConfigItemSlider(1, 100)] + public uint ConstructionSpeed { get; set; } = 50; + + /// + /// Gets or sets the percentage of the Cims that will go out for lunch. + /// Valid values are 0..100. + /// + [ConfigItem("2Quotas", 0)] + [ConfigItemSlider(0, 100)] + public uint LunchQuota { get; set; } = 80; + + /// + /// Gets or sets the percentage of the population that will search locally for buildings. + /// Valid values are 0..100. + /// + [ConfigItem("2Quotas", 1)] + [ConfigItemSlider(0, 100)] + public uint LocalBuildingSearchQuota { get; set; } = 60; + + /// + /// Gets or sets the percentage of the Cims that will go to and leave their work or school + /// on time (no overtime!). + /// Valid values are 0..100. + /// + [ConfigItem("2Quotas", 2)] + [ConfigItemSlider(0, 100)] + public uint OnTimeQuota { get; set; } = 80; + + [ConfigItem("3Events", 0)] + [ConfigItemSlider(0, 23.75f, 0.25f, SliderValueType.Time)] + public float EarliestHourEventStartWeekday { get; set; } = 16f; + + [ConfigItem("3Events", 1)] + [ConfigItemSlider(0, 23.75f, 0.25f, SliderValueType.Time)] + public float LatestHourEventStartWeekday { get; set; } = 20f; + + [ConfigItem("3Events", 2)] + [ConfigItemSlider(0, 23.75f, 0.25f, SliderValueType.Time)] + public float EarliestHourEventStartWeekend { get; set; } = 8f; + + [ConfigItem("3Events", 3)] + [ConfigItemSlider(0, 23.75f, 0.25f, SliderValueType.Time)] + public float LatestHourEventStartWeekend { get; set; } = 22f; + + /// + /// Gets or sets the work start daytime hour. The adult Cims must be at work. + /// + [ConfigItem("4Time", 0)] + [ConfigItemSlider(4, 11, 0.25f, SliderValueType.Time)] + public float WorkBegin { get; set; } = 9f; + + /// + /// Gets or sets the daytime hour when the adult Cims return from work. + /// + [ConfigItem("4Time", 1)] + [ConfigItemSlider(12, 20, 0.25f, SliderValueType.Time)] + public float WorkEnd { get; set; } = 18f; + + /// + /// Gets or sets the daytime hour when the Cims go out for lunch. + /// + [ConfigItem("4Time", 2)] + [ConfigItemSlider(11, 13, 0.25f, SliderValueType.Time)] + public float LunchBegin { get; set; } = 12f; + + /// + /// Gets or sets the daytime hour when the Cims return from lunch back to work. + /// + [ConfigItem("4Time", 3)] + [ConfigItemSlider(13, 15, 0.25f, SliderValueType.Time)] + public float LunchEnd { get; set; } = 13f; + + /// + /// Gets or sets the maximum overtime for the Cims. They come to work earlier or stay at work longer for at most this + /// amout of hours. This applies only for those Cims that are not on time, see . + /// The young Cims (school and university) don't do overtime. + /// + [ConfigItem("4Time", 4)] + [ConfigItemSlider(0, 4, 0.25f, SliderValueType.Duration)] + public float MaxOvertime { get; set; } = 2f; + + /// + /// Gets or sets the school start daytime hour. The young Cims must be at school or university. + /// + [ConfigItem("4Time", 5)] + [ConfigItemSlider(4, 10, 0.25f, SliderValueType.Time)] + public float SchoolBegin { get; set; } = 8f; + + /// + /// Gets or sets the daytime hour when the young Cims return from school or university. + /// + [ConfigItem("4Time", 6)] + [ConfigItemSlider(11, 16, 0.25f, SliderValueType.Time)] + public float SchoolEnd { get; set; } = 14f; + } +} diff --git a/src/RealTime/Core/IStorageData.cs b/src/RealTime/Core/IStorageData.cs new file mode 100644 index 00000000..ceb1cef5 --- /dev/null +++ b/src/RealTime/Core/IStorageData.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Core +{ + using System.IO; + + internal interface IStorageData + { + string StorageDataId { get; } + + void ReadData(Stream source); + + void StoreData(Stream target); + } +} diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs new file mode 100644 index 00000000..fe85730c --- /dev/null +++ b/src/RealTime/Core/RealTimeCore.cs @@ -0,0 +1,233 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Core +{ + using System; + using System.Collections.Generic; + using System.Security.Permissions; + using RealTime.Config; + using RealTime.CustomAI; + using RealTime.Events; + using RealTime.Events.Storage; + using RealTime.GameConnection; + using RealTime.Localization; + using RealTime.Simulation; + using RealTime.Tools; + using RealTime.UI; + using Redirection; + + /// + /// The core component of the Real Time mod. Activates and deactivates + /// the different parts of the mod's logic. + /// + internal sealed class RealTimeCore + { + private readonly List storageData = new List(); + private readonly TimeAdjustment timeAdjustment; + private readonly CustomTimeBar timeBar; + private readonly RealTimeEventManager eventManager; + + private bool isEnabled; + + private RealTimeCore(TimeAdjustment timeAdjustment, CustomTimeBar timeBar, RealTimeEventManager eventManager) + { + this.timeAdjustment = timeAdjustment; + this.timeBar = timeBar; + this.eventManager = eventManager; + isEnabled = true; + } + + /// + /// Runs the mod by activating its parts. + /// + /// + /// Thrown when is null. + /// + /// The configuration to run with. + /// The path to the mod's assembly. Additinal files are stored here too. + /// The to use for text translation. + /// + /// A instance that can be used to stop the mod. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static RealTimeCore Run(RealTimeConfig config, string rootPath, LocalizationProvider localizationProvider) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (string.IsNullOrEmpty(rootPath)) + { + throw new ArgumentException("The root path cannot be null or empty string", nameof(rootPath)); + } + + if (localizationProvider == null) + { + throw new ArgumentNullException(nameof(localizationProvider)); + } + + try + { + int redirectedCount = Redirector.PerformRedirections(); + Log.Info($"Successfully redirected {redirectedCount} methods."); + } + catch (Exception ex) + { + Log.Error("Failed to perform method redirections: " + ex.Message); + return null; + } + + var timeAdjustment = new TimeAdjustment(); + DateTime gameDate = timeAdjustment.Enable(); + + var timeInfo = new TimeInfo(); + var buildingManager = new BuildingManagerConnection(); + var simulationManager = new SimulationManagerConnection(); + + var gameConnections = new GameConnections( + timeInfo, + new CitizenConnection(), + new CitizenManagerConnection(), + buildingManager, + simulationManager, + new TransferManagerConnection()); + + var eventManager = new RealTimeEventManager( + config, + CityEventsLoader.Istance, + new EventManagerConnection(), + buildingManager, + simulationManager, + timeInfo); + + SetupCustomAI(timeInfo, config, gameConnections, eventManager); + + RealTimeEventSimulation.EventManager = eventManager; + CityEventsLoader.Istance.ReloadEvents(rootPath); + + var customTimeBar = new CustomTimeBar(); + customTimeBar.Enable(gameDate); + customTimeBar.CityEventClick += CustomTimeBarCityEventClick; + + var result = new RealTimeCore(timeAdjustment, customTimeBar, eventManager); + eventManager.EventsChanged += result.CityEventsChanged; + DaylightTimeSimulation.NewDay += result.CityEventsChanged; + + RealTimeStorage.Instance.GameSaving += result.GameSaving; + result.storageData.Add(eventManager); + result.LoadStorageData(); + + result.Translate(localizationProvider); + + return result; + } + + /// + /// Stops the mod by deactivating all its parts. + /// + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public void Stop() + { + if (!isEnabled) + { + return; + } + + timeAdjustment.Disable(); + timeBar.CityEventClick -= CustomTimeBarCityEventClick; + timeBar.Disable(); + eventManager.EventsChanged -= CityEventsChanged; + DaylightTimeSimulation.NewDay -= CityEventsChanged; + + CityEventsLoader.Istance.Clear(); + + RealTimeStorage.Instance.GameSaving -= GameSaving; + + ResidentAIHook.RealTimeAI = null; + TouristAIHook.RealTimeAI = null; + PrivateBuildingAIHook.RealTimeAI = null; + RealTimeEventSimulation.EventManager = null; + + try + { + Redirector.RevertRedirections(); + Log.Info($"Successfully reverted all method redirections."); + } + catch (Exception ex) + { + Log.Error("Failed to revert method redirections: " + ex.Message); + } + + isEnabled = false; + } + + public void Translate(LocalizationProvider localizationProvider) + { + if (localizationProvider == null) + { + throw new ArgumentNullException(nameof(localizationProvider)); + } + + timeBar.Translate(localizationProvider.CurrentCulture); + } + + private static void SetupCustomAI( + TimeInfo timeInfo, + RealTimeConfig config, + GameConnections gameConnections, + RealTimeEventManager eventManager) + { + var realTimeResidentAI = new RealTimeResidentAI( + config, + gameConnections, + ResidentAIHook.GetResidentAIConnection(), + eventManager); + + ResidentAIHook.RealTimeAI = realTimeResidentAI; + + var realTimeTouristAI = new RealTimeTouristAI( + config, + gameConnections, + TouristAIHook.GetTouristAIConnection(), + eventManager); + + TouristAIHook.RealTimeAI = realTimeTouristAI; + + var realTimePrivateBuildingAI = new RealTimePrivateBuildingAI( + config, + timeInfo, + new ToolManagerConnection()); + + PrivateBuildingAIHook.RealTimeAI = realTimePrivateBuildingAI; + } + + private static void CustomTimeBarCityEventClick(object sender, CustomTimeBarClickEventArgs e) + { + CameraHelper.NavigateToBuilding(e.CityEventBuildingId); + } + + private void CityEventsChanged(object sender, EventArgs e) + { + timeBar.UpdateEventsDisplay(eventManager.CityEvents); + } + + private void LoadStorageData() + { + foreach (IStorageData item in storageData) + { + RealTimeStorage.Instance.Deserialize(item); + } + } + + private void GameSaving(object sender, EventArgs e) + { + var storage = (RealTimeStorage)sender; + foreach (IStorageData item in storageData) + { + storage.Serialize(item); + } + } + } +} diff --git a/src/RealTime/Core/RealTimeMod.cs b/src/RealTime/Core/RealTimeMod.cs new file mode 100644 index 00000000..9843a99e --- /dev/null +++ b/src/RealTime/Core/RealTimeMod.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Core +{ + using System; + using System.Linq; + using System.Security.Permissions; + using ColossalFramework; + using ColossalFramework.Globalization; + using ColossalFramework.Plugins; + using ICities; + using RealTime.Config; + using RealTime.Localization; + using RealTime.Tools; + using RealTime.UI; + + /// + /// The main class of the Real Time mod. + /// + public sealed class RealTimeMod : LoadingExtensionBase, IUserMod + { + private readonly string modVersion = GitVersion.GetAssemblyVersion(typeof(RealTimeMod).Assembly); + private readonly string modPath = GetModPath(); + + private RealTimeConfig config; + private RealTimeCore core; + private ConfigUI configUI; + private LocalizationProvider localizationProvider; + + /// + /// Gets the name of this mod. + /// + public string Name => "Real Time"; + + /// + /// Gets the description string of this mod. + /// + public string Description => "Adjusts the time flow and the Cims behavior to make them more real. Version: " + modVersion; + + /// + /// Called when this mod is enabled. + /// + public void OnEnabled() + { + Log.Info("The 'Real Time' mod has been enabled, version: " + modVersion); + config = ConfigurationProvider.LoadConfiguration(); + localizationProvider = new LocalizationProvider(modPath); + LocaleManager.eventLocaleChanged += ApplyLanguage; + } + + /// + /// Called when this mod is disabled. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Must be instance method due to C:S API")] + public void OnDisabled() + { + Log.Info("The 'Real Time' mod has been disabled."); + ConfigurationProvider.SaveConfiguration(config); + LocaleManager.eventLocaleChanged -= ApplyLanguage; + config = null; + configUI = null; + } + + /// + /// Called when this mod's settings page needs to be created. + /// + /// + /// An reference that can be used + /// to construct the mod's settings page. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Must be instance method due to C:S API")] + public void OnSettingsUI(UIHelperBase helper) + { + IViewItemFactory itemFactory = new UnityViewItemFactory(helper); + configUI = ConfigUI.Create(config, itemFactory); + ApplyLanguage(); + } + + /// + /// Calles when a game level is loaded. If applicable, activates the Real Time mod + /// for the loaded level. + /// + /// + /// The a game level is loaded in. + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + public override void OnLevelLoaded(LoadMode mode) + { + switch (mode) + { + case LoadMode.LoadGame: + case LoadMode.NewGame: + case LoadMode.LoadScenario: + case LoadMode.NewGameFromScenario: + break; + + default: + return; + } + + core = RealTimeCore.Run(config, modPath, localizationProvider); + } + + /// + /// Calles when a game level is about to be unloaded. If the Real Time mod was activated + /// for this level, deactivates the mod for this level. + /// + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + public override void OnLevelUnloading() + { + if (core != null) + { + core.Stop(); + core = null; + } + + ConfigurationProvider.SaveConfiguration(config); + } + + private static string GetModPath() + { + PluginManager.PluginInfo pluginInfo = PluginManager.instance.GetPluginsInfo() + .FirstOrDefault(pi => pi.name == typeof(RealTimeMod).Assembly.GetName().Name); + + return pluginInfo == null + ? Environment.CurrentDirectory + : pluginInfo.modPath; + } + + private void ApplyLanguage() + { + if (!SingletonLite.exists) + { + return; + } + + Log.Info($"The 'Real Time' mod changes the language to '{LocaleManager.instance.language}'."); + localizationProvider.LoadTranslation(LocaleManager.instance.language); + configUI?.Translate(localizationProvider); + core?.Translate(localizationProvider); + } + } +} diff --git a/src/RealTime/Core/RealTimeStorage.cs b/src/RealTime/Core/RealTimeStorage.cs new file mode 100644 index 00000000..cfe89098 --- /dev/null +++ b/src/RealTime/Core/RealTimeStorage.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Core +{ + using System; + using System.IO; + using ICities; + using RealTime.Tools; + + public sealed class RealTimeStorage : SerializableDataExtensionBase + { + private static readonly string StorageDataPrefix = typeof(RealTimeStorage).Assembly.GetName().Name + "."; + + internal event EventHandler GameSaving; + + internal static RealTimeStorage Instance { get; private set; } + + public override void OnSaveData() + { + // TODO: investigate the exception on overwriting an existing game file + GameSaving?.Invoke(this, EventArgs.Empty); + } + + public override void OnCreated(ISerializableData serializableData) + { + base.OnCreated(serializableData); + Instance = this; + } + + public override void OnReleased() + { + base.OnReleased(); + Instance = null; + } + + internal void Serialize(IStorageData data) + { + string dataKey = StorageDataPrefix + data.StorageDataId; + + try + { + using (var stream = new MemoryStream()) + { + data.StoreData(stream); + serializableDataManager.SaveData(dataKey, stream.ToArray()); + } + } + catch (Exception ex) + { + Log.Error($"The 'Real Time' mod failed to save its data (key {dataKey}), error message: {ex}"); + } + } + + internal void Deserialize(IStorageData data) + { + string dataKey = StorageDataPrefix + data.StorageDataId; + + try + { + byte[] rawData = serializableDataManager.LoadData(dataKey); + if (rawData == null) + { + return; + } + + using (var stream = new MemoryStream(rawData)) + { + data.ReadData(stream); + } + } + catch (Exception ex) + { + Log.Error($"The 'Real Time' mod failed to load its data (key {dataKey}), error message: {ex}"); + } + } + } +} diff --git a/src/RealTime/CustomAI/Constants.cs b/src/RealTime/CustomAI/Constants.cs new file mode 100644 index 00000000..43ce10a4 --- /dev/null +++ b/src/RealTime/CustomAI/Constants.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + internal static class Constants + { + public const int ShoppingGoodsAmount = 100; + + public const float LocalSearchDistance = 500f; + public const float FullSearchDistance = 270f * 64f / 2f; + + public const uint AbandonTourChance = 25; + public const uint AbandonTransportWaitChance = 80; + public const uint GoShoppingChance = 50; + public const uint ReturnFromShoppingChance = 25; + public const uint ReturnFromVisitChance = 40; + public const uint FindHotelChance = 80; + public const uint TouristShoppingChance = 50; + public const uint TouristEventChance = 70; + + public const int TouristDoNothingProbability = 5000; + + public const float MaxHoursOnTheWay = 2.5f; + public const float MinHoursOnTheWay = 0.5f; + public const float OnTheWayDistancePerHour = 500f; + } +} diff --git a/src/RealTime/CustomAI/RealTimeHumanAIBase.cs b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs new file mode 100644 index 00000000..bbbcbf83 --- /dev/null +++ b/src/RealTime/CustomAI/RealTimeHumanAIBase.cs @@ -0,0 +1,232 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using ColossalFramework.Math; + using RealTime.Config; + using RealTime.Events; + using RealTime.GameConnection; + using RealTime.Simulation; + using RealTime.Tools; + using static Constants; + + internal abstract class RealTimeHumanAIBase + where TCitizen : struct + { + private Randomizer randomizer; + + protected RealTimeHumanAIBase(RealTimeConfig config, GameConnections connections, RealTimeEventManager eventManager) + { + Config = config ?? throw new ArgumentNullException(nameof(config)); + EventMgr = eventManager ?? throw new ArgumentNullException(nameof(eventManager)); + + if (connections == null) + { + throw new ArgumentNullException(nameof(connections)); + } + + CitizenMgr = connections.CitizenManager; + BuildingMgr = connections.BuildingManager; + TransferMgr = connections.TransferManager; + CitizenProxy = connections.CitizenConnection; + TimeInfo = connections.TimeInfo; + randomizer = connections.SimulationManager.GetRandomizer(); + } + + protected bool IsWeekend => Config.IsWeekendEnabled && TimeInfo.Now.IsWeekend(); + + protected bool IsWorkDay => !Config.IsWeekendEnabled || !TimeInfo.Now.IsWeekend(); + + protected RealTimeConfig Config { get; } + + protected RealTimeEventManager EventMgr { get; } + + protected ICitizenConnection CitizenProxy { get; } + + protected ICitizenManagerConnection CitizenMgr { get; } + + protected IBuildingManagerConnection BuildingMgr { get; } + + protected ITransferManagerConnection TransferMgr { get; } + + protected ITimeInfo TimeInfo { get; } + + protected ref Randomizer Randomizer => ref randomizer; + + protected bool IsChance(uint chance) + { + return Randomizer.UInt32(100u) < chance; + } + + protected bool IsWorkDayAndBetweenHours(float fromInclusive, float toExclusive) + { + float currentHour = TimeInfo.CurrentHour; + return IsWorkDay && (currentHour >= fromInclusive && currentHour < toExclusive); + } + + protected bool IsWorkDayMorning(Citizen.AgeGroup citizenAge) + { + if (!IsWorkDay) + { + return false; + } + + float workBeginHour; + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + workBeginHour = Config.SchoolBegin; + break; + + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + workBeginHour = Config.WorkBegin; + break; + + default: + return false; + } + + float currentHour = TimeInfo.CurrentHour; + return currentHour >= TimeInfo.SunriseHour && currentHour <= workBeginHour; + } + + protected uint GetGoOutChance(Citizen.AgeGroup citizenAge) + { + float currentHour = TimeInfo.CurrentHour; + + uint weekdayModifier; + if (Config.IsWeekendEnabled) + { + weekdayModifier = TimeInfo.Now.IsWeekendTime(GetSpareTimeBeginHour(citizenAge), TimeInfo.SunsetHour) + ? 11u + : 1u; + } + else + { + weekdayModifier = 1u; + } + + bool isDayTime = !TimeInfo.IsNightTime; + float timeModifier; + if (isDayTime) + { + timeModifier = 5f; + } + else + { + float nightDuration = TimeInfo.NightDuration; + float relativeHour = currentHour - TimeInfo.SunsetHour; + if (relativeHour < 0) + { + relativeHour += 24f; + } + + timeModifier = 5f / nightDuration * (nightDuration - relativeHour); + } + + switch (citizenAge) + { + case Citizen.AgeGroup.Child when isDayTime: + case Citizen.AgeGroup.Teen when isDayTime: + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + return (uint)((timeModifier + weekdayModifier) * timeModifier); + + case Citizen.AgeGroup.Senior when isDayTime: + return 80 + weekdayModifier; + + default: + return 0; + } + } + + protected float GetSpareTimeBeginHour(Citizen.AgeGroup citiztenAge) + { + switch (citiztenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + return Config.SchoolEnd; + + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + return Config.WorkEnd; + + default: + return 0; + } + } + + protected bool EnsureCitizenValid(uint citizenId, ref TCitizen citizen) + { + if (CitizenProxy.GetHomeBuilding(ref citizen) == 0 + && CitizenProxy.GetWorkBuilding(ref citizen) == 0 + && CitizenProxy.GetVisitBuilding(ref citizen) == 0 + && CitizenProxy.GetInstance(ref citizen) == 0 + && CitizenProxy.GetVehicle(ref citizen) == 0) + { + CitizenMgr.ReleaseCitizen(citizenId); + return false; + } + + if (CitizenProxy.IsCollapsed(ref citizen)) + { + Log.Debug($"{GetCitizenDesc(citizenId, ref citizen)} is collapsed, doing nothing..."); + return false; + } + + return true; + } + + protected bool AttendUpcomingEvent(uint citizenId, ref TCitizen citizen, out ushort eventBuildingId) + { + eventBuildingId = default; + + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (EventMgr.GetEventState(currentBuilding, DateTime.MaxValue) == CityEventState.OnGoing) + { + return false; + } + + DateTime earliestStart = TimeInfo.Now.AddHours(MinHoursOnTheWay); + DateTime latestStart = TimeInfo.Now.AddHours(MaxHoursOnTheWay); + + ICityEvent upcomingEvent = EventMgr.GetUpcomingCityEvent(earliestStart, latestStart); + if (upcomingEvent != null && CanAttendEvent(citizenId, ref citizen, upcomingEvent)) + { + eventBuildingId = upcomingEvent.BuildingId; + return true; + } + + return false; + } + + protected void FindEvacuationPlace(uint citizenId, TransferManager.TransferReason reason) + { + TransferMgr.AddOutgoingOfferFromCurrentPosition(citizenId, reason); + } + + protected string GetCitizenDesc(uint citizenId, ref TCitizen citizen) + { + string employment = CitizenProxy.GetWorkBuilding(ref citizen) == 0 ? "unempl." : "empl."; + return $"Citizen {citizenId} ({employment}, {CitizenProxy.GetAge(ref citizen)})"; + } + + private bool CanAttendEvent(uint citizenId, ref TCitizen citizen, ICityEvent cityEvent) + { + Citizen.AgeGroup age = CitizenProxy.GetAge(ref citizen); + Citizen.Gender gender = CitizenProxy.GetGender(citizenId); + Citizen.Education education = CitizenProxy.GetEducationLevel(ref citizen); + Citizen.Wealth wealth = CitizenProxy.GetWealthLevel(ref citizen); + Citizen.Wellbeing wellbeing = CitizenProxy.GetWellbeingLevel(ref citizen); + Citizen.Happiness happiness = CitizenProxy.GetHappinessLevel(ref citizen); + + return cityEvent.TryAcceptAttendee(age, gender, education, wealth, wellbeing, happiness, ref randomizer); + } + } +} diff --git a/src/RealTime/CustomBuildingAI/RealTimePrivateBuildingAI.cs b/src/RealTime/CustomBuildingAI/RealTimePrivateBuildingAI.cs new file mode 100644 index 00000000..bb0d810d --- /dev/null +++ b/src/RealTime/CustomBuildingAI/RealTimePrivateBuildingAI.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using RealTime.Config; + using RealTime.GameConnection; + using RealTime.Simulation; + + internal sealed class RealTimePrivateBuildingAI + { + private const int ConstructionSpeedPaused = 10880; + private const int ConstructionSpeedMinimum = 1088; + + private readonly RealTimeConfig config; + private readonly ITimeInfo timeInfo; + private readonly IToolManagerConnection toolManager; + + private uint lastConfigValue; + private double value; + + public RealTimePrivateBuildingAI(RealTimeConfig config, ITimeInfo timeInfo, IToolManagerConnection toolManager) + { + this.config = config ?? throw new System.ArgumentNullException(nameof(config)); + this.timeInfo = timeInfo ?? throw new System.ArgumentNullException(nameof(timeInfo)); + this.toolManager = toolManager ?? throw new System.ArgumentNullException(nameof(toolManager)); + } + + public int GetConstructionTime() + { + if ((toolManager.GetCurrentMode() & ItemClass.Availability.AssetEditor) != 0) + { + return 0; + } + +#if DEBUG + return 0; +#else + if (config.ConstructionSpeed != lastConfigValue) + { + lastConfigValue = config.ConstructionSpeed; + double inverted = 101d - lastConfigValue; + value = inverted * inverted * inverted / 1_000_000d; + } + + // This causes the constuction to not advance in the night time + return timeInfo.IsNightTime && config.StopConstructionAtNight + ? ConstructionSpeedPaused + : (int)(ConstructionSpeedMinimum * value); +#endif + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.Common.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Common.cs new file mode 100644 index 00000000..0ad488df --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Common.cs @@ -0,0 +1,208 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using RealTime.Tools; + + internal sealed partial class RealTimeResidentAI + { + private void ProcessCitizenDead(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + Citizen.Location currentLocation = CitizenProxy.GetLocation(ref citizen); + + if (currentBuilding == 0 || (currentLocation == Citizen.Location.Moving && CitizenProxy.GetVehicle(ref citizen) == 0)) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is released"); + CitizenMgr.ReleaseCitizen(citizenId); + return; + } + + if (currentLocation != Citizen.Location.Home && CitizenProxy.GetHomeBuilding(ref citizen) != 0) + { + CitizenProxy.SetHome(ref citizen, citizenId, 0); + } + + if (currentLocation != Citizen.Location.Work && CitizenProxy.GetWorkBuilding(ref citizen) != 0) + { + CitizenProxy.SetWorkplace(ref citizen, citizenId, 0); + } + + if (currentLocation != Citizen.Location.Visit && CitizenProxy.GetVisitBuilding(ref citizen) != 0) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + } + + if (currentLocation == Citizen.Location.Moving || CitizenProxy.GetVehicle(ref citizen) != 0) + { + return; + } + + if (currentLocation == Citizen.Location.Visit + && BuildingMgr.GetBuildingService(CitizenProxy.GetVisitBuilding(ref citizen)) == ItemClass.Service.HealthCare) + { + return; + } + + residentAI.FindHospital(instance, citizenId, currentBuilding, TransferManager.TransferReason.Dead); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is dead, body should get serviced"); + } + + private bool ProcessCitizenArrested(ref TCitizen citizen) + { + switch (CitizenProxy.GetLocation(ref citizen)) + { + case Citizen.Location.Moving: + return false; + case Citizen.Location.Visit + when BuildingMgr.GetBuildingService(CitizenProxy.GetVisitBuilding(ref citizen)) == ItemClass.Service.PoliceDepartment: + return true; + } + + CitizenProxy.SetArrested(ref citizen, false); + return false; + } + + private bool ProcessCitizenSick(TAI instance, uint citizenId, ref TCitizen citizen) + { + Citizen.Location currentLocation = CitizenProxy.GetLocation(ref citizen); + if (currentLocation == Citizen.Location.Moving) + { + return false; + } + + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + + if (currentLocation != Citizen.Location.Home && currentBuilding == 0) + { + Log.Warning($"Teleporting {GetCitizenDesc(citizenId, ref citizen)} back home because they are sick but no building is specified"); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + return true; + } + + if (currentLocation != Citizen.Location.Home && CitizenProxy.GetVehicle(ref citizen) != 0) + { + return true; + } + + if (currentLocation == Citizen.Location.Visit) + { + ItemClass.Service service = BuildingMgr.GetBuildingService(CitizenProxy.GetVisitBuilding(ref citizen)); + if (service == ItemClass.Service.HealthCare || service == ItemClass.Service.Disaster) + { + return true; + } + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is sick, trying to get to a hospital"); + residentAI.FindHospital(instance, citizenId, currentBuilding, TransferManager.TransferReason.Sick); + return true; + } + + private void ProcessCitizenEvacuation(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort building = CitizenProxy.GetCurrentBuilding(ref citizen); + if (building != 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is trying to find an evacuation place"); + residentAI.FindEvacuationPlace(instance, citizenId, building, residentAI.GetEvacuationReason(instance, building)); + } + } + + private bool StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort visitBuilding) + { + if (visitBuilding == 0) + { + return false; + } + + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + + residentAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding); + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); + + return true; + } + + private ResidentState GetResidentState(ref TCitizen citizen) + { + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + ItemClass.Service buildingService = BuildingMgr.GetBuildingService(currentBuilding); + + if ((BuildingMgr.GetBuildingFlags(currentBuilding) & Building.Flags.Evacuating) != 0 + && buildingService != ItemClass.Service.Disaster) + { + return ResidentState.Evacuating; + } + + switch (CitizenProxy.GetLocation(ref citizen)) + { + case Citizen.Location.Home: + if ((CitizenProxy.GetFlags(ref citizen) & Citizen.Flags.MovingIn) != 0) + { + return ResidentState.LeftCity; + } + + if (currentBuilding != 0) + { + return ResidentState.AtHome; + } + + return ResidentState.Unknown; + + case Citizen.Location.Work: + if (buildingService == ItemClass.Service.Disaster && CitizenProxy.GetFlags(ref citizen) == Citizen.Flags.Evacuating) + { + return ResidentState.InShelter; + } + + return currentBuilding != 0 + ? ResidentState.AtSchoolOrWork + : ResidentState.Unknown; + + case Citizen.Location.Visit: + if (currentBuilding == 0) + { + return ResidentState.Unknown; + } + + switch (buildingService) + { + case ItemClass.Service.Commercial: + if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDay + && TimeInfo.CurrentHour > Config.LunchBegin && TimeInfo.CurrentHour < GetSpareTimeBeginHour(CitizenProxy.GetAge(ref citizen))) + { + return ResidentState.AtLunch; + } + + if (BuildingMgr.GetBuildingSubService(currentBuilding) == ItemClass.SubService.CommercialLeisure) + { + return ResidentState.AtLeisureArea; + } + + return ResidentState.Shopping; + + case ItemClass.Service.Beautification: + return ResidentState.AtLeisureArea; + + case ItemClass.Service.Disaster: + return ResidentState.InShelter; + } + + return ResidentState.Visiting; + + case Citizen.Location.Moving: + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + return homeBuilding != 0 && CitizenMgr.GetTargetBuilding(CitizenProxy.GetInstance(ref citizen)) == homeBuilding + ? ResidentState.MovingHome + : ResidentState.MovingToTarget; + + default: + return ResidentState.Unknown; + } + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.Home.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Home.cs new file mode 100644 index 00000000..d0a780c2 --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Home.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Tools; + + internal sealed partial class RealTimeResidentAI + { + private void ProcessCitizenAtHome(TAI instance, uint citizenId, ref TCitizen citizen) + { + if (CitizenProxy.GetHomeBuilding(ref citizen) == 0) + { + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: at home with no home building. Releasing the poor citizen."); + CitizenMgr.ReleaseCitizen(citizenId); + return; + } + + ushort vehicle = CitizenProxy.GetVehicle(ref citizen); + if (vehicle != 0) + { + Log.Debug(TimeInfo.Now, $"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at home but vehicle = {vehicle}"); + return; + } + + if (CitizenGoesWorking(instance, citizenId, ref citizen)) + { + return; + } + + if (IsBusyAtHomeInTheMorning(CitizenProxy.GetAge(ref citizen)) || !residentAI.DoRandomMove(instance)) + { + return; + } + + if (CitizenGoesShopping(instance, citizenId, ref citizen) || CitizenGoesToEvent(instance, citizenId, ref citizen)) + { + return; + } + + CitizenGoesRelaxing(instance, citizenId, ref citizen); + } + + private bool IsBusyAtHomeInTheMorning(Citizen.AgeGroup citizenAge) + { + float offset = IsWeekend ? 2 : 0; + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + return IsBusyAtHomeInTheMorning(8 + offset); + + case Citizen.AgeGroup.Teen: + case Citizen.AgeGroup.Young: + return IsBusyAtHomeInTheMorning(9 + offset); + + case Citizen.AgeGroup.Adult: + return IsBusyAtHomeInTheMorning(8 + (offset / 2f)); + + case Citizen.AgeGroup.Senior: + return IsBusyAtHomeInTheMorning(7); + + default: + return true; + } + + bool IsBusyAtHomeInTheMorning(float latestHour) + { + float currentHour = TimeInfo.CurrentHour; + if (currentHour >= latestHour || currentHour < TimeInfo.SunriseHour) + { + return false; + } + + float sunriseHour = TimeInfo.SunriseHour; + float dx = latestHour - sunriseHour; + float x = currentHour - sunriseHour; + + // A cubic probability curve from sunrise (0%) to latestHour (100%) + uint chance = (uint)((100f / dx * x) - ((dx - x) * (dx - x) * x)); + return !IsChance(chance); + } + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.Moving.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Moving.cs new file mode 100644 index 00000000..9e104f87 --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Moving.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using RealTime.Tools; + using static Constants; + + internal sealed partial class RealTimeResidentAI + { + private void ProcessCitizenMoving(TAI instance, uint citizenId, ref TCitizen citizen, bool mayCancel) + { + ushort instanceId = CitizenProxy.GetInstance(ref citizen); + ushort vehicleId = CitizenProxy.GetVehicle(ref citizen); + + // TODO: implement bored of traffic jam trip abandon + if (vehicleId == 0 && instanceId == 0) + { + if (CitizenProxy.GetVisitBuilding(ref citizen) != 0) + { + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + } + + Log.Debug($"Teleporting {GetCitizenDesc(citizenId, ref citizen)} back home because they are moving but no instance is specified"); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + CitizenProxy.SetArrested(ref citizen, false); + + return; + } + + if (vehicleId == 0 && CitizenMgr.IsAreaEvacuating(instanceId) && (CitizenProxy.GetFlags(ref citizen) & Citizen.Flags.Evacuating) == 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} was on the way, but the area evacuates. Finding an evacuation place."); + TransferMgr.AddOutgoingOfferFromCurrentPosition(citizenId, residentAI.GetEvacuationReason(instance, 0)); + return; + } + + CitizenInstance.Flags instanceFlags = CitizenMgr.GetInstanceFlags(instanceId); + CitizenInstance.Flags onTourFlags = CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour; + + if ((instanceFlags & onTourFlags) == onTourFlags) + { + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + if (IsChance(AbandonTourChance) && homeBuilding != 0) + { + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); + residentAI.StartMoving(instance, citizenId, ref citizen, 0, homeBuilding); + } + } + else if ((instanceFlags & (CitizenInstance.Flags.WaitingTransport | CitizenInstance.Flags.WaitingTaxi)) != 0) + { + if (mayCancel && CitizenMgr.GetInstanceWaitCounter(instanceId) == 255 && IsChance(AbandonTransportWaitChance)) + { + ushort home = CitizenProxy.GetHomeBuilding(ref citizen); + if (home == 0) + { + return; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} doesn't want to wait for transport anymore, goes back home"); + residentAI.StartMoving(instance, citizenId, ref citizen, 0, home); + } + } + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.SchoolWork.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.SchoolWork.cs new file mode 100644 index 00000000..888b1912 --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.SchoolWork.cs @@ -0,0 +1,217 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using RealTime.Tools; + using UnityEngine; + using static Constants; + + internal sealed partial class RealTimeResidentAI + { + private bool IsLunchHour => IsWorkDayAndBetweenHours(Config.LunchBegin, Config.LunchEnd); + + private void ProcessCitizenAtSchoolOrWork(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); + if (workBuilding == 0) + { + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: at school/work with no work building. Teleporting home."); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + return; + } + + if (ShouldGoToLunch(CitizenProxy.GetAge(ref citizen))) + { + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + Citizen.Location currentLocation = CitizenProxy.GetLocation(ref citizen); + + ushort lunchPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + if (lunchPlace != 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going for lunch from {currentBuilding} ({currentLocation}) to {lunchPlace}"); + } + else + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanted to go for lunch from {currentBuilding} ({currentLocation}), but there were no buildings close enough"); + } + + return; + } + + if (!ShouldReturnFromSchoolOrWork(CitizenProxy.GetAge(ref citizen))) + { + return; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} leaves their workplace {workBuilding}"); + + if (CitizenGoesToEvent(instance, citizenId, ref citizen)) + { + return; + } + + if (!CitizenGoesShopping(instance, citizenId, ref citizen) && !CitizenGoesRelaxing(instance, citizenId, ref citizen)) + { + residentAI.StartMoving(instance, citizenId, ref citizen, workBuilding, CitizenProxy.GetHomeBuilding(ref citizen)); + } + } + + private bool CitizenGoesWorking(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + + if (!ShouldMoveToSchoolOrWork(workBuilding, currentBuilding, CitizenProxy.GetAge(ref citizen))) + { + return false; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} is going from {currentBuilding} ({CitizenProxy.GetLocation(ref citizen)}) to school/work {workBuilding}"); + + residentAI.StartMoving(instance, citizenId, ref citizen, homeBuilding, workBuilding); + return true; + } + + private bool ShouldMoveToSchoolOrWork(ushort workBuilding, ushort currentBuilding, Citizen.AgeGroup citizenAge) + { + if (workBuilding == 0 || IsWeekend) + { + return false; + } + + float currentHour = TimeInfo.CurrentHour; + float gotoWorkHour; + float leaveWorkHour; + bool overtime; + + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + gotoWorkHour = Config.SchoolBegin; + leaveWorkHour = Config.SchoolEnd; + overtime = false; + break; + + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + gotoWorkHour = Config.WorkBegin; + leaveWorkHour = Config.WorkEnd; + overtime = IsChance(Config.OnTimeQuota); + break; + + default: + return false; + } + + // Performance optimization: + // If the current hour is far away from the working hours, don't even calculate the overtime and on the way time + if (overtime + && (currentHour < gotoWorkHour - MaxHoursOnTheWay - Config.MaxOvertime || currentHour > leaveWorkHour + Config.MaxOvertime)) + { + return false; + } + else if (currentHour < gotoWorkHour - MaxHoursOnTheWay || currentHour > leaveWorkHour) + { + return false; + } + + if (overtime) + { + gotoWorkHour -= Config.MaxOvertime * Randomizer.Int32(100) / 200f; + leaveWorkHour += Config.MaxOvertime; + } + + float distance = BuildingMgr.GetDistanceBetweenBuildings(currentBuilding, workBuilding); + float onTheWay = Mathf.Clamp(distance / OnTheWayDistancePerHour, MinHoursOnTheWay, MaxHoursOnTheWay); + + gotoWorkHour -= onTheWay; + + return currentHour >= gotoWorkHour && currentHour < leaveWorkHour; + } + + private bool ShouldReturnFromSchoolOrWork(Citizen.AgeGroup citizenAge) + { + if (IsWeekend) + { + return true; + } + + float currentHour = TimeInfo.CurrentHour; + + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + return currentHour >= Config.SchoolEnd || currentHour < Config.SchoolBegin - MaxHoursOnTheWay; + + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + if (currentHour >= (Config.WorkEnd + Config.MaxOvertime) || currentHour < Config.WorkBegin - MaxHoursOnTheWay) + { + return true; + } + else if (currentHour >= Config.WorkEnd) + { + return IsChance(Config.OnTimeQuota); + } + + break; + + default: + return true; + } + + return false; + } + + private bool ShouldGoToLunch(Citizen.AgeGroup citizenAge) + { + if (!Config.IsLunchtimeEnabled) + { + return false; + } + + switch (citizenAge) + { + case Citizen.AgeGroup.Child: + case Citizen.AgeGroup.Teen: + case Citizen.AgeGroup.Senior: + return false; + } + + float currentHour = TimeInfo.CurrentHour; + if (currentHour >= Config.LunchBegin && currentHour <= Config.LunchEnd) + { + return IsChance(Config.LunchQuota); + } + + return false; + } + + private bool CitizenReturnsFromLunch(TAI instance, uint citizenId, ref TCitizen citizen) + { + if (IsLunchHour) + { + return false; + } + + ushort workBuilding = CitizenProxy.GetWorkBuilding(ref citizen); + if (workBuilding != 0) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from lunch to {workBuilding}"); + ReturnFromVisit(instance, citizenId, ref citizen, workBuilding); + } + else + { + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is at lunch but no work building. Teleporting home."); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + } + + return true; + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.Visit.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Visit.cs new file mode 100644 index 00000000..2691f536 --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.Visit.cs @@ -0,0 +1,258 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Events; + using RealTime.Tools; + using static Constants; + + internal sealed partial class RealTimeResidentAI + { + private void ProcessCitizenVisit(TAI instance, ResidentState citizenState, uint citizenId, ref TCitizen citizen) + { + if (CitizenProxy.GetVisitBuilding(ref citizen) == 0) + { + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} is in corrupt state: visiting with no visit building. Teleporting home."); + CitizenProxy.SetLocation(ref citizen, Citizen.Location.Home); + return; + } + + switch (citizenState) + { + case ResidentState.AtLunch: + CitizenReturnsFromLunch(instance, citizenId, ref citizen); + + return; + + case ResidentState.AtLeisureArea: + case ResidentState.Visiting: + if (!CitizenGoesWorking(instance, citizenId, ref citizen)) + { + CitizenReturnsHomeFromVisit(instance, citizenId, ref citizen); + } + + return; + + case ResidentState.Shopping: + if ((CitizenProxy.GetFlags(ref citizen) & Citizen.Flags.NeedGoods) != 0) + { + BuildingMgr.ModifyMaterialBuffer(CitizenProxy.GetVisitBuilding(ref citizen), TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.NeedGoods); + } + + if (CitizenGoesWorking(instance, citizenId, ref citizen) || CitizenGoesToEvent(instance, citizenId, ref citizen)) + { + return; + } + + if (IsChance(ReturnFromShoppingChance) || IsWorkDayMorning(CitizenProxy.GetAge(ref citizen))) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from shopping back home"); + ReturnFromVisit(instance, citizenId, ref citizen, CitizenProxy.GetHomeBuilding(ref citizen)); + } + + return; + } + } + + private bool CitzenReturnsFromShelter(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (BuildingMgr.GetBuildingService(visitBuilding) != ItemClass.Service.Disaster) + { + return true; + } + + if ((BuildingMgr.GetBuildingFlags(visitBuilding) & Building.Flags.Downgrading) == 0) + { + return false; + } + + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + if (homeBuilding == 0) + { + Log.Debug($"WARNING: {GetCitizenDesc(citizenId, ref citizen)} was in a shelter but seems to be homeless. Releasing the citizen."); + CitizenMgr.ReleaseCitizen(citizenId); + return true; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from evacuation place back home"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding); + return true; + } + + private bool CitizenReturnsHomeFromVisit(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort homeBuilding = CitizenProxy.GetHomeBuilding(ref citizen); + if (homeBuilding == 0 || CitizenProxy.GetVehicle(ref citizen) != 0) + { + return false; + } + + ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + switch (EventMgr.GetEventState(visitBuilding, TimeInfo.Now.AddHours(MaxHoursOnTheWay))) + { + case CityEventState.Upcoming: + case CityEventState.OnGoing: + return false; + + case CityEventState.Finished: + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from an event at {visitBuilding} back home to {homeBuilding}"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding); + return true; + } + + if (IsChance(ReturnFromVisitChance)) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} returning from visit back home"); + ReturnFromVisit(instance, citizenId, ref citizen, homeBuilding); + return true; + } + + return false; + } + + private void ReturnFromVisit(TAI instance, uint citizenId, ref TCitizen citizen, ushort targetBuilding) + { + if (targetBuilding != 0 && CitizenProxy.GetVehicle(ref citizen) == 0) + { + CitizenProxy.RemoveFlags(ref citizen, Citizen.Flags.Evacuating); + residentAI.StartMoving(instance, citizenId, ref citizen, CitizenProxy.GetVisitBuilding(ref citizen), targetBuilding); + CitizenProxy.SetVisitPlace(ref citizen, citizenId, 0); + } + } + + private bool CitizenGoesShopping(TAI instance, uint citizenId, ref TCitizen citizen) + { + if ((CitizenProxy.GetFlags(ref citizen) & Citizen.Flags.NeedGoods) == 0) + { + return false; + } + + if (TimeInfo.IsNightTime) + { + if (IsChance(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) + { + ushort localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping at night, trying local shop '{localVisitPlace}'"); + return localVisitPlace > 0; + } + + return false; + } + + if (IsChance(GoShoppingChance)) + { + bool localOnly = CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(CitizenProxy.GetAge(ref citizen)); + ushort localVisitPlace = 0; + + if (IsChance(Config.LocalBuildingSearchQuota)) + { + localVisitPlace = MoveToCommercialBuilding(instance, citizenId, ref citizen, LocalSearchDistance); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, tries a local shop '{localVisitPlace}'"); + } + + if (localVisitPlace == 0) + { + if (localOnly) + { + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, but didn't find a local shop"); + return false; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna go shopping, heading to a random shop"); + residentAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetHomeBuilding(ref citizen), residentAI.GetShoppingReason(instance)); + } + + return true; + } + + return false; + } + + private bool CitizenGoesToEvent(TAI instance, uint citizenId, ref TCitizen citizen) + { + if (!IsChance(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) + { + return false; + } + + if (!AttendUpcomingEvent(citizenId, ref citizen, out ushort buildingId)) + { + return false; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna attend an event at '{buildingId}', on the way now."); + return StartMovingToVisitBuilding(instance, citizenId, ref citizen, buildingId); + } + + private bool CitizenGoesRelaxing(TAI instance, uint citizenId, ref TCitizen citizen) + { + Citizen.AgeGroup citizenAge = CitizenProxy.GetAge(ref citizen); + if (!IsChance(GetGoOutChance(citizenAge))) + { + return false; + } + + ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); + if (buildingId == 0) + { + return false; + } + + if (TimeInfo.IsNightTime) + { + ushort leisure = MoveToLeisure(instance, citizenId, ref citizen, buildingId); + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax at night, trying leisure area '{leisure}'"); + return leisure != 0; + } + + if (CitizenProxy.GetWorkBuilding(ref citizen) != 0 && IsWorkDayMorning(citizenAge)) + { + return false; + } + + Log.Debug(TimeInfo.Now, $"{GetCitizenDesc(citizenId, ref citizen)} wanna relax, heading to an entertainment place"); + residentAI.FindVisitPlace(instance, citizenId, buildingId, residentAI.GetEntertainmentReason(instance)); + return true; + } + + private ushort MoveToCommercialBuilding(TAI instance, uint citizenId, ref TCitizen citizen, float distance) + { + ushort buildingId = CitizenProxy.GetCurrentBuilding(ref citizen); + if (buildingId == 0) + { + return 0; + } + + ushort foundBuilding = BuildingMgr.FindActiveBuilding(buildingId, distance, ItemClass.Service.Commercial); + if (foundBuilding == CitizenProxy.GetWorkBuilding(ref citizen)) + { + return 0; + } + + StartMovingToVisitBuilding(instance, citizenId, ref citizen, foundBuilding); + return foundBuilding; + } + + private ushort MoveToLeisure(TAI instance, uint citizenId, ref TCitizen citizen, ushort buildingId) + { + ushort leisureBuilding = BuildingMgr.FindActiveBuilding( + buildingId, + FullSearchDistance, + ItemClass.Service.Commercial, + ItemClass.SubService.CommercialLeisure); + + if (leisureBuilding == CitizenProxy.GetWorkBuilding(ref citizen)) + { + return 0; + } + + StartMovingToVisitBuilding(instance, citizenId, ref citizen, leisureBuilding); + return leisureBuilding; + } + } +} diff --git a/src/RealTime/CustomResidentAI/RealTimeResidentAI.cs b/src/RealTime/CustomResidentAI/RealTimeResidentAI.cs new file mode 100644 index 00000000..761f21a1 --- /dev/null +++ b/src/RealTime/CustomResidentAI/RealTimeResidentAI.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Config; + using RealTime.Events; + using RealTime.GameConnection; + + internal sealed partial class RealTimeResidentAI : RealTimeHumanAIBase + where TAI : class + where TCitizen : struct + { + private readonly ResidentAIConnection residentAI; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Thrown when any argument is null. + /// + /// A instance containing the mod's configuration. + /// A instance that provides the game connection implementation. + /// A connection to the game's resident AI. + /// A instance. + public RealTimeResidentAI( + RealTimeConfig config, + GameConnections connections, + ResidentAIConnection residentAI, + RealTimeEventManager eventManager) + : base(config, connections, eventManager) + { + this.residentAI = residentAI ?? throw new ArgumentNullException(nameof(residentAI)); + } + + /// + /// The main method of the custom AI. + /// + /// + /// A reference to an object instance of the original AI. + /// The ID of the citizen to process. + /// A reference to process. + public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) + { + if (!EnsureCitizenValid(citizenId, ref citizen)) + { + return; + } + + if (CitizenProxy.IsDead(ref citizen)) + { + ProcessCitizenDead(instance, citizenId, ref citizen); + return; + } + + if ((CitizenProxy.IsSick(ref citizen) && ProcessCitizenSick(instance, citizenId, ref citizen)) + || (CitizenProxy.IsArrested(ref citizen) && ProcessCitizenArrested(ref citizen))) + { + return; + } + + ResidentState residentState = GetResidentState(ref citizen); + + switch (residentState) + { + case ResidentState.LeftCity: + CitizenMgr.ReleaseCitizen(citizenId); + break; + + case ResidentState.MovingHome: + ProcessCitizenMoving(instance, citizenId, ref citizen, false); + break; + + case ResidentState.AtHome: + ProcessCitizenAtHome(instance, citizenId, ref citizen); + break; + + case ResidentState.MovingToTarget: + ProcessCitizenMoving(instance, citizenId, ref citizen, true); + break; + + case ResidentState.AtSchoolOrWork: + ProcessCitizenAtSchoolOrWork(instance, citizenId, ref citizen); + break; + + case ResidentState.AtLunch: + case ResidentState.Shopping: + case ResidentState.AtLeisureArea: + case ResidentState.Visiting: + ProcessCitizenVisit(instance, residentState, citizenId, ref citizen); + break; + + case ResidentState.Evacuating: + ProcessCitizenEvacuation(instance, citizenId, ref citizen); + break; + + case ResidentState.InShelter: + CitzenReturnsFromShelter(instance, citizenId, ref citizen); + return; + } + } + } +} diff --git a/src/RealTime/CustomResidentAI/ResidentAI.cs b/src/RealTime/CustomResidentAI/ResidentAI.cs new file mode 100644 index 00000000..314dcab1 --- /dev/null +++ b/src/RealTime/CustomResidentAI/ResidentAI.cs @@ -0,0 +1,2871 @@ +using ColossalFramework; +using ColossalFramework.Globalization; +using ColossalFramework.Math; +using ColossalFramework.PlatformServices; +using ColossalFramework.Threading; +using System; +using UnityEngine; + +public class ResidentAI1 : HumanAI +{ + public const int UNIVERSITY_DURATION = 15; + + public const int BREED_INTERVAL = 12; + + public const int GAY_PROBABILITY = 5; + + public const int CAR_PROBABILITY_CHILD = 0; + + public const int CAR_PROBABILITY_TEEN = 5; + + public const int CAR_PROBABILITY_YOUNG = 15; + + public const int CAR_PROBABILITY_ADULT = 20; + + public const int CAR_PROBABILITY_SENIOR = 10; + + public const int BIKE_PROBABILITY_CHILD = 40; + + public const int BIKE_PROBABILITY_TEEN = 30; + + public const int BIKE_PROBABILITY_YOUNG = 20; + + public const int BIKE_PROBABILITY_ADULT = 10; + + public const int BIKE_PROBABILITY_SENIOR = 0; + + public const int TAXI_PROBABILITY_CHILD = 0; + + public const int TAXI_PROBABILITY_TEEN = 2; + + public const int TAXI_PROBABILITY_YOUNG = 2; + + public const int TAXI_PROBABILITY_ADULT = 4; + + public const int TAXI_PROBABILITY_SENIOR = 6; + + public override Color GetColor(ushort instanceID, ref CitizenInstance data, InfoManager.InfoMode infoMode) + { + switch (infoMode) + { + case InfoManager.InfoMode.Health: + { + int health2 = Singleton.instance.m_citizens.m_buffer[data.m_citizen].m_health; + return Color.Lerp(Singleton.instance.m_properties.m_modeProperties[(int)infoMode].m_negativeColor, Singleton.instance.m_properties.m_modeProperties[(int)infoMode].m_targetColor, (float)Citizen.GetHealthLevel(health2) * 0.2f); + } + case InfoManager.InfoMode.Happiness: + { + int health = Singleton.instance.m_citizens.m_buffer[data.m_citizen].m_health; + int wellbeing = Singleton.instance.m_citizens.m_buffer[data.m_citizen].m_wellbeing; + int happiness = Citizen.GetHappiness(health, wellbeing); + return Color.Lerp(Singleton.instance.m_properties.m_modeProperties[(int)infoMode].m_negativeColor, Singleton.instance.m_properties.m_modeProperties[(int)infoMode].m_targetColor, (float)Citizen.GetHappinessLevel(happiness) * 0.25f); + } + default: + return base.GetColor(instanceID, ref data, infoMode); + } + } + + public override void SetRenderParameters(RenderManager.CameraInfo cameraInfo, ushort instanceID, ref CitizenInstance data, Vector3 position, Quaternion rotation, Vector3 velocity, Color color, bool underground) + { + if ((data.m_flags & CitizenInstance.Flags.AtTarget) != 0) + { + if ((data.m_flags & CitizenInstance.Flags.SittingDown) != 0) + { + base.m_info.SetRenderParameters(position, rotation, velocity, color, 2, underground); + return; + } + if ((data.m_flags & (CitizenInstance.Flags.Panicking | CitizenInstance.Flags.Blown | CitizenInstance.Flags.Floating)) == CitizenInstance.Flags.Panicking) + { + base.m_info.SetRenderParameters(position, rotation, velocity, color, 1, underground); + return; + } + if ((data.m_flags & (CitizenInstance.Flags.Blown | CitizenInstance.Flags.Floating | CitizenInstance.Flags.Cheering)) == CitizenInstance.Flags.Cheering) + { + base.m_info.SetRenderParameters(position, rotation, velocity, color, 5, underground); + return; + } + } + if ((data.m_flags & CitizenInstance.Flags.RidingBicycle) != 0) + { + base.m_info.SetRenderParameters(position, rotation, velocity, color, 3, underground); + } + else if ((data.m_flags & (CitizenInstance.Flags.Blown | CitizenInstance.Flags.Floating)) != 0) + { + base.m_info.SetRenderParameters(position, rotation, Vector3.zero, color, 1, underground); + } + else + { + base.m_info.SetRenderParameters(position, rotation, velocity, color, instanceID & 4, underground); + } + } + + public override string GetLocalizedStatus(ushort instanceID, ref CitizenInstance data, out InstanceID target) + { + if ((data.m_flags & (CitizenInstance.Flags.Blown | CitizenInstance.Flags.Floating)) != 0) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_CONFUSED"); + } + CitizenManager instance = Singleton.instance; + uint citizen = data.m_citizen; + bool flag = false; + ushort num = 0; + ushort num2 = 0; + ushort num3 = 0; + if (citizen != 0) + { + num = instance.m_citizens.m_buffer[citizen].m_homeBuilding; + num2 = instance.m_citizens.m_buffer[citizen].m_workBuilding; + num3 = instance.m_citizens.m_buffer[citizen].m_vehicle; + flag = ((instance.m_citizens.m_buffer[citizen].m_flags & Citizen.Flags.Student) != Citizen.Flags.None); + } + ushort targetBuilding = data.m_targetBuilding; + bool flag2; + bool flag3; + if (targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + if (num3 != 0) + { + VehicleManager instance2 = Singleton.instance; + VehicleInfo info = instance2.m_vehicles.m_buffer[num3].Info; + if (info.m_class.m_service == ItemClass.Service.Residential && info.m_vehicleType != VehicleInfo.VehicleType.Bicycle) + { + if (info.m_vehicleAI.GetOwnerID(num3, ref instance2.m_vehicles.m_buffer[num3]).Citizen == citizen) + { + target = InstanceID.Empty; + target.NetNode = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_DRIVINGTO"); + } + } + else if (info.m_class.m_service == ItemClass.Service.PublicTransport || info.m_class.m_service == ItemClass.Service.Disaster) + { + ushort transportLine = Singleton.instance.m_nodes.m_buffer[targetBuilding].m_transportLine; + if ((data.m_flags & CitizenInstance.Flags.WaitingTaxi) != 0) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_WAITING_TAXI"); + } + if (instance2.m_vehicles.m_buffer[num3].m_transportLine != transportLine) + { + target = InstanceID.Empty; + target.NetNode = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_TRAVELLINGTO"); + } + } + } + if ((data.m_flags & CitizenInstance.Flags.OnTour) != 0) + { + target = InstanceID.Empty; + target.NetNode = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_VISITING"); + } + target = InstanceID.Empty; + target.NetNode = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_GOINGTO"); + } + flag2 = ((Singleton.instance.m_buildings.m_buffer[targetBuilding].m_flags & Building.Flags.IncomingOutgoing) != Building.Flags.None); + flag3 = (data.m_path == 0 && (data.m_flags & CitizenInstance.Flags.HangAround) != CitizenInstance.Flags.None); + if (num3 != 0) + { + VehicleManager instance3 = Singleton.instance; + VehicleInfo info2 = instance3.m_vehicles.m_buffer[num3].Info; + if (info2.m_class.m_service == ItemClass.Service.Residential && info2.m_vehicleType != VehicleInfo.VehicleType.Bicycle) + { + if (info2.m_vehicleAI.GetOwnerID(num3, ref instance3.m_vehicles.m_buffer[num3]).Citizen == citizen) + { + if (flag2) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_DRIVINGTO_OUTSIDE"); + } + if (targetBuilding == num) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_DRIVINGTO_HOME"); + } + if (targetBuilding == num2) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get((!flag) ? "CITIZEN_STATUS_DRIVINGTO_WORK" : "CITIZEN_STATUS_DRIVINGTO_SCHOOL"); + } + target = InstanceID.Empty; + target.Building = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_DRIVINGTO"); + } + goto IL_0480; + } + if (info2.m_class.m_service != ItemClass.Service.PublicTransport && info2.m_class.m_service != ItemClass.Service.Disaster) + { + goto IL_0480; + } + if ((data.m_flags & CitizenInstance.Flags.WaitingTaxi) != 0) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_WAITING_TAXI"); + } + if (flag2) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_TRAVELLINGTO_OUTSIDE"); + } + if (targetBuilding == num) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_TRAVELLINGTO_HOME"); + } + if (targetBuilding == num2) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get((!flag) ? "CITIZEN_STATUS_TRAVELLINGTO_WORK" : "CITIZEN_STATUS_TRAVELLINGTO_SCHOOL"); + } + target = InstanceID.Empty; + target.Building = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_TRAVELLINGTO"); + } + goto IL_0480; + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_CONFUSED"); + IL_0480: + if (flag2) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_GOINGTO_OUTSIDE"); + } + if (targetBuilding == num) + { + if (flag3) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_AT_HOME"); + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_GOINGTO_HOME"); + } + if (targetBuilding == num2) + { + if (flag3) + { + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get((!flag) ? "CITIZEN_STATUS_AT_WORK" : "CITIZEN_STATUS_AT_SCHOOL"); + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get((!flag) ? "CITIZEN_STATUS_GOINGTO_WORK" : "CITIZEN_STATUS_GOINGTO_SCHOOL"); + } + if (flag3) + { + target = InstanceID.Empty; + target.Building = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_VISITING"); + } + target = InstanceID.Empty; + target.Building = targetBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_GOINGTO"); + } + + public override string GetLocalizedStatus(uint citizenID, ref Citizen data, out InstanceID target) + { + CitizenManager instance = Singleton.instance; + ushort instance2 = data.m_instance; + if (instance2 != 0) + { + return GetLocalizedStatus(instance2, ref instance.m_instances.m_buffer[instance2], out target); + } + Citizen.Location currentLocation = data.CurrentLocation; + ushort homeBuilding = data.m_homeBuilding; + ushort workBuilding = data.m_workBuilding; + ushort visitBuilding = data.m_visitBuilding; + bool flag = (data.m_flags & Citizen.Flags.Student) != Citizen.Flags.None; + switch (currentLocation) + { + case Citizen.Location.Home: + if (homeBuilding == 0) + { + break; + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_AT_HOME"); + case Citizen.Location.Work: + if (workBuilding == 0) + { + break; + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get((!flag) ? "CITIZEN_STATUS_AT_WORK" : "CITIZEN_STATUS_AT_SCHOOL"); + case Citizen.Location.Visit: + if (visitBuilding == 0) + { + break; + } + target = InstanceID.Empty; + target.Building = visitBuilding; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_VISITING"); + } + target = InstanceID.Empty; + return ColossalFramework.Globalization.Locale.Get("CITIZEN_STATUS_CONFUSED"); + } + + public override void LoadInstance(ushort instanceID, ref CitizenInstance data) + { + base.LoadInstance(instanceID, ref data); + if (data.m_sourceBuilding != 0) + { + Singleton.instance.m_buildings.m_buffer[data.m_sourceBuilding].AddSourceCitizen(instanceID, ref data); + } + if (data.m_targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + else + { + Singleton.instance.m_buildings.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + } + } + + public override void SimulationStep(ushort instanceID, ref CitizenInstance citizenData, ref CitizenInstance.Frame frameData, bool lodPhysics) + { + uint currentFrameIndex = Singleton.instance.m_currentFrameIndex; + if ((currentFrameIndex >> 4 & 0x3F) == (instanceID & 0x3F)) + { + CitizenManager instance = Singleton.instance; + uint citizen = citizenData.m_citizen; + if (citizen != 0 && (instance.m_citizens.m_buffer[citizen].m_flags & Citizen.Flags.NeedGoods) != 0) + { + BuildingManager instance2 = Singleton.instance; + ushort homeBuilding = instance.m_citizens.m_buffer[citizen].m_homeBuilding; + ushort num = instance2.FindBuilding(frameData.m_position, 32f, ItemClass.Service.Commercial, ItemClass.SubService.None, Building.Flags.Created | Building.Flags.Active, Building.Flags.Deleted); + if (homeBuilding != 0 && num != 0) + { + BuildingInfo info = instance2.m_buildings.m_buffer[num].Info; + int num2 = -100; + info.m_buildingAI.ModifyMaterialBuffer(num, ref instance2.m_buildings.m_buffer[num], TransferManager.TransferReason.Shopping, ref num2); + uint containingUnit = instance.m_citizens.m_buffer[citizen].GetContainingUnit(citizen, instance2.m_buildings.m_buffer[homeBuilding].m_citizenUnits, CitizenUnit.Flags.Home); + if (containingUnit != 0) + { + instance.m_units.m_buffer[containingUnit].m_goods += (ushort)(-num2); + } + instance.m_citizens.m_buffer[citizen].m_flags &= ~Citizen.Flags.NeedGoods; + } + } + } + base.SimulationStep(instanceID, ref citizenData, ref frameData, lodPhysics); + } + + public override void SimulationStep(uint citizenID, ref Citizen data) + { + if (!data.Dead && UpdateAge(citizenID, ref data)) + { + return; + } + if (!data.Dead) + { + UpdateHome(citizenID, ref data); + } + if (!data.Sick && !data.Dead) + { + if (UpdateHealth(citizenID, ref data)) + { + return; + } + UpdateWellbeing(citizenID, ref data); + UpdateWorkplace(citizenID, ref data); + } + UpdateLocation(citizenID, ref data); + } + + public override void SimulationStep(uint homeID, ref CitizenUnit data) + { + CitizenManager instance = Singleton.instance; + ushort building = instance.m_units.m_buffer[homeID].m_building; + if (data.m_citizen0 != 0 && data.m_citizen1 != 0 && (data.m_citizen2 == 0 || data.m_citizen3 == 0 || data.m_citizen4 == 0)) + { + bool flag = CanMakeBabies(data.m_citizen0, ref instance.m_citizens.m_buffer[data.m_citizen0]); + bool flag2 = CanMakeBabies(data.m_citizen1, ref instance.m_citizens.m_buffer[data.m_citizen1]); + if (flag && flag2 && Singleton.instance.m_randomizer.Int32(12u) == 0) + { + int family = instance.m_citizens.m_buffer[data.m_citizen0].m_family; + if (instance.CreateCitizen(out uint num, 0, family, ref Singleton.instance.m_randomizer)) + { + instance.m_citizens.m_buffer[num].SetHome(num, 0, homeID); + instance.m_citizens.m_buffer[num].m_flags |= Citizen.Flags.Original; + if (building != 0) + { + DistrictManager instance2 = Singleton.instance; + Vector3 position = Singleton.instance.m_buildings.m_buffer[building].m_position; + byte district = instance2.GetDistrict(position); + instance2.m_districts.m_buffer[district].m_birthData.m_tempCount += 1u; + } + } + } + } + if (data.m_citizen0 != 0 && data.m_citizen1 == 0) + { + TryFindPartner(data.m_citizen0, ref instance.m_citizens.m_buffer[data.m_citizen0]); + } + else if (data.m_citizen1 != 0 && data.m_citizen0 == 0) + { + TryFindPartner(data.m_citizen1, ref instance.m_citizens.m_buffer[data.m_citizen1]); + } + if (data.m_citizen2 != 0) + { + TryMoveAwayFromHome(data.m_citizen2, ref instance.m_citizens.m_buffer[data.m_citizen2]); + } + if (data.m_citizen3 != 0) + { + TryMoveAwayFromHome(data.m_citizen3, ref instance.m_citizens.m_buffer[data.m_citizen3]); + } + if (data.m_citizen4 != 0) + { + TryMoveAwayFromHome(data.m_citizen4, ref instance.m_citizens.m_buffer[data.m_citizen4]); + } + data.m_goods = (ushort)Mathf.Max(0, data.m_goods - 20); + if (data.m_goods < 200) + { + int num2 = Singleton.instance.m_randomizer.Int32(5u); + for (int i = 0; i < 5; i++) + { + uint citizen = data.GetCitizen((num2 + i) % 5); + if (citizen != 0) + { + instance.m_citizens.m_buffer[citizen].m_flags |= Citizen.Flags.NeedGoods; + break; + } + } + } + if (building != 0 && ((long)Singleton.instance.m_buildings.m_buffer[building].m_problems & -4611686018427387904L) != 0) + { + uint num3 = 0u; + int num4 = 0; + if (data.m_citizen4 != 0 && !instance.m_citizens.m_buffer[data.m_citizen4].Dead) + { + num4++; + num3 = data.m_citizen4; + } + if (data.m_citizen3 != 0 && !instance.m_citizens.m_buffer[data.m_citizen3].Dead) + { + num4++; + num3 = data.m_citizen3; + } + if (data.m_citizen2 != 0 && !instance.m_citizens.m_buffer[data.m_citizen2].Dead) + { + num4++; + num3 = data.m_citizen2; + } + if (data.m_citizen1 != 0 && !instance.m_citizens.m_buffer[data.m_citizen1].Dead) + { + num4++; + num3 = data.m_citizen1; + } + if (data.m_citizen0 != 0 && !instance.m_citizens.m_buffer[data.m_citizen0].Dead) + { + num4++; + num3 = data.m_citizen0; + } + if (num3 != 0) + { + TryMoveFamily(num3, ref instance.m_citizens.m_buffer[num3], num4); + } + } + } + + protected override void PathfindSuccess(ushort instanceID, ref CitizenInstance data) + { + uint citizen = data.m_citizen; + if (citizen != 0) + { + CitizenManager instance = Singleton.instance; + if ((instance.m_citizens.m_buffer[citizen].m_flags & (Citizen.Flags.MovingIn | Citizen.Flags.DummyTraffic)) == Citizen.Flags.MovingIn) + { + StatisticBase statisticBase = Singleton.instance.Acquire(StatisticType.MoveRate); + statisticBase.Add(1); + } + } + base.PathfindSuccess(instanceID, ref data); + } + + protected override void Spawn(ushort instanceID, ref CitizenInstance data) + { + if ((data.m_flags & CitizenInstance.Flags.Character) == CitizenInstance.Flags.None) + { + data.Spawn(instanceID); + uint citizen = data.m_citizen; + ushort targetBuilding = data.m_targetBuilding; + if (citizen != 0 && targetBuilding != 0) + { + Randomizer randomizer = new Randomizer(citizen); + if (randomizer.Int32(20u) == 0) + { + CitizenManager instance = Singleton.instance; + DistrictManager instance2 = Singleton.instance; + Vector3 position; + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + NetManager instance3 = Singleton.instance; + position = instance3.m_nodes.m_buffer[targetBuilding].m_position; + } + else + { + BuildingManager instance4 = Singleton.instance; + position = instance4.m_buildings.m_buffer[targetBuilding].m_position; + } + byte district = instance2.GetDistrict(data.m_targetPos); + byte district2 = instance2.GetDistrict(position); + DistrictPolicies.Services servicePolicies = instance2.m_districts.m_buffer[district].m_servicePolicies; + DistrictPolicies.Services servicePolicies2 = instance2.m_districts.m_buffer[district2].m_servicePolicies; + if (((servicePolicies | servicePolicies2) & DistrictPolicies.Services.PetBan) == DistrictPolicies.Services.None) + { + CitizenInfo groupAnimalInfo = instance.GetGroupAnimalInfo(ref randomizer, base.m_info.m_class.m_service, base.m_info.m_class.m_subService); + if ((object)groupAnimalInfo != null && instance.CreateCitizenInstance(out ushort num, ref randomizer, groupAnimalInfo, 0u)) + { + groupAnimalInfo.m_citizenAI.SetSource(num, ref instance.m_instances.m_buffer[num], instanceID); + groupAnimalInfo.m_citizenAI.SetTarget(num, ref instance.m_instances.m_buffer[num], instanceID); + } + } + } + } + } + } + + private bool UpdateAge(uint citizenID, ref Citizen data) + { + int num = data.Age + 1; + if (num <= 45) + { + if (num == 15 || num == 45) + { + FinishSchoolOrWork(citizenID, ref data); + } + } + else if (num == 90 || num == 180) + { + FinishSchoolOrWork(citizenID, ref data); + } + else if ((data.m_flags & Citizen.Flags.Student) != 0 && num % 15 == 0) + { + FinishSchoolOrWork(citizenID, ref data); + } + if ((data.m_flags & Citizen.Flags.Original) != 0) + { + CitizenManager instance = Singleton.instance; + if (instance.m_tempOldestOriginalResident < num) + { + instance.m_tempOldestOriginalResident = num; + } + if (num == 240) + { + Singleton.instance.Acquire(StatisticType.FullLifespans).Add(1); + } + } + data.Age = num; + if (num >= 240 && data.CurrentLocation != Citizen.Location.Moving && data.m_vehicle == 0 && Singleton.instance.m_randomizer.Int32(240, 255) <= num) + { + Die(citizenID, ref data); + if (Singleton.instance.m_randomizer.Int32(2u) == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + return true; + } + } + return false; + } + + private void Die(uint citizenID, ref Citizen data) + { + data.Sick = false; + data.Dead = true; + data.SetParkedVehicle(citizenID, 0); + if ((data.m_flags & Citizen.Flags.MovingIn) == Citizen.Flags.None) + { + ushort num = data.GetBuildingByLocation(); + if (num == 0) + { + num = data.m_homeBuilding; + } + if (num != 0) + { + DistrictManager instance = Singleton.instance; + Vector3 position = Singleton.instance.m_buildings.m_buffer[num].m_position; + byte district = instance.GetDistrict(position); + instance.m_districts.m_buffer[district].m_deathData.m_tempCount += 1u; + } + } + } + + private void UpdateHome(uint citizenID, ref Citizen data) + { + if (data.m_homeBuilding == 0 && (data.m_flags & Citizen.Flags.DummyTraffic) == Citizen.Flags.None) + { + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + offer.Priority = 7; + offer.Citizen = citizenID; + offer.Amount = 1; + offer.Active = true; + if (data.m_workBuilding != 0) + { + BuildingManager instance = Singleton.instance; + offer.Position = instance.m_buildings.m_buffer[data.m_workBuilding].m_position; + } + else + { + offer.PositionX = Singleton.instance.m_randomizer.Int32(256u); + offer.PositionZ = Singleton.instance.m_randomizer.Int32(256u); + } + if (Singleton.instance.m_randomizer.Int32(2u) == 0) + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3, offer); + break; + } + } + else + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0B, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1B, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2B, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3B, offer); + break; + } + } + } + } + + private void UpdateWorkplace(uint citizenID, ref Citizen data) + { + if (data.m_workBuilding == 0 && data.m_homeBuilding != 0) + { + BuildingManager instance = Singleton.instance; + Vector3 position = instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(position); + DistrictPolicies.Services servicePolicies = instance2.m_districts.m_buffer[district].m_servicePolicies; + int age = data.Age; + TransferManager.TransferReason transferReason = TransferManager.TransferReason.None; + switch (Citizen.GetAgeGroup(age)) + { + case Citizen.AgeGroup.Child: + if (!data.Education1) + { + transferReason = TransferManager.TransferReason.Student1; + } + break; + case Citizen.AgeGroup.Teen: + if (!data.Education2) + { + transferReason = TransferManager.TransferReason.Student2; + } + break; + case Citizen.AgeGroup.Young: + case Citizen.AgeGroup.Adult: + if (!data.Education3) + { + transferReason = TransferManager.TransferReason.Student3; + } + break; + } + if (data.Unemployed != 0 && ((servicePolicies & DistrictPolicies.Services.EducationBoost) == DistrictPolicies.Services.None || transferReason != TransferManager.TransferReason.Student3 || age % 5 > 2)) + { + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + offer.Priority = Singleton.instance.m_randomizer.Int32(8u); + offer.Citizen = citizenID; + offer.Position = position; + offer.Amount = 1; + offer.Active = true; + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Worker0, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Worker1, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Worker2, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Worker3, offer); + break; + } + } + switch (transferReason) + { + case TransferManager.TransferReason.None: + return; + case TransferManager.TransferReason.Student3: + if ((servicePolicies & DistrictPolicies.Services.SchoolsOut) != 0 && age % 5 <= 1) + { + return; + } + break; + } + TransferManager.TransferOffer offer2 = default(TransferManager.TransferOffer); + offer2.Priority = Singleton.instance.m_randomizer.Int32(8u); + offer2.Citizen = citizenID; + offer2.Position = position; + offer2.Amount = 1; + offer2.Active = true; + Singleton.instance.AddOutgoingOffer(transferReason, offer2); + } + } + + private bool UpdateHealth(uint citizenID, ref Citizen data) + { + if (data.m_homeBuilding == 0) + { + return false; + } + int num = 20; + BuildingManager instance = Singleton.instance; + BuildingInfo info = instance.m_buildings.m_buffer[data.m_homeBuilding].Info; + Vector3 position = instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(position); + DistrictPolicies.Services servicePolicies = instance2.m_districts.m_buffer[district].m_servicePolicies; + DistrictPolicies.CityPlanning cityPlanningPolicies = instance2.m_districts.m_buffer[district].m_cityPlanningPolicies; + if ((servicePolicies & DistrictPolicies.Services.SmokingBan) != 0) + { + num += 10; + } + if (data.Age >= 180 && (cityPlanningPolicies & DistrictPolicies.CityPlanning.AntiSlip) != 0) + { + num += 10; + } + info.m_buildingAI.GetMaterialAmount(data.m_homeBuilding, ref instance.m_buildings.m_buffer[data.m_homeBuilding], TransferManager.TransferReason.Garbage, out int num2, out int _); + num2 /= 1000; + if (num2 <= 2) + { + num += 12; + } + else if (num2 >= 4) + { + num -= num2 - 3; + } + int healthCareRequirement = Citizen.GetHealthCareRequirement(Citizen.GetAgePhase(data.EducationLevel, data.Age)); + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.HealthCare, position, out int num4, out int num5); + if (healthCareRequirement != 0) + { + if (num4 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num4, healthCareRequirement, 500, 20, 40); + } + if (num5 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num5, healthCareRequirement >> 1, 250, 5, 20); + } + } + Singleton.instance.CheckLocalResource(ImmaterialResourceManager.Resource.NoisePollution, position, out int num6); + if (num6 != 0) + { + num = ((info.m_class.m_subService != ItemClass.SubService.ResidentialLowEco && info.m_class.m_subService != ItemClass.SubService.ResidentialHighEco) ? (num - num6 * 100 / 255) : (num - num6 * 150 / 255)); + } + Singleton.instance.CheckLocalResource(ImmaterialResourceManager.Resource.CrimeRate, position, out int num7); + if (num7 > 3) + { + num = ((num7 > 30) ? ((num7 > 70) ? (num - 15) : (num - 5)) : (num - 2)); + } + Singleton.instance.CheckWater(position, out bool flag, out bool flag2, out byte b); + if (flag) + { + num += 12; + data.NoWater = 0; + } + else + { + int noWater = data.NoWater; + if (noWater < 2) + { + data.NoWater = noWater + 1; + } + else + { + num -= 5; + } + } + if (flag2) + { + num += 12; + data.NoSewage = 0; + } + else + { + int noSewage = data.NoSewage; + if (noSewage < 2) + { + data.NoSewage = noSewage + 1; + } + else + { + num -= 5; + } + } + num = ((b >= 35) ? (num - (b * 2 - 35)) : (num - b)); + Singleton.instance.CheckPollution(position, out byte b2); + if (b2 != 0) + { + num = ((info.m_class.m_subService != ItemClass.SubService.ResidentialLowEco && info.m_class.m_subService != ItemClass.SubService.ResidentialHighEco) ? (num - b2 * 100 / 255) : (num - b2 * 200 / 255)); + } + num = Mathf.Clamp(num, 0, 100); + data.m_health = (byte)num; + int num8 = 0; + if (num <= 10) + { + int badHealth = data.BadHealth; + if (badHealth < 3) + { + num8 = 15; + data.BadHealth = badHealth + 1; + } + else + { + num8 = ((num5 != 0) ? 50 : 75); + } + } + else if (num <= 25) + { + data.BadHealth = 0; + num8 += 10; + } + else if (num <= 50) + { + data.BadHealth = 0; + num8 += 3; + } + else + { + data.BadHealth = 0; + } + Citizen.Location currentLocation = data.CurrentLocation; + if (currentLocation != Citizen.Location.Moving && data.m_vehicle == 0 && num8 != 0 && Singleton.instance.m_randomizer.Int32(100u) < num8) + { + if (Singleton.instance.m_randomizer.Int32(3u) == 0) + { + Die(citizenID, ref data); + if (Singleton.instance.m_randomizer.Int32(2u) == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + return true; + } + } + else + { + data.Sick = true; + } + } + return false; + } + + private void UpdateWellbeing(uint citizenID, ref Citizen data) + { + if (data.m_homeBuilding != 0) + { + int num = 0; + BuildingManager instance = Singleton.instance; + BuildingInfo info = instance.m_buildings.m_buffer[data.m_homeBuilding].Info; + Vector3 position = instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + ItemClass @class = info.m_class; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(position); + DistrictPolicies.Services servicePolicies = instance2.m_districts.m_buffer[district].m_servicePolicies; + DistrictPolicies.Taxation taxationPolicies = instance2.m_districts.m_buffer[district].m_taxationPolicies; + DistrictPolicies.CityPlanning cityPlanningPolicies = instance2.m_districts.m_buffer[district].m_cityPlanningPolicies; + int health = data.m_health; + if (health > 80) + { + num += 10; + } + else if (health > 60) + { + num += 5; + } + num -= Mathf.Clamp(50 - health, 0, 30); + if ((servicePolicies & DistrictPolicies.Services.PetBan) != 0) + { + num -= 5; + } + if ((servicePolicies & DistrictPolicies.Services.SmokingBan) != 0) + { + num -= 15; + } + Building.Frame lastFrameData = instance.m_buildings.m_buffer[data.m_homeBuilding].GetLastFrameData(); + if (lastFrameData.m_fireDamage != 0) + { + num -= 15; + } + Citizen.Wealth wealthLevel = data.WealthLevel; + Citizen.AgePhase agePhase = Citizen.GetAgePhase(data.EducationLevel, data.Age); + int taxRate = Singleton.instance.GetTaxRate(@class, taxationPolicies); + int num2 = (int)(8 - wealthLevel); + int num3 = (int)(11 - wealthLevel); + if (@class.m_subService == ItemClass.SubService.ResidentialHigh) + { + num2++; + num3++; + } + if (taxRate < num2) + { + num += num2 - taxRate; + } + if (taxRate > num3) + { + num -= taxRate - num3; + } + int policeDepartmentRequirement = Citizen.GetPoliceDepartmentRequirement(agePhase); + if (policeDepartmentRequirement != 0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.PoliceDepartment, position, out int num4, out int num5); + if (num4 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num4, policeDepartmentRequirement, 500, 20, 40); + } + if (num5 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num5, policeDepartmentRequirement >> 1, 250, 5, 20); + } + } + int fireDepartmentRequirement = Citizen.GetFireDepartmentRequirement(agePhase); + if (fireDepartmentRequirement != 0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.FireDepartment, position, out int num6, out int num7); + if (num6 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num6, fireDepartmentRequirement, 500, 20, 40); + } + if (num7 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num7, fireDepartmentRequirement >> 1, 250, 5, 20); + } + } + int educationRequirement = Citizen.GetEducationRequirement(agePhase); + if (educationRequirement != 0) + { + int num8; + int num9; + if (agePhase < Citizen.AgePhase.Teen0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.EducationElementary, position, out num8, out num9); + if (num8 > 1000 && !data.Education1 && Singleton.instance.m_randomizer.Int32(9000u) < num8 - 1000) + { + data.Education1 = true; + } + } + else if (agePhase < Citizen.AgePhase.Young0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.EducationHighSchool, position, out num8, out num9); + if (num8 > 1000 && !data.Education2 && Singleton.instance.m_randomizer.Int32(9000u) < num8 - 1000) + { + data.Education2 = true; + } + } + else + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.EducationUniversity, position, out num8, out num9); + if (num8 > 1000 && !data.Education3 && Singleton.instance.m_randomizer.Int32(9000u) < num8 - 1000) + { + data.Education3 = true; + } + } + if (num8 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num8, educationRequirement, 500, 20, 40); + } + if (num9 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num9, educationRequirement >> 1, 250, 5, 20); + } + } + int entertainmentRequirement = Citizen.GetEntertainmentRequirement(agePhase); + if (entertainmentRequirement != 0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.Entertainment, position, out int num10, out int num11); + if (num10 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num10, entertainmentRequirement, 500, 30, 60); + } + if (num11 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num11, entertainmentRequirement >> 1, 250, 10, 40); + } + } + int transportRequirement = Citizen.GetTransportRequirement(agePhase); + if (transportRequirement != 0) + { + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.PublicTransport, position, out int num12, out int num13); + if (num12 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num12, transportRequirement, 500, 20, 40); + } + if (num13 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num13, transportRequirement >> 1, 250, 5, 20); + } + } + int deathCareRequirement = Citizen.GetDeathCareRequirement(agePhase); + Singleton.instance.CheckResource(ImmaterialResourceManager.Resource.DeathCare, position, out int num14, out int num15); + if (deathCareRequirement != 0) + { + if (num14 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num14, deathCareRequirement, 500, 10, 20); + } + if (num15 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num15, deathCareRequirement >> 1, 250, 3, 10); + } + } + Singleton.instance.CheckLocalResource(ImmaterialResourceManager.Resource.RadioCoverage, position, out int num16); + if (num16 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num16, 50, 100, 2, 3); + } + Singleton.instance.CheckLocalResource(ImmaterialResourceManager.Resource.DisasterCoverage, position, out int num17); + if (num17 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num17, 50, 100, 3, 4); + } + Singleton.instance.CheckLocalResource(ImmaterialResourceManager.Resource.FirewatchCoverage, position, out int num18); + if (num18 != 0) + { + num += ImmaterialResourceManager.CalculateResourceEffect(num18, 100, 1000, 0, 3); + } + Singleton.instance.CheckElectricity(position, out bool flag); + if (flag) + { + num += 12; + data.NoElectricity = 0; + } + else + { + int noElectricity = data.NoElectricity; + if (noElectricity < 2) + { + data.NoElectricity = noElectricity + 1; + } + else + { + num -= 5; + } + } + Singleton.instance.CheckHeating(position, out bool flag2); + if (flag2) + { + num += 5; + } + else if ((servicePolicies & DistrictPolicies.Services.NoElectricity) != 0) + { + num -= 10; + } + if ((cityPlanningPolicies & DistrictPolicies.CityPlanning.ElectricCars) != 0) + { + int carProbability = GetCarProbability(Citizen.GetAgeGroup(data.Age)); + if (new Randomizer(citizenID).Int32(100u) < carProbability) + { + Singleton.instance.FetchResource(EconomyManager.Resource.PolicyCost, 200, @class); + } + } + bool flag3 = Singleton.instance.Unlocked(ItemClass.Service.PoliceDepartment); + int workRequirement = Citizen.GetWorkRequirement(agePhase); + if (workRequirement != 0) + { + if (data.m_workBuilding == 0) + { + int unemployed = data.Unemployed; + num -= unemployed * workRequirement / 100; + if (flag3) + { + data.Unemployed = unemployed + 1; + } + else + { + data.Unemployed = Mathf.Min(1, unemployed + 1); + } + } + else + { + data.Unemployed = 0; + } + } + else + { + data.Unemployed = 0; + } + num = Mathf.Clamp(num, 0, 100); + data.m_wellbeing = (byte)num; + if (flag3) + { + Randomizer randomizer = new Randomizer(citizenID * 7931 + 123); + int maxCrimeRate = Citizen.GetMaxCrimeRate(Citizen.GetWellbeingLevel(data.EducationLevel, num)); + int num19 = Mathf.Min(maxCrimeRate, Citizen.GetCrimeRate(data.Unemployed)); + data.Criminal = (randomizer.Int32(500u) < num19); + } + else + { + data.Criminal = false; + } + } + } + + private void FinishSchoolOrWork(uint citizenID, ref Citizen data) + { + if (data.m_workBuilding != 0) + { + if (data.CurrentLocation == Citizen.Location.Work && data.m_homeBuilding != 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_workBuilding, data.m_homeBuilding); + } + BuildingManager instance = Singleton.instance; + CitizenManager instance2 = Singleton.instance; + uint num = instance.m_buildings.m_buffer[data.m_workBuilding].m_citizenUnits; + int num2 = 0; + do + { + if (num == 0) + { + return; + } + uint nextUnit = instance2.m_units.m_buffer[num].m_nextUnit; + CitizenUnit.Flags flags = instance2.m_units.m_buffer[num].m_flags; + if ((flags & (CitizenUnit.Flags.Work | CitizenUnit.Flags.Student)) != 0) + { + if ((flags & CitizenUnit.Flags.Student) != 0) + { + if (data.RemoveFromUnit(citizenID, ref instance2.m_units.m_buffer[num])) + { + BuildingInfo info = instance.m_buildings.m_buffer[data.m_workBuilding].Info; + if (info.m_buildingAI.GetEducationLevel1()) + { + data.Education1 = true; + } + if (info.m_buildingAI.GetEducationLevel2()) + { + data.Education2 = true; + } + if (info.m_buildingAI.GetEducationLevel3()) + { + data.Education3 = true; + } + data.m_workBuilding = 0; + data.m_flags &= ~Citizen.Flags.Student; + if ((data.m_flags & Citizen.Flags.Original) != 0 && data.EducationLevel == Citizen.Education.ThreeSchools && instance2.m_fullyEducatedOriginalResidents++ == 0 && Singleton.instance.m_metaData.m_disableAchievements != SimulationMetaData.MetaBool.True) + { + ThreadHelper.dispatcher.Dispatch(delegate + { + if (!PlatformService.achievements["ClimbingTheSocialLadder"].achieved) + { + PlatformService.achievements["ClimbingTheSocialLadder"].Unlock(); + } + }); + } + return; + } + } + else if (data.RemoveFromUnit(citizenID, ref instance2.m_units.m_buffer[num])) + { + data.m_workBuilding = 0; + data.m_flags &= ~Citizen.Flags.Student; + return; + } + } + num = nextUnit; + } + while (++num2 <= 524288); + CODebugBase.Error(LogChannel.Core, "Invalid list detected!\n" + Environment.StackTrace); + } + } + + private bool DoRandomMove() + { + uint vehicleCount = (uint)Singleton.instance.m_vehicleCount; + uint instanceCount = (uint)Singleton.instance.m_instanceCount; + if (vehicleCount * 65536 > instanceCount * 16384) + { + return Singleton.instance.m_randomizer.UInt32(16384u) > vehicleCount; + } + return Singleton.instance.m_randomizer.UInt32(65536u) > instanceCount; + } + + private TransferManager.TransferReason GetShoppingReason() + { + switch (Singleton.instance.m_randomizer.Int32(8u)) + { + case 0: + return TransferManager.TransferReason.Shopping; + case 1: + return TransferManager.TransferReason.ShoppingB; + case 2: + return TransferManager.TransferReason.ShoppingC; + case 3: + return TransferManager.TransferReason.ShoppingD; + case 4: + return TransferManager.TransferReason.ShoppingE; + case 5: + return TransferManager.TransferReason.ShoppingF; + case 6: + return TransferManager.TransferReason.ShoppingG; + case 7: + return TransferManager.TransferReason.ShoppingH; + default: + return TransferManager.TransferReason.Shopping; + } + } + + private TransferManager.TransferReason GetEntertainmentReason() + { + switch (Singleton.instance.m_randomizer.Int32(4u)) + { + case 0: + return TransferManager.TransferReason.Entertainment; + case 1: + return TransferManager.TransferReason.EntertainmentB; + case 2: + return TransferManager.TransferReason.EntertainmentC; + case 3: + return TransferManager.TransferReason.EntertainmentD; + default: + return TransferManager.TransferReason.Entertainment; + } + } + + private TransferManager.TransferReason GetEvacuationReason(ushort sourceBuilding) + { + if (sourceBuilding != 0) + { + BuildingManager instance = Singleton.instance; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(instance.m_buildings.m_buffer[sourceBuilding].m_position); + DistrictPolicies.CityPlanning cityPlanningPolicies = instance2.m_districts.m_buffer[district].m_cityPlanningPolicies; + if ((cityPlanningPolicies & DistrictPolicies.CityPlanning.VIPArea) != 0) + { + switch (Singleton.instance.m_randomizer.Int32(4u)) + { + case 0: + return TransferManager.TransferReason.EvacuateVipA; + case 1: + return TransferManager.TransferReason.EvacuateVipB; + case 2: + return TransferManager.TransferReason.EvacuateVipC; + case 3: + return TransferManager.TransferReason.EvacuateVipD; + default: + return TransferManager.TransferReason.EvacuateVipA; + } + } + } + switch (Singleton.instance.m_randomizer.Int32(4u)) + { + case 0: + return TransferManager.TransferReason.EvacuateA; + case 1: + return TransferManager.TransferReason.EvacuateB; + case 2: + return TransferManager.TransferReason.EvacuateC; + case 3: + return TransferManager.TransferReason.EvacuateD; + default: + return TransferManager.TransferReason.EvacuateA; + } + } + + private void UpdateLocation(uint citizenID, ref Citizen data) + { + if (data.m_homeBuilding == 0 && data.m_workBuilding == 0 && data.m_visitBuilding == 0 && data.m_instance == 0 && data.m_vehicle == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + } + else + { + switch (data.CurrentLocation) + { + case Citizen.Location.Home: + if ((data.m_flags & Citizen.Flags.MovingIn) != 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + return; + } + if (data.Dead) + { + if (data.m_homeBuilding == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + } + else + { + if (data.m_workBuilding != 0) + { + data.SetWorkplace(citizenID, 0, 0u); + } + if (data.m_visitBuilding != 0) + { + data.SetVisitplace(citizenID, 0, 0u); + } + if (data.m_vehicle != 0) + { + break; + } + if (FindHospital(citizenID, data.m_homeBuilding, TransferManager.TransferReason.Dead)) + { + break; + } + } + return; + } + if (data.Arrested) + { + data.Arrested = false; + } + else if (data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + if (data.Sick) + { + if (FindHospital(citizenID, data.m_homeBuilding, TransferManager.TransferReason.Sick)) + { + break; + } + return; + } + if ((Singleton.instance.m_buildings.m_buffer[data.m_homeBuilding].m_flags & Building.Flags.Evacuating) != 0) + { + base.FindEvacuationPlace(citizenID, data.m_homeBuilding, GetEvacuationReason(data.m_homeBuilding)); + } + else if ((data.m_flags & Citizen.Flags.NeedGoods) != 0) + { + base.FindVisitPlace(citizenID, data.m_homeBuilding, GetShoppingReason()); + } + else + { + if (data.m_instance == 0 && !DoRandomMove()) + { + break; + } + int dayTimeFrame2 = (int)Singleton.instance.m_dayTimeFrame; + int dAYTIME_FRAMES2 = (int)SimulationManager.DAYTIME_FRAMES; + int num9 = dAYTIME_FRAMES2 / 40; + int num10 = (int)(SimulationManager.DAYTIME_FRAMES * 8) / 24; + int num11 = dayTimeFrame2 - num10 & dAYTIME_FRAMES2 - 1; + int num12 = Mathf.Abs(num11 - (dAYTIME_FRAMES2 >> 1)); + num12 = num12 * num12 / (dAYTIME_FRAMES2 >> 1); + int num13 = Singleton.instance.m_randomizer.Int32((uint)dAYTIME_FRAMES2); + if (num13 < num9) + { + base.FindVisitPlace(citizenID, data.m_homeBuilding, GetEntertainmentReason()); + } + else if (num13 < num9 + num12 && data.m_workBuilding != 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_homeBuilding, data.m_workBuilding); + } + } + } + break; + case Citizen.Location.Work: + if (data.Dead) + { + if (data.m_workBuilding == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + } + else + { + if (data.m_homeBuilding != 0) + { + data.SetHome(citizenID, 0, 0u); + } + if (data.m_visitBuilding != 0) + { + data.SetVisitplace(citizenID, 0, 0u); + } + if (data.m_vehicle != 0) + { + break; + } + if (FindHospital(citizenID, data.m_workBuilding, TransferManager.TransferReason.Dead)) + { + break; + } + } + return; + } + if (data.Arrested) + { + data.Arrested = false; + } + else + { + if (data.Sick) + { + if (data.m_workBuilding == 0) + { + data.CurrentLocation = Citizen.Location.Home; + break; + } + if (data.m_vehicle != 0) + { + break; + } + if (FindHospital(citizenID, data.m_workBuilding, TransferManager.TransferReason.Sick)) + { + break; + } + return; + } + if (data.m_workBuilding == 0) + { + data.CurrentLocation = Citizen.Location.Home; + } + else + { + BuildingManager instance = Singleton.instance; + ushort eventIndex = instance.m_buildings.m_buffer[data.m_workBuilding].m_eventIndex; + if ((instance.m_buildings.m_buffer[data.m_workBuilding].m_flags & Building.Flags.Evacuating) != 0) + { + if (data.m_vehicle == 0) + { + base.FindEvacuationPlace(citizenID, data.m_workBuilding, GetEvacuationReason(data.m_workBuilding)); + } + } + else if (eventIndex == 0 || (Singleton.instance.m_events.m_buffer[eventIndex].m_flags & (EventData.Flags.Preparing | EventData.Flags.Active | EventData.Flags.Ready)) == EventData.Flags.None) + { + if ((data.m_flags & Citizen.Flags.NeedGoods) != 0) + { + if (data.m_vehicle == 0) + { + base.FindVisitPlace(citizenID, data.m_workBuilding, GetShoppingReason()); + } + } + else + { + if (data.m_instance == 0 && !DoRandomMove()) + { + break; + } + int dayTimeFrame = (int)Singleton.instance.m_dayTimeFrame; + int dAYTIME_FRAMES = (int)SimulationManager.DAYTIME_FRAMES; + int num2 = dAYTIME_FRAMES / 40; + int num3 = (int)(SimulationManager.DAYTIME_FRAMES * 16) / 24; + int num4 = dayTimeFrame - num3 & dAYTIME_FRAMES - 1; + int num5 = Mathf.Abs(num4 - (dAYTIME_FRAMES >> 1)); + num5 = num5 * num5 / (dAYTIME_FRAMES >> 1); + int num6 = Singleton.instance.m_randomizer.Int32((uint)dAYTIME_FRAMES); + if (num6 < num2) + { + if (data.m_vehicle == 0) + { + base.FindVisitPlace(citizenID, data.m_workBuilding, GetEntertainmentReason()); + } + } + else if (num6 < num2 + num5 && data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_workBuilding, data.m_homeBuilding); + } + } + } + } + } + break; + case Citizen.Location.Visit: + if (data.Dead) + { + if (data.m_visitBuilding == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + } + else + { + if (data.m_homeBuilding != 0) + { + data.SetHome(citizenID, 0, 0u); + } + if (data.m_workBuilding != 0) + { + data.SetWorkplace(citizenID, 0, 0u); + } + if (data.m_vehicle != 0) + { + break; + } + if (Singleton.instance.m_buildings.m_buffer[data.m_visitBuilding].Info.m_class.m_service == ItemClass.Service.HealthCare) + { + break; + } + if (FindHospital(citizenID, data.m_visitBuilding, TransferManager.TransferReason.Dead)) + { + break; + } + } + return; + } + if (data.Arrested) + { + if (data.m_visitBuilding == 0) + { + data.Arrested = false; + } + } + else if (!data.Collapsed) + { + if (data.Sick) + { + if (data.m_visitBuilding == 0) + { + data.CurrentLocation = Citizen.Location.Home; + break; + } + if (data.m_vehicle != 0) + { + break; + } + BuildingManager instance2 = Singleton.instance; + ItemClass.Service service = instance2.m_buildings.m_buffer[data.m_visitBuilding].Info.m_class.m_service; + if (service == ItemClass.Service.HealthCare) + { + break; + } + if (service == ItemClass.Service.Disaster) + { + break; + } + if (FindHospital(citizenID, data.m_visitBuilding, TransferManager.TransferReason.Sick)) + { + break; + } + return; + } + BuildingManager instance3 = Singleton.instance; + ItemClass.Service service2 = ItemClass.Service.None; + if (data.m_visitBuilding != 0) + { + service2 = instance3.m_buildings.m_buffer[data.m_visitBuilding].Info.m_class.m_service; + } + switch (service2) + { + case ItemClass.Service.HealthCare: + case ItemClass.Service.PoliceDepartment: + if (data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_visitBuilding, data.m_homeBuilding); + data.SetVisitplace(citizenID, 0, 0u); + } + break; + case ItemClass.Service.Disaster: + if ((instance3.m_buildings.m_buffer[data.m_visitBuilding].m_flags & Building.Flags.Downgrading) != 0 && data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_visitBuilding, data.m_homeBuilding); + data.SetVisitplace(citizenID, 0, 0u); + } + break; + default: + if (data.m_visitBuilding == 0) + { + data.CurrentLocation = Citizen.Location.Home; + } + else if ((instance3.m_buildings.m_buffer[data.m_visitBuilding].m_flags & Building.Flags.Evacuating) != 0) + { + if (data.m_vehicle == 0) + { + base.FindEvacuationPlace(citizenID, data.m_visitBuilding, GetEvacuationReason(data.m_visitBuilding)); + } + } + else if ((data.m_flags & Citizen.Flags.NeedGoods) != 0) + { + BuildingInfo info = instance3.m_buildings.m_buffer[data.m_visitBuilding].Info; + int num7 = -100; + info.m_buildingAI.ModifyMaterialBuffer(data.m_visitBuilding, ref instance3.m_buildings.m_buffer[data.m_visitBuilding], TransferManager.TransferReason.Shopping, ref num7); + } + else + { + ushort eventIndex2 = instance3.m_buildings.m_buffer[data.m_visitBuilding].m_eventIndex; + if (eventIndex2 != 0) + { + if ((Singleton.instance.m_events.m_buffer[eventIndex2].m_flags & (EventData.Flags.Preparing | EventData.Flags.Active | EventData.Flags.Ready)) == EventData.Flags.None && data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_visitBuilding, data.m_homeBuilding); + data.SetVisitplace(citizenID, 0, 0u); + } + } + else + { + if (data.m_instance == 0 && !DoRandomMove()) + { + break; + } + int num8 = Singleton.instance.m_randomizer.Int32(40u); + if (num8 < 10 && data.m_homeBuilding != 0 && data.m_vehicle == 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, data.m_visitBuilding, data.m_homeBuilding); + data.SetVisitplace(citizenID, 0, 0u); + } + } + } + break; + } + } + break; + case Citizen.Location.Moving: + if (data.Dead) + { + if (data.m_vehicle == 0) + { + Singleton.instance.ReleaseCitizen(citizenID); + return; + } + if (data.m_homeBuilding != 0) + { + data.SetHome(citizenID, 0, 0u); + } + if (data.m_workBuilding != 0) + { + data.SetWorkplace(citizenID, 0, 0u); + } + if (data.m_visitBuilding != 0) + { + data.SetVisitplace(citizenID, 0, 0u); + } + } + else if (data.m_vehicle == 0 && data.m_instance == 0) + { + if (data.m_visitBuilding != 0) + { + data.SetVisitplace(citizenID, 0, 0u); + } + data.CurrentLocation = Citizen.Location.Home; + data.Arrested = false; + } + else if (data.m_instance != 0 && (Singleton.instance.m_instances.m_buffer[data.m_instance].m_flags & (CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour)) == (CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour)) + { + int num = Singleton.instance.m_randomizer.Int32(40u); + if (num < 10 && data.m_homeBuilding != 0) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + base.StartMoving(citizenID, ref data, 0, data.m_homeBuilding); + } + } + break; + } + data.m_flags &= ~Citizen.Flags.NeedGoods; + } + } + + public bool CanMakeBabies(uint citizenID, ref Citizen data) + { + if (data.Dead) + { + return false; + } + if (Citizen.GetAgeGroup(data.Age) != Citizen.AgeGroup.Adult) + { + return false; + } + if ((data.m_flags & Citizen.Flags.MovingIn) != 0) + { + return false; + } + return true; + } + + public void TryMoveAwayFromHome(uint citizenID, ref Citizen data) + { + if (!data.Dead && data.m_homeBuilding != 0) + { + Citizen.AgeGroup ageGroup = Citizen.GetAgeGroup(data.Age); + if (ageGroup != Citizen.AgeGroup.Young && ageGroup != Citizen.AgeGroup.Adult) + { + return; + } + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + if (ageGroup == Citizen.AgeGroup.Young) + { + offer.Priority = 1; + } + else + { + offer.Priority = Singleton.instance.m_randomizer.Int32(2, 4); + } + offer.Citizen = citizenID; + offer.Position = Singleton.instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + offer.Amount = 1; + offer.Active = true; + if (Singleton.instance.m_randomizer.Int32(2u) == 0) + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3, offer); + break; + } + } + else + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0B, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1B, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2B, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3B, offer); + break; + } + } + } + } + + public void TryMoveFamily(uint citizenID, ref Citizen data, int familySize) + { + if (!data.Dead && data.m_homeBuilding != 0) + { + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + offer.Priority = Singleton.instance.m_randomizer.Int32(1, 7); + offer.Citizen = citizenID; + offer.Position = Singleton.instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + offer.Amount = 1; + offer.Active = true; + if (familySize == 1) + { + if (Singleton.instance.m_randomizer.Int32(2u) == 0) + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3, offer); + break; + } + } + else + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single0B, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single1B, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single2B, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Single3B, offer); + break; + } + } + } + else + { + switch (data.EducationLevel) + { + case Citizen.Education.Uneducated: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Family0, offer); + break; + case Citizen.Education.OneSchool: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Family1, offer); + break; + case Citizen.Education.TwoSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Family2, offer); + break; + case Citizen.Education.ThreeSchools: + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Family3, offer); + break; + } + } + } + } + + public void TryFindPartner(uint citizenID, ref Citizen data) + { + if (!data.Dead && data.m_homeBuilding != 0) + { + Citizen.AgeGroup ageGroup = Citizen.GetAgeGroup(data.Age); + TransferManager.TransferReason material = TransferManager.TransferReason.None; + switch (ageGroup) + { + case Citizen.AgeGroup.Young: + material = TransferManager.TransferReason.PartnerYoung; + break; + case Citizen.AgeGroup.Adult: + material = TransferManager.TransferReason.PartnerAdult; + break; + } + if (ageGroup != Citizen.AgeGroup.Young && ageGroup != Citizen.AgeGroup.Adult) + { + return; + } + Vector3 position = Singleton.instance.m_buildings.m_buffer[data.m_homeBuilding].m_position; + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + offer.Priority = Singleton.instance.m_randomizer.Int32(8u); + offer.Citizen = citizenID; + offer.Position = position; + offer.Amount = 1; + offer.Active = (Singleton.instance.m_randomizer.Int32(2u) == 0); + bool flag = Singleton.instance.m_randomizer.Int32(100u) < 5; + if (Citizen.GetGender(citizenID) == Citizen.Gender.Female != flag) + { + Singleton.instance.AddIncomingOffer(material, offer); + } + else + { + Singleton.instance.AddOutgoingOffer(material, offer); + } + } + } + + private bool FindHospital(uint citizenID, ushort sourceBuilding, TransferManager.TransferReason reason) + { + if (reason == TransferManager.TransferReason.Dead) + { + if (Singleton.instance.Unlocked(UnlockManager.Feature.DeathCare)) + { + return true; + } + Singleton.instance.ReleaseCitizen(citizenID); + return false; + } + if (Singleton.instance.Unlocked(ItemClass.Service.HealthCare)) + { + BuildingManager instance = Singleton.instance; + DistrictManager instance2 = Singleton.instance; + Vector3 position = instance.m_buildings.m_buffer[sourceBuilding].m_position; + byte district = instance2.GetDistrict(position); + DistrictPolicies.Services servicePolicies = instance2.m_districts.m_buffer[district].m_servicePolicies; + TransferManager.TransferOffer offer = default(TransferManager.TransferOffer); + offer.Priority = 6; + offer.Citizen = citizenID; + offer.Position = position; + offer.Amount = 1; + if ((servicePolicies & DistrictPolicies.Services.HelicopterPriority) != 0) + { + instance2.m_districts.m_buffer[district].m_servicePoliciesEffect |= DistrictPolicies.Services.HelicopterPriority; + offer.Active = false; + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Sick2, offer); + } + else if ((instance.m_buildings.m_buffer[sourceBuilding].m_flags & Building.Flags.RoadAccessFailed) != 0 || Singleton.instance.m_randomizer.Int32(20u) == 0) + { + offer.Active = false; + Singleton.instance.AddOutgoingOffer(TransferManager.TransferReason.Sick2, offer); + } + else + { + offer.Active = (Singleton.instance.m_randomizer.Int32(2u) == 0); + Singleton.instance.AddOutgoingOffer(reason, offer); + } + return true; + } + Singleton.instance.ReleaseCitizen(citizenID); + return false; + } + + public override void StartTransfer(uint citizenID, ref Citizen data, TransferManager.TransferReason reason, TransferManager.TransferOffer offer) + { + if (data.m_flags != 0) + { + if (data.Dead && reason != TransferManager.TransferReason.Dead) + { + return; + } + switch (reason) + { + case TransferManager.TransferReason.Sick: + if (data.Sick) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + if (base.StartMoving(citizenID, ref data, 0, offer)) + { + data.SetVisitplace(citizenID, offer.Building, 0u); + } + } + break; + case TransferManager.TransferReason.Dead: + if (data.Dead) + { + data.SetVisitplace(citizenID, offer.Building, 0u); + if (data.m_visitBuilding != 0) + { + data.CurrentLocation = Citizen.Location.Visit; + } + } + break; + case TransferManager.TransferReason.Worker0: + case TransferManager.TransferReason.Worker1: + case TransferManager.TransferReason.Worker2: + case TransferManager.TransferReason.Worker3: + if (data.m_workBuilding == 0) + { + data.SetWorkplace(citizenID, offer.Building, 0u); + } + break; + case TransferManager.TransferReason.Student1: + case TransferManager.TransferReason.Student2: + case TransferManager.TransferReason.Student3: + if (data.m_workBuilding == 0) + { + data.SetStudentplace(citizenID, offer.Building, 0u); + } + break; + case TransferManager.TransferReason.Shopping: + case TransferManager.TransferReason.ShoppingB: + case TransferManager.TransferReason.ShoppingC: + case TransferManager.TransferReason.ShoppingD: + case TransferManager.TransferReason.ShoppingE: + case TransferManager.TransferReason.ShoppingF: + case TransferManager.TransferReason.ShoppingG: + case TransferManager.TransferReason.ShoppingH: + if (data.m_homeBuilding != 0 && !data.Sick) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + if (base.StartMoving(citizenID, ref data, 0, offer)) + { + data.SetVisitplace(citizenID, offer.Building, 0u); + CitizenManager instance3 = Singleton.instance; + BuildingManager instance4 = Singleton.instance; + uint containingUnit = data.GetContainingUnit(citizenID, instance4.m_buildings.m_buffer[data.m_homeBuilding].m_citizenUnits, CitizenUnit.Flags.Home); + if (containingUnit != 0) + { + instance3.m_units.m_buffer[containingUnit].m_goods += 100; + } + } + } + break; + case TransferManager.TransferReason.Entertainment: + case TransferManager.TransferReason.EntertainmentB: + case TransferManager.TransferReason.EntertainmentC: + case TransferManager.TransferReason.EntertainmentD: + if (data.m_homeBuilding != 0 && !data.Sick) + { + data.m_flags &= ~Citizen.Flags.Evacuating; + if (base.StartMoving(citizenID, ref data, 0, offer)) + { + data.SetVisitplace(citizenID, offer.Building, 0u); + } + } + break; + case TransferManager.TransferReason.Single0: + case TransferManager.TransferReason.Single1: + case TransferManager.TransferReason.Single2: + case TransferManager.TransferReason.Single3: + case TransferManager.TransferReason.Single0B: + case TransferManager.TransferReason.Single1B: + case TransferManager.TransferReason.Single2B: + case TransferManager.TransferReason.Single3B: + data.SetHome(citizenID, offer.Building, 0u); + if (data.m_homeBuilding == 0) + { + if (data.CurrentLocation == Citizen.Location.Visit && (data.m_flags & Citizen.Flags.Evacuating) != 0) + { + break; + } + Singleton.instance.ReleaseCitizen(citizenID); + } + break; + case TransferManager.TransferReason.Family0: + case TransferManager.TransferReason.Family1: + case TransferManager.TransferReason.Family2: + case TransferManager.TransferReason.Family3: + if (data.m_homeBuilding != 0 && offer.Building != 0) + { + uint num = Singleton.instance.m_buildings.m_buffer[data.m_homeBuilding].FindCitizenUnit(CitizenUnit.Flags.Home, citizenID); + if (num != 0) + { + MoveFamily(num, ref Singleton.instance.m_units.m_buffer[num], offer.Building); + } + } + break; + case TransferManager.TransferReason.PartnerYoung: + case TransferManager.TransferReason.PartnerAdult: + { + uint citizen = offer.Citizen; + if (citizen != 0) + { + CitizenManager instance = Singleton.instance; + BuildingManager instance2 = Singleton.instance; + ushort homeBuilding = instance.m_citizens.m_buffer[citizen].m_homeBuilding; + if (homeBuilding != 0 && !instance.m_citizens.m_buffer[citizen].Dead) + { + uint num2 = instance2.m_buildings.m_buffer[homeBuilding].FindCitizenUnit(CitizenUnit.Flags.Home, citizen); + if (num2 != 0) + { + data.SetHome(citizenID, 0, num2); + data.m_family = instance.m_citizens.m_buffer[citizen].m_family; + } + } + } + break; + } + case TransferManager.TransferReason.EvacuateA: + case TransferManager.TransferReason.EvacuateB: + case TransferManager.TransferReason.EvacuateC: + case TransferManager.TransferReason.EvacuateD: + case TransferManager.TransferReason.EvacuateVipA: + case TransferManager.TransferReason.EvacuateVipB: + case TransferManager.TransferReason.EvacuateVipC: + case TransferManager.TransferReason.EvacuateVipD: + data.m_flags |= Citizen.Flags.Evacuating; + if (base.StartMoving(citizenID, ref data, 0, offer)) + { + data.SetVisitplace(citizenID, offer.Building, 0u); + } + else + { + data.SetVisitplace(citizenID, offer.Building, 0u); + if (data.m_visitBuilding != 0 && data.m_visitBuilding == offer.Building) + { + data.CurrentLocation = Citizen.Location.Visit; + } + } + break; + } + } + } + + private void MoveFamily(uint homeID, ref CitizenUnit data, ushort targetBuilding) + { + BuildingManager instance = Singleton.instance; + CitizenManager instance2 = Singleton.instance; + uint unitID = 0u; + if (targetBuilding != 0) + { + unitID = instance.m_buildings.m_buffer[targetBuilding].GetEmptyCitizenUnit(CitizenUnit.Flags.Home); + } + for (int i = 0; i < 5; i++) + { + uint citizen = data.GetCitizen(i); + if (citizen != 0 && !instance2.m_citizens.m_buffer[citizen].Dead) + { + instance2.m_citizens.m_buffer[citizen].SetHome(citizen, 0, unitID); + if (instance2.m_citizens.m_buffer[citizen].m_homeBuilding == 0) + { + instance2.ReleaseCitizen(citizen); + } + } + } + } + + public override void SetSource(ushort instanceID, ref CitizenInstance data, ushort sourceBuilding) + { + if (sourceBuilding != data.m_sourceBuilding) + { + if (data.m_sourceBuilding != 0) + { + Singleton.instance.m_buildings.m_buffer[data.m_sourceBuilding].RemoveSourceCitizen(instanceID, ref data); + } + data.m_sourceBuilding = sourceBuilding; + if (data.m_sourceBuilding != 0) + { + Singleton.instance.m_buildings.m_buffer[data.m_sourceBuilding].AddSourceCitizen(instanceID, ref data); + } + } + if (sourceBuilding != 0) + { + BuildingManager instance = Singleton.instance; + BuildingInfo info = instance.m_buildings.m_buffer[sourceBuilding].Info; + data.Unspawn(instanceID); + Randomizer randomizer = new Randomizer(instanceID); + info.m_buildingAI.CalculateSpawnPosition(sourceBuilding, ref instance.m_buildings.m_buffer[sourceBuilding], ref randomizer, base.m_info, out Vector3 vector, out Vector3 a); + Quaternion rotation = Quaternion.identity; + Vector3 forward = a - vector; + if (forward.sqrMagnitude > 0.01f) + { + rotation = Quaternion.LookRotation(forward); + } + data.m_frame0.m_velocity = Vector3.zero; + data.m_frame0.m_position = vector; + data.m_frame0.m_rotation = rotation; + data.m_frame1 = data.m_frame0; + data.m_frame2 = data.m_frame0; + data.m_frame3 = data.m_frame0; + data.m_targetPos = new Vector4(a.x, a.y, a.z, 1f); + ushort eventIndex = 0; + if (data.m_citizen != 0 && Singleton.instance.m_citizens.m_buffer[data.m_citizen].m_workBuilding != sourceBuilding) + { + eventIndex = instance.m_buildings.m_buffer[sourceBuilding].m_eventIndex; + } + Color32 eventCitizenColor = Singleton.instance.GetEventCitizenColor(eventIndex, data.m_citizen); + if (eventCitizenColor.a == 255) + { + data.m_color = eventCitizenColor; + data.m_flags |= CitizenInstance.Flags.CustomColor; + } + } + } + + public override void SetTarget(ushort instanceID, ref CitizenInstance data, ushort targetIndex, bool targetIsNode) + { + int dayTimeFrame = (int)Singleton.instance.m_dayTimeFrame; + int dAYTIME_FRAMES = (int)SimulationManager.DAYTIME_FRAMES; + int num = Mathf.Max(dAYTIME_FRAMES >> 2, Mathf.Abs(dayTimeFrame - (dAYTIME_FRAMES >> 1))); + if (Singleton.instance.m_randomizer.Int32((uint)dAYTIME_FRAMES >> 1) < num) + { + data.m_flags &= ~CitizenInstance.Flags.CannotUseTaxi; + } + else + { + data.m_flags |= CitizenInstance.Flags.CannotUseTaxi; + } + data.m_flags &= ~CitizenInstance.Flags.CannotUseTransport; + if (targetIndex != data.m_targetBuilding || targetIsNode != ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != CitizenInstance.Flags.None)) + { + if (data.m_targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].RemoveTargetCitizen(instanceID, ref data); + ushort num2 = 0; + if (targetIsNode) + { + num2 = Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].m_transportLine; + } + if ((data.m_flags & CitizenInstance.Flags.OnTour) != 0) + { + ushort transportLine = Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].m_transportLine; + uint citizen = data.m_citizen; + if (transportLine != 0 && transportLine != num2 && citizen != 0) + { + TransportManager instance = Singleton.instance; + TransportInfo info = instance.m_lines.m_buffer[transportLine].Info; + if ((object)info != null && info.m_vehicleType == VehicleInfo.VehicleType.None) + { + data.m_flags &= ~CitizenInstance.Flags.OnTour; + } + } + } + if (!targetIsNode) + { + data.m_flags &= ~CitizenInstance.Flags.TargetIsNode; + } + } + else + { + Singleton.instance.m_buildings.m_buffer[data.m_targetBuilding].RemoveTargetCitizen(instanceID, ref data); + } + } + data.m_targetBuilding = targetIndex; + if (targetIsNode) + { + data.m_flags |= CitizenInstance.Flags.TargetIsNode; + } + if (data.m_targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + else + { + Singleton.instance.m_buildings.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + data.m_targetSeed = (byte)Singleton.instance.m_randomizer.Int32(256u); + } + } + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) == CitizenInstance.Flags.None && IsRoadConnection(targetIndex)) + { + goto IL_02a3; + } + if (IsRoadConnection(data.m_sourceBuilding)) + { + goto IL_02a3; + } + data.m_flags &= ~CitizenInstance.Flags.BorrowCar; + goto IL_02c6; + IL_02a3: + data.m_flags |= CitizenInstance.Flags.BorrowCar; + goto IL_02c6; + IL_02c6: + if (targetIndex != 0 && (data.m_flags & (CitizenInstance.Flags.Character | CitizenInstance.Flags.TargetIsNode)) == CitizenInstance.Flags.None) + { + ushort eventIndex = 0; + if (data.m_citizen != 0 && Singleton.instance.m_citizens.m_buffer[data.m_citizen].m_workBuilding != targetIndex) + { + eventIndex = Singleton.instance.m_buildings.m_buffer[targetIndex].m_eventIndex; + } + Color32 eventCitizenColor = Singleton.instance.GetEventCitizenColor(eventIndex, data.m_citizen); + if (eventCitizenColor.a == 255) + { + data.m_color = eventCitizenColor; + data.m_flags |= CitizenInstance.Flags.CustomColor; + } + } + if (!StartPathFind(instanceID, ref data)) + { + data.Unspawn(instanceID); + } + } + + public override void BuildingRelocated(ushort instanceID, ref CitizenInstance data, ushort building) + { + base.BuildingRelocated(instanceID, ref data, building); + if (building == data.m_targetBuilding && (data.m_flags & CitizenInstance.Flags.TargetIsNode) == CitizenInstance.Flags.None) + { + base.InvalidPath(instanceID, ref data); + } + } + + public override void JoinTarget(ushort instanceID, ref CitizenInstance data, ushort otherInstance) + { + ushort num = 0; + bool flag = false; + bool flag2 = false; + if (otherInstance != 0) + { + num = Singleton.instance.m_instances.m_buffer[otherInstance].m_targetBuilding; + flag = ((Singleton.instance.m_instances.m_buffer[otherInstance].m_flags & CitizenInstance.Flags.TargetIsNode) != CitizenInstance.Flags.None); + flag2 = ((Singleton.instance.m_instances.m_buffer[otherInstance].m_flags & CitizenInstance.Flags.OnTour) != CitizenInstance.Flags.None); + } + if (num != data.m_targetBuilding || flag != ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != CitizenInstance.Flags.None)) + { + if (data.m_targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].RemoveTargetCitizen(instanceID, ref data); + data.m_flags &= ~(CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour); + } + else + { + Singleton.instance.m_buildings.m_buffer[data.m_targetBuilding].RemoveTargetCitizen(instanceID, ref data); + } + } + data.m_targetBuilding = num; + if (flag) + { + data.m_flags |= CitizenInstance.Flags.TargetIsNode; + } + if (flag2) + { + data.m_flags |= CitizenInstance.Flags.OnTour; + } + if (data.m_targetBuilding != 0) + { + if ((data.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + Singleton.instance.m_nodes.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + else + { + Singleton.instance.m_buildings.m_buffer[data.m_targetBuilding].AddTargetCitizen(instanceID, ref data); + } + } + } + if (otherInstance != 0) + { + PathManager instance = Singleton.instance; + CitizenManager instance2 = Singleton.instance; + data.Unspawn(instanceID); + data.m_frame3 = (data.m_frame2 = (data.m_frame1 = (data.m_frame0 = instance2.m_instances.m_buffer[otherInstance].GetLastFrameData()))); + data.m_targetPos = instance2.m_instances.m_buffer[otherInstance].m_targetPos; + uint path = instance2.m_instances.m_buffer[otherInstance].m_path; + if (instance.AddPathReference(path)) + { + if (data.m_path != 0) + { + instance.ReleasePath(data.m_path); + } + data.m_path = path; + if ((instance2.m_instances.m_buffer[otherInstance].m_flags & CitizenInstance.Flags.WaitingPath) != 0) + { + data.m_flags |= CitizenInstance.Flags.WaitingPath; + } + else + { + data.Spawn(instanceID); + } + } + } + } + + private bool IsRoadConnection(ushort building) + { + if (building != 0) + { + BuildingManager instance = Singleton.instance; + if ((instance.m_buildings.m_buffer[building].m_flags & Building.Flags.IncomingOutgoing) != 0 && instance.m_buildings.m_buffer[building].Info.m_class.m_service == ItemClass.Service.Road) + { + return true; + } + } + return false; + } + + protected override bool SpawnVehicle(ushort instanceID, ref CitizenInstance citizenData, PathUnit.Position pathPos) + { + VehicleManager instance = Singleton.instance; + float num = 20f; + int num2 = Mathf.Max((int)((citizenData.m_targetPos.x - num) / 32f + 270f), 0); + int num3 = Mathf.Max((int)((citizenData.m_targetPos.z - num) / 32f + 270f), 0); + int num4 = Mathf.Min((int)((citizenData.m_targetPos.x + num) / 32f + 270f), 539); + int num5 = Mathf.Min((int)((citizenData.m_targetPos.z + num) / 32f + 270f), 539); + for (int i = num3; i <= num5; i++) + { + for (int j = num2; j <= num4; j++) + { + ushort num6 = instance.m_vehicleGrid[i * 540 + j]; + int num7 = 0; + while (num6 != 0) + { + if (TryJoinVehicle(instanceID, ref citizenData, num6, ref instance.m_vehicles.m_buffer[num6])) + { + citizenData.m_flags |= CitizenInstance.Flags.EnteringVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.TryingSpawnVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.BoredOfWaiting; + citizenData.m_waitCounter = 0; + return true; + } + num6 = instance.m_vehicles.m_buffer[num6].m_nextGridVehicle; + if (++num7 > 16384) + { + CODebugBase.Error(LogChannel.Core, "Invalid list detected!\n" + Environment.StackTrace); + break; + } + } + } + } + NetManager instance2 = Singleton.instance; + CitizenManager instance3 = Singleton.instance; + Vector3 vector = Vector3.zero; + Quaternion rotation = Quaternion.identity; + ushort num8 = instance3.m_citizens.m_buffer[citizenData.m_citizen].m_parkedVehicle; + if (num8 != 0) + { + vector = instance.m_parkedVehicles.m_buffer[num8].m_position; + rotation = instance.m_parkedVehicles.m_buffer[num8].m_rotation; + } + VehicleInfo vehicleInfo; + VehicleInfo vehicleInfo2 = GetVehicleInfo(instanceID, ref citizenData, false, out vehicleInfo); + if ((object)vehicleInfo2 != null && vehicleInfo2.m_vehicleType != VehicleInfo.VehicleType.Bicycle) + { + if (vehicleInfo2.m_class.m_subService == ItemClass.SubService.PublicTransportTaxi) + { + instance3.m_citizens.m_buffer[citizenData.m_citizen].SetParkedVehicle(citizenData.m_citizen, 0); + if ((citizenData.m_flags & CitizenInstance.Flags.WaitingTaxi) == CitizenInstance.Flags.None && instance2.m_segments.m_buffer[pathPos.m_segment].Info.m_hasPedestrianLanes) + { + citizenData.m_flags |= CitizenInstance.Flags.WaitingTaxi; + citizenData.m_flags &= ~CitizenInstance.Flags.BoredOfWaiting; + citizenData.m_waitCounter = 0; + } + return true; + } + uint laneID = PathManager.GetLaneID(pathPos); + Vector3 vector2 = citizenData.m_targetPos; + if (num8 != 0 && Vector3.SqrMagnitude(vector - vector2) < 1024f) + { + vector2 = vector; + } + else + { + num8 = 0; + } + instance2.m_lanes.m_buffer[laneID].GetClosestPosition(vector2, out Vector3 vector3, out float num9); + byte lastPathOffset = (byte)Mathf.Clamp(Mathf.RoundToInt(num9 * 255f), 0, 255); + vector3 = vector2 + Vector3.ClampMagnitude(vector3 - vector2, 5f); + if (instance.CreateVehicle(out ushort num10, ref Singleton.instance.m_randomizer, vehicleInfo2, vector2, TransferManager.TransferReason.None, false, false)) + { + Vehicle.Frame frame = instance.m_vehicles.m_buffer[num10].m_frame0; + if (num8 != 0) + { + frame.m_rotation = rotation; + } + else + { + Vector3 a = vector3; + CitizenInstance.Frame lastFrameData = citizenData.GetLastFrameData(); + Vector3 forward = a - lastFrameData.m_position; + if (forward.sqrMagnitude > 0.01f) + { + frame.m_rotation = Quaternion.LookRotation(forward); + } + } + instance.m_vehicles.m_buffer[num10].m_frame0 = frame; + instance.m_vehicles.m_buffer[num10].m_frame1 = frame; + instance.m_vehicles.m_buffer[num10].m_frame2 = frame; + instance.m_vehicles.m_buffer[num10].m_frame3 = frame; + vehicleInfo2.m_vehicleAI.FrameDataUpdated(num10, ref instance.m_vehicles.m_buffer[num10], ref frame); + instance.m_vehicles.m_buffer[num10].m_targetPos0 = new Vector4(vector3.x, vector3.y, vector3.z, 2f); + instance.m_vehicles.m_buffer[num10].m_flags |= Vehicle.Flags.Stopped; + instance.m_vehicles.m_buffer[num10].m_path = citizenData.m_path; + instance.m_vehicles.m_buffer[num10].m_pathPositionIndex = citizenData.m_pathPositionIndex; + instance.m_vehicles.m_buffer[num10].m_lastPathOffset = lastPathOffset; + instance.m_vehicles.m_buffer[num10].m_transferSize = (ushort)(citizenData.m_citizen & 0xFFFF); + if ((object)vehicleInfo != null) + { + instance.m_vehicles.m_buffer[num10].CreateTrailer(num10, vehicleInfo, false); + } + vehicleInfo2.m_vehicleAI.TrySpawn(num10, ref instance.m_vehicles.m_buffer[num10]); + if (num8 != 0) + { + InstanceID empty = InstanceID.Empty; + empty.ParkedVehicle = num8; + InstanceID empty2 = InstanceID.Empty; + empty2.Vehicle = num10; + Singleton.instance.ChangeInstance(empty, empty2); + } + citizenData.m_path = 0u; + instance3.m_citizens.m_buffer[citizenData.m_citizen].SetParkedVehicle(citizenData.m_citizen, 0); + instance3.m_citizens.m_buffer[citizenData.m_citizen].SetVehicle(citizenData.m_citizen, num10, 0u); + citizenData.m_flags |= CitizenInstance.Flags.EnteringVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.TryingSpawnVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.BoredOfWaiting; + citizenData.m_waitCounter = 0; + return true; + } + instance3.m_citizens.m_buffer[citizenData.m_citizen].SetParkedVehicle(citizenData.m_citizen, 0); + if ((citizenData.m_flags & CitizenInstance.Flags.TryingSpawnVehicle) == CitizenInstance.Flags.None) + { + citizenData.m_flags |= CitizenInstance.Flags.TryingSpawnVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.BoredOfWaiting; + citizenData.m_waitCounter = 0; + } + return true; + } + instance3.m_citizens.m_buffer[citizenData.m_citizen].SetParkedVehicle(citizenData.m_citizen, 0); + if ((citizenData.m_flags & CitizenInstance.Flags.TryingSpawnVehicle) == CitizenInstance.Flags.None) + { + citizenData.m_flags |= CitizenInstance.Flags.TryingSpawnVehicle; + citizenData.m_flags &= ~CitizenInstance.Flags.BoredOfWaiting; + citizenData.m_waitCounter = 0; + } + return true; + } + + protected override bool SpawnBicycle(ushort instanceID, ref CitizenInstance citizenData, PathUnit.Position pathPos) + { + VehicleInfo vehicleInfo; + VehicleInfo vehicleInfo2 = GetVehicleInfo(instanceID, ref citizenData, false, out vehicleInfo); + if ((object)vehicleInfo2 != null && vehicleInfo2.m_vehicleType == VehicleInfo.VehicleType.Bicycle) + { + CitizenManager instance = Singleton.instance; + VehicleManager instance2 = Singleton.instance; + CitizenInstance.Frame lastFrameData = citizenData.GetLastFrameData(); + if (instance2.CreateVehicle(out ushort num, ref Singleton.instance.m_randomizer, vehicleInfo2, lastFrameData.m_position, TransferManager.TransferReason.None, false, false)) + { + Vehicle.Frame frame = instance2.m_vehicles.m_buffer[num].m_frame0; + frame.m_rotation = lastFrameData.m_rotation; + instance2.m_vehicles.m_buffer[num].m_frame0 = frame; + instance2.m_vehicles.m_buffer[num].m_frame1 = frame; + instance2.m_vehicles.m_buffer[num].m_frame2 = frame; + instance2.m_vehicles.m_buffer[num].m_frame3 = frame; + vehicleInfo2.m_vehicleAI.FrameDataUpdated(num, ref instance2.m_vehicles.m_buffer[num], ref frame); + if ((object)vehicleInfo != null) + { + instance2.m_vehicles.m_buffer[num].CreateTrailer(num, vehicleInfo, false); + } + vehicleInfo2.m_vehicleAI.TrySpawn(num, ref instance2.m_vehicles.m_buffer[num]); + instance.m_citizens.m_buffer[citizenData.m_citizen].SetParkedVehicle(citizenData.m_citizen, 0); + instance.m_citizens.m_buffer[citizenData.m_citizen].SetVehicle(citizenData.m_citizen, num, 0u); + citizenData.m_flags |= CitizenInstance.Flags.RidingBicycle; + return true; + } + } + return false; + } + + private bool TryJoinVehicle(ushort instanceID, ref CitizenInstance citizenData, ushort vehicleID, ref Vehicle vehicleData) + { + if ((vehicleData.m_flags & Vehicle.Flags.Stopped) == (Vehicle.Flags)0) + { + return false; + } + CitizenManager instance = Singleton.instance; + uint num = vehicleData.m_citizenUnits; + int num2 = 0; + while (num != 0) + { + uint nextUnit = instance.m_units.m_buffer[num].m_nextUnit; + for (int i = 0; i < 5; i++) + { + uint citizen = instance.m_units.m_buffer[num].GetCitizen(i); + if (citizen != 0) + { + ushort instance2 = instance.m_citizens.m_buffer[citizen].m_instance; + if (instance2 == 0) + { + break; + } + if (instance.m_instances.m_buffer[instance2].m_targetBuilding != citizenData.m_targetBuilding) + { + break; + } + if ((instance.m_instances.m_buffer[instance2].m_flags & CitizenInstance.Flags.TargetIsNode) != (citizenData.m_flags & CitizenInstance.Flags.TargetIsNode)) + { + break; + } + instance.m_citizens.m_buffer[citizenData.m_citizen].SetVehicle(citizenData.m_citizen, vehicleID, 0u); + if (instance.m_citizens.m_buffer[citizenData.m_citizen].m_vehicle != vehicleID) + { + break; + } + if (citizenData.m_path != 0) + { + Singleton.instance.ReleasePath(citizenData.m_path); + citizenData.m_path = 0u; + } + return true; + } + } + num = nextUnit; + if (++num2 > 524288) + { + CODebugBase.Error(LogChannel.Core, "Invalid list detected!\n" + Environment.StackTrace); + break; + } + } + return false; + } + + protected override void SwitchBuildingTargetPos(ushort instanceID, ref CitizenInstance citizenData) + { + if (citizenData.m_path == 0 && citizenData.m_targetBuilding != 0 && (citizenData.m_flags & CitizenInstance.Flags.TargetIsNode) == CitizenInstance.Flags.None) + { + BuildingManager instance = Singleton.instance; + BuildingInfo info = instance.m_buildings.m_buffer[citizenData.m_targetBuilding].Info; + if (info.m_hasPedestrianPaths) + { + Randomizer randomizer = new Randomizer(instanceID << 8 | citizenData.m_targetSeed); + info.m_buildingAI.CalculateUnspawnPosition(citizenData.m_targetBuilding, ref instance.m_buildings.m_buffer[citizenData.m_targetBuilding], ref randomizer, base.m_info, instanceID, out Vector3 _, out Vector3 vector2, out Vector2 _, out CitizenInstance.Flags _); + float num = Vector3.Distance(citizenData.m_targetPos, vector2); + if (num > 10f) + { + base.StartPathFind(instanceID, ref citizenData, citizenData.m_targetPos, vector2, null, true, false); + } + } + } + } + + public override void EnterParkArea(ushort instanceID, ref CitizenInstance citizenData, byte park, ushort gateID) + { + if (gateID != 0) + { + DistrictManager instance = Singleton.instance; + instance.m_parks.m_buffer[park].m_tempResidentCount++; + } + base.EnterParkArea(instanceID, ref citizenData, park, gateID); + } + + protected override bool StartPathFind(ushort instanceID, ref CitizenInstance citizenData) + { + if (citizenData.m_citizen != 0) + { + CitizenManager instance = Singleton.instance; + VehicleManager instance2 = Singleton.instance; + ushort vehicle = instance.m_citizens.m_buffer[citizenData.m_citizen].m_vehicle; + if (vehicle != 0) + { + VehicleInfo info = instance2.m_vehicles.m_buffer[vehicle].Info; + if ((object)info != null) + { + uint citizen = info.m_vehicleAI.GetOwnerID(vehicle, ref instance2.m_vehicles.m_buffer[vehicle]).Citizen; + if (citizen == citizenData.m_citizen) + { + info.m_vehicleAI.SetTarget(vehicle, ref instance2.m_vehicles.m_buffer[vehicle], 0); + return false; + } + } + bool flag = false; + if (instance2.m_vehicles.m_buffer[vehicle].m_transportLine != 0) + { + NetManager instance3 = Singleton.instance; + ushort targetBuilding = instance2.m_vehicles.m_buffer[vehicle].m_targetBuilding; + if (targetBuilding != 0) + { + uint lane = instance3.m_nodes.m_buffer[targetBuilding].m_lane; + int laneOffset = instance3.m_nodes.m_buffer[targetBuilding].m_laneOffset; + if (lane != 0) + { + ushort segment = instance3.m_lanes.m_buffer[lane].m_segment; + if (instance3.m_segments.m_buffer[segment].GetClosestLane(lane, NetInfo.LaneType.Pedestrian, VehicleInfo.VehicleType.None, out lane, out NetInfo.Lane _)) + { + citizenData.m_targetPos = instance3.m_lanes.m_buffer[lane].CalculatePosition((float)laneOffset * 0.003921569f); + flag = true; + } + } + } + } + if (!flag) + { + instance.m_citizens.m_buffer[citizenData.m_citizen].SetVehicle(citizenData.m_citizen, 0, 0u); + return false; + } + } + } + if (citizenData.m_targetBuilding != 0) + { + VehicleInfo vehicleInfo; + VehicleInfo vehicleInfo2 = GetVehicleInfo(instanceID, ref citizenData, false, out vehicleInfo); + if ((citizenData.m_flags & CitizenInstance.Flags.TargetIsNode) != 0) + { + NetManager instance4 = Singleton.instance; + Vector3 endPos = instance4.m_nodes.m_buffer[citizenData.m_targetBuilding].m_position; + uint lane3 = instance4.m_nodes.m_buffer[citizenData.m_targetBuilding].m_lane; + if (lane3 != 0) + { + ushort segment2 = instance4.m_lanes.m_buffer[lane3].m_segment; + if (instance4.m_segments.m_buffer[segment2].GetClosestLane(lane3, NetInfo.LaneType.Pedestrian, VehicleInfo.VehicleType.None, out lane3, out NetInfo.Lane _)) + { + int laneOffset2 = instance4.m_nodes.m_buffer[citizenData.m_targetBuilding].m_laneOffset; + endPos = instance4.m_lanes.m_buffer[lane3].CalculatePosition((float)laneOffset2 * 0.003921569f); + } + } + return base.StartPathFind(instanceID, ref citizenData, citizenData.m_targetPos, endPos, vehicleInfo2, true, false); + } + BuildingManager instance5 = Singleton.instance; + BuildingInfo info2 = instance5.m_buildings.m_buffer[citizenData.m_targetBuilding].Info; + Randomizer randomizer = new Randomizer(instanceID << 8 | citizenData.m_targetSeed); + info2.m_buildingAI.CalculateUnspawnPosition(citizenData.m_targetBuilding, ref instance5.m_buildings.m_buffer[citizenData.m_targetBuilding], ref randomizer, base.m_info, instanceID, out Vector3 _, out Vector3 endPos2, out Vector2 _, out CitizenInstance.Flags _); + return base.StartPathFind(instanceID, ref citizenData, citizenData.m_targetPos, endPos2, vehicleInfo2, true, false); + } + return false; + } + + protected override VehicleInfo GetVehicleInfo(ushort instanceID, ref CitizenInstance citizenData, bool forceProbability, out VehicleInfo trailer) + { + trailer = null; + if (citizenData.m_citizen != 0) + { + Citizen.AgeGroup ageGroup; + switch (base.m_info.m_agePhase) + { + case Citizen.AgePhase.Child: + ageGroup = Citizen.AgeGroup.Child; + break; + case Citizen.AgePhase.Teen0: + case Citizen.AgePhase.Teen1: + ageGroup = Citizen.AgeGroup.Teen; + break; + case Citizen.AgePhase.Young0: + case Citizen.AgePhase.Young1: + case Citizen.AgePhase.Young2: + ageGroup = Citizen.AgeGroup.Young; + break; + case Citizen.AgePhase.Adult0: + case Citizen.AgePhase.Adult1: + case Citizen.AgePhase.Adult2: + case Citizen.AgePhase.Adult3: + ageGroup = Citizen.AgeGroup.Adult; + break; + case Citizen.AgePhase.Senior0: + case Citizen.AgePhase.Senior1: + case Citizen.AgePhase.Senior2: + case Citizen.AgePhase.Senior3: + ageGroup = Citizen.AgeGroup.Senior; + break; + default: + ageGroup = Citizen.AgeGroup.Adult; + break; + } + int num; + int num2; + if (forceProbability || (citizenData.m_flags & CitizenInstance.Flags.BorrowCar) != 0) + { + num = 100; + num2 = 0; + } + else + { + num = GetCarProbability(instanceID, ref citizenData, ageGroup); + num2 = GetBikeProbability(instanceID, ref citizenData, ageGroup); + } + Randomizer randomizer = new Randomizer(citizenData.m_citizen); + bool flag = randomizer.Int32(100u) < num; + bool flag2 = randomizer.Int32(100u) < num2; + bool flag3; + bool flag4; + if (flag) + { + int electricCarProbability = GetElectricCarProbability(instanceID, ref citizenData, base.m_info.m_agePhase); + flag3 = false; + flag4 = (randomizer.Int32(100u) < electricCarProbability); + } + else + { + int taxiProbability = GetTaxiProbability(instanceID, ref citizenData, ageGroup); + flag3 = (randomizer.Int32(100u) < taxiProbability); + flag4 = false; + } + ItemClass.Service service = ItemClass.Service.Residential; + ItemClass.SubService subService = (!flag4) ? ItemClass.SubService.ResidentialLow : ItemClass.SubService.ResidentialLowEco; + if (!flag && flag3) + { + service = ItemClass.Service.PublicTransport; + subService = ItemClass.SubService.PublicTransportTaxi; + } + VehicleInfo randomVehicleInfo = Singleton.instance.GetRandomVehicleInfo(ref randomizer, service, subService, ItemClass.Level.Level1); + VehicleInfo randomVehicleInfo2 = Singleton.instance.GetRandomVehicleInfo(ref randomizer, ItemClass.Service.Residential, ItemClass.SubService.ResidentialHigh, (ageGroup != 0) ? ItemClass.Level.Level2 : ItemClass.Level.Level1); + if (flag2 && (object)randomVehicleInfo2 != null) + { + return randomVehicleInfo2; + } + if ((flag || flag3) && (object)randomVehicleInfo != null) + { + return randomVehicleInfo; + } + return null; + } + return null; + } + + private int GetCarProbability(ushort instanceID, ref CitizenInstance citizenData, Citizen.AgeGroup ageGroup) + { + return GetCarProbability(ageGroup); + } + + private int GetCarProbability(Citizen.AgeGroup ageGroup) + { + switch (ageGroup) + { + case Citizen.AgeGroup.Child: + return 0; + case Citizen.AgeGroup.Teen: + return 5; + case Citizen.AgeGroup.Young: + return 15; + case Citizen.AgeGroup.Adult: + return 20; + case Citizen.AgeGroup.Senior: + return 10; + default: + return 0; + } + } + + private int GetBikeProbability(ushort instanceID, ref CitizenInstance citizenData, Citizen.AgeGroup ageGroup) + { + CitizenManager instance = Singleton.instance; + uint citizen = citizenData.m_citizen; + ushort homeBuilding = instance.m_citizens.m_buffer[citizen].m_homeBuilding; + int num = 0; + if (homeBuilding != 0) + { + Vector3 position = Singleton.instance.m_buildings.m_buffer[homeBuilding].m_position; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(position); + DistrictPolicies.CityPlanning cityPlanningPolicies = instance2.m_districts.m_buffer[district].m_cityPlanningPolicies; + if ((cityPlanningPolicies & DistrictPolicies.CityPlanning.EncourageBiking) != 0) + { + num = 10; + } + } + switch (ageGroup) + { + case Citizen.AgeGroup.Child: + return 40 + num; + case Citizen.AgeGroup.Teen: + return 30 + num; + case Citizen.AgeGroup.Young: + return 20 + num; + case Citizen.AgeGroup.Adult: + return 10 + num; + case Citizen.AgeGroup.Senior: + return num; + default: + return 0; + } + } + + private int GetTaxiProbability(ushort instanceID, ref CitizenInstance citizenData, Citizen.AgeGroup ageGroup) + { + switch (ageGroup) + { + case Citizen.AgeGroup.Child: + return 0; + case Citizen.AgeGroup.Teen: + return 2; + case Citizen.AgeGroup.Young: + return 2; + case Citizen.AgeGroup.Adult: + return 4; + case Citizen.AgeGroup.Senior: + return 6; + default: + return 0; + } + } + + private int GetElectricCarProbability(ushort instanceID, ref CitizenInstance citizenData, Citizen.AgePhase agePhase) + { + CitizenManager instance = Singleton.instance; + uint citizen = citizenData.m_citizen; + ushort homeBuilding = instance.m_citizens.m_buffer[citizen].m_homeBuilding; + if (homeBuilding != 0) + { + Vector3 position = Singleton.instance.m_buildings.m_buffer[homeBuilding].m_position; + DistrictManager instance2 = Singleton.instance; + byte district = instance2.GetDistrict(position); + DistrictPolicies.CityPlanning cityPlanningPolicies = instance2.m_districts.m_buffer[district].m_cityPlanningPolicies; + if ((cityPlanningPolicies & DistrictPolicies.CityPlanning.ElectricCars) != 0) + { + return 100; + } + } + switch (agePhase) + { + case Citizen.AgePhase.Child: + case Citizen.AgePhase.Teen0: + case Citizen.AgePhase.Young0: + case Citizen.AgePhase.Adult0: + case Citizen.AgePhase.Senior0: + return 5; + case Citizen.AgePhase.Teen1: + case Citizen.AgePhase.Young1: + case Citizen.AgePhase.Adult1: + case Citizen.AgePhase.Senior1: + return 10; + case Citizen.AgePhase.Young2: + case Citizen.AgePhase.Adult2: + case Citizen.AgePhase.Senior2: + return 15; + case Citizen.AgePhase.Adult3: + case Citizen.AgePhase.Senior3: + return 20; + default: + return 0; + } + } +} diff --git a/src/RealTime/CustomResidentAI/ResidentState.cs b/src/RealTime/CustomResidentAI/ResidentState.cs new file mode 100644 index 00000000..8e1c3c41 --- /dev/null +++ b/src/RealTime/CustomResidentAI/ResidentState.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + internal enum ResidentState + { + Unknown, + LeftCity, + MovingHome, + AtHome, + MovingToTarget, + AtSchoolOrWork, + AtLunch, + Shopping, + AtLeisureArea, + Visiting, + Evacuating, + InShelter + } +} diff --git a/src/RealTime/CustomTouristAI/RealTimeTouristAI.cs b/src/RealTime/CustomTouristAI/RealTimeTouristAI.cs new file mode 100644 index 00000000..ce6b387b --- /dev/null +++ b/src/RealTime/CustomTouristAI/RealTimeTouristAI.cs @@ -0,0 +1,225 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.CustomAI +{ + using System; + using RealTime.Config; + using RealTime.Events; + using RealTime.GameConnection; + using RealTime.Tools; + using static Constants; + + internal sealed class RealTimeTouristAI : RealTimeHumanAIBase + where TAI : class + where TCitizen : struct + { + private readonly TouristAIConnection touristAI; + + public RealTimeTouristAI( + RealTimeConfig config, + GameConnections connections, + TouristAIConnection touristAI, + RealTimeEventManager eventManager) + : base(config, connections, eventManager) + { + this.touristAI = touristAI ?? throw new System.ArgumentNullException(nameof(touristAI)); + } + + public void UpdateLocation(TAI instance, uint citizenId, ref TCitizen citizen) + { + if (!EnsureCitizenValid(citizenId, ref citizen)) + { + return; + } + + if (CitizenProxy.IsDead(ref citizen) || CitizenProxy.IsSick(ref citizen)) + { + CitizenMgr.ReleaseCitizen(citizenId); + return; + } + + switch (CitizenProxy.GetLocation(ref citizen)) + { + case Citizen.Location.Home: + case Citizen.Location.Work: + CitizenMgr.ReleaseCitizen(citizenId); + break; + + case Citizen.Location.Visit: + ProcessVisit(instance, citizenId, ref citizen); + break; + + case Citizen.Location.Moving: + ProcessMoving(instance, citizenId, ref citizen); + break; + } + } + + private void ProcessMoving(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort instanceId = CitizenProxy.GetInstance(ref citizen); + ushort vehicleId = CitizenProxy.GetVehicle(ref citizen); + + if (instanceId == 0) + { + if (vehicleId == 0) + { + CitizenMgr.ReleaseCitizen(citizenId); + } + + return; + } + + if (vehicleId == 0 && CitizenMgr.IsAreaEvacuating(instanceId) && (CitizenProxy.GetFlags(ref citizen) & Citizen.Flags.Evacuating) == 0) + { + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} was on the way, but the area evacuates. Leaving the city."); + touristAI.FindVisitPlace(instance, citizenId, CitizenProxy.GetCurrentBuilding(ref citizen), touristAI.GetLeavingReason(instance, citizenId, ref citizen)); + return; + } + + CitizenInstance.Flags flags = CitizenInstance.Flags.TargetIsNode | CitizenInstance.Flags.OnTour; + if ((CitizenMgr.GetInstanceFlags(instanceId) & flags) == flags) + { + FindRandomVisitPlace(instance, citizenId, ref citizen, TouristDoNothingProbability, 0); + } + } + + private void ProcessVisit(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort visitBuilding = CitizenProxy.GetVisitBuilding(ref citizen); + if (visitBuilding == 0) + { + CitizenMgr.ReleaseCitizen(citizenId); + return; + } + + Building.Flags buildingFlags = BuildingMgr.GetBuildingFlags(visitBuilding); + if ((buildingFlags & Building.Flags.Evacuating) != 0) + { + touristAI.FindEvacuationPlace(instance, citizenId, visitBuilding, touristAI.GetEvacuationReason(instance, visitBuilding)); + return; + } + + switch (BuildingMgr.GetBuildingService(visitBuilding)) + { + case ItemClass.Service.Disaster: + if ((buildingFlags & Building.Flags.Downgrading) != 0) + { + FindRandomVisitPlace(instance, citizenId, ref citizen, 0, visitBuilding); + } + + return; + + // Tourist is sleeping in a hotel + case ItemClass.Service.Commercial + when TimeInfo.IsNightTime && BuildingMgr.GetBuildingSubService(visitBuilding) == ItemClass.SubService.CommercialTourist: + return; + } + + if (IsChance(TouristEventChance) && AttendUpcomingEvent(citizenId, ref citizen, out ushort eventBuilding)) + { + StartMovingToVisitBuilding(instance, citizenId, ref citizen, CitizenProxy.GetCurrentBuilding(ref citizen), eventBuilding); + touristAI.AddTouristVisit(instance, citizenId, eventBuilding); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} attending an event at {eventBuilding}"); + return; + } + + bool doShopping; + switch (EventMgr.GetEventState(visitBuilding, DateTime.MaxValue)) + { + case CityEventState.OnGoing: + doShopping = IsChance(TouristShoppingChance); + break; + + case CityEventState.Finished: + doShopping = !FindRandomVisitPlace(instance, citizenId, ref citizen, 0, visitBuilding); + break; + + default: + doShopping = false; + break; + } + + if (doShopping || !FindRandomVisitPlace(instance, citizenId, ref citizen, TouristDoNothingProbability, visitBuilding)) + { + BuildingMgr.ModifyMaterialBuffer(visitBuilding, TransferManager.TransferReason.Shopping, -ShoppingGoodsAmount); + touristAI.AddTouristVisit(instance, citizenId, visitBuilding); + } + } + + private bool FindRandomVisitPlace(TAI instance, uint citizenId, ref TCitizen citizen, int doNothingProbability, ushort visitBuilding) + { + int targetType = touristAI.GetRandomTargetType(instance, doNothingProbability); + if (targetType == 1) + { + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} decides to leave the city"); + touristAI.FindVisitPlace(instance, citizenId, visitBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); + return true; + } + + if (CitizenProxy.GetInstance(ref citizen) == 0 && !touristAI.DoRandomMove(instance)) + { + return false; + } + + if (!IsChance(GetGoOutChance(CitizenProxy.GetAge(ref citizen)))) + { + FindHotel(instance, citizenId, ref citizen); + return true; + } + + switch (targetType) + { + case 2: + touristAI.FindVisitPlace(instance, citizenId, visitBuilding, touristAI.GetShoppingReason(instance)); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in the city, goes shopping"); + break; + + case 3: + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in the city, goes relaxing"); + touristAI.FindVisitPlace(instance, citizenId, visitBuilding, touristAI.GetEntertainmentReason(instance)); + break; + } + + return true; + } + + private void FindHotel(TAI instance, uint citizenId, ref TCitizen citizen) + { + ushort currentBuilding = CitizenProxy.GetCurrentBuilding(ref citizen); + if (!IsChance(FindHotelChance)) + { + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} didn't want to stay in a hotel, leaving the city"); + touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); + return; + } + + ushort hotel = BuildingMgr.FindActiveBuilding( + currentBuilding, + FullSearchDistance, + ItemClass.Service.Commercial, + ItemClass.SubService.CommercialTourist); + + if (hotel == 0) + { + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} didn't find a hotel, leaving the city"); + touristAI.FindVisitPlace(instance, citizenId, currentBuilding, touristAI.GetLeavingReason(instance, citizenId, ref citizen)); + return; + } + + StartMovingToVisitBuilding(instance, citizenId, ref citizen, currentBuilding, hotel); + + touristAI.AddTouristVisit(instance, citizenId, hotel); + Log.Debug(TimeInfo.Now, $"Tourist {GetCitizenDesc(citizenId, ref citizen)} stays in a hotel {hotel}"); + } + + private void StartMovingToVisitBuilding(TAI instance, uint citizenId, ref TCitizen citizen, ushort currentBuilding, ushort visitBuilding) + { + touristAI.StartMoving(instance, citizenId, ref citizen, currentBuilding, visitBuilding); + CitizenProxy.SetVisitPlace(ref citizen, citizenId, visitBuilding); + CitizenProxy.SetVisitBuilding(ref citizen, visitBuilding); + } + } +} diff --git a/src/RealTime/Events/CityEventBase.cs b/src/RealTime/Events/CityEventBase.cs new file mode 100644 index 00000000..5e599f42 --- /dev/null +++ b/src/RealTime/Events/CityEventBase.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using System; + using ColossalFramework.Math; + + internal abstract class CityEventBase : ICityEvent + { + public DateTime StartTime { get; private set; } + + public DateTime EndTime => StartTime.AddHours(GetDuration()); + + public ushort BuildingId { get; private set; } + + public string BuildingName { get; private set; } + + public virtual bool TryAcceptAttendee( + Citizen.AgeGroup age, + Citizen.Gender gender, + Citizen.Education education, + Citizen.Wealth wealth, + Citizen.Wellbeing wellbeing, + Citizen.Happiness happiness, + ref Randomizer randomizer) + { + return true; + } + + public void Configure(ushort buildingId, string buildingName, DateTime startTime) + { + BuildingId = buildingId; + BuildingName = buildingName ?? string.Empty; + StartTime = startTime; + } + + protected static float GetCitizenBudgetForEvent(Citizen.Wealth wealth, ref Randomizer randomizer) + { + switch (wealth) + { + case Citizen.Wealth.Low: + return 30f + randomizer.Int32(60); + + case Citizen.Wealth.Medium: + return 80f + randomizer.Int32(80); + + case Citizen.Wealth.High: + return 120f + randomizer.Int32(320); + + default: + return 0; + } + } + + protected abstract float GetDuration(); + } +} diff --git a/src/RealTime/Events/CityEventState.cs b/src/RealTime/Events/CityEventState.cs new file mode 100644 index 00000000..fa7b5856 --- /dev/null +++ b/src/RealTime/Events/CityEventState.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + internal enum CityEventState + { + None, + Upcoming, + OnGoing, + Finished + } +} diff --git a/src/RealTime/Events/ICityEvent.cs b/src/RealTime/Events/ICityEvent.cs new file mode 100644 index 00000000..0c1b057a --- /dev/null +++ b/src/RealTime/Events/ICityEvent.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using System; + using ColossalFramework.Math; + + internal interface ICityEvent + { + DateTime StartTime { get; } + + DateTime EndTime { get; } + + ushort BuildingId { get; } + + string BuildingName { get; } + + void Configure(ushort buildingId, string buildingName, DateTime startTime); + + bool TryAcceptAttendee( + Citizen.AgeGroup age, + Citizen.Gender gender, + Citizen.Education education, + Citizen.Wealth wealth, + Citizen.Wellbeing wellbeing, + Citizen.Happiness happiness, + ref Randomizer randomizer); + } +} \ No newline at end of file diff --git a/src/RealTime/Events/ICityEventsProvider.cs b/src/RealTime/Events/ICityEventsProvider.cs new file mode 100644 index 00000000..ecc1c1e0 --- /dev/null +++ b/src/RealTime/Events/ICityEventsProvider.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using RealTime.Events.Storage; + + internal interface ICityEventsProvider + { + ICityEvent GetRandomEvent(string buildingClass); + + CityEventTemplate GetEventTemplate(string eventName, string buildingClassName); + } +} diff --git a/src/RealTime/Events/RealTimeCityEvent.cs b/src/RealTime/Events/RealTimeCityEvent.cs new file mode 100644 index 00000000..96da22c6 --- /dev/null +++ b/src/RealTime/Events/RealTimeCityEvent.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using System; + using System.Linq; + using ColossalFramework.Math; + using RealTime.Events.Storage; + + internal sealed class RealTimeCityEvent : CityEventBase + { + private readonly CityEventTemplate eventTemplate; + + private readonly int attendChanceAdjustment; + + private int attendeesCount; + + public RealTimeCityEvent(CityEventTemplate eventTemplate) + { + this.eventTemplate = eventTemplate ?? throw new ArgumentNullException(nameof(eventTemplate)); + var incentives = eventTemplate.Incentives?.Where(i => i.ActiveWhenRandomEvent).ToList(); + if (incentives != null) + { + attendChanceAdjustment = incentives.Sum(i => i.PositiveEffect) - incentives.Sum(i => i.NegativeEffect); + } + } + + public RealTimeCityEvent(CityEventTemplate eventTemplate, int attendeesCount) + : this(eventTemplate) + { + this.attendeesCount = attendeesCount; + } + + public override bool TryAcceptAttendee( + Citizen.AgeGroup age, + Citizen.Gender gender, + Citizen.Education education, + Citizen.Wealth wealth, + Citizen.Wellbeing wellbeing, + Citizen.Happiness happiness, + ref Randomizer randomizer) + { + if (attendeesCount > eventTemplate.Capacity) + { + return false; + } + + if (eventTemplate.Costs != null && eventTemplate.Costs.Entry > GetCitizenBudgetForEvent(wealth, ref randomizer)) + { + return false; + } + + CityEventAttendees attendees = eventTemplate.Attendees; + float chanceAdjustment = 1f + (attendChanceAdjustment / 100f); + + float randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + + if (!CheckAge(age, attendees, randomPercentage)) + { + return false; + } + + randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + if (!CheckGender(gender, attendees, randomPercentage)) + { + return false; + } + + randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + if (!CheckEducation(education, attendees, randomPercentage)) + { + return false; + } + + randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + if (!CheckWealth(wealth, attendees, randomPercentage)) + { + return false; + } + + randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + if (!CheckWellbeing(wellbeing, attendees, randomPercentage)) + { + return false; + } + + randomPercentage = randomizer.Int32(100u) / chanceAdjustment; + if (!CheckHappiness(happiness, attendees, randomPercentage)) + { + return false; + } + + attendeesCount++; + return true; + } + + public RealTimeEventStorage GetStorageData() + { + return new RealTimeEventStorage + { + EventName = eventTemplate.EventName, + BuildingClassName = eventTemplate.BuildingClassName, + StartTime = StartTime.Ticks, + BuildingId = BuildingId, + BuildingName = BuildingName, + AttendeesCount = attendeesCount + }; + } + + protected override float GetDuration() + { + return (float)eventTemplate.Duration; + } + + private static bool CheckAge(Citizen.AgeGroup age, CityEventAttendees attendees, float randomPercentage) + { + switch (age) + { + case Citizen.AgeGroup.Child: + return randomPercentage < attendees.Children; + case Citizen.AgeGroup.Teen: + return randomPercentage < attendees.Teens; + case Citizen.AgeGroup.Young: + return randomPercentage < attendees.YoungAdults; + case Citizen.AgeGroup.Adult: + return randomPercentage < attendees.Adults; + case Citizen.AgeGroup.Senior: + return randomPercentage < attendees.Seniors; + } + + return false; + } + + private static bool CheckWellbeing(Citizen.Wellbeing wellbeing, CityEventAttendees attendees, float randomPercentage) + { + switch (wellbeing) + { + case Citizen.Wellbeing.VeryUnhappy: + return randomPercentage < attendees.VeryUnhappyWellbeing; + case Citizen.Wellbeing.Unhappy: + return randomPercentage < attendees.UnhappyWellbeing; + case Citizen.Wellbeing.Satisfied: + return randomPercentage < attendees.SatisfiedWellbeing; + case Citizen.Wellbeing.Happy: + return randomPercentage < attendees.HappyWellbeing; + case Citizen.Wellbeing.VeryHappy: + return randomPercentage < attendees.VeryHappyWellbeing; + } + + return false; + } + + private static bool CheckHappiness(Citizen.Happiness happiness, CityEventAttendees attendees, float randomPercentage) + { + switch (happiness) + { + case Citizen.Happiness.Bad: + return randomPercentage < attendees.BadHappiness; + case Citizen.Happiness.Poor: + return randomPercentage < attendees.PoorHappiness; + case Citizen.Happiness.Good: + return randomPercentage < attendees.GoodHappiness; + case Citizen.Happiness.Excellent: + return randomPercentage < attendees.ExcellentHappiness; + case Citizen.Happiness.Suberb: + return randomPercentage < attendees.SuperbHappiness; + } + + return false; + } + + private static bool CheckGender(Citizen.Gender gender, CityEventAttendees attendees, float randomPercentage) + { + switch (gender) + { + case Citizen.Gender.Female: + return randomPercentage < attendees.Females; + case Citizen.Gender.Male: + return randomPercentage < attendees.Males; + } + + return false; + } + + private static bool CheckEducation(Citizen.Education education, CityEventAttendees attendees, float randomPercentage) + { + switch (education) + { + case Citizen.Education.Uneducated: + return randomPercentage < attendees.Uneducated; + case Citizen.Education.OneSchool: + return randomPercentage < attendees.OneSchool; + case Citizen.Education.TwoSchools: + return randomPercentage < attendees.TwoSchools; + case Citizen.Education.ThreeSchools: + return randomPercentage < attendees.ThreeSchools; + } + + return false; + } + + private static bool CheckWealth(Citizen.Wealth wealth, CityEventAttendees attendees, float randomPercentage) + { + switch (wealth) + { + case Citizen.Wealth.Low: + return randomPercentage < attendees.LowWealth; + case Citizen.Wealth.Medium: + return randomPercentage < attendees.MediumWealth; + case Citizen.Wealth.High: + return randomPercentage < attendees.HighWealth; + } + + return false; + } + } +} diff --git a/src/RealTime/Events/RealTimeEventManager.cs b/src/RealTime/Events/RealTimeEventManager.cs new file mode 100644 index 00000000..5edde16a --- /dev/null +++ b/src/RealTime/Events/RealTimeEventManager.cs @@ -0,0 +1,352 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Xml.Serialization; + using RealTime.Config; + using RealTime.Core; + using RealTime.Events.Storage; + using RealTime.GameConnection; + using RealTime.Simulation; + using RealTime.Tools; + + internal sealed class RealTimeEventManager : IStorageData + { + private const int MaximumEventsCount = 5; + private const string StorageDataId = "RealTimeEvents"; + private const uint EventIntervalVariance = 48u; + + private static readonly TimeSpan MinimumIntervalBetweenEvents = TimeSpan.FromHours(3); + private static readonly TimeSpan EventStartTimeGranularity = TimeSpan.FromMinutes(30); + private static readonly TimeSpan EventProcessInterval = TimeSpan.FromMinutes(15); + + private static readonly ItemClass.Service[] EventBuildingServices = new[] { ItemClass.Service.Monument, ItemClass.Service.Beautification }; + + private readonly LinkedList upcomingEvents; + private readonly RealTimeConfig config; + private readonly ICityEventsProvider eventProvider; + private readonly IEventManagerConnection eventManager; + private readonly IBuildingManagerConnection buildingManager; + private readonly ISimulationManagerConnection simulationManager; + private readonly ITimeInfo timeInfo; + + private ICityEvent lastActiveEvent; + private ICityEvent activeEvent; + private DateTime lastProcessed; + private DateTime earliestEvent; + + public RealTimeEventManager( + RealTimeConfig config, + ICityEventsProvider eventProvider, + IEventManagerConnection eventManager, + IBuildingManagerConnection buildingManager, + ISimulationManagerConnection simulationManager, + ITimeInfo timeInfo) + { + this.config = config ?? throw new ArgumentNullException(nameof(config)); + this.eventProvider = eventProvider ?? throw new ArgumentNullException(nameof(eventProvider)); + this.eventManager = eventManager ?? throw new ArgumentNullException(nameof(eventManager)); + this.buildingManager = buildingManager ?? throw new ArgumentNullException(nameof(buildingManager)); + this.simulationManager = simulationManager ?? throw new ArgumentNullException(nameof(simulationManager)); + this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + upcomingEvents = new LinkedList(); + } + + public event EventHandler EventsChanged; + + public IEnumerable CityEvents + { + get + { + if (lastActiveEvent != null) + { + yield return lastActiveEvent; + } + + if (activeEvent != null) + { + yield return activeEvent; + } + + foreach (ICityEvent upcomingEvent in upcomingEvents) + { + yield return upcomingEvent; + } + } + } + + string IStorageData.StorageDataId => StorageDataId; + + public CityEventState GetEventState(ushort buildingId, DateTime latestStart) + { + if (buildingId == 0) + { + return CityEventState.None; + } + + ushort eventId = buildingManager.GetEvent(buildingId); + if (eventId != 0) + { + 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) + { + return CityEventState.Upcoming; + } + + return CityEventState.None; + } + else if ((vanillaEventState & EventData.Flags.Active) != 0) + { + return CityEventState.OnGoing; + } + else if (vanillaEventState != EventData.Flags.None) + { + return CityEventState.Finished; + } + } + + if (activeEvent != null && activeEvent.BuildingId == buildingId) + { + return CityEventState.OnGoing; + } + else if (lastActiveEvent != null && lastActiveEvent.BuildingId == buildingId) + { + return CityEventState.Finished; + } + + if (upcomingEvents.FirstOrDefaultNode(e => e.BuildingId == buildingId && e.StartTime <= latestStart) != null) + { + return CityEventState.Upcoming; + } + + return CityEventState.None; + } + + public ICityEvent GetUpcomingCityEvent(DateTime earliestStartTime, DateTime latestStartTime) + { + if (upcomingEvents.Count == 0) + { + return null; + } + + ICityEvent upcomingEvent = upcomingEvents.First.Value; + return upcomingEvent.StartTime >= earliestStartTime && upcomingEvent.StartTime <= latestStartTime + ? upcomingEvent + : null; + } + + public void ProcessEvents() + { + if ((timeInfo.Now - lastProcessed) < EventProcessInterval) + { + return; + } + + lastProcessed = timeInfo.Now; + + Update(); + if (upcomingEvents.Count >= MaximumEventsCount) + { + return; + } + + ushort building = buildingManager.GetRandomBuilding(EventBuildingServices); + if ((buildingManager.GetBuildingFlags(building) & Building.Flags.Active) == 0) + { + return; + } + + CreateRandomEvent(building); + } + + void IStorageData.ReadData(Stream source) + { + upcomingEvents.Clear(); + + var serializer = new XmlSerializer(typeof(RealTimeEventStorageContainer)); + var data = (RealTimeEventStorageContainer)serializer.Deserialize(source); + + earliestEvent = new DateTime(data.EarliestEvent); + + foreach (RealTimeEventStorage storedEvent in data.Events) + { + if (string.IsNullOrEmpty(storedEvent.EventName) || string.IsNullOrEmpty(storedEvent.BuildingClassName)) + { + continue; + } + + CityEventTemplate template = eventProvider.GetEventTemplate(storedEvent.EventName, storedEvent.BuildingClassName); + var realTimeEvent = new RealTimeCityEvent(template, storedEvent.AttendeesCount); + realTimeEvent.Configure(storedEvent.BuildingId, storedEvent.BuildingName, new DateTime(storedEvent.StartTime)); + + if (realTimeEvent.EndTime < timeInfo.Now) + { + lastActiveEvent = realTimeEvent; + } + else + { + upcomingEvents.AddLast(realTimeEvent); + } + } + + OnEventsChanged(); + } + + void IStorageData.StoreData(Stream target) + { + var serializer = new XmlSerializer(typeof(RealTimeEventStorageContainer)); + var data = new RealTimeEventStorageContainer(); + + data.EarliestEvent = earliestEvent.Ticks; + + AddEventToStorage(lastActiveEvent); + AddEventToStorage(activeEvent); + foreach (ICityEvent cityEvent in upcomingEvents) + { + AddEventToStorage(cityEvent); + } + + serializer.Serialize(target, data); + + void AddEventToStorage(ICityEvent cityEvent) + { + if (cityEvent != null && cityEvent is RealTimeCityEvent realTimeEvent) + { + data.Events.Add(realTimeEvent.GetStorageData()); + } + } + } + + private void Update() + { + if (activeEvent != null && activeEvent.EndTime <= timeInfo.Now) + { + Log.Debug(timeInfo.Now, $"Event finished in {activeEvent.BuildingId}, started at {activeEvent.StartTime}, end time {activeEvent.EndTime}"); + lastActiveEvent = activeEvent; + activeEvent = null; + } + + bool eventsChanged = false; + foreach (ushort eventId in eventManager.GetUpcomingEvents(timeInfo.Now, timeInfo.Now.AddDays(1))) + { + eventManager.TryGetEventInfo(eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice); + + if (upcomingEvents.Concat(new[] { activeEvent }) + .OfType() + .Any(e => e.BuildingId == buildingId && e.StartTime == startTime)) + { + continue; + } + + var newEvent = new VanillaEvent(duration, ticketPrice); + newEvent.Configure(buildingId, buildingManager.GetBuildingName(buildingId), startTime); + eventsChanged = true; + Log.Debug(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); + } + } + + if (upcomingEvents.Count == 0) + { + return; + } + + ICityEvent upcomingEvent = upcomingEvents.First.Value; + if (upcomingEvent.StartTime <= timeInfo.Now) + { + activeEvent = upcomingEvent; + upcomingEvents.RemoveFirst(); + eventsChanged = true; + Log.Debug(timeInfo.Now, $"Event started! Building {activeEvent.BuildingId}, ends on {activeEvent.EndTime}"); + } + + if (eventsChanged) + { + OnEventsChanged(); + } + } + + private void CreateRandomEvent(ushort buildingId) + { + string buildingClass = buildingManager.GetBuildingClassName(buildingId); + if (string.IsNullOrEmpty(buildingClass)) + { + return; + } + + ICityEvent newEvent = eventProvider.GetRandomEvent(buildingClass); + if (newEvent == null) + { + return; + } + + DateTime startTime = GetRandomEventStartTime(); + if (startTime < earliestEvent) + { + return; + } + + earliestEvent = startTime.AddHours(simulationManager.GetRandomizer().Int32(EventIntervalVariance)); + + newEvent.Configure(buildingId, buildingManager.GetBuildingName(buildingId), startTime); + upcomingEvents.AddLast(newEvent); + OnEventsChanged(); + Log.Debug(timeInfo.Now, $"New event created for building {newEvent.BuildingId}, starts on {newEvent.StartTime}, ends on {newEvent.EndTime}"); + } + + private DateTime GetRandomEventStartTime() + { + DateTime result = upcomingEvents.Count == 0 + ? timeInfo.Now + : upcomingEvents.Last.Value.EndTime.Add(MinimumIntervalBetweenEvents); + + float earliestHour; + float latestHour; + if (config.IsWeekendEnabled && result.IsWeekend()) + { + earliestHour = config.EarliestHourEventStartWeekend; + latestHour = config.LatestHourEventStartWeekend; + } + else + { + earliestHour = config.EarliestHourEventStartWeekday; + latestHour = config.LatestHourEventStartWeekday; + } + + float randomOffset = simulationManager.GetRandomizer().Int32((uint)((latestHour - earliestHour) * 60f)) / 60f; + result = result.AddHours(randomOffset).RoundCeil(EventStartTimeGranularity); + + if (result.Hour >= latestHour) + { + result = result.Date.AddHours(24 + earliestHour + randomOffset).RoundCeil(EventStartTimeGranularity); + } + else if (result.Hour < earliestHour) + { + result = result.AddHours(earliestHour - result.Hour + randomOffset).RoundCeil(EventStartTimeGranularity); + } + + return result; + } + + private void OnEventsChanged() + { + EventsChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/RealTime/Events/RealTimeEventSimulation.cs b/src/RealTime/Events/RealTimeEventSimulation.cs new file mode 100644 index 00000000..3a2650f3 --- /dev/null +++ b/src/RealTime/Events/RealTimeEventSimulation.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using ICities; + + public sealed class RealTimeEventSimulation : ThreadingExtensionBase + { + internal static RealTimeEventManager EventManager { get; set; } + + public override void OnAfterSimulationTick() + { + EventManager?.ProcessEvents(); + } + } +} diff --git a/src/RealTime/Events/Storage/CityEventAttendees.cs b/src/RealTime/Events/Storage/CityEventAttendees.cs new file mode 100644 index 00000000..02c673c9 --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventAttendees.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Xml.Serialization; + + public sealed class CityEventAttendees + { + [XmlElement("Males")] + public int Males { get; set; } = 100; + + [XmlElement("Females")] + public int Females { get; set; } = 100; + + [XmlElement("Children")] + public int Children { get; set; } = 100; + + [XmlElement("Teens")] + public int Teens { get; set; } = 100; + + [XmlElement("YoungAdults")] + public int YoungAdults { get; set; } = 100; + + [XmlElement("Adults")] + public int Adults { get; set; } = 100; + + [XmlElement("Seniors")] + public int Seniors { get; set; } = 100; + + [XmlElement("LowWealth")] + public int LowWealth { get; set; } = 60; + + [XmlElement("MediumWealth")] + public int MediumWealth { get; set; } = 100; + + [XmlElement("HighWealth")] + public int HighWealth { get; set; } = 100; + + [XmlElement("Uneducated")] + public int Uneducated { get; set; } = 100; + + [XmlElement("OneSchool")] + public int OneSchool { get; set; } = 100; + + [XmlElement("TwoSchools")] + public int TwoSchools { get; set; } = 100; + + [XmlElement("ThreeSchools")] + public int ThreeSchools { get; set; } = 100; + + [XmlElement("BadHappiness")] + public int BadHappiness { get; set; } = 40; + + [XmlElement("PoorHappiness")] + public int PoorHappiness { get; set; } = 60; + + [XmlElement("GoodHappiness")] + public int GoodHappiness { get; set; } = 100; + + [XmlElement("ExcellentHappiness")] + public int ExcellentHappiness { get; set; } = 100; + + [XmlElement("SuperbHappiness")] + public int SuperbHappiness { get; set; } = 100; + + [XmlElement("VeryUnhappyWellbeing")] + public int VeryUnhappyWellbeing { get; set; } = 20; + + [XmlElement("UnhappyWellbeing")] + public int UnhappyWellbeing { get; set; } = 50; + + [XmlElement("SatisfiedWellbeing")] + public int SatisfiedWellbeing { get; set; } = 100; + + [XmlElement("HappyWellbeing")] + public int HappyWellbeing { get; set; } = 100; + + [XmlElement("VeryHappyWellbeing")] + public int VeryHappyWellbeing { get; set; } = 100; + } +} diff --git a/src/RealTime/Events/Storage/CityEventContainer.cs b/src/RealTime/Events/Storage/CityEventContainer.cs new file mode 100644 index 00000000..ee6cebdb --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventContainer.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Collections.Generic; + using System.Xml.Serialization; + + [XmlRoot("EventContainer")] + public sealed class CityEventContainer + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "XML serialization")] + [XmlArray("Events", IsNullable = false)] + [XmlArrayItem("Event")] + public List Templates { get; } = new List(); + } +} diff --git a/src/RealTime/Events/Storage/CityEventCosts.cs b/src/RealTime/Events/Storage/CityEventCosts.cs new file mode 100644 index 00000000..2feb06c0 --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventCosts.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Xml.Serialization; + + public class CityEventCosts + { + [XmlElement("Creation")] + public float Creation { get; set; } = 100; + + [XmlElement("PerHead")] + public float PerHead { get; set; } = 5; + + [XmlElement("AdvertisingSigns")] + public float AdvertisingSigns { get; set; } = 20000; + + [XmlElement("AdvertisingTV")] + public float AdvertisingTV { get; set; } = 5000; + + [XmlElement("EntryCost")] + public float Entry { get; set; } = 10; + } +} diff --git a/src/RealTime/Events/Storage/CityEventIncentive.cs b/src/RealTime/Events/Storage/CityEventIncentive.cs new file mode 100644 index 00000000..94b8328f --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventIncentive.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Xml.Serialization; + + public sealed class CityEventIncentive + { + [XmlAttribute("Name")] + public string Name { get; set; } = string.Empty; + + [XmlAttribute("Cost")] + public float Cost { get; set; } = 3; + + [XmlAttribute("ReturnCost")] + public float ReturnCost { get; set; } = 10; + + [XmlAttribute("ActiveWhenRandomEvent")] + public bool ActiveWhenRandomEvent { get; set; } + + [XmlElement("Description")] + public string Description { get; set; } = string.Empty; + + [XmlElement("PositiveEffect")] + public int PositiveEffect { get; set; } = 10; + + [XmlElement("NegativeEffect")] + public int NegativeEffect { get; set; } = 10; + } +} diff --git a/src/RealTime/Events/Storage/CityEventTemplate.cs b/src/RealTime/Events/Storage/CityEventTemplate.cs new file mode 100644 index 00000000..211fe444 --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventTemplate.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Collections.Generic; + using System.Xml.Serialization; + + public sealed class CityEventTemplate + { + [XmlAttribute("EventName")] + public string EventName { get; set; } = string.Empty; + + [XmlAttribute("BuildingName")] + public string BuildingClassName { get; set; } = string.Empty; + + [XmlAttribute("UserEventName")] + public string UserEventName { get; set; } = string.Empty; + + [XmlAttribute("Capacity")] + public int Capacity { get; set; } = 1000; + + [XmlAttribute("LengthInHours")] + public double Duration { get; set; } = 1.5; + + [XmlAttribute("SupportsRandomEvents")] + public bool SupportsRandomEvents { get; set; } = true; + + [XmlAttribute("SupportsUserEvents")] + public bool SupportsUserEvents { get; set; } + + [XmlAttribute("CanBeWatchedOnTV")] + public bool CanBeWatchedOnTV { get; set; } + + [XmlElement("ChanceOfAttendingPercentage", IsNullable = false)] + public CityEventAttendees Attendees { get; set; } = new CityEventAttendees(); + + [XmlElement("Costs", IsNullable = false)] + public CityEventCosts Costs { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "XML serialization")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "XML serialization")] + [XmlArray("Incentives", IsNullable = false)] + [XmlArrayItem("Incentive")] + public List Incentives { get; set; } = new List(); + } +} diff --git a/src/RealTime/Events/Storage/CityEventsLoader.cs b/src/RealTime/Events/Storage/CityEventsLoader.cs new file mode 100644 index 00000000..4465e122 --- /dev/null +++ b/src/RealTime/Events/Storage/CityEventsLoader.cs @@ -0,0 +1,104 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Xml.Serialization; + using RealTime.Tools; + + internal sealed class CityEventsLoader : ICityEventsProvider + { + private const string EventsFolder = "Events"; + private const string EventFileSearchPattern = "*.xml"; + + private readonly List events = new List(); + + private CityEventsLoader() + { + } + + public static CityEventsLoader Istance { get; } = new CityEventsLoader(); + + public void ReloadEvents(string rootPath) + { + events.Clear(); + string searchPath = Path.Combine(rootPath, EventsFolder); + if (!Directory.Exists(searchPath)) + { + Log.Warning($"The 'Real Time' mod did not found any event templates, the directory '{searchPath}' doesn't exist"); + return; + } + + LoadEvents(Directory.GetFiles(searchPath, EventFileSearchPattern)); + } + + public void Clear() + { + events.Clear(); + } + + ICityEvent ICityEventsProvider.GetRandomEvent(string buildingClass) + { + if (buildingClass == null) + { + throw new ArgumentNullException(nameof(buildingClass)); + } + + var buildingEvents = events.Where(e => e.BuildingClassName == buildingClass).ToList(); + if (buildingEvents.Count == 0) + { + return null; + } + + int eventNumber = SimulationManager.instance.m_randomizer.Int32((uint)buildingEvents.Count); + return new RealTimeCityEvent(buildingEvents[eventNumber]); + } + + CityEventTemplate ICityEventsProvider.GetEventTemplate(string eventName, string buildingClassName) + { + if (eventName == null) + { + throw new ArgumentNullException(nameof(eventName)); + } + + if (buildingClassName == null) + { + throw new ArgumentNullException(nameof(buildingClassName)); + } + + return events.FirstOrDefault(e => e.EventName == eventName && e.BuildingClassName == buildingClassName); + } + + private void LoadEvents(IEnumerable files) + { + var serializer = new XmlSerializer(typeof(CityEventContainer)); + + foreach (string file in files) + { + try + { + using (var sr = new StreamReader(file)) + { + var container = (CityEventContainer)serializer.Deserialize(sr); + foreach (CityEventTemplate @event in container.Templates.Where(e => !events.Any(ev => ev.EventName == e.EventName))) + { + events.Add(@event); + Log.Debug($"Loaded event template '{@event.EventName}' for '{@event.BuildingClassName}'"); + } + } + } + catch (Exception ex) + { + Log.Error($"The 'Real Time' mod was unable to load an event template from file '{file}', error message: '{ex.Message}'"); + } + } + + Log.Debug($"Successfully loaded {events.Count} event templates"); + } + } +} diff --git a/src/RealTime/Events/Storage/RealTimeEventStorage.cs b/src/RealTime/Events/Storage/RealTimeEventStorage.cs new file mode 100644 index 00000000..5f040a94 --- /dev/null +++ b/src/RealTime/Events/Storage/RealTimeEventStorage.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Xml.Serialization; + + public sealed class RealTimeEventStorage + { + [XmlAttribute] + public string EventName { get; set; } + + [XmlAttribute] + public string BuildingClassName { get; set; } + + [XmlElement] + public long StartTime { get; set; } + + [XmlElement] + public ushort BuildingId { get; set; } + + [XmlElement] + public string BuildingName { get; set; } + + [XmlElement] + public int AttendeesCount { get; set; } + } +} diff --git a/src/RealTime/Events/Storage/RealTimeEventStorageContainer.cs b/src/RealTime/Events/Storage/RealTimeEventStorageContainer.cs new file mode 100644 index 00000000..34dba233 --- /dev/null +++ b/src/RealTime/Events/Storage/RealTimeEventStorageContainer.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events.Storage +{ + using System.Collections.Generic; + using System.Xml.Serialization; + + [XmlRoot("RealTimeEventStorage")] + public sealed class RealTimeEventStorageContainer + { + [XmlAttribute] + public int Version { get; set; } = 1; + + [XmlElement] + public long EarliestEvent { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "XML serialization")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "XML serialization")] + [XmlArray("Events")] + [XmlArrayItem("RealTimeEventStorage")] + public List Events { get; set; } = new List(); + } +} diff --git a/src/RealTime/Events/VanillaEvent.cs b/src/RealTime/Events/VanillaEvent.cs new file mode 100644 index 00000000..98110b2b --- /dev/null +++ b/src/RealTime/Events/VanillaEvent.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Events +{ + using ColossalFramework.Math; + + internal sealed class VanillaEvent : CityEventBase + { + private readonly float duration; + private readonly float ticketPrice; + + public VanillaEvent(float duration, float ticketPrice) + { + this.duration = duration; + this.ticketPrice = ticketPrice; + } + + public ushort EventId { get; set; } + + public override bool TryAcceptAttendee( + Citizen.AgeGroup age, + Citizen.Gender gender, + Citizen.Education education, + Citizen.Wealth wealth, + Citizen.Wellbeing wellbeing, + Citizen.Happiness happiness, + ref Randomizer randomizer) + { + return ticketPrice <= GetCitizenBudgetForEvent(wealth, ref randomizer); + } + + protected override float GetDuration() + { + return duration; + } + } +} diff --git a/src/RealTime/GameConnection/BuildingManagerConnection.cs b/src/RealTime/GameConnection/BuildingManagerConnection.cs new file mode 100644 index 00000000..c8c83657 --- /dev/null +++ b/src/RealTime/GameConnection/BuildingManagerConnection.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Collections.Generic; + using System.Linq; + using UnityEngine; + + internal sealed class BuildingManagerConnection : IBuildingManagerConnection + { + public ItemClass.Service GetBuildingService(ushort buildingId) + { + return buildingId == 0 + ? ItemClass.Service.None + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.m_class.m_service; + } + + public ItemClass.SubService GetBuildingSubService(ushort buildingId) + { + return buildingId == 0 + ? ItemClass.SubService.None + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.m_class.m_subService; + } + + public Building.Flags GetBuildingFlags(ushort buildingId) + { + return buildingId == 0 + ? Building.Flags.None + : BuildingManager.instance.m_buildings.m_buffer[buildingId].m_flags; + } + + public float GetDistanceBetweenBuildings(ushort building1, ushort building2) + { + if (building1 == 0 || building2 == 0) + { + return float.MaxValue; + } + + Building[] buildings = BuildingManager.instance.m_buildings.m_buffer; + return Vector3.Distance(buildings[building1].m_position, buildings[building2].m_position); + } + + public void ModifyMaterialBuffer(ushort buildingId, TransferManager.TransferReason reason, int delta) + { + if (buildingId == 0 || delta == 0) + { + return; + } + + ref Building building = ref BuildingManager.instance.m_buildings.m_buffer[buildingId]; + building.Info.m_buildingAI.ModifyMaterialBuffer(buildingId, ref building, reason, ref delta); + } + + public ushort FindActiveBuilding( + ushort searchAreaCenterBuilding, + float maxDistance, + ItemClass.Service service, + ItemClass.SubService subService = ItemClass.SubService.None) + { + if (searchAreaCenterBuilding == 0) + { + return 0; + } + + Vector3 currentPosition = BuildingManager.instance.m_buildings.m_buffer[searchAreaCenterBuilding].m_position; + + 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); + } + + public ushort GetEvent(ushort buildingId) + { + return buildingId == 0 + ? (ushort)0 + : BuildingManager.instance.m_buildings.m_buffer[buildingId].m_eventIndex; + } + + /// + /// Gets a random building ID for a bulding in the city that belongs + /// to any of the provided . + /// + /// + /// Thrown when the argument is null. + /// + /// A collection of that specifies + /// in which services to search the random building in. + /// + /// An ID of a building; or 0 if none found. + /// + /// NOTE: this method creates objects on the heap. To avoid memory pressure, + /// don't call it on every simulation step. + public ushort GetRandomBuilding(IEnumerable services) + { + // No memory pressure here because this method will not be called on each simulation step + var buildings = new List>(); + + int totalCount = 0; + foreach (FastList serviceBuildings in services + .Select(s => BuildingManager.instance.GetServiceBuildings(s)) + .Where(b => b != null)) + { + totalCount += serviceBuildings.m_size; + buildings.Add(serviceBuildings); + } + + if (totalCount == 0) + { + return 0; + } + + int buildingNumber = SimulationManager.instance.m_randomizer.Int32((uint)totalCount); + totalCount = 0; + foreach (FastList serviceBuildings in buildings) + { + if (buildingNumber < totalCount + serviceBuildings.m_size) + { + return serviceBuildings[buildingNumber - totalCount]; + } + + totalCount += serviceBuildings.m_size; + } + + return 0; + } + + public string GetBuildingClassName(ushort buildingId) + { + return buildingId == 0 + ? string.Empty + : BuildingManager.instance.m_buildings.m_buffer[buildingId].Info.name; + } + + public string GetBuildingName(ushort buildingId) + { + return buildingId == 0 + ? string.Empty + : BuildingManager.instance.GetBuildingName(buildingId, InstanceID.Empty); + } + } +} diff --git a/src/RealTime/GameConnection/CameraHelper.cs b/src/RealTime/GameConnection/CameraHelper.cs new file mode 100644 index 00000000..722bb0f4 --- /dev/null +++ b/src/RealTime/GameConnection/CameraHelper.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using UnityEngine; + + internal static class CameraHelper + { + public static void NavigateToBuilding(ushort buildingId) + { + if (buildingId == 0) + { + return; + } + + InstanceID instance = default; + instance.Building = buildingId; + + Vector3 buildingPosition = BuildingManager.instance.m_buildings.m_buffer[buildingId].m_position; + ToolsModifierControl.cameraController.SetTarget(instance, buildingPosition, true); + } + } +} diff --git a/src/RealTime/GameConnection/CitizenConnection.cs b/src/RealTime/GameConnection/CitizenConnection.cs new file mode 100644 index 00000000..85930dbb --- /dev/null +++ b/src/RealTime/GameConnection/CitizenConnection.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal sealed class CitizenConnection : ICitizenConnection + { + public Citizen.Flags AddFlags(ref Citizen citizen, Citizen.Flags flags) + { + Citizen.Flags currentFlags = citizen.m_flags; + currentFlags |= flags; + return citizen.m_flags = currentFlags; + } + + public Citizen.AgeGroup GetAge(ref Citizen citizen) + { + return Citizen.GetAgeGroup(citizen.m_age); + } + + public Citizen.Wealth GetWealthLevel(ref Citizen citizen) + { + return citizen.WealthLevel; + } + + public Citizen.Education GetEducationLevel(ref Citizen citizen) + { + return citizen.EducationLevel; + } + + public Citizen.Gender GetGender(uint citizenId) + { + return Citizen.GetGender(citizenId); + } + + public Citizen.Happiness GetHappinessLevel(ref Citizen citizen) + { + return Citizen.GetHappinessLevel(Citizen.GetHappiness(citizen.m_health, citizen.m_wellbeing)); + } + + public Citizen.Wellbeing GetWellbeingLevel(ref Citizen citizen) + { + return Citizen.GetWellbeingLevel(citizen.EducationLevel, citizen.m_wellbeing); + } + + public ushort GetCurrentBuilding(ref Citizen citizen) + { + return citizen.GetBuildingByLocation(); + } + + public Citizen.Flags GetFlags(ref Citizen citizen) + { + return citizen.m_flags; + } + + public ushort GetHomeBuilding(ref Citizen citizen) + { + return citizen.m_homeBuilding; + } + + public ushort GetInstance(ref Citizen citizen) + { + return citizen.m_instance; + } + + public Citizen.Location GetLocation(ref Citizen citizen) + { + return citizen.CurrentLocation; + } + + public ushort GetVehicle(ref Citizen citizen) + { + return citizen.m_vehicle; + } + + public ushort GetVisitBuilding(ref Citizen citizen) + { + return citizen.m_visitBuilding; + } + + public ushort GetWorkBuilding(ref Citizen citizen) + { + return citizen.m_workBuilding; + } + + public bool IsArrested(ref Citizen citizen) + { + return citizen.Arrested; + } + + public bool IsCollapsed(ref Citizen citizen) + { + return citizen.Collapsed; + } + + public bool IsDead(ref Citizen citizen) + { + return citizen.Dead; + } + + public bool IsSick(ref Citizen citizen) + { + return citizen.Sick; + } + + public Citizen.Flags RemoveFlags(ref Citizen citizen, Citizen.Flags flags) + { + Citizen.Flags currentFlags = citizen.m_flags; + currentFlags &= ~flags; + return citizen.m_flags = currentFlags; + } + + public void SetArrested(ref Citizen citizen, bool isArrested) + { + citizen.Arrested = isArrested; + } + + public void SetHome(ref Citizen citizen, uint citizenId, ushort buildingId) + { + citizen.SetHome(citizenId, buildingId, 0u); + } + + public void SetLocation(ref Citizen citizen, Citizen.Location location) + { + citizen.CurrentLocation = location; + } + + public void SetVisitBuilding(ref Citizen citizen, ushort visitBuilding) + { + citizen.m_visitBuilding = visitBuilding; + } + + public void SetVisitPlace(ref Citizen citizen, uint citizenId, ushort buildingId) + { + citizen.SetVisitplace(citizenId, buildingId, 0u); + } + + public void SetWorkplace(ref Citizen citizen, uint citizenId, ushort buildingId) + { + citizen.SetWorkplace(citizenId, buildingId, 0u); + } + } +} diff --git a/src/RealTime/GameConnection/CitizenManagerConnection.cs b/src/RealTime/GameConnection/CitizenManagerConnection.cs new file mode 100644 index 00000000..f805ca5c --- /dev/null +++ b/src/RealTime/GameConnection/CitizenManagerConnection.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using UnityEngine; + + internal sealed class CitizenManagerConnection : ICitizenManagerConnection + { + public void ReleaseCitizen(uint citizenId) + { + CitizenManager.instance.ReleaseCitizen(citizenId); + } + + public ushort GetTargetBuilding(ushort instanceId) + { + if (instanceId == 0) + { + return 0; + } + + ref CitizenInstance instance = ref CitizenManager.instance.m_instances.m_buffer[instanceId]; + return (instance.m_flags & CitizenInstance.Flags.TargetIsNode) == 0 + ? instance.m_targetBuilding + : (ushort)0; + } + + public CitizenInstance.Flags GetInstanceFlags(ushort instanceId) + { + return instanceId == 0 + ? CitizenInstance.Flags.None + : CitizenManager.instance.m_instances.m_buffer[instanceId].m_flags; + } + + public byte GetInstanceWaitCounter(ushort instanceId) + { + return instanceId == 0 + ? (byte)0 + : CitizenManager.instance.m_instances.m_buffer[instanceId].m_waitCounter; + } + + public bool IsAreaEvacuating(ushort instanceId) + { + if (instanceId == 0) + { + return false; + } + + Vector3 position = CitizenManager.instance.m_instances.m_buffer[instanceId].GetLastFramePosition(); + return DisasterManager.instance.IsEvacuating(position); + } + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/EventManagerConnection.cs b/src/RealTime/GameConnection/EventManagerConnection.cs new file mode 100644 index 00000000..87f8b23c --- /dev/null +++ b/src/RealTime/GameConnection/EventManagerConnection.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Collections.Generic; + + internal sealed class EventManagerConnection : IEventManagerConnection + { + public EventData.Flags GetEventFlags(ushort eventId) + { + return eventId == 0 + ? EventData.Flags.None + : EventManager.instance.m_events.m_buffer[eventId].m_flags; + } + + public IEnumerable GetUpcomingEvents(DateTime earliestTime, DateTime latestTime) + { + FastList events = EventManager.instance.m_events; + for (ushort i = 0; i < events.m_size && i < EventManager.MAX_EVENT_COUNT; ++i) + { + EventData eventData = events.m_buffer[i]; + if ((eventData.m_flags & (EventData.Flags.Preparing | EventData.Flags.Ready | EventData.Flags.Active)) == 0) + { + continue; + } + + if (eventData.StartTime >= earliestTime && eventData.StartTime < latestTime) + { + yield return i; + } + } + } + + public bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice) + { + buildingId = default; + duration = default; + startTime = default; + ticketPrice = default; + if (eventId == 0 || eventId >= EventManager.instance.m_events.m_size) + { + return false; + } + + EventData eventData = EventManager.instance.m_events.m_buffer[eventId]; + buildingId = eventData.m_building; + startTime = eventData.StartTime; + duration = eventData.Info.m_eventAI.m_eventDuration; + ticketPrice = eventData.m_ticketPrice / 100f; + return true; + } + } +} diff --git a/src/RealTime/GameConnection/GameConnections.cs b/src/RealTime/GameConnection/GameConnections.cs new file mode 100644 index 00000000..de4e4c4e --- /dev/null +++ b/src/RealTime/GameConnection/GameConnections.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using RealTime.Simulation; + + internal sealed class GameConnections + where TCitizen : struct + { + /// + /// Initializes a new instance of the class. + /// + /// An object that provides the game time information. + /// A proxy object that provides a way to call the game-specific methods of the struct. + /// A proxy object that provides a way to call the game-specific methods of the class. + /// A proxy object that provides a way to call the game-specific methods of the class. + /// A proxy object that provides a way to call the game-specific methods of the class. + /// A proxy object that provides a way to call the game-specific methods of the class. + public GameConnections( + ITimeInfo timeInfo, + ICitizenConnection citizenConnection, + ICitizenManagerConnection citizenManager, + IBuildingManagerConnection buildingManager, + ISimulationManagerConnection simulationManager, + ITransferManagerConnection transferManager) + { + TimeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + CitizenConnection = citizenConnection ?? throw new ArgumentNullException(nameof(citizenConnection)); + CitizenManager = citizenManager ?? throw new ArgumentNullException(nameof(citizenManager)); + BuildingManager = buildingManager ?? throw new ArgumentNullException(nameof(buildingManager)); + SimulationManager = simulationManager ?? throw new ArgumentNullException(nameof(simulationManager)); + TransferManager = transferManager ?? throw new ArgumentNullException(nameof(transferManager)); + } + + public ITimeInfo TimeInfo { get; } + + public ICitizenConnection CitizenConnection { get; } + + public ICitizenManagerConnection CitizenManager { get; } + + public IBuildingManagerConnection BuildingManager { get; } + + public ISimulationManagerConnection SimulationManager { get; } + + public ITransferManagerConnection TransferManager { get; } + } +} diff --git a/src/RealTime/GameConnection/IBuildingManagerConnection.cs b/src/RealTime/GameConnection/IBuildingManagerConnection.cs new file mode 100644 index 00000000..8ac4e541 --- /dev/null +++ b/src/RealTime/GameConnection/IBuildingManagerConnection.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Collections.Generic; + + internal interface IBuildingManagerConnection + { + ItemClass.Service GetBuildingService(ushort buildingId); + + ItemClass.SubService GetBuildingSubService(ushort buildingId); + + Building.Flags GetBuildingFlags(ushort buildingId); + + float GetDistanceBetweenBuildings(ushort building1, ushort building2); + + void ModifyMaterialBuffer(ushort buildingId, TransferManager.TransferReason reason, int delta); + + ushort FindActiveBuilding( + ushort searchAreaCenterBuilding, + float maxDistance, + ItemClass.Service service, + ItemClass.SubService subService = ItemClass.SubService.None); + + ushort GetEvent(ushort buildingId); + + /// + /// Gets a random building ID for a bulding in the city that belongs + /// to any of the provided . + /// + /// + /// Thrown when the argument is null. + /// + /// A collection of that specifies + /// in which services to search the random building in. + /// + /// An ID of a building; or 0 if none found. + /// + /// NOTE: this method creates objects on the heap. To avoid memory pressure, + /// don't call it on every simulation step. + ushort GetRandomBuilding(IEnumerable services); + + string GetBuildingClassName(ushort buildingId); + + string GetBuildingName(ushort buildingId); + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/ICitizenConnection.cs b/src/RealTime/GameConnection/ICitizenConnection.cs new file mode 100644 index 00000000..9626195d --- /dev/null +++ b/src/RealTime/GameConnection/ICitizenConnection.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal interface ICitizenConnection + where T : struct + { + ushort GetHomeBuilding(ref T citizen); + + ushort GetWorkBuilding(ref T citizen); + + ushort GetVisitBuilding(ref T citizen); + + void SetVisitBuilding(ref T citizen, ushort visitBuilding); + + ushort GetInstance(ref T citizen); + + ushort GetVehicle(ref T citizen); + + bool IsCollapsed(ref T citizen); + + bool IsDead(ref T citizen); + + bool IsSick(ref T citizen); + + bool IsArrested(ref T citizen); + + void SetArrested(ref T citizen, bool isArrested); + + ushort GetCurrentBuilding(ref T citizen); + + Citizen.Location GetLocation(ref T citizen); + + void SetLocation(ref T citizen, Citizen.Location location); + + Citizen.AgeGroup GetAge(ref T citizen); + + Citizen.Wealth GetWealthLevel(ref T citizen); + + Citizen.Education GetEducationLevel(ref T citizen); + + Citizen.Gender GetGender(uint citizenId); + + Citizen.Happiness GetHappinessLevel(ref T citizen); + + Citizen.Wellbeing GetWellbeingLevel(ref T citizen); + + Citizen.Flags GetFlags(ref T citizen); + + void SetHome(ref T citizen, uint citizenId, ushort buildingId); + + void SetWorkplace(ref T citizen, uint citizenId, ushort buildingId); + + void SetVisitPlace(ref T citizen, uint citizenId, ushort buildingId); + + Citizen.Flags AddFlags(ref T citizen, Citizen.Flags flags); + + Citizen.Flags RemoveFlags(ref T citizen, Citizen.Flags flags); + } +} diff --git a/src/RealTime/GameConnection/ICitizenManagerConnection.cs b/src/RealTime/GameConnection/ICitizenManagerConnection.cs new file mode 100644 index 00000000..e470aa07 --- /dev/null +++ b/src/RealTime/GameConnection/ICitizenManagerConnection.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal interface ICitizenManagerConnection + { + void ReleaseCitizen(uint citizenId); + + ushort GetTargetBuilding(ushort instanceId); + + CitizenInstance.Flags GetInstanceFlags(ushort instanceId); + + byte GetInstanceWaitCounter(ushort instanceId); + + bool IsAreaEvacuating(ushort instanceId); + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/IEventManagerConnection.cs b/src/RealTime/GameConnection/IEventManagerConnection.cs new file mode 100644 index 00000000..c010e5b3 --- /dev/null +++ b/src/RealTime/GameConnection/IEventManagerConnection.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Collections.Generic; + + internal interface IEventManagerConnection + { + EventData.Flags GetEventFlags(ushort eventId); + + IEnumerable GetUpcomingEvents(DateTime earliestTime, DateTime latestTime); + + bool TryGetEventInfo(ushort eventId, out ushort buildingId, out DateTime startTime, out float duration, out float ticketPrice); + } +} diff --git a/src/RealTime/GameConnection/ISimulationManagerConnection.cs b/src/RealTime/GameConnection/ISimulationManagerConnection.cs new file mode 100644 index 00000000..dbc80402 --- /dev/null +++ b/src/RealTime/GameConnection/ISimulationManagerConnection.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using ColossalFramework.Math; + + internal interface ISimulationManagerConnection + { + // TODO: implement randomizer abstraction to get rid of refs and ColossalFramework.Math dependency + ref Randomizer GetRandomizer(); + } +} diff --git a/src/RealTime/GameConnection/IToolManagerConnection.cs b/src/RealTime/GameConnection/IToolManagerConnection.cs new file mode 100644 index 00000000..b44fbb91 --- /dev/null +++ b/src/RealTime/GameConnection/IToolManagerConnection.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal interface IToolManagerConnection + { + ItemClass.Availability GetCurrentMode(); + } +} diff --git a/src/RealTime/GameConnection/ITransferManagerConnection.cs b/src/RealTime/GameConnection/ITransferManagerConnection.cs new file mode 100644 index 00000000..e5c02ded --- /dev/null +++ b/src/RealTime/GameConnection/ITransferManagerConnection.cs @@ -0,0 +1,11 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal interface ITransferManagerConnection + { + void AddOutgoingOfferFromCurrentPosition(uint citizenId, TransferManager.TransferReason reason); + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/PrivateBuildingAIHook.cs b/src/RealTime/GameConnection/PrivateBuildingAIHook.cs new file mode 100644 index 00000000..fde5818d --- /dev/null +++ b/src/RealTime/GameConnection/PrivateBuildingAIHook.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using RealTime.CustomAI; + using Redirection; + + internal static class PrivateBuildingAIHook + { + public static RealTimePrivateBuildingAI RealTimeAI { get; set; } + + [RedirectFrom(typeof(PrivateBuildingAI))] + private static int GetConstructionTime(PrivateBuildingAI instance) + { + return RealTimeAI?.GetConstructionTime() ?? 0; + } + } +} diff --git a/src/RealTime/GameConnection/ResidentAIConnection.cs b/src/RealTime/GameConnection/ResidentAIConnection.cs new file mode 100644 index 00000000..ed994ed8 --- /dev/null +++ b/src/RealTime/GameConnection/ResidentAIConnection.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + + internal sealed class ResidentAIConnection + where TAI : class + where TCitizen : struct + { + public ResidentAIConnection( + DoRandomMoveDelegate doRandomMove, + FindEvacuationPlaceDelegate findEvacuationPlace, + FindHospitalDelegate findHospital, + FindVisitPlaceDelegate findVisitPlace, + GetEntertainmentReasonDelegate getEntertainmentReason, + GetEvacuationReasonDelegate getEvacuationReason, + GetShoppingReasonDelegate getShoppingReason, + StartMovingDelegate startMoving, + StartMovingWithOfferDelegate startMovingWithOffer) + { + DoRandomMove = doRandomMove ?? throw new ArgumentNullException(nameof(doRandomMove)); + FindEvacuationPlace = findEvacuationPlace ?? throw new ArgumentNullException(nameof(findEvacuationPlace)); + FindHospital = findHospital ?? throw new ArgumentNullException(nameof(findHospital)); + FindVisitPlace = findVisitPlace ?? throw new ArgumentNullException(nameof(findVisitPlace)); + GetEntertainmentReason = getEntertainmentReason ?? throw new ArgumentNullException(nameof(getEntertainmentReason)); + GetEvacuationReason = getEvacuationReason ?? throw new ArgumentNullException(nameof(getEvacuationReason)); + GetShoppingReason = getShoppingReason ?? throw new ArgumentNullException(nameof(getShoppingReason)); + StartMoving = startMoving ?? throw new ArgumentNullException(nameof(startMoving)); + StartMovingWithOffer = startMovingWithOffer ?? throw new ArgumentNullException(nameof(startMovingWithOffer)); + } + + public delegate bool DoRandomMoveDelegate(TAI instance); + + public delegate void FindEvacuationPlaceDelegate(TAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason); + + public delegate bool FindHospitalDelegate(TAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason); + + public delegate void FindVisitPlaceDelegate(TAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason); + + public delegate TransferManager.TransferReason GetEntertainmentReasonDelegate(TAI instance); + + public delegate TransferManager.TransferReason GetEvacuationReasonDelegate(TAI instance, ushort sourceBuilding); + + public delegate TransferManager.TransferReason GetShoppingReasonDelegate(TAI instance); + + public delegate bool StartMovingDelegate(TAI instance, uint citizenId, ref TCitizen citizen, ushort sourceBuilding, ushort targetBuilding); + + public delegate bool StartMovingWithOfferDelegate(TAI instance, uint citizenId, ref TCitizen citizen, ushort sourceBuilding, TransferManager.TransferOffer offer); + + public DoRandomMoveDelegate DoRandomMove { get; } + + public FindEvacuationPlaceDelegate FindEvacuationPlace { get; } + + public FindHospitalDelegate FindHospital { get; } + + public FindVisitPlaceDelegate FindVisitPlace { get; } + + public GetEntertainmentReasonDelegate GetEntertainmentReason { get; } + + public GetEvacuationReasonDelegate GetEvacuationReason { get; } + + public GetShoppingReasonDelegate GetShoppingReason { get; } + + public StartMovingDelegate StartMoving { get; } + + public StartMovingWithOfferDelegate StartMovingWithOffer { get; } + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/ResidentAIHook.cs b/src/RealTime/GameConnection/ResidentAIHook.cs new file mode 100644 index 00000000..92dd73b8 --- /dev/null +++ b/src/RealTime/GameConnection/ResidentAIHook.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Runtime.CompilerServices; + using RealTime.CustomAI; + using Redirection; + + internal static class ResidentAIHook + { + private const string RedirectNeededMessage = "This method must be redirected to the original game implementation"; + + internal static RealTimeResidentAI RealTimeAI { get; set; } + + internal static ResidentAIConnection GetResidentAIConnection() + { + return new ResidentAIConnection( + DoRandomMove, + FindEvacuationPlace, + FindHospital, + FindVisitPlace, + GetEntertainmentReason, + GetEvacuationReason, + GetShoppingReason, + StartMoving, + StartMoving); + } + + [RedirectFrom(typeof(ResidentAI))] + private static void UpdateLocation(ResidentAI instance, uint citizenId, ref Citizen citizen) + { + RealTimeAI?.UpdateLocation(instance, citizenId, ref citizen); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool FindHospital(ResidentAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void FindEvacuationPlace(ResidentAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetEvacuationReason(ResidentAI instance, ushort sourceBuilding) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void FindVisitPlace(ResidentAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetShoppingReason(ResidentAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool DoRandomMove(ResidentAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(ResidentAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetEntertainmentReason(ResidentAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(HumanAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool StartMoving(ResidentAI instance, uint citizenId, ref Citizen citizen, ushort sourceBuilding, ushort targetBuilding) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(HumanAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool StartMoving(ResidentAI instance, uint citizenId, ref Citizen citizen, ushort sourceBuilding, TransferManager.TransferOffer offer) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + } +} diff --git a/src/RealTime/GameConnection/SimulationManagerConnection.cs b/src/RealTime/GameConnection/SimulationManagerConnection.cs new file mode 100644 index 00000000..2b92a422 --- /dev/null +++ b/src/RealTime/GameConnection/SimulationManagerConnection.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using ColossalFramework.Math; + + internal class SimulationManagerConnection : ISimulationManagerConnection + { + public ref Randomizer GetRandomizer() + { + return ref SimulationManager.instance.m_randomizer; + } + } +} diff --git a/src/RealTime/GameConnection/TimeInfo.cs b/src/RealTime/GameConnection/TimeInfo.cs new file mode 100644 index 00000000..58cc6c0c --- /dev/null +++ b/src/RealTime/GameConnection/TimeInfo.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using RealTime.Simulation; + + internal sealed class TimeInfo : ITimeInfo + { + public DateTime Now => SimulationManager.instance.m_currentGameTime; + + public float CurrentHour => SimulationManager.instance.m_currentDayTimeHour; + + public float SunriseHour => SimulationManager.SUNRISE_HOUR; + + public float SunsetHour => SimulationManager.SUNSET_HOUR; + + public bool IsNightTime => SimulationManager.instance.m_isNightTime; + + public float DayDuration => SunsetHour - SunriseHour; + + public float NightDuration => 24f - DayDuration; + } +} diff --git a/src/RealTime/GameConnection/ToolManagerConnection.cs b/src/RealTime/GameConnection/ToolManagerConnection.cs new file mode 100644 index 00000000..fb856192 --- /dev/null +++ b/src/RealTime/GameConnection/ToolManagerConnection.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal sealed class ToolManagerConnection : IToolManagerConnection + { + public ItemClass.Availability GetCurrentMode() + { + return ToolManager.instance.m_properties.m_mode; + } + } +} diff --git a/src/RealTime/GameConnection/TouristAIConnection.cs b/src/RealTime/GameConnection/TouristAIConnection.cs new file mode 100644 index 00000000..6c977d99 --- /dev/null +++ b/src/RealTime/GameConnection/TouristAIConnection.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + + internal sealed class TouristAIConnection + where TAI : class + where TCitizen : struct + { + public TouristAIConnection( + GetRandomTargetTypeDelegate getRandomTargetType, + GetLeavingReasonDelegate getLeavingReason, + AddTouristVisitDelegate addTouristVisit, + DoRandomMoveDelegate doRandomMove, + FindEvacuationPlaceDelegate findEvacuationPlace, + FindVisitPlaceDelegate findVisitPlace, + GetEntertainmentReasonDelegate getEntertainmentReason, + GetEvacuationReasonDelegate getEvacuationReason, + GetShoppingReasonDelegate getShoppingReason, + StartMovingDelegate startMoving) + { + GetRandomTargetType = getRandomTargetType ?? throw new ArgumentNullException(nameof(getRandomTargetType)); + GetLeavingReason = getLeavingReason ?? throw new ArgumentNullException(nameof(getLeavingReason)); + AddTouristVisit = addTouristVisit ?? throw new ArgumentNullException(nameof(addTouristVisit)); + DoRandomMove = doRandomMove ?? throw new ArgumentNullException(nameof(doRandomMove)); + FindEvacuationPlace = findEvacuationPlace ?? throw new ArgumentNullException(nameof(findEvacuationPlace)); + FindVisitPlace = findVisitPlace ?? throw new ArgumentNullException(nameof(findVisitPlace)); + GetEntertainmentReason = getEntertainmentReason ?? throw new ArgumentNullException(nameof(getEntertainmentReason)); + GetEvacuationReason = getEvacuationReason ?? throw new ArgumentNullException(nameof(getEvacuationReason)); + GetShoppingReason = getShoppingReason ?? throw new ArgumentNullException(nameof(getShoppingReason)); + StartMoving = startMoving ?? throw new ArgumentNullException(nameof(startMoving)); + } + + public delegate int GetRandomTargetTypeDelegate(TAI instance, int doNothingProbability); + + public delegate TransferManager.TransferReason GetLeavingReasonDelegate(TAI instance, uint citizenId, ref TCitizen citizen); + + public delegate void AddTouristVisitDelegate(TAI instance, uint citizenId, ushort buildingId); + + public delegate bool DoRandomMoveDelegate(TAI instance); + + public delegate void FindEvacuationPlaceDelegate(TAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason); + + public delegate void FindVisitPlaceDelegate(TAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason); + + public delegate TransferManager.TransferReason GetEntertainmentReasonDelegate(TAI instance); + + public delegate TransferManager.TransferReason GetEvacuationReasonDelegate(TAI instance, ushort sourceBuilding); + + public delegate TransferManager.TransferReason GetShoppingReasonDelegate(TAI instance); + + public delegate bool StartMovingDelegate(TAI instance, uint citizenId, ref TCitizen citizen, ushort sourceBuilding, ushort targetBuilding); + + public GetRandomTargetTypeDelegate GetRandomTargetType { get; } + + public GetLeavingReasonDelegate GetLeavingReason { get; } + + public AddTouristVisitDelegate AddTouristVisit { get; } + + public DoRandomMoveDelegate DoRandomMove { get; } + + public FindEvacuationPlaceDelegate FindEvacuationPlace { get; } + + public FindVisitPlaceDelegate FindVisitPlace { get; } + + public GetEntertainmentReasonDelegate GetEntertainmentReason { get; } + + public GetEvacuationReasonDelegate GetEvacuationReason { get; } + + public GetShoppingReasonDelegate GetShoppingReason { get; } + + public StartMovingDelegate StartMoving { get; } + } +} \ No newline at end of file diff --git a/src/RealTime/GameConnection/TouristAIHook.cs b/src/RealTime/GameConnection/TouristAIHook.cs new file mode 100644 index 00000000..68a4f7d9 --- /dev/null +++ b/src/RealTime/GameConnection/TouristAIHook.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + using System; + using System.Runtime.CompilerServices; + using RealTime.CustomAI; + using Redirection; + + internal static class TouristAIHook + { + private const string RedirectNeededMessage = "This method must be redirected to the original game implementation"; + + internal static RealTimeTouristAI RealTimeAI { get; set; } + + internal static TouristAIConnection GetTouristAIConnection() + { + return new TouristAIConnection( + GetRandomTargetType, + GetLeavingReason, + AddTouristVisit, + DoRandomMove, + FindEvacuationPlace, + FindVisitPlace, + GetEntertainmentReason, + GetEvacuationReason, + GetShoppingReason, + StartMoving); + } + + [RedirectFrom(typeof(TouristAI))] + private static void UpdateLocation(TouristAI instance, uint citizenId, ref Citizen citizen) + { + RealTimeAI?.UpdateLocation(instance, citizenId, ref citizen); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static int GetRandomTargetType(TouristAI instance, int doNothingProbability) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(HumanAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetLeavingReason(TouristAI instance, uint citizenId, ref Citizen citizen) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddTouristVisit(TouristAI instance, uint citizenId, ushort buildingId) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void FindEvacuationPlace(TouristAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetEvacuationReason(TouristAI instance, ushort sourceBuilding) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void FindVisitPlace(TouristAI instance, uint citizenId, ushort sourceBuilding, TransferManager.TransferReason reason) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetShoppingReason(TouristAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool DoRandomMove(TouristAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(TouristAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static TransferManager.TransferReason GetEntertainmentReason(TouristAI instance) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + + [RedirectTo(typeof(HumanAI))] + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool StartMoving(TouristAI instance, uint citizenId, ref Citizen citizen, ushort sourceBuilding, ushort targetBuilding) + { + throw new InvalidOperationException(RedirectNeededMessage); + } + } +} diff --git a/src/RealTime/GameConnection/TransferManagerConnection.cs b/src/RealTime/GameConnection/TransferManagerConnection.cs new file mode 100644 index 00000000..ea88d381 --- /dev/null +++ b/src/RealTime/GameConnection/TransferManagerConnection.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection +{ + internal sealed class TransferManagerConnection : ITransferManagerConnection + { + public void AddOutgoingOfferFromCurrentPosition(uint citizenId, TransferManager.TransferReason reason) + { + if (citizenId == 0) + { + return; + } + + ushort instanceId = CitizenManager.instance.m_citizens.m_buffer[citizenId].m_instance; + if (instanceId == 0) + { + return; + } + + UnityEngine.Vector3 position = CitizenManager.instance.m_instances.m_buffer[instanceId].GetLastFramePosition(); + + TransferManager.TransferOffer offer = default; + offer.Priority = SimulationManager.instance.m_randomizer.Int32(8u); + offer.Citizen = citizenId; + offer.Position = position; + offer.Amount = 1; + offer.Active = true; + TransferManager.instance.AddOutgoingOffer(reason, offer); + } + } +} diff --git a/src/RealTime/Localization/Constants.cs b/src/RealTime/Localization/Constants.cs new file mode 100644 index 00000000..219aaf53 --- /dev/null +++ b/src/RealTime/Localization/Constants.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Localization +{ + internal static class Constants + { + public const string Placeholder = "Placeholder"; + public const string Tooltip = "Tooltip"; + + public const string NoLocale = "No Locale found"; + + public const string LocaleFolder = "Localization"; + public const string FileExtension = ".xml"; + + public const string XmlKeyAttribute = "id"; + public const string XmlValueAttribute = "value"; + } +} diff --git a/src/RealTime/Localization/LocalizationProvider.cs b/src/RealTime/Localization/LocalizationProvider.cs new file mode 100644 index 00000000..face2923 --- /dev/null +++ b/src/RealTime/Localization/LocalizationProvider.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Localization +{ + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Xml; + using ColossalFramework.Globalization; + using static Constants; + + internal sealed class LocalizationProvider + { + private readonly string localeStorage; + private readonly Dictionary translation = new Dictionary(); + + public LocalizationProvider(string rootPath) + { + localeStorage = Path.Combine(rootPath, LocaleFolder); + } + + public string CurrentLanguage { get; private set; } = "en"; + + public CultureInfo CurrentCulture { get; private set; } = CultureInfo.CurrentCulture; + + public string Translate(string id) + { + if (translation.TryGetValue(id, out string value)) + { + return value; + } + + return NoLocale; + } + + public void LoadTranslation(string language) + { + if (!Load(language)) + { + Load("en"); + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "No security issues here")] + private static string GetLocaleNameFromLanguage(string language) + { + switch (language.ToLowerInvariant()) + { + case "de": + return "de-DE"; + case "es": + return "es-ES"; + case "fr": + return "fr-FR"; + case "ko": + return "ko-KR"; + case "pl": + return "pl-PL"; + case "pt": + return "pt-PT"; + case "ru": + return "ru-RU"; + case "zh": + return "zh-CN"; + default: + return "en-US"; + } + } + + private bool Load(string language) + { + translation.Clear(); + + string path = Path.Combine(localeStorage, language + FileExtension); + if (!File.Exists(path)) + { + return false; + } + + try + { + var doc = new XmlDocument(); + doc.Load(path); + + foreach (XmlNode node in doc.DocumentElement.ChildNodes) + { + translation[node.Attributes[XmlKeyAttribute].Value] = node.Attributes[XmlValueAttribute].Value; + } + + try + { + CurrentCulture = new CultureInfo(GetLocaleNameFromLanguage(language)); + } + catch + { + CurrentCulture = LocaleManager.cultureInfo; + } + } + catch + { + translation.Clear(); + return false; + } + + CurrentLanguage = language; + return true; + } + } +} diff --git a/src/RealTime/Localization/Translations/de.xml b/src/RealTime/Localization/Translations/de.xml new file mode 100644 index 00000000..da418938 --- /dev/null +++ b/src/RealTime/Localization/Translations/de.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RealTime/Localization/Translations/en.xml b/src/RealTime/Localization/Translations/en.xml new file mode 100644 index 00000000..63b9569a --- /dev/null +++ b/src/RealTime/Localization/Translations/en.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/RealTime/Localization/Translations/ru.xml b/src/RealTime/Localization/Translations/ru.xml new file mode 100644 index 00000000..992f86ab --- /dev/null +++ b/src/RealTime/Localization/Translations/ru.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RealTime/Properties/AssemblyInfo.cs b/src/RealTime/Properties/AssemblyInfo.cs similarity index 77% rename from RealTime/Properties/AssemblyInfo.cs rename to src/RealTime/Properties/AssemblyInfo.cs index 801c6c3f..3dd05555 100644 --- a/RealTime/Properties/AssemblyInfo.cs +++ b/src/RealTime/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ // using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; [assembly: AssemblyTitle("RealTime")] @@ -10,8 +11,10 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("dymanoid")] [assembly: AssemblyProduct("RealTime")] -[assembly: AssemblyCopyright("Copyright © dymanoid 2018")] +[assembly: AssemblyCopyright("Copyright © 2018, dymanoid")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] + +[assembly: InternalsVisibleTo("RealTimeTests")] diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj new file mode 100644 index 00000000..e2ac4235 --- /dev/null +++ b/src/RealTime/RealTime.csproj @@ -0,0 +1,191 @@ + + + + + Debug + AnyCPU + {7CD7702C-E7D3-4E61-BF3A-B10F7950DE52} + Library + Properties + RealTime + RealTime + v3.5 + 512 + + + + + + true + ..\bin\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + ..\bin\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + + False + + + False + + + False + + + + + + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {7dcc08ef-dc85-47a4-bd6f-79fc52c7ef13} + Redirection + + + + + stylecop.json + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + if not exist "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" +xcopy /y /q /d "$(TargetPath)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" +xcopy /y /q /i /d "$(TargetDir)Localization\Translations" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)\Localization" +xcopy /y /q /i /d "$(ProjectDir)RealTimeEvents" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)\Events" + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/Aquarium.xml b/src/RealTime/RealTimeEvents/Aquarium.xml new file mode 100644 index 00000000..74596200 --- /dev/null +++ b/src/RealTime/RealTimeEvents/Aquarium.xml @@ -0,0 +1,82 @@ + + + + + + Can't wait to see the fish at the #aquarium in {0}! #event #fishhh + Wait what? The #aquarium has an event on in {0}? Count me in! #event + Excited to see the #dolphin whisperer at the Aquarium #event in {0} + FYI, tickets are almost sold out for the #event at the Aquarium in {0}. + + + + Yay! The #event is starting at the #aquarium! Time to stop #chirping for a while + That's a lot of fish! #aquarium #event + We're going to need a bigger boat... #aquarium #event + + + + Awwww, the event's over already? I was enjoying myself :( #event + So long and thanks for all the #fish! #event + + + + 80 + 80 + + 100 + 30 + 20 + 100 + 20 + + 10 + 100 + 70 + + 5 + 80 + 100 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 20000 + 15 + 5000 + 5000 + 40 + + + + + Allows citizens to buy food at the event. + 0 + 25 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows citizens to jump in and visit some of the fish. + 10 + 0 + + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/ArtMuseum.xml b/src/RealTime/RealTimeEvents/ArtMuseum.xml new file mode 100644 index 00000000..48d24223 --- /dev/null +++ b/src/RealTime/RealTimeEvents/ArtMuseum.xml @@ -0,0 +1,54 @@ + + + + + + Got my tickets to the #art exhibit in {0}! #first #event + One of my favourite artists is going to be at the #art exhibit in {0}! Can't wait! #event + Anyone want a spare ticket for the #art exhibit in {0}? I've got a few going. #event + I think I just got the last ticket for the #art exhibit in {0}. Feeling #lucky! #event + + + + And the doors have finally opened! Time to see some lovely #art. #event + Wow, they've certainly packed this place full. I'm never going to get around all of this! #event + I didn't event realise this city enjoyed #art as much as this! #event + + + + + + + 90 + 90 + + 5 + 40 + 100 + 100 + 40 + + 0 + 70 + 80 + + 0 + 40 + 100 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/ExpoCenter.xml b/src/RealTime/RealTimeEvents/ExpoCenter.xml new file mode 100644 index 00000000..42d6547c --- /dev/null +++ b/src/RealTime/RealTimeEvents/ExpoCenter.xml @@ -0,0 +1,413 @@ + + + + + + Anyone else going to the #business expo in {0}? I need someone to give me a #lift #event + If anyone's going to the #business expo in {0}, you better get tickets quick! #event + + + + Talk about a lot of stalls! I didn't even realise you could fit this many people into the Expo Center. #event + The #business expo has begun! #event + + + + + + + 70 + 70 + + 0 + 0 + 50 + 100 + 10 + + 0 + 20 + 100 + + 0 + 20 + 60 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 120000 + 20 + 5000 + 5000 + 100 + + + + + Allows citizens to buy food at the event. + 0 + 25 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows attendees to use WiFi in the event for a small cost. + 10 + 10 + + + Gives citizens free WiFi at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + + + + + Awww, there's only a #caravan expo going on in {0}... I was hoping for something more exciting. #event + + + + + + + + + + + 100 + 40 + + 0 + 0 + 0 + 20 + 100 + + 0 + 20 + 100 + + 0 + 20 + 60 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + + + YES! FINALLY! An actually exciting #event going on at the Expo Center. #comicbooks + + + + The doors have opened! #comicbooks #event + + + + Shame it closes so early. I still had tons of places to visit #comicbooks #event + + + + 80 + 20 + + 5 + 100 + 100 + 20 + 0 + + 0 + 80 + 100 + + 0 + 20 + 100 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 160000 + 20 + 5000 + 5000 + 100 + + + + + Allows citizens to buy food at the event. + 0 + 25 + + + Allows citizens to buy drinks at the event. + 0 + 50 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Gives citizens free drinks at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows attendees to use WiFi in the event for a small cost. + 10 + 10 + + + Gives citizens free WiFi at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows citizens to get their comic books signed by the authors. + 30 + 0 + + + + + + + An electronic expo?! In {0}?! Tickets please! #event + + + + I wonder how much all these #electronics cost... #event + + + + Awwww, the event's over already? I was enjoying myself :( #event + Got myself some awesome stuff! A new #chirper phone? Yes please! #event + + + + 80 + 60 + + 0 + 100 + 100 + 100 + 40 + + 0 + 80 + 100 + + 0 + 20 + 60 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 200000 + 10 + 5000 + 5000 + 120 + + + + + Allows citizens to buy food at the event. + 0 + 25 + + + Allows citizens to buy drinks at the event. + 0 + 50 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Gives citizens free drinks at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows attendees to use WiFi in the event for a small cost. + 10 + 10 + + + Gives citizens free WiFi at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows citizens to have the chance to win some electronics! + 50 + 0 + + + + + + + A #games expo? In my city? In {0}? Whaaaat? #event + Can't wait to see what companies are attending the #games expo in {0}. I wonder if the city building game will be there... #event + Got my tickets for the #game expo in {0}! So excited! #event + + + + So many #games! I'm so excited! #event + Can't wait to get my hands on some of the #VR equipment! #event + The games are so realistic this year! #event + + + + Over already? I can't believe it. I hope there's another soon. #event + + + + 80 + 30 + + 5 + 100 + 100 + 20 + 0 + + 0 + 80 + 100 + + 0 + 20 + 60 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 120000 + 30 + 5000 + 5000 + 110 + + + + + Allows citizens to buy food at the event. + 0 + 25 + + + Allows citizens to buy drinks at the event. + 0 + 50 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Gives citizens free drinks at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows attendees to use WiFi in the event for a small cost. + 10 + 10 + + + Gives citizens free WiFi at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows citizens to buy games! + 7 + 5 + + + Allows citizens to have the chance to win some games! + 50 + 0 + + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/Library.xml b/src/RealTime/RealTimeEvents/Library.xml new file mode 100644 index 00000000..5a3504be --- /dev/null +++ b/src/RealTime/RealTimeEvents/Library.xml @@ -0,0 +1,54 @@ + + + + + + Yes! My favourite author is in town doing book signings in {0}. Count me there. #event + Ooo, a book signing #event? Shame I've never heard of the author. It's on in {0} if anyone's interested. + If anyone wants to get their #book signed, the author's in town in {0} for a few hours. #event + + + + Next in queue to get my book signed! Can't wait. #event + Just got my book signed! How do I sell things online again? #event + + + + Well, that was fun while it lasted. #event + Didn't even manage to get my #book signed. Too many people :( #event + + + + 100 + 100 + + 0 + 80 + 100 + 60 + 1 + + 50 + 100 + 100 + + 100 + 100 + 100 + 100 + + 30 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/Opera.xml b/src/RealTime/RealTimeEvents/Opera.xml new file mode 100644 index 00000000..cc273ab6 --- /dev/null +++ b/src/RealTime/RealTimeEvents/Opera.xml @@ -0,0 +1,50 @@ + + + + + + Got my tickets to the #Opera in {0}. #event + + + + Phone's off, so I won't be replying any time soon. #opera #event + + + + That Opera was fantastic. I'd watch that again! #event + + + + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + 0 + 20 + 100 + + 0 + 20 + 60 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/PoshMall.xml b/src/RealTime/RealTimeEvents/PoshMall.xml new file mode 100644 index 00000000..275895d7 --- /dev/null +++ b/src/RealTime/RealTimeEvents/PoshMall.xml @@ -0,0 +1,55 @@ + + + + + + Yet another shop opening in {0}? How many more can they fit in there? #event + Finally! They're opening another shop! #event + + + + This #shop's pretty huge! It's going to take years to get around here. #event + Wow, and I thought I'd seen everything. #event + So... Expensive... #event + + + + Got sooo much stuff... #event + Ok... How am I going to get all this stuff home? Could someone pick me up? #shops #event + Well, that was #underwhelming... #event + + + + 60 + 100 + + 10 + 50 + 100 + 60 + 5 + + 0 + 10 + 100 + + 0 + 10 + 80 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/Stadium.xml b/src/RealTime/RealTimeEvents/Stadium.xml new file mode 100644 index 00000000..b9671626 --- /dev/null +++ b/src/RealTime/RealTimeEvents/Stadium.xml @@ -0,0 +1,89 @@ + + + + + + Yeahhh. My favourite team is playing at the #stadium in {0}. Hope I can get some tickets. #event + "Ahhh! Not another game at the #stadium. At least I know when to head out #shopping. {0}... #event + "Why is it never the team I want playing at the #stadium? Oh well, maybe next time. #event + + + + Kickoff! See you in 90 minutes. #stadium #event + + + + What a match! Can't wait until the next one. #stadium #event + See that ludicrous display? #walkitin #event + + + + 80 + 30 + + 30 + 90 + 100 + 100 + 60 + + 100 + 90 + 10 + + 100 + 100 + 50 + 10 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + 85000 + 15 + 5000 + 5000 + 60 + + + + + Allows citizens to buy beer at the event. + 0 + 16 + + + Allows citizens to buy food at the event. + 0 + 25 + + + Gives citizens free beer at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Gives citizens free food at the event. Costs more to purchase, and there's no return on your investment. + 30 + 0 + + + Allows citizens to meet with some of the players after the match. + 10 + 0 + + + + + \ No newline at end of file diff --git a/src/RealTime/RealTimeEvents/Theater.xml b/src/RealTime/RealTimeEvents/Theater.xml new file mode 100644 index 00000000..987b2979 --- /dev/null +++ b/src/RealTime/RealTimeEvents/Theater.xml @@ -0,0 +1,50 @@ + + + + + + Anyone want to go to the #theater with me in {0}? I've got some free time. #event + + + + Wow, this really is a theater of wonders! #event + + + + That was awesome! #event + + + + 100 + 100 + + 20 + 40 + 100 + 100 + 60 + + 0 + 60 + 100 + + 0 + 20 + 50 + 100 + + 0 + 30 + 80 + 100 + 100 + + 0 + 10 + 50 + 100 + 100 + + + + \ No newline at end of file diff --git a/src/RealTime/Simulation/DayTime.cs b/src/RealTime/Simulation/DayTime.cs new file mode 100644 index 00000000..045e2dab --- /dev/null +++ b/src/RealTime/Simulation/DayTime.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + using UnityEngine; + + /// + /// Calculates the sunrise and sunset time based on the map latitude and current date. + /// + internal sealed class DayTime + { + private float phase; + private float halfAmplitude; + + /// + /// Gets a value indicating whether this object has been properly set up + /// and can be used for the calculations. + /// + public bool IsReady { get; private set; } + + /// + /// Sets up this object so that it can correctly perform the sunrise and sunset time + /// calculations. + /// + /// + /// The latitude coordinate to assume for the city. + /// Valid values are -80° to 80°. + public void Setup(float latitude) + { + bool southSemisphere = latitude < 0; + + latitude = Mathf.Clamp(Mathf.Abs(latitude), 0f, 80f); + halfAmplitude = (0.5f + (latitude / 15f)) / 2f; + + phase = southSemisphere ? 0f : (float)Math.PI; + + IsReady = true; + } + + /// + /// Calculates the sunrise and sunset hours for the provided . + /// If this object is not properly set up yet (so returns false), + /// then the out values will be initialized with default empty s. + /// + /// + /// The game date to calculate the sunrise and sunset times for. + /// The calculated sunrise hour (relative to the midnight). + /// The calculated sunset hour (relative to the midnight). + /// + /// True when the values are successfully calculated; otherwise, false. + public bool Calculate(DateTime date, out float sunriseHour, out float sunsetHour) + { + if (!IsReady) + { + sunriseHour = default; + sunsetHour = default; + return false; + } + + float modifier = (float)Math.Cos((2 * Math.PI * (date.DayOfYear + 10) / 365.25) + phase); + sunriseHour = 6f - (halfAmplitude * modifier); + sunsetHour = 18f + (halfAmplitude * modifier); + return true; + } + } +} diff --git a/src/RealTime/Simulation/DaylightTimeSimulation.cs b/src/RealTime/Simulation/DaylightTimeSimulation.cs new file mode 100644 index 00000000..84e477fe --- /dev/null +++ b/src/RealTime/Simulation/DaylightTimeSimulation.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + using ICities; + + /// + /// A simulation extension that manages the daytime calculation (sunrise and sunset). + /// + public sealed class DaylightTimeSimulation : ThreadingExtensionBase + { + private readonly DayTime dayTime; + private DateTime lastCalculation; + + /// + /// Initializes a new instance of the class. + /// + public DaylightTimeSimulation() + { + dayTime = new DayTime(); + } + + /// + /// Occurs whent a new day in the game begins. + /// + internal static event EventHandler NewDay; + + /// + /// Called after each game simulation tick. Performs the actual work. + /// + public override void OnAfterSimulationTick() + { + if (!dayTime.IsReady) + { + if (DayNightProperties.instance != null) + { + dayTime.Setup(DayNightProperties.instance.m_Latitude); + } + else + { + return; + } + } + + DateTime currentDate = SimulationManager.instance.m_currentGameTime.Date; + if (currentDate != lastCalculation) + { + CalculateDaylight(currentDate); + OnNewDay(this); + } + } + + private static void OnNewDay(DaylightTimeSimulation sender) + { + NewDay?.Invoke(sender, EventArgs.Empty); + } + + private void CalculateDaylight(DateTime date) + { + if (!dayTime.Calculate(date, out float sunriseHour, out float sunsetHour)) + { + return; + } + + SimulationManager.SUNRISE_HOUR = sunriseHour; + SimulationManager.SUNSET_HOUR = sunsetHour; + lastCalculation = date.Date; + } + } +} diff --git a/src/RealTime/Simulation/ITimeInfo.cs b/src/RealTime/Simulation/ITimeInfo.cs new file mode 100644 index 00000000..10696d5e --- /dev/null +++ b/src/RealTime/Simulation/ITimeInfo.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + + internal interface ITimeInfo + { + DateTime Now { get; } + + float CurrentHour { get; } + + float SunriseHour { get; } + + float SunsetHour { get; } + + bool IsNightTime { get; } + + float DayDuration { get; } + + float NightDuration { get; } + } +} diff --git a/src/RealTime/Simulation/TimeAdjustment.cs b/src/RealTime/Simulation/TimeAdjustment.cs new file mode 100644 index 00000000..8e1e09a9 --- /dev/null +++ b/src/RealTime/Simulation/TimeAdjustment.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + using RealTime.Tools; + + /// + /// Manages the customized time adjustment. This class depends on the class. + /// + internal sealed class TimeAdjustment + { + private const int CustomFramesPerDay = 1 << 17; + private static readonly TimeSpan CustomTimePerFrame = new TimeSpan(24L * 3600L * 10_000_000L / CustomFramesPerDay); + + private readonly uint vanillaFramesPerDay; + private readonly TimeSpan vanillaTimePerFrame; + + /// + /// Initializes a new instance of the class. + /// + public TimeAdjustment() + { + vanillaFramesPerDay = SimulationManager.DAYTIME_FRAMES; + vanillaTimePerFrame = SimulationManager.instance.m_timePerFrame; + } + + /// + /// Enables the customized time adjustment. + /// + /// + /// The current game date and time. + public DateTime Enable() + { + if (vanillaTimePerFrame == CustomTimePerFrame) + { + Log.Warning("The 'Real Time' mod has not been properly deactivated! Check the TimeAdjustment.Disable() calls."); + } + + return UpdateTimeSimulationValues(CustomFramesPerDay, CustomTimePerFrame); + } + + /// + /// Disables the customized time adjustment restoring the default vanilla values. + /// + public void Disable() + { + UpdateTimeSimulationValues(vanillaFramesPerDay, vanillaTimePerFrame); + } + + private static DateTime UpdateTimeSimulationValues(uint framesPerDay, TimeSpan timePerFrame) + { + SimulationManager sm = SimulationManager.instance; + DateTime originalDate = sm.m_ThreadingWrapper.simulationTime; + + SimulationManager.DAYTIME_FRAMES = framesPerDay; + SimulationManager.DAYTIME_FRAME_TO_HOUR = 24f / SimulationManager.DAYTIME_FRAMES; + SimulationManager.DAYTIME_HOUR_TO_FRAME = SimulationManager.DAYTIME_FRAMES / 24f; + + sm.m_timePerFrame = timePerFrame; + sm.m_timeOffsetTicks = originalDate.Ticks - (sm.m_currentFrameIndex * sm.m_timePerFrame.Ticks); + sm.m_currentGameTime = originalDate; + + sm.m_currentDayTimeHour = (float)sm.m_currentGameTime.TimeOfDay.TotalHours; + sm.m_dayTimeFrame = (uint)(SimulationManager.DAYTIME_FRAMES * sm.m_currentDayTimeHour / 24f); + sm.m_dayTimeOffsetFrames = sm.m_dayTimeFrame - sm.m_currentFrameIndex & SimulationManager.DAYTIME_FRAMES - 1; + + return sm.m_currentGameTime; + } + } +} diff --git a/src/RealTime/Tools/DateTimeExtensions.cs b/src/RealTime/Tools/DateTimeExtensions.cs new file mode 100644 index 00000000..4c9d5c3a --- /dev/null +++ b/src/RealTime/Tools/DateTimeExtensions.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + using System; + + /// + /// A class containing various extension methods for the struct. + /// + internal static class DateTimeExtensions + { + /// + /// Determines whether the 's day of week is Saturday or Sunday. + /// + /// + /// The to check. + /// + /// True if the 's day of week value is Saturday or Sunday; + /// otherwise, false. + public static bool IsWeekend(this DateTime dateTime) + { + return dateTime.DayOfWeek == DayOfWeek.Saturday || dateTime.DayOfWeek == DayOfWeek.Sunday; + } + + public static bool IsWeekendTime(this DateTime dateTime, float fridayStartHour, float sundayEndHour) + { + switch (dateTime.DayOfWeek) + { + case DayOfWeek.Friday when dateTime.Hour >= fridayStartHour: + case DayOfWeek.Saturday: + case DayOfWeek.Sunday when dateTime.Hour < sundayEndHour: + return true; + + default: + return false; + } + } + + public static DateTime RoundCeil(this DateTime dateTime, TimeSpan interval) + { + long overflow = dateTime.Ticks % interval.Ticks; + return overflow == 0 ? dateTime : dateTime.AddTicks(interval.Ticks - overflow); + } + } +} diff --git a/src/RealTime/Tools/GitVersion.cs b/src/RealTime/Tools/GitVersion.cs new file mode 100644 index 00000000..0bd092cd --- /dev/null +++ b/src/RealTime/Tools/GitVersion.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + using System; + using System.Reflection; + + /// + /// A helper class that interacts with the special types injected by the GitVersion toolset. + /// + internal static class GitVersion + { + private const string GitVersionTypeName = ".GitVersionInformation"; + private const string VersionFieldName = "FullSemVer"; + + /// + /// Gets a string representation of the full semantic assembly version of the provided . + /// This assembly should be built using the GitVersion toolset; otherwise, a "?" version string will + /// be returned. + /// + /// + /// Thrown when the argument is null. + /// + /// An to get the version of. Should be built using the GitVersion toolset. + /// + /// A string representation of the full semantic version of the provided , + /// or "?" if the version could not be determined. + public static string GetAssemblyVersion(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + Type gitVersionInformationType = assembly.GetType(assembly.GetName().Name + GitVersionTypeName); + if (gitVersionInformationType == null) + { + Log.Error("Attempting to retrieve the assembly version of an assembly that is built without GitVersion support."); + return "?"; + } + + FieldInfo versionField = gitVersionInformationType.GetField(VersionFieldName); + if (versionField == null) + { + Log.Error($"Internal error: the '{GitVersionTypeName}' type has no field '{VersionFieldName}'."); + return "?"; + } + + string version = versionField.GetValue(null) as string; + if (string.IsNullOrEmpty(version)) + { + Log.Warning($"The '{GitVersionTypeName}.{VersionFieldName}' value is empty."); + return "?"; + } + + return version; + } + } +} diff --git a/src/RealTime/Tools/LinkedListExtensions.cs b/src/RealTime/Tools/LinkedListExtensions.cs new file mode 100644 index 00000000..0d55340d --- /dev/null +++ b/src/RealTime/Tools/LinkedListExtensions.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + using System; + using System.Collections.Generic; + + internal static class LinkedListExtensions + { + public static LinkedListNode FirstOrDefaultNode(this LinkedList list, Predicate predicate) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (list.Count == 0) + { + return null; + } + + LinkedListNode node = list.First; + while (node != null) + { + if (predicate(node.Value)) + { + return node; + } + + node = node.Next; + } + + return null; + } + } +} diff --git a/src/RealTime/Tools/Log.cs b/src/RealTime/Tools/Log.cs new file mode 100644 index 00000000..b46bb682 --- /dev/null +++ b/src/RealTime/Tools/Log.cs @@ -0,0 +1,174 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Text; + using System.Timers; + + /// + /// Manages the logging. In 'Release' mode, only logs to the Unity's debug log. + /// Also, the method calls will be eliminated in this mode. + /// In 'Debug' mode logs additionaly to a text file that is located on the Desktop. + /// + internal static class Log + { +#if DEBUG + private const int FileWriteInterval = 1000; // ms + private const string LogFileName = "RealTime.log"; + private const string TypeDebug = "DBG"; + private const string TypeInfo = "INF"; + private const string TypeWarning = "WRN"; + private const string TypeError = "ERR"; + + private static readonly object SyncObject = new object(); + private static readonly Queue Storage = new Queue(); + + private static readonly Timer FlushTimer = new Timer(FileWriteInterval) { AutoReset = false }; + private static readonly string LogFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), LogFileName); + + // Note: the official Unity 5 docs state that the ThreadStaticAttribute will cause the engine to crash. + // However, this doesn't occur on my system. Anyway, this is only compiled in debug builds and won't affect the mod users. + [ThreadStatic] + private static StringBuilder messageBuilder; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Special debug configuration")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Debug build only")] + static Log() + { + FlushTimer.Elapsed += FlushTimer_Elapsed; + FlushTimer.Start(); + } +#endif + + /// + /// Logs a debug information. This method won't be compiled in the 'Release' mode. + /// + /// + /// The text to log. + [Conditional("DEBUG")] + public static void Debug(string text) + { +#if DEBUG + DebugLog(text, TypeDebug); +#endif + } + + /// + /// Logs a debug information. This method won't be compiled in the 'Release' mode. + /// + /// + /// The current date and time in the game. + /// The text to log. + [Conditional("DEBUG")] + public static void Debug(DateTime gameTime, string text) + { +#if DEBUG + DebugLog(gameTime.ToString("dd.MM.yy HH:mm") + " --> " + text, TypeDebug); +#endif + } + + /// + /// Logs an information text. + /// + /// + /// The text to log. + public static void Info(string text) + { + UnityEngine.Debug.Log(text); + +#if DEBUG + DebugLog(text, TypeInfo); +#endif + } + + /// + /// Logs a warning text. + /// + /// + /// The text to log. + public static void Warning(string text) + { + UnityEngine.Debug.LogWarning(text); + +#if DEBUG + DebugLog(text, TypeWarning); +#endif + } + + /// + /// Logs an error text. + /// + /// + /// The text to log. + public static void Error(string text) + { + UnityEngine.Debug.LogError(text); + +#if DEBUG + DebugLog(text, TypeError); +#endif + } + +#if DEBUG + private static void DebugLog(string text, string type) + { + if (messageBuilder == null) + { + messageBuilder = new StringBuilder(1024); + } + + messageBuilder.Length = 0; + messageBuilder.Append(DateTime.Now.ToString("HH:mm:ss.ffff")); + messageBuilder.Append('\t'); + messageBuilder.Append(type); + messageBuilder.Append("\t\t"); + messageBuilder.Append(text); + string message = messageBuilder.ToString(); + lock (SyncObject) + { + Storage.Enqueue(message); + } + } + + private static void FlushTimer_Elapsed(object sender, ElapsedEventArgs e) + { + List storageCopy; + lock (SyncObject) + { + if (Storage.Count == 0) + { + FlushTimer.Start(); + return; + } + + storageCopy = Storage.ToList(); + Storage.Clear(); + } + + try + { + using (StreamWriter writer = File.AppendText(LogFilePath)) + { + foreach (string line in storageCopy) + { + writer.WriteLine(line); + } + } + } + catch (Exception ex) + { + UnityEngine.Debug.LogError("Error writing to the log file: " + ex.Message); + } + + FlushTimer.Start(); + } +#endif + } +} diff --git a/src/RealTime/UI/ConfigItemAttribute.cs b/src/RealTime/UI/ConfigItemAttribute.cs new file mode 100644 index 00000000..b3b9d540 --- /dev/null +++ b/src/RealTime/UI/ConfigItemAttribute.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + internal sealed class ConfigItemAttribute : Attribute + { + public ConfigItemAttribute(string groupId, uint order) + { + if (string.IsNullOrEmpty(groupId)) + { + throw new ArgumentException("The config group ID cannot be null or an empty string"); + } + + GroupId = groupId; + Order = order; + } + + public string GroupId { get; } + + public uint Order { get; } + } +} diff --git a/src/RealTime/UI/ConfigItemCheckBoxAttribute.cs b/src/RealTime/UI/ConfigItemCheckBoxAttribute.cs new file mode 100644 index 00000000..28e2fd1d --- /dev/null +++ b/src/RealTime/UI/ConfigItemCheckBoxAttribute.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + internal sealed class ConfigItemCheckBoxAttribute : ConfigItemUIBaseAttribute + { + } +} diff --git a/src/RealTime/UI/ConfigItemSliderAttribute.cs b/src/RealTime/UI/ConfigItemSliderAttribute.cs new file mode 100644 index 00000000..cf7da106 --- /dev/null +++ b/src/RealTime/UI/ConfigItemSliderAttribute.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + internal sealed class ConfigItemSliderAttribute : ConfigItemUIBaseAttribute + { + public ConfigItemSliderAttribute(float min, float max, float step, SliderValueType valueType) + { + if (max <= min) + { + throw new ArgumentException("The maximum value must be greater than the minimum value"); + } + + if (step == 0) + { + throw new ArgumentException("The step value must be greater than 0"); + } + + Min = min; + Max = max; + Step = step; + ValueType = valueType; + } + + public ConfigItemSliderAttribute(float min, float max) + : this(min, max, 1f, SliderValueType.Percentage) + { + } + + public float Min { get; } + + public float Max { get; } + + public float Step { get; } + + public SliderValueType ValueType { get; } + } +} diff --git a/src/RealTime/UI/ConfigItemUIBaseAttribute.cs b/src/RealTime/UI/ConfigItemUIBaseAttribute.cs new file mode 100644 index 00000000..89b034ec --- /dev/null +++ b/src/RealTime/UI/ConfigItemUIBaseAttribute.cs @@ -0,0 +1,12 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + + internal abstract class ConfigItemUIBaseAttribute : Attribute + { + } +} diff --git a/src/RealTime/UI/ConfigUI.cs b/src/RealTime/UI/ConfigUI.cs new file mode 100644 index 00000000..234f2738 --- /dev/null +++ b/src/RealTime/UI/ConfigUI.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using RealTime.Localization; + + internal sealed class ConfigUI + { + private readonly IEnumerable viewItems; + + private ConfigUI(IEnumerable viewItems) + { + this.viewItems = viewItems; + } + + public static ConfigUI Create(object config, IViewItemFactory itemFactory) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (itemFactory == null) + { + throw new ArgumentNullException(nameof(itemFactory)); + } + + var properties = config.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => new { Property = p, Attribute = GetCustomItemAttribute(p) }) + .Where(v => v.Attribute != null); + + var viewItems = new List(); + + foreach (var group in properties.GroupBy(p => p.Attribute.GroupId).OrderBy(p => p.Key)) + { + IContainerViewItem groupItem = itemFactory.CreateGroup(group.Key); + viewItems.Add(groupItem); + + foreach (var item in group.OrderBy(i => i.Attribute.Order)) + { + IViewItem viewItem = CreateViewItem(groupItem, item.Property, config, itemFactory); + if (viewItem != null) + { + viewItems.Add(viewItem); + } + } + } + + return new ConfigUI(viewItems); + } + + public void Translate(LocalizationProvider localizationProvider) + { + foreach (IViewItem item in viewItems) + { + item.Translate(localizationProvider); + } + } + + private static IViewItem CreateViewItem(IContainerViewItem container, PropertyInfo property, object config, IViewItemFactory itemFactory) + { + switch (GetCustomItemAttribute(property)) + { + case ConfigItemSliderAttribute slider when property.PropertyType.IsPrimitive: + if (property.PropertyType == typeof(bool) || property.PropertyType == typeof(IntPtr) + || property.PropertyType == typeof(UIntPtr) || property.PropertyType == typeof(char)) + { + goto default; + } + + return itemFactory.CreateSlider(container, property.Name, property, config, slider.Min, slider.Max, slider.Step, slider.ValueType); + + case ConfigItemCheckBoxAttribute _ when property.PropertyType == typeof(bool): + return itemFactory.CreateCheckBox(container, property.Name, property, config); + + default: + return null; + } + } + + private static T GetCustomItemAttribute(PropertyInfo property, bool inherit = false) + where T : Attribute + { + return (T)property.GetCustomAttributes(typeof(T), inherit).FirstOrDefault(); + } + } +} diff --git a/src/RealTime/UI/CustomTimeBar.cs b/src/RealTime/UI/CustomTimeBar.cs new file mode 100644 index 00000000..d259feea --- /dev/null +++ b/src/RealTime/UI/CustomTimeBar.cs @@ -0,0 +1,303 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Reflection; + using ColossalFramework.UI; + using RealTime.Events; + using RealTime.Tools; + using UnityEngine; + + /// + /// Manages the time bar customization. The customized time bar will show the day of the week + /// and the current time instead of the date. The date will be displayed in the time bar's tooltip. + /// + internal sealed class CustomTimeBar + { + private const string UIInfoPanel = "InfoPanel"; + private const string UIWrapperField = "m_GameTime"; + private const string UIPanelTime = "PanelTime"; + private const string UISpriteProgress = "Sprite"; + private const string UILabelTime = "Time"; + private const string UISpriteEvent = "Event"; + + private static readonly Color32 EventColor = new Color32(180, 0, 90, 160); + + private readonly List displayedEvents = new List(); + + private CultureInfo currentCulture = CultureInfo.CurrentCulture; + private RealTimeUIDateTimeWrapper customDateTimeWrapper; + private UIDateTimeWrapper originalWrapper; + private UISprite progressSprite; + + public event EventHandler CityEventClick; + + /// + /// Enables the time bar customization. If the customization is already enabled, has no effect. + /// + /// + /// The current game date to set as the time bar's initial value. + public void Enable(DateTime currentDate) + { + if (originalWrapper != null) + { + Log.Warning("Trying to enable the CustomTimeBar multiple times."); + return; + } + + customDateTimeWrapper = new RealTimeUIDateTimeWrapper(currentDate); + originalWrapper = SetUIDateTimeWrapper(customDateTimeWrapper, true); + } + + /// + /// Disables the time bar configuration. If the customization is already disabled or was not enabled, + /// has no effect. + /// + public void Disable() + { + if (originalWrapper == null) + { + return; + } + + RemoveAllCityEvents(); + SetUIDateTimeWrapper(originalWrapper, false); + originalWrapper = null; + progressSprite = null; + customDateTimeWrapper = null; + } + + public void Translate(CultureInfo cultureInfo) + { + currentCulture = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); + customDateTimeWrapper.Translate(cultureInfo); + TranslateTooltip(progressSprite, cultureInfo); + + DateTime todayStart = customDateTimeWrapper.CurrentValue.Date; + DateTime todayEnd = todayStart.AddDays(1).AddMilliseconds(-1); + foreach (UISprite item in progressSprite.components.Where(c => c.name != null && c.name.StartsWith(UISpriteEvent, StringComparison.Ordinal))) + { + SetEventTooltip(item, todayStart, todayEnd); + } + } + + public void UpdateEventsDisplay(IEnumerable availableEvents) + { + DateTime todayStart = customDateTimeWrapper.CurrentValue.Date; + DateTime todayEnd = todayStart.AddDays(1).AddMilliseconds(-1); + + var eventsToDisplay = availableEvents + .Where(e => (e.StartTime >= todayStart && e.StartTime <= todayEnd) || (e.EndTime >= todayStart && e.EndTime <= todayEnd)) + .ToList(); + + if (displayedEvents.SequenceEqual(eventsToDisplay)) + { + return; + } + + RemoveAllCityEvents(); + + if (progressSprite == null) + { + return; + } + + displayedEvents.AddRange(eventsToDisplay); + foreach (ICityEvent cityEvent in displayedEvents) + { + DisplayCityEvent(cityEvent, todayStart, todayEnd); + } + } + + private static UIDateTimeWrapper ReplaceUIDateTimeWrapperInPanel(UIPanel infoPanel, UIDateTimeWrapper wrapper) + { + Bindings bindings = infoPanel.GetUIView().GetComponent(); + if (bindings == null) + { + Log.Warning($"The UIPanel '{UIInfoPanel}' contains no '{nameof(Bindings)}' component"); + return null; + } + + FieldInfo field = typeof(Bindings).GetField(UIWrapperField, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance); + if (field == null) + { + Log.Warning($"The UIPanel {UIInfoPanel} has no field '{UIWrapperField}'"); + return null; + } + + var originalWrapper = field.GetValue(bindings) as UIDateTimeWrapper; + if (originalWrapper == null) + { + Log.Warning($"The '{nameof(Bindings)}' component has no '{nameof(UIDateTimeWrapper)}'"); + return null; + } + + field.SetValue(bindings, wrapper); + return originalWrapper; + } + + private static UISprite GetProgressSprite(UIPanel infoPanel) + { + UIPanel panelTime = infoPanel.Find(UIPanelTime); + if (panelTime == null) + { + Log.Warning("No UIPanel found: " + UIPanelTime); + return null; + } + + UISprite progressSprite = panelTime.Find(UISpriteProgress); + if (progressSprite == null) + { + Log.Warning("No UISprite found: " + UISpriteProgress); + return null; + } + + return progressSprite; + } + + private static void CustomizeTimePanel(UISprite progressSprite) + { + UILabel dateLabel = progressSprite.Find(UILabelTime); + if (dateLabel == null) + { + Log.Warning("No UILabel found: " + UILabelTime); + return; + } + + dateLabel.autoSize = false; + dateLabel.size = progressSprite.size; + dateLabel.textAlignment = UIHorizontalAlignment.Center; + dateLabel.relativePosition = new Vector3(0, 0, 0); + } + + private static void SetTooltip(UIComponent component, CultureInfo cultureInfo, bool customize) + { + DateTooltipBehavior tooltipBehavior = component.gameObject.GetComponent(); + if (tooltipBehavior == null && customize) + { + tooltipBehavior = component.gameObject.AddComponent(); + tooltipBehavior.IgnoredComponentNamePrefix = UISpriteEvent; + tooltipBehavior.Translate(cultureInfo); + } + else if (tooltipBehavior != null && !customize) + { + UnityEngine.Object.Destroy(tooltipBehavior); + } + } + + private static void TranslateTooltip(UIComponent tooltipParent, CultureInfo cultureInfo) + { + DateTooltipBehavior tooltipBehavior = tooltipParent.gameObject.GetComponent(); + if (tooltipBehavior != null) + { + tooltipBehavior.Translate(cultureInfo); + } + } + + private UIDateTimeWrapper SetUIDateTimeWrapper(UIDateTimeWrapper wrapper, bool customize) + { + UIPanel infoPanel = UIView.Find(UIInfoPanel); + if (infoPanel == null) + { + Log.Warning("No UIPanel found: " + UIInfoPanel); + return null; + } + + progressSprite = GetProgressSprite(infoPanel); + if (progressSprite != null) + { + SetTooltip(progressSprite, currentCulture, customize); + + if (customize) + { + CustomizeTimePanel(progressSprite); + } + } + + return ReplaceUIDateTimeWrapperInPanel(infoPanel, wrapper); + } + + private void DisplayCityEvent(ICityEvent cityEvent, DateTime todayStart, DateTime todayEnd) + { + float startPercent = cityEvent.StartTime <= todayStart + ? startPercent = 0 + : (float)cityEvent.StartTime.TimeOfDay.TotalHours / 24f; + + float endPercent = cityEvent.EndTime >= todayEnd + ? endPercent = 1f + : (float)cityEvent.EndTime.TimeOfDay.TotalHours / 24f; + + float startPosition = progressSprite.width * startPercent; + float endPosition = progressSprite.width * endPercent; + + UISprite eventSprite = progressSprite.AddUIComponent(); + eventSprite.name = UISpriteEvent + cityEvent.BuildingId; + eventSprite.relativePosition = new Vector3(startPosition, 0); + eventSprite.atlas = progressSprite.atlas; + eventSprite.spriteName = progressSprite.spriteName; + eventSprite.height = progressSprite.height; + eventSprite.width = endPosition - startPosition; + eventSprite.fillDirection = UIFillDirection.Horizontal; + eventSprite.color = EventColor; + eventSprite.fillAmount = 1f; + eventSprite.objectUserData = cityEvent; + eventSprite.eventClicked += EventSprite_Clicked; + SetEventTooltip(eventSprite, todayStart, todayEnd); + } + + private void SetEventTooltip(UISprite eventSprite, DateTime todayStart, DateTime todayEnd) + { + if (eventSprite.objectUserData is ICityEvent cityEvent) + { + string startString = cityEvent.StartTime <= todayStart + ? cityEvent.StartTime.ToString(currentCulture) + : cityEvent.StartTime.ToString("t", currentCulture); + + string endString = cityEvent.EndTime >= todayEnd + ? cityEvent.EndTime.ToString(currentCulture) + : cityEvent.EndTime.ToString("t", currentCulture); + + eventSprite.tooltip = $"{cityEvent.BuildingName} ({startString} - {endString})"; + } + } + + private void RemoveAllCityEvents() + { + if (progressSprite == null) + { + return; + } + + foreach (ICityEvent cityEvent in displayedEvents) + { + UISprite sprite = progressSprite.Find(UISpriteEvent + cityEvent.BuildingId); + if (sprite != null) + { + sprite.eventClicked -= EventSprite_Clicked; + UnityEngine.Object.Destroy(sprite); + } + } + + displayedEvents.Clear(); + } + + private void EventSprite_Clicked(UIComponent component, UIMouseEventParameter eventParam) + { + if (component.objectUserData is ICityEvent cityEvent) + { + OnCityEventClick(cityEvent.BuildingId); + } + } + + private void OnCityEventClick(ushort buildingId) + { + CityEventClick?.Invoke(this, new CustomTimeBarClickEventArgs(buildingId)); + } + } +} diff --git a/src/RealTime/UI/CustomTimeBarClickEventArgs.cs b/src/RealTime/UI/CustomTimeBarClickEventArgs.cs new file mode 100644 index 00000000..e6439f63 --- /dev/null +++ b/src/RealTime/UI/CustomTimeBarClickEventArgs.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + + internal sealed class CustomTimeBarClickEventArgs : EventArgs + { + public CustomTimeBarClickEventArgs(ushort cityEventBuildingId) + { + CityEventBuildingId = cityEventBuildingId; + } + + public ushort CityEventBuildingId { get; } + } +} diff --git a/src/RealTime/UI/DateTooltipBehavior.cs b/src/RealTime/UI/DateTooltipBehavior.cs new file mode 100644 index 00000000..e52a1c8a --- /dev/null +++ b/src/RealTime/UI/DateTooltipBehavior.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Tools +{ + using System; + using System.Globalization; + using ColossalFramework.UI; + using UnityEngine; + + /// + /// A script that can be attached to any . + /// Observes the value and sets the tooltip + /// of the to the date part of that value. The current + /// is used for string conversion. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated by Unity Engine")] + internal sealed class DateTooltipBehavior : MonoBehaviour + { + private UIComponent target; + private DateTime lastValue; + private string tooltip; + private CultureInfo currentCulture = CultureInfo.CurrentCulture; + + public string IgnoredComponentNamePrefix { get; set; } + + public void Translate(CultureInfo cultureInfo) + { + currentCulture = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); + } + + /// + /// is called on the frame when a script is enabled + /// just before any of the methods are called the first time. + /// + public void Start() + { + target = gameObject.GetComponent(); + } + + /// + /// is called every frame, if the is enabled. + /// + public void Update() + { + if (target == null) + { + return; + } + + DateTime newValue = SimulationManager.instance.m_currentGameTime; + if (lastValue.Date != newValue.Date) + { + tooltip = newValue.ToString("d", currentCulture); + } + + lastValue = newValue; + + if (!target.containsMouse) + { + return; + } + + if (!string.IsNullOrEmpty(IgnoredComponentNamePrefix)) + { + UIComponent hovered = UIInput.hoveredComponent; + if (hovered != null && hovered.name != null && hovered.name.StartsWith(IgnoredComponentNamePrefix, StringComparison.Ordinal)) + { + return; + } + } + + target.tooltip = tooltip; + target.RefreshTooltip(); + } + } +} diff --git a/src/RealTime/UI/IContainerViewItem.cs b/src/RealTime/UI/IContainerViewItem.cs new file mode 100644 index 00000000..771c02f2 --- /dev/null +++ b/src/RealTime/UI/IContainerViewItem.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using ICities; + + internal interface IContainerViewItem : IViewItem + { + UIHelperBase Container { get; } + } +} diff --git a/src/RealTime/UI/IViewItem.cs b/src/RealTime/UI/IViewItem.cs new file mode 100644 index 00000000..bde2e6f5 --- /dev/null +++ b/src/RealTime/UI/IViewItem.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using RealTime.Localization; + + internal interface IViewItem + { + void Translate(LocalizationProvider localizationProvider); + } +} diff --git a/src/RealTime/UI/IViewItemFactory.cs b/src/RealTime/UI/IViewItemFactory.cs new file mode 100644 index 00000000..0c9d2565 --- /dev/null +++ b/src/RealTime/UI/IViewItemFactory.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System.Reflection; + + internal interface IViewItemFactory + { + IContainerViewItem CreateGroup(string id); + + IViewItem CreateCheckBox(IContainerViewItem container, string id, PropertyInfo property, object config); + + IViewItem CreateSlider( + IContainerViewItem container, + string id, + PropertyInfo property, + object config, + float min, + float max, + float step, + SliderValueType valueType); + } +} diff --git a/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs b/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs new file mode 100644 index 00000000..f943df0d --- /dev/null +++ b/src/RealTime/UI/RealTimeUIDateTimeWrapper.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Globalization; + + /// + /// A wrapper that converts a value to a string representation + /// containing the day of week and the time parts. The current + /// is used for string conversion. + /// + public sealed class RealTimeUIDateTimeWrapper : UIDateTimeWrapper + { + private CultureInfo currentCulture = CultureInfo.CurrentCulture; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The initial value to use for conversion. + internal RealTimeUIDateTimeWrapper(DateTime initial) + : base(initial) + { + CurrentValue = initial; + Convert(); + } + + public DateTime CurrentValue { get; private set; } + + /// + /// Checks the provided value whether it should be converted to a + /// string representation. Converts the value when necessary. + /// + /// + /// The value to process. + public override void Check(DateTime newVal) + { + if (m_Value.Minute == newVal.Minute && m_Value.Hour == newVal.Hour && m_Value.DayOfWeek == newVal.DayOfWeek) + { + return; + } + + m_Value = newVal; + CurrentValue = newVal; + Convert(); + } + + public void Translate(CultureInfo cultureInfo) + { + currentCulture = cultureInfo ?? throw new ArgumentNullException(nameof(cultureInfo)); + Convert(); + } + + private void Convert() + { + m_String = m_Value.ToString("t", currentCulture) + ", " + m_Value.ToString("dddd", currentCulture); + } + } +} diff --git a/src/RealTime/UI/SliderValueType.cs b/src/RealTime/UI/SliderValueType.cs new file mode 100644 index 00000000..599b694e --- /dev/null +++ b/src/RealTime/UI/SliderValueType.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + internal enum SliderValueType + { + Default, + Percentage, + Time, + Duration + } +} diff --git a/src/RealTime/UI/UnityCheckBoxItem.cs b/src/RealTime/UI/UnityCheckBoxItem.cs new file mode 100644 index 00000000..d7b06942 --- /dev/null +++ b/src/RealTime/UI/UnityCheckBoxItem.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System.Reflection; + using ColossalFramework.UI; + using ICities; + using RealTime.Localization; + + internal sealed class UnityCheckBoxItem : UnityViewItem + { + public UnityCheckBoxItem(UIHelperBase uiHelper, string id, PropertyInfo property, object config) + : base(uiHelper, id, property, config) + { + } + + public override void Translate(LocalizationProvider localizationProvider) + { + if (localizationProvider == null) + { + throw new System.ArgumentNullException(nameof(localizationProvider)); + } + + UIComponent.text = localizationProvider.Translate(UIComponent.name); + UIComponent.tooltip = localizationProvider.Translate(UIComponent.name + Constants.Tooltip); + } + + protected override UICheckBox CreateItem(UIHelperBase uiHelper, bool defaultValue) + { + if (uiHelper == null) + { + throw new System.ArgumentNullException(nameof(uiHelper)); + } + + return (UICheckBox)uiHelper.AddCheckbox(Constants.Placeholder, defaultValue, ValueChanged); + } + } +} diff --git a/src/RealTime/UI/UnityPageViewItem.cs b/src/RealTime/UI/UnityPageViewItem.cs new file mode 100644 index 00000000..947fedff --- /dev/null +++ b/src/RealTime/UI/UnityPageViewItem.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using ColossalFramework.UI; + using ICities; + using RealTime.Localization; + + internal sealed class UnityPageViewItem : IContainerViewItem + { + private const string LabelName = "Label"; + private readonly string id; + + public UnityPageViewItem(UIHelperBase page, string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException("The page ID cannot be null or empty string", nameof(id)); + } + + Container = page ?? throw new ArgumentNullException(nameof(page)); + this.id = id; + } + + public UIHelperBase Container { get; } + + public void Translate(LocalizationProvider localizationProvider) + { + var content = Container as UIHelper; + if (content == null) + { + return; + } + + UIComponent panel = ((UIComponent)content.self).parent; + if (panel == null) + { + return; + } + + UILabel label = panel.Find(LabelName); + if (label != null) + { + label.text = localizationProvider.Translate(id); + } + } + } +} diff --git a/src/RealTime/UI/UnitySliderItem.cs b/src/RealTime/UI/UnitySliderItem.cs new file mode 100644 index 00000000..ecdc4313 --- /dev/null +++ b/src/RealTime/UI/UnitySliderItem.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Globalization; + using System.Reflection; + using ColossalFramework.UI; + using ICities; + using RealTime.Localization; + + internal sealed class UnitySliderItem : UnityViewItem + { + private const string LabelName = "Label"; + private const int SliderValueLabelPadding = 20; + + private readonly UILabel valueLabel; + private readonly SliderValueType valueType; + private CultureInfo currentCulture; + + public UnitySliderItem( + UIHelperBase uiHelper, + string id, + PropertyInfo property, + object config, + float min, + float max, + float step, + SliderValueType valueType) + : base(uiHelper, id, property, config) + { + UIComponent.minValue = min; + UIComponent.maxValue = max; + UIComponent.stepSize = step; + this.valueType = valueType; + + var parentPanel = UIComponent.parent as UIPanel; + if (parentPanel != null) + { + parentPanel.autoLayoutDirection = LayoutDirection.Horizontal; + parentPanel.autoSize = true; + } + + if (UIComponent.parent != null) + { + valueLabel = UIComponent.parent.AddUIComponent(); + valueLabel.padding.left = SliderValueLabelPadding; + valueLabel.name = id + LabelName; + UpdateValueLabel(Value); + } + } + + public override void Translate(LocalizationProvider localizationProvider) + { + if (localizationProvider == null) + { + throw new ArgumentNullException(nameof(localizationProvider)); + } + + UIComponent panel = UIComponent.parent; + if (panel == null) + { + return; + } + + panel.tooltip = localizationProvider.Translate(UIComponent.name + Constants.Tooltip); + + UILabel label = panel.Find(LabelName); + if (label != null) + { + label.text = localizationProvider.Translate(UIComponent.name); + } + + currentCulture = localizationProvider.CurrentCulture; + UpdateValueLabel(Value); + } + + protected override UISlider CreateItem(UIHelperBase uiHelper, float defaultValue) + { + if (uiHelper == null) + { + throw new ArgumentNullException(nameof(uiHelper)); + } + + return (UISlider)uiHelper.AddSlider(Constants.Placeholder, defaultValue, defaultValue + 1, 1, defaultValue, ValueChanged); + } + + protected override void ValueChanged(float newValue) + { + base.ValueChanged(newValue); + if (valueLabel == null) + { + return; + } + + UpdateValueLabel(newValue); + } + + private void UpdateValueLabel(float value) + { + string valueString; + switch (valueType) + { + case SliderValueType.Percentage: + valueString = (value / 100).ToString("P0", currentCulture ?? CultureInfo.CurrentCulture); + break; + + case SliderValueType.Time: + valueString = default(DateTime).AddHours(value).ToString("t", currentCulture ?? CultureInfo.CurrentCulture); + break; + + case SliderValueType.Duration: + TimeSpan ts = TimeSpan.FromHours(value); + valueString = $"{ts.Hours}:{ts.Minutes:00}"; + break; + + default: + return; + } + + valueLabel.text = valueString; + } + } +} diff --git a/src/RealTime/UI/UnityViewItem.cs b/src/RealTime/UI/UnityViewItem.cs new file mode 100644 index 00000000..5a960680 --- /dev/null +++ b/src/RealTime/UI/UnityViewItem.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Reflection; + using ColossalFramework.UI; + using ICities; + using RealTime.Localization; + + internal abstract class UnityViewItem : IViewItem + where TItem : UIComponent + { + private readonly PropertyInfo property; + private readonly object config; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Pure methods invoked")] + protected UnityViewItem(UIHelperBase uiHelper, string id, PropertyInfo property, object config) + { + if (uiHelper == null) + { + throw new ArgumentNullException(nameof(uiHelper)); + } + + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentException("The view item ID cannot be null", nameof(id)); + } + + this.property = property ?? throw new ArgumentNullException(nameof(property)); + this.config = config ?? throw new ArgumentNullException(nameof(config)); + + TItem component = CreateItem(uiHelper, Value); + component.name = id; + UIComponent = component; + } + + protected TItem UIComponent { get; } + + protected TValue Value + { + get => (TValue)Convert.ChangeType(property.GetValue(config, null), typeof(TValue)); + private set => property.SetValue(config, Convert.ChangeType(value, property.PropertyType), null); + } + + public abstract void Translate(LocalizationProvider localizationProvider); + + protected abstract TItem CreateItem(UIHelperBase uiHelper, TValue defaultValue); + + protected virtual void ValueChanged(TValue newValue) + { + Value = newValue; + } + } +} diff --git a/src/RealTime/UI/UnityViewItemFactory.cs b/src/RealTime/UI/UnityViewItemFactory.cs new file mode 100644 index 00000000..4d576117 --- /dev/null +++ b/src/RealTime/UI/UnityViewItemFactory.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using System.Reflection; + using ICities; + using RealTime.Localization; + + internal sealed class UnityViewItemFactory : IViewItemFactory + { + private readonly UIHelperBase uiHelper; + + public UnityViewItemFactory(UIHelperBase uiHelper) + { + this.uiHelper = uiHelper ?? throw new ArgumentNullException(nameof(uiHelper)); + } + + public IContainerViewItem CreateGroup(string id) + { + return new UnityPageViewItem(uiHelper.AddGroup(Constants.Placeholder), id); + } + + public IViewItem CreateCheckBox(IContainerViewItem container, string id, PropertyInfo property, object config) + { + return new UnityCheckBoxItem(container.Container, id, property, config); + } + + public IViewItem CreateSlider( + IContainerViewItem container, + string id, + PropertyInfo property, + object config, + float min, + float max, + float step, + SliderValueType valueType) + { + return new UnitySliderItem(container.Container, id, property, config, min, max, step, valueType); + } + } +} diff --git a/RealTime/packages.config b/src/RealTime/packages.config similarity index 100% rename from RealTime/packages.config rename to src/RealTime/packages.config diff --git a/src/RealTimeTests/Properties/AssemblyInfo.cs b/src/RealTimeTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..5c85bb42 --- /dev/null +++ b/src/RealTimeTests/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("RealTimeTests")] +[assembly: AssemblyDescription("Unit tests for the Real Time mods")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("dymanoid")] +[assembly: AssemblyProduct("RealTime")] +[assembly: AssemblyCopyright("Copyright © 2018, dymanoid")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/src/RealTimeTests/RealTimeTests.csproj b/src/RealTimeTests/RealTimeTests.csproj new file mode 100644 index 00000000..7107f0bd --- /dev/null +++ b/src/RealTimeTests/RealTimeTests.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {687CE9B5-2E2E-454E-83FC-512250CACF3E} + Library + Properties + RealTimeTests + RealTimeTests + v4.7.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + ..\bin\Debug\ + DEBUG;TRACE + full + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + ..\bin\Release\ + TRACE + true + pdbonly + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + + ..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + stylecop.json + + + + + + + + + + {7cd7702c-e7d3-4e61-bf3a-b10f7950de52} + RealTime + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/src/RealTimeTests/packages.config b/src/RealTimeTests/packages.config new file mode 100644 index 00000000..303bdfae --- /dev/null +++ b/src/RealTimeTests/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Redirection/BaseRedirectAttribute.cs b/src/Redirection/BaseRedirectAttribute.cs new file mode 100644 index 00000000..0e2ba299 --- /dev/null +++ b/src/Redirection/BaseRedirectAttribute.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + + /// + /// A base class for the special redirection attributes that can be applied to the methods. + /// + public abstract class BaseRedirectAttribute : Attribute + { + protected BaseRedirectAttribute(Type methodType, string methodName, bool isInstanceMethod) + { + MethodType = methodType ?? throw new ArgumentNullException(nameof(methodType)); + MethodName = methodName; + IsInstanceMethod = isInstanceMethod; + } + + protected BaseRedirectAttribute(Type methodType, string methodName) + : this(methodType, methodName, true) + { + } + + protected BaseRedirectAttribute(Type methodType, bool isInstanceMethod) + : this(methodType, null, isInstanceMethod) + { + } + + protected BaseRedirectAttribute(Type methodType) + : this(methodType, null, true) + { + } + + /// + /// Gets or sets the type where the method that will be redirected is defined. + /// + public Type MethodType { get; set; } + + /// + /// Gets or sets the method name to redirect. If not set, the name of the + /// method this attribute is attached to will be used. + /// + public string MethodName { get; set; } + + /// + /// Gets or sets a value indicating whether the method of the foreign class is an instance + /// method. + /// + public bool IsInstanceMethod { get; set; } + } +} diff --git a/src/Redirection/MethodInfoExtensions.cs b/src/Redirection/MethodInfoExtensions.cs new file mode 100644 index 00000000..a0bbafa0 --- /dev/null +++ b/src/Redirection/MethodInfoExtensions.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Security.Permissions; + + /// + /// Contains various extension methods for the class. + /// + internal static class MethodInfoExtensions + { + /// + /// Creates a instance for the + /// using the target and the + /// assembly and redirects the calls from to the . + /// + /// + /// Thrown when any argument is null. + /// + /// A method to create the redirection for. + /// The target method for the redirection. + /// An that is the redirection source. + /// + /// A instance containing the redirected method description. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + internal static MethodRedirection CreateRedirectionTo(this MethodInfo method, MethodInfo target, Assembly redirectionSource) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (redirectionSource == null) + { + throw new ArgumentNullException(nameof(redirectionSource)); + } + + return new MethodRedirection(method, target, redirectionSource); + } + + /// + /// Compares the provided methods to determine whether a redirection from + /// to this method is possible. + /// + /// + /// Thrown when any argument is null. + /// + /// The method to compare. + /// The method to compare with. + /// if true, a static method with first argument that is + /// compatible with the type of the will be considered + /// compatible. + /// + /// True if the methods are compatible; otherwise, false. + internal static bool IsCompatibleWith(this MethodInfo method, MethodInfo otherMethod, bool asInstanceMethod) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (otherMethod == null) + { + throw new ArgumentNullException(nameof(otherMethod)); + } + + if (method.ReturnType != otherMethod.ReturnType) + { + return false; + } + + var thisParameters = method.GetParameters().ToList(); + var otherParameters = otherMethod.GetParameters().ToList(); + if (asInstanceMethod) + { + RemoveInstanceParameter(thisParameters, otherMethod.ReflectedType); + RemoveInstanceParameter(otherParameters, method.ReflectedType); + } + + if (thisParameters.Count != otherParameters.Count) + { + return false; + } + + for (int i = 0; i < thisParameters.Count; i++) + { + if (!otherParameters[i].ParameterType.IsAssignableFrom(thisParameters[i].ParameterType)) + { + return false; + } + } + + return true; + } + + private static void RemoveInstanceParameter(List parameters, Type methodClass) + { + ParameterInfo firstParameter = parameters.FirstOrDefault(); + + if (firstParameter == null) + { + return; + } + + if (methodClass.IsValueType) + { + if (firstParameter.ParameterType != methodClass.MakeByRefType()) + { + return; + } + } + else if (!methodClass.IsAssignableFrom(firstParameter.ParameterType)) + { + return; + } + + parameters.RemoveAt(0); + } + } +} \ No newline at end of file diff --git a/src/Redirection/MethodRedirection.cs b/src/Redirection/MethodRedirection.cs new file mode 100644 index 00000000..31b740b6 --- /dev/null +++ b/src/Redirection/MethodRedirection.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + using System.Reflection; + using System.Security.Permissions; + + internal sealed class MethodRedirection : IDisposable + { + private readonly RedirectCallsState callsState; + + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public MethodRedirection(MethodInfo method, MethodInfo target, Assembly redirectionSource) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + Method = method ?? throw new ArgumentNullException(nameof(method)); + callsState = RedirectionHelper.RedirectCalls(method, target); + RedirectionSource = redirectionSource ?? throw new ArgumentNullException(nameof(redirectionSource)); + } + + public MethodInfo Method { get; private set; } + + public Assembly RedirectionSource { get; private set; } + + public bool IsDisposed { get; set; } + + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public void Dispose() + { + if (IsDisposed) + { + return; + } + + RedirectionHelper.RevertRedirect(Method, callsState); + Method = null; + RedirectionSource = null; + IsDisposed = true; + } + } +} diff --git a/src/Redirection/Properties/AssemblyInfo.cs b/src/Redirection/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..16efb181 --- /dev/null +++ b/src/Redirection/Properties/AssemblyInfo.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Redirection")] +[assembly: AssemblyDescription("Provides a toolset for method calls redirection")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RealTime")] +[assembly: AssemblyCopyright("Copyright © dymanoid 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] diff --git a/src/Redirection/RedirectCallsState.cs b/src/Redirection/RedirectCallsState.cs new file mode 100644 index 00000000..d5b7c587 --- /dev/null +++ b/src/Redirection/RedirectCallsState.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + /// + /// A container class for the redirection state. + /// Holds internal redirection data. Can be used as a token to revert the redirection. + /// + public sealed class RedirectCallsState + { + internal byte CallSite { get; set; } + + internal byte Offset1 { get; set; } + + internal byte Offset10 { get; set; } + + internal byte Offset11 { get; set; } + + internal byte Offset12 { get; set; } + + internal ulong Addr { get; set; } + } +} \ No newline at end of file diff --git a/src/Redirection/RedirectFromAttribute.cs b/src/Redirection/RedirectFromAttribute.cs new file mode 100644 index 00000000..a74bede0 --- /dev/null +++ b/src/Redirection/RedirectFromAttribute.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + + /// + /// Marks a method for redirection. All marked methods are redirected by calling + /// and reverted by + /// + /// + /// + /// NOTE: only the methods belonging to the same assembly that calls Perform/RevertRedirections are redirected. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class RedirectFromAttribute : BaseRedirectAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Thrown when is null. + /// + /// The type where the source method is defined. + /// The name of the source method that will be redirected. If null, + /// the name of the attribute's target method will be used. + /// true if the source method is an instance method; + /// otherwise, false. + public RedirectFromAttribute(Type methodType, string methodName, bool isInstanceMethod) + : base(methodType, methodName, isInstanceMethod) + { + } + + /// + /// Initializes a new instance of the class with + /// set to true. + /// + /// Thrown when is null. + /// Thrown when is null or an empty string. + /// + /// The type where the source method is defined. + /// The name of the source method that will be redirected. + public RedirectFromAttribute(Type methodType, string methodName) + : base(methodType) + { + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentException($"The {nameof(methodName)} cannot be null or an empty string"); + } + } + + /// + /// Initializes a new instance of the class with + /// empty . + /// The name of the method this attribute is attached to will be used. + /// + /// Thrown when is null. + /// + /// The type where the source method is defined. + /// true if the source method is an instance method; + /// otherwise, false. + public RedirectFromAttribute(Type methodType, bool isInstanceMethod) + : base(methodType, isInstanceMethod) + { + } + + /// + /// Initializes a new instance of the class with + /// empty and + /// set to true. The name of the method this attribute is attached to will be used. + /// + /// Thrown when is null. + /// + /// The type where the source method is defined. + public RedirectFromAttribute(Type methodType) + : base(methodType) + { + } + } +} diff --git a/src/Redirection/RedirectToAttribute.cs b/src/Redirection/RedirectToAttribute.cs new file mode 100644 index 00000000..c38d3c16 --- /dev/null +++ b/src/Redirection/RedirectToAttribute.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + + /// + /// Marks a method for redirection. All marked methods are redirected by calling + /// and reverted by + /// + /// + /// NOTE: only the methods belonging to the same assembly that calls Perform/RevertRedirections are redirected. + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public sealed class RedirectToAttribute : BaseRedirectAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// Thrown when is null. + /// + /// The type where the target method is defined. + /// The name of the target method. If null, + /// the name of the attribute's target method will be used. + /// true if the target method is an instance method; + /// otherwise, false. + public RedirectToAttribute(Type methodType, string methodName, bool isInstanceMethod) + : base(methodType, methodName) + { + } + + /// + /// Initializes a new instance of the class with + /// set to true. + /// + /// Thrown when is null. + /// Thrown when is null or an empty string. + /// + /// The type where the target method is defined. + /// The name of the target method. + public RedirectToAttribute(Type methodType, string methodName) + : base(methodType) + { + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentException($"The {nameof(methodName)} cannot be null or empty string"); + } + } + + /// + /// Initializes a new instance of the class with + /// empty . + /// The name of the method this attribute is attached to will be used. + /// + /// Thrown when is null. + /// + /// The type where the target method is defined. + /// true if the target method is an instance method; + /// otherwise, false. + public RedirectToAttribute(Type methodType, bool isInstanceMethod) + : base(methodType, isInstanceMethod) + { + } + + /// + /// Initializes a new instance of the class with + /// empty and + /// set to true. The name of the method this attribute is attached to will be used. + /// + /// Thrown when is null. + /// + /// The type where the target method is defined. + public RedirectToAttribute(Type methodType) + : base(methodType) + { + } + } +} diff --git a/src/Redirection/Redirection.csproj b/src/Redirection/Redirection.csproj new file mode 100644 index 00000000..34cbaf3f --- /dev/null +++ b/src/Redirection/Redirection.csproj @@ -0,0 +1,85 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {7DCC08EF-DC85-47A4-BD6F-79FC52C7EF13} + Library + Properties + Redirection + Redirection + v3.5 + 512 + + + + + + true + ..\bin\Debug\ + DEBUG;TRACE + true + full + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + ..\bin\Release\ + TRACE + true + true + pdbonly + x64 + 7.3 + prompt + ..\BuildEnvironment\RealTime.ruleset + + + + + + + + + + + + + + + + + + + stylecop.json + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + if not exist "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" +xcopy /y /q /d "$(TargetPath)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)" + + + \ No newline at end of file diff --git a/src/Redirection/RedirectionHelper.cs b/src/Redirection/RedirectionHelper.cs new file mode 100644 index 00000000..121f7bcb --- /dev/null +++ b/src/Redirection/RedirectionHelper.cs @@ -0,0 +1,174 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + using System.Reflection; + using System.Security.Permissions; + + /// + /// Helper class to deal with detours. This version is for Unity 5 x64 on Windows. + /// We provide three different methods of detouring. + /// + internal static class RedirectionHelper + { + /// + /// Redirects all calls from method '' to method ''. + /// + /// + /// Thrown when any argument is null. + /// + /// The method to redirect from. + /// The method to redicrect to. + /// + /// An instance that holds the data for reverting + /// the redirection. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static RedirectCallsState RedirectCalls(MethodBase from, MethodBase to) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (to == null) + { + throw new ArgumentNullException(nameof(to)); + } + + // GetFunctionPointer enforces compilation of the method. + IntPtr fptr1 = from.MethodHandle.GetFunctionPointer(); + IntPtr fptr2 = to.MethodHandle.GetFunctionPointer(); + + return PatchJumpTo(fptr1, fptr2); + } + + /// + /// Redirects all calls from method '' to method ''. + /// + /// + /// The method to redirect from. + /// The method to redicrect to. + /// + /// An instance that holds the data for reverting + /// the redirection. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static RedirectCallsState RedirectCalls(RuntimeMethodHandle from, RuntimeMethodHandle to) + { + // GetFunctionPointer enforces compilation of the method. + IntPtr fptr1 = from.GetFunctionPointer(); + IntPtr fptr2 = to.GetFunctionPointer(); + + return PatchJumpTo(fptr1, fptr2); + } + + /// + /// Reverts a method redirection previously created with + /// or methods. + /// + /// + /// Thrown when any argument is null. + /// + /// The method to revert the redirection of. + /// A instance holding the data for + /// reverting the redirection. + /// + /// True on success; otherwise, false. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static bool RevertRedirect(MethodBase method, RedirectCallsState state) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + try + { + IntPtr fptr1 = method.MethodHandle.GetFunctionPointer(); + RevertJumpTo(fptr1, state); + return true; + } + catch + { + return false; + } + } + + /// + /// Primitive patching. Inserts a jump to '' at ''. + /// Works even if both methods' callers have already been compiled. + /// + /// + /// A pointer of the jump site. + /// A pointer to the jump target. + /// + /// An instance that holds the data for reverting + /// the redirection. + private static RedirectCallsState PatchJumpTo(IntPtr site, IntPtr target) + { + var state = new RedirectCallsState(); + + // R11 is volatile. + unsafe + { + byte* sitePtr = (byte*)site.ToPointer(); + state.CallSite = *sitePtr; + state.Offset1 = *(sitePtr + 1); + state.Offset10 = *(sitePtr + 10); + state.Offset11 = *(sitePtr + 11); + state.Offset12 = *(sitePtr + 12); + state.Addr = *((ulong*)(sitePtr + 2)); + + *sitePtr = 0x49; // mov r11, target + *(sitePtr + 1) = 0xBB; + *((ulong*)(sitePtr + 2)) = (ulong)target.ToInt64(); + *(sitePtr + 10) = 0x41; // jmp r11 + *(sitePtr + 11) = 0xFF; + *(sitePtr + 12) = 0xE3; + } + + return state; + } + + private static void RevertJumpTo(IntPtr site, RedirectCallsState state) + { + unsafe + { + byte* sitePtr = (byte*)site.ToPointer(); + *sitePtr = state.CallSite; // mov r11, target + *(sitePtr + 1) = state.Offset1; + *((ulong*)(sitePtr + 2)) = state.Addr; + *(sitePtr + 10) = state.Offset10; // jmp r11 + *(sitePtr + 11) = state.Offset11; + *(sitePtr + 12) = state.Offset12; + } + } + } +} \ No newline at end of file diff --git a/src/Redirection/Redirector.cs b/src/Redirection/Redirector.cs new file mode 100644 index 00000000..46a631a9 --- /dev/null +++ b/src/Redirection/Redirector.cs @@ -0,0 +1,162 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +/* +The MIT License (MIT) +Copyright (c) 2015 Sebastian Schöner +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +namespace Redirection +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Security.Permissions; + + /// + /// A static class that provides the functionality of automatic method call redirection. + /// + public static class Redirector + { + private static Dictionary redirections = new Dictionary(); + + /// + /// Perform the method call redirections for all methods in the calling assembly + /// that are marked with or . + /// + /// + /// The number of methods that have been redirected. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static int PerformRedirections() + { + var callingAssembly = Assembly.GetCallingAssembly(); + return PerformRedirections(0, callingAssembly); + } + + /// + /// Perform the method call redirections for all methods in the calling assembly + /// that are marked with or . + /// + /// + /// The bitmask to filter the methods with. + /// + /// The number of methods that have been redirected. + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static int PerformRedirections(ulong bitmask) + { + var callingAssembly = Assembly.GetCallingAssembly(); + return PerformRedirections(bitmask, callingAssembly); + } + + /// + /// Reverts the method call redirection of all methods from the calling assembly + /// that are marked with or . + /// + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + public static void RevertRedirections() + { + var callingAssembly = Assembly.GetCallingAssembly(); + RevertRedirections(callingAssembly); + } + + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + private static int PerformRedirections(ulong bitmask, Assembly callingAssembly) + { + IEnumerable allMethods = callingAssembly + .GetTypes() + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)); + + int result = 0; + + try + { + foreach (MethodInfo method in allMethods) + { + foreach (BaseRedirectAttribute attribute in method.GetCustomAttributes(typeof(BaseRedirectAttribute), false)) + { + ProcessMethod(method, callingAssembly, attribute, bitmask); + ++result; + } + } + } + catch + { + RevertRedirections(callingAssembly); + throw; + } + + return result; + } + + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + private static void RevertRedirections(Assembly callingAssembly) + { + var redirectionsToRemove = redirections.Values.Where(r => r.RedirectionSource == callingAssembly).ToList(); + foreach (MethodRedirection item in redirectionsToRemove) + { + redirections.Remove(item.Method); + item.Dispose(); + } + } + + [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")] + private static void ProcessMethod(MethodInfo method, Assembly callingAssembly, BaseRedirectAttribute attribute, ulong bitmask) + { + string methodName = string.IsNullOrEmpty(attribute.MethodName) ? method.Name : attribute.MethodName; + + IEnumerable externalMethods = + attribute.MethodType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) + .Where(m => m.Name == methodName); + + if (attribute is RedirectFromAttribute) + { + MethodInfo sourceMethod = externalMethods.FirstOrDefault(m => method.IsCompatibleWith(m, attribute.IsInstanceMethod)); + + if (sourceMethod == null) + { + throw new InvalidOperationException($"No compatible source method '{methodName}' found in '{attribute.MethodType.FullName}'"); + } + + if (!redirections.ContainsKey(sourceMethod)) + { + redirections.Add(sourceMethod, sourceMethod.CreateRedirectionTo(method, callingAssembly)); + } + } + else if (attribute is RedirectToAttribute) + { + MethodInfo targetMethod = externalMethods.FirstOrDefault(m => m.IsCompatibleWith(method, attribute.IsInstanceMethod)); + + if (targetMethod == null) + { + throw new InvalidOperationException($"No compatible target method '{methodName}' found in '{attribute.MethodType.FullName}'"); + } + + if (!redirections.ContainsKey(method)) + { + redirections.Add(method, method.CreateRedirectionTo(targetMethod, callingAssembly)); + } + } + else + { + throw new NotSupportedException($"The attribute '{attribute.GetType().Name}' is currently not supported"); + } + } + } +} diff --git a/src/Redirection/packages.config b/src/Redirection/packages.config new file mode 100644 index 00000000..b0aaa025 --- /dev/null +++ b/src/Redirection/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file