From 07349570370b94f7712c9be4defd68021539a38d Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Mon, 30 Jul 2018 00:11:02 +0200 Subject: [PATCH] Change weekly stats labels to dynamically determined (closes #59) --- src/RealTime/Core/RealTimeCore.cs | 14 + src/RealTime/Localization/TranslationKeys.cs | 3 + src/RealTime/RealTime.csproj | 1 + src/RealTime/Simulation/SimulationHandler.cs | 4 + src/RealTime/Simulation/Statistics.cs | 260 +++++++++++++++++++ 5 files changed, 282 insertions(+) create mode 100644 src/RealTime/Simulation/Statistics.cs diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index 6648a72..02e28d9 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -143,6 +143,17 @@ namespace RealTime.Core var result = new RealTimeCore(timeAdjustment, customTimeBar, eventManager, patcher); eventManager.EventsChanged += result.CityEventsChanged; + + var statistics = new Statistics(timeInfo, localizationProvider); + if (statistics.Initialize()) + { + statistics.RefreshUnits(); + } + else + { + statistics = null; + } + SimulationHandler.NewDay += result.CityEventsChanged; SimulationHandler.TimeAdjustment = timeAdjustment; @@ -152,6 +163,7 @@ namespace RealTime.Core SimulationHandler.Buildings = BuildingAIPatches.RealTimeAI; SimulationHandler.Buildings.UpdateFrameDuration(); SimulationHandler.Buildings.InitializeLightState(); + SimulationHandler.Statistics = statistics; AwakeSleepSimulation.Install(configProvider.Configuration); @@ -205,6 +217,8 @@ namespace RealTime.Core SimulationHandler.WeatherInfo = null; SimulationHandler.Buildings = null; SimulationHandler.CitizenProcessor = null; + SimulationHandler.Statistics?.Close(); + SimulationHandler.Statistics = null; isEnabled = false; } diff --git a/src/RealTime/Localization/TranslationKeys.cs b/src/RealTime/Localization/TranslationKeys.cs index b7de428..03c007e 100644 --- a/src/RealTime/Localization/TranslationKeys.cs +++ b/src/RealTime/Localization/TranslationKeys.cs @@ -20,5 +20,8 @@ namespace RealTime.Localization /// The key for a 'incompatible mods found' message. public const string IncompatibleModsFoundMessage = "IncompatibleModsFoundMessage"; + + /// The key for the abbreviated 'minutes' text. + public const string Minutes = "Minutes"; } } diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index e0bba36..27346c6 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -139,6 +139,7 @@ + diff --git a/src/RealTime/Simulation/SimulationHandler.cs b/src/RealTime/Simulation/SimulationHandler.cs index 92dc4b8..8509065 100644 --- a/src/RealTime/Simulation/SimulationHandler.cs +++ b/src/RealTime/Simulation/SimulationHandler.cs @@ -46,6 +46,9 @@ namespace RealTime.Simulation /// Gets or sets the citizen processing class instance. internal static CitizenProcessor CitizenProcessor { get; set; } + /// Gets or sets the statistics processing class instance. + internal static Statistics Statistics { get; set; } + /// /// Called before each game simulation tick. A tick contains multiple frames. /// Performs the dispatching for this simulation phase. @@ -70,6 +73,7 @@ namespace RealTime.Simulation if (updateFrameLength) { Buildings?.UpdateFrameDuration(); + Statistics?.RefreshUnits(); } if (DayTimeSimulation == null || CitizenProcessor == null) diff --git a/src/RealTime/Simulation/Statistics.cs b/src/RealTime/Simulation/Statistics.cs new file mode 100644 index 0000000..9942d7c --- /dev/null +++ b/src/RealTime/Simulation/Statistics.cs @@ -0,0 +1,260 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.Simulation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using ColossalFramework.Globalization; + using ColossalFramework.UI; + using RealTime.Localization; + using RealTime.Tools; + using UnityEngine; + + /// + /// Handles the customization of the game's statistics numbers. + /// + internal sealed class Statistics + { + private const int VanillaFramesPerWeek = 3840; + private const string UnitPlaceholder = "{unit}"; + private const string OverrddenTranslationType = "Units"; + private const string CityInfoPanelName = "(Library) CityInfoPanel"; + private const string DistrictInfoPanelName = "(Library) DistrictWorldInfoPanel"; + private const string TouristsPanelName = "Tourists"; + private const string TouristsLabelName = "Label"; + private const string InfoPanelName = "InfoPanel"; + private const string CitizensChangeLabelName = "ProjectedChange"; + private const string IncomeLabelName = "ProjectedIncome"; + private const string BuildingsButtonsContainer = "TSContainer"; + + private readonly ITimeInfo timeInfo; + private readonly ILocalizationProvider localizationProvider; + private readonly Locale customLocale; + + private Locale mainLocale; + private UILabel cityInfoPanelTourists; + private UILabel districtInfoPanelTourists; + private UILabel labelPopulation; + private UILabel labelIncome; + private UITabContainer buildingsTabContainer; + + /// Initializes a new instance of the class. + /// An object that provides the game's time information. + /// The object to get the current locale from. + /// Thrown when any argument is null. + public Statistics(ITimeInfo timeInfo, ILocalizationProvider localizationProvider) + { + this.timeInfo = timeInfo ?? throw new ArgumentNullException(nameof(timeInfo)); + this.localizationProvider = localizationProvider ?? throw new ArgumentNullException(nameof(localizationProvider)); + + customLocale = new Locale(); + } + + /// Initializes this instance by preparing connections to the necessary game parts. + /// true on success; otherwise, false. + public bool Initialize() + { + if (mainLocale != null) + { + return true; + } + + try + { + FieldInfo field = typeof(LocaleManager).GetField("m_Locale", BindingFlags.Instance | BindingFlags.NonPublic); + mainLocale = field.GetValue(LocaleManager.instance) as Locale; + } + catch (Exception ex) + { + Log.Warning("The 'Real Time' mod could not obtain the locale field of the LocaleManager, error message: " + ex); + return false; + } + + cityInfoPanelTourists = GameObject + .Find(CityInfoPanelName)? + .GetComponent()? + .Find(TouristsPanelName)? + .Find(TouristsLabelName); + + if (cityInfoPanelTourists == null) + { + Log.Warning("The 'Real Time' mod could not obtain the CityInfoPanel.Tourists.Label object"); + } + + districtInfoPanelTourists = GameObject + .Find(DistrictInfoPanelName)? + .GetComponent()? + .Find(TouristsPanelName)? + .Find(TouristsLabelName); + + if (districtInfoPanelTourists == null) + { + Log.Warning("The 'Real Time' mod could not obtain the DistrictWorldInfoPanel.Tourists.Label object"); + } + + UIPanel infoPanel = UIView.Find(InfoPanelName); + if (infoPanel == null) + { + Log.Warning("The 'Real Time' mod could not obtain the InfoPanel object"); + } + else + { + labelPopulation = infoPanel.Find(CitizensChangeLabelName); + if (labelPopulation == null) + { + Log.Warning("The 'Real Time' mod could not obtain the ProjectedChange object"); + } + + labelIncome = infoPanel.Find(IncomeLabelName); + if (labelIncome == null) + { + Log.Warning("The 'Real Time' mod could not obtain the ProjectedIncome object"); + } + } + + buildingsTabContainer = UIView.Find(BuildingsButtonsContainer); + if (buildingsTabContainer == null) + { + Log.Warning("The 'Real Time' mod could not obtain the TSContainer object"); + } + + LocaleManager.eventLocaleChanged += LocaleChanged; + return true; + } + + /// Shuts down this instance. + public void Close() + { + LocaleManager.eventLocaleChanged -= LocaleChanged; + mainLocale = null; + cityInfoPanelTourists = null; + districtInfoPanelTourists = null; + labelIncome = null; + labelPopulation = null; + buildingsTabContainer = null; + } + + /// Refreshes the statistics units for current game speed. + public void RefreshUnits() + { + if (mainLocale == null) + { + return; + } + + var unit = TimeSpan.FromHours(VanillaFramesPerWeek * timeInfo.HoursPerFrame); + + double minutes = Math.Round(unit.TotalMinutes); + if (minutes >= 30d) + { + minutes = Math.Round(minutes / 10d) * 10d; + } + else if (minutes >= 10d) + { + minutes = Math.Round(minutes / 5d) * 5d; + } + + string displayUnit = $"{minutes:F0} {localizationProvider.Translate(TranslationKeys.Minutes)}"; + if (RefreshUnits(displayUnit)) + { + RefreshUI(); + } + } + + private static void RefreshEconomyPanel() + { + IEnumerable components = ToolsModifierControl.economyPanel? + .GetComponentsInChildren()? + .Where(c => !string.IsNullOrEmpty(c.tooltipLocaleID)); + + if (components == null) + { + return; + } + + foreach (UIComponent component in components.Where(c => c is UISprite || c is UITextComponent)) + { + component.tooltip = Locale.Get(component.tooltipLocaleID); + } + } + + private bool RefreshUnits(string displayUnit) + { + customLocale.Reset(); + + IDictionary overridden = localizationProvider.GetOverriddenTranslations(OverrddenTranslationType); + if (overridden == null || overridden.Count == 0) + { + return false; + } + + foreach (KeyValuePair value in overridden) + { + string translated = value.Value.Replace(UnitPlaceholder, displayUnit); + customLocale.AddLocalizedString(new Locale.Key { m_Identifier = value.Key }, translated); + } + + mainLocale.Merge(null, customLocale); + return true; + } + + private void RefreshUI() + { + if (labelIncome != null) + { + labelIncome.tooltip = Locale.Get(labelIncome.tooltipLocaleID); + } + + if (labelPopulation != null) + { + labelPopulation.tooltip = Locale.Get(labelPopulation.tooltipLocaleID); + } + + if (cityInfoPanelTourists != null) + { + cityInfoPanelTourists.text = Locale.Get(cityInfoPanelTourists.localeID); + } + + if (districtInfoPanelTourists != null) + { + districtInfoPanelTourists.text = Locale.Get(districtInfoPanelTourists.localeID); + } + + RefreshEconomyPanel(); + RefreshBuildingsButtons(); + } + + private void RefreshBuildingsButtons() + { + if (buildingsTabContainer == null) + { + return; + } + + // This creates objects on heap, but it won't cause memory pressure because it's not called + // in the simulation loop + var items = buildingsTabContainer.GetComponentsInChildren()? + .Select(b => new { Info = b.objectUserData as PrefabInfo, Button = b }) + .Where(i => i.Info is BuildingInfo || i.Info is NetInfo); + + if (items == null) + { + return; + } + + foreach (var item in items) + { + item.Button.tooltip = item.Info.GetLocalizedTooltip(); + } + } + + private void LocaleChanged() + { + RefreshUnits(); + } + } +} -- GitLab