From 110eccc57d97853ec51378c8426813bfa83e889a Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 5 Aug 2018 00:08:16 +0200 Subject: [PATCH 1/2] Update SkyTools sub-module to version 1.2.1 --- src/RealTime/Core/RealTimeCore.cs | 2 +- src/SkyTools | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index 948b30df..a3da54fa 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -325,7 +325,7 @@ private static bool SetupCustomAI( private static void CustomTimeBarCityEventClick(object sender, CustomTimeBarClickEventArgs e) { - CameraHelper.NavigateToBuilding(e.CityEventBuildingId); + CameraHelper.NavigateToBuilding(e.CityEventBuildingId, true); } private static void LoadStorageData(IEnumerable storageData, StorageBase storage) diff --git a/src/SkyTools b/src/SkyTools index 285a5e3e..3ea7ce1f 160000 --- a/src/SkyTools +++ b/src/SkyTools @@ -1 +1 @@ -Subproject commit 285a5e3ec456970fc5cf247031ef5e81b27b58b7 +Subproject commit 3ea7ce1f27eaad0fef516c1affdd802661dd0750 From 5d7676e8b64e0326c1122bdb2538e918650065e2 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sun, 5 Aug 2018 01:16:38 +0200 Subject: [PATCH 2/2] Add custom citizen information into info panels (closes #83) --- src/RealTime/Core/RealTimeCore.cs | 10 ++ src/RealTime/CustomAI/RealTimeResidentAI.cs | 15 ++ .../Patches/WorldInfoPanelPatches.cs | 55 ++++++ src/RealTime/Localization/TranslationKeys.cs | 12 ++ src/RealTime/Localization/Translations/de.xml | 13 ++ src/RealTime/Localization/Translations/en.xml | 13 ++ src/RealTime/Localization/Translations/es.xml | 13 ++ src/RealTime/Localization/Translations/fr.xml | 13 ++ src/RealTime/Localization/Translations/ko.xml | 13 ++ src/RealTime/Localization/Translations/pl.xml | 13 ++ src/RealTime/Localization/Translations/pt.xml | 13 ++ src/RealTime/Localization/Translations/ru.xml | 13 ++ src/RealTime/Localization/Translations/zh.xml | 13 ++ src/RealTime/RealTime.csproj | 4 + src/RealTime/UI/CustomCitizenInfoPanel.cs | 47 +++++ src/RealTime/UI/CustomVehicleInfoPanel.cs | 125 ++++++++++++++ src/RealTime/UI/RealTimeInfoPanelBase.cs | 163 ++++++++++++++++++ 17 files changed, 548 insertions(+) create mode 100644 src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs create mode 100644 src/RealTime/UI/CustomCitizenInfoPanel.cs create mode 100644 src/RealTime/UI/CustomVehicleInfoPanel.cs create mode 100644 src/RealTime/UI/RealTimeInfoPanelBase.cs diff --git a/src/RealTime/Core/RealTimeCore.cs b/src/RealTime/Core/RealTimeCore.cs index a3da54fa..c5387c41 100644 --- a/src/RealTime/Core/RealTimeCore.cs +++ b/src/RealTime/Core/RealTimeCore.cs @@ -159,6 +159,9 @@ public static RealTimeCore Run(ConfigurationProvider configProvi SimulationHandler.Buildings.InitializeLightState(); SimulationHandler.Statistics = statistics; + WorldInfoPanelPatches.CitizenInfoPanel = CustomCitizenInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider); + WorldInfoPanelPatches.VehicleInfoPanel = CustomVehicleInfoPanel.Enable(ResidentAIPatch.RealTimeAI, localizationProvider); + AwakeSleepSimulation.Install(configProvider.Configuration); result.storageData.Add(eventManager); @@ -214,6 +217,12 @@ public void Stop() SimulationHandler.Statistics?.Close(); SimulationHandler.Statistics = null; + WorldInfoPanelPatches.CitizenInfoPanel?.Disable(); + WorldInfoPanelPatches.CitizenInfoPanel = null; + + WorldInfoPanelPatches.VehicleInfoPanel?.Disable(); + WorldInfoPanelPatches.VehicleInfoPanel = null; + isEnabled = false; } @@ -249,6 +258,7 @@ private static IEnumerable GetMethodPatches() ResidentAIPatch.ArriveAtTarget, TouristAIPatch.Location, TransferManagerPatch.AddOutgoingOffer, + WorldInfoPanelPatches.UpdateBindings, UIGraphPatches.MinDataPoints, UIGraphPatches.VisibleEndTime, UIGraphPatches.BuildLabels diff --git a/src/RealTime/CustomAI/RealTimeResidentAI.cs b/src/RealTime/CustomAI/RealTimeResidentAI.cs index 7591c16d..9f9caa71 100644 --- a/src/RealTime/CustomAI/RealTimeResidentAI.cs +++ b/src/RealTime/CustomAI/RealTimeResidentAI.cs @@ -178,5 +178,20 @@ public IStorageData GetStorageService() { return new CitizenScheduleStorage(residentSchedules, CitizenMgr.GetCitizensArray(), TimeInfo); } + + /// Gets the citizen schedule. Note that the method returns the reference + /// and thus doesn't prevent changing the schedule. + /// The ID of the citizen to get the schedule for. + /// The original schedule of the citizen. + /// Thrown when is 0. + public ref CitizenSchedule GetCitizenSchedule(uint citizenId) + { + if (citizenId == 0) + { + throw new ArgumentOutOfRangeException(nameof(citizenId), citizenId, "The citizen ID cannot be 0"); + } + + return ref residentSchedules[citizenId]; + } } } \ No newline at end of file diff --git a/src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs b/src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs new file mode 100644 index 00000000..84f64e42 --- /dev/null +++ b/src/RealTime/GameConnection/Patches/WorldInfoPanelPatches.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.GameConnection.Patches +{ + using System; + using System.Reflection; + using RealTime.UI; + using SkyTools.Patching; + + /// + /// A static class that provides the patch objects for the world info panel game methods. + /// + internal static class WorldInfoPanelPatches + { + /// Gets or sets the customized citizen information panel. + public static CustomCitizenInfoPanel CitizenInfoPanel { get; set; } + + /// Gets or sets the customized vehicle information panel. + public static CustomVehicleInfoPanel VehicleInfoPanel { get; set; } + + /// Gets the patch for the update bindings method. + public static IPatch UpdateBindings { get; } = new WorldInfoPanel_UpdateBindings(); + + private sealed class WorldInfoPanel_UpdateBindings : PatchBase + { + protected override MethodInfo GetMethod() + { + return typeof(WorldInfoPanel).GetMethod( + "UpdateBindings", + BindingFlags.Instance | BindingFlags.NonPublic, + null, + new Type[0], + new ParameterModifier[0]); + } + +#pragma warning disable SA1313 // Parameter names must begin with lower-case letter + private static void Postfix(WorldInfoPanel __instance, ref InstanceID ___m_InstanceID) + { + switch (__instance) + { + case CitizenWorldInfoPanel _: + CitizenInfoPanel?.UpdateCustomInfo(ref ___m_InstanceID); + break; + + case VehicleWorldInfoPanel _: + VehicleInfoPanel?.UpdateCustomInfo(ref ___m_InstanceID); + break; + } + } +#pragma warning restore SA1313 // Parameter names must begin with lower-case letter + } + } +} diff --git a/src/RealTime/Localization/TranslationKeys.cs b/src/RealTime/Localization/TranslationKeys.cs index 03c007ec..08332e03 100644 --- a/src/RealTime/Localization/TranslationKeys.cs +++ b/src/RealTime/Localization/TranslationKeys.cs @@ -23,5 +23,17 @@ internal static class TranslationKeys /// The key for the abbreviated 'minutes' text. public const string Minutes = "Minutes"; + + /// The key for the scheduled action text. + public const string ScheduledAction = "ScheduledAction"; + + /// The key for the next scheduled action text. + public const string NextScheduledAction = "NextScheduledAction"; + + /// The key for the work shift text. + public const string WorkShiftKey = "WorkShift"; + + /// The key for the vacation text. + public const string WorkStatusOnVacation = "WorkStatus.OnVacation"; } } diff --git a/src/RealTime/Localization/Translations/de.xml b/src/RealTime/Localization/Translations/de.xml index e9c5adcc..c59b2229 100644 --- a/src/RealTime/Localization/Translations/de.xml +++ b/src/RealTime/Localization/Translations/de.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/en.xml b/src/RealTime/Localization/Translations/en.xml index 9080a661..f23f07aa 100644 --- a/src/RealTime/Localization/Translations/en.xml +++ b/src/RealTime/Localization/Translations/en.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/es.xml b/src/RealTime/Localization/Translations/es.xml index 24be88d3..2ddb5bbc 100644 --- a/src/RealTime/Localization/Translations/es.xml +++ b/src/RealTime/Localization/Translations/es.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/fr.xml b/src/RealTime/Localization/Translations/fr.xml index 482992b9..e07799b2 100644 --- a/src/RealTime/Localization/Translations/fr.xml +++ b/src/RealTime/Localization/Translations/fr.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/ko.xml b/src/RealTime/Localization/Translations/ko.xml index dd7d485d..b59b6a63 100644 --- a/src/RealTime/Localization/Translations/ko.xml +++ b/src/RealTime/Localization/Translations/ko.xml @@ -82,6 +82,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/pl.xml b/src/RealTime/Localization/Translations/pl.xml index 5550454c..e8e61a97 100644 --- a/src/RealTime/Localization/Translations/pl.xml +++ b/src/RealTime/Localization/Translations/pl.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/pt.xml b/src/RealTime/Localization/Translations/pt.xml index 2c537a5a..e205b724 100644 --- a/src/RealTime/Localization/Translations/pt.xml +++ b/src/RealTime/Localization/Translations/pt.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/ru.xml b/src/RealTime/Localization/Translations/ru.xml index 4c22f81d..a8e18e0e 100644 --- a/src/RealTime/Localization/Translations/ru.xml +++ b/src/RealTime/Localization/Translations/ru.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/Localization/Translations/zh.xml b/src/RealTime/Localization/Translations/zh.xml index ab03d7bc..bfaf4fa6 100644 --- a/src/RealTime/Localization/Translations/zh.xml +++ b/src/RealTime/Localization/Translations/zh.xml @@ -83,6 +83,19 @@ + + + + + + + + + + + + + diff --git a/src/RealTime/RealTime.csproj b/src/RealTime/RealTime.csproj index 3d1e447b..9e20499f 100644 --- a/src/RealTime/RealTime.csproj +++ b/src/RealTime/RealTime.csproj @@ -114,6 +114,7 @@ + @@ -148,9 +149,12 @@ + + + diff --git a/src/RealTime/UI/CustomCitizenInfoPanel.cs b/src/RealTime/UI/CustomCitizenInfoPanel.cs new file mode 100644 index 00000000..bbe88711 --- /dev/null +++ b/src/RealTime/UI/CustomCitizenInfoPanel.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using RealTime.CustomAI; + using SkyTools.Localization; + + /// + /// A customized citizen info panel that additionally shows the origin building of the citizen. + /// + internal sealed class CustomCitizenInfoPanel : RealTimeInfoPanelBase + { + private const string GameInfoPanelName = "(Library) CitizenWorldInfoPanel"; + + private CustomCitizenInfoPanel(string panelName, RealTimeResidentAI residentAI, ILocalizationProvider localizationProvider) + : base(panelName, residentAI, localizationProvider) + { + } + + /// Enables the citizen info panel customization. Can return null on failure. + /// The custom resident AI. + /// The localization provider to use for text translation. + /// An instance of the object that can be used for disabling + /// the customization, or null when the customization fails. + public static CustomCitizenInfoPanel Enable(RealTimeResidentAI residentAI, ILocalizationProvider localizationProvider) + { + var result = new CustomCitizenInfoPanel(GameInfoPanelName, residentAI, localizationProvider); + return result.Initialize() ? result : null; + } + + /// Updates the origin building display. + /// The game object instance to get the information from. + public override void UpdateCustomInfo(ref InstanceID instance) + { + if (instance.Type != InstanceType.Citizen) + { + UpdateCitizenInfo(0); + } + else + { + UpdateCitizenInfo(instance.Citizen); + } + } + } +} diff --git a/src/RealTime/UI/CustomVehicleInfoPanel.cs b/src/RealTime/UI/CustomVehicleInfoPanel.cs new file mode 100644 index 00000000..9156595c --- /dev/null +++ b/src/RealTime/UI/CustomVehicleInfoPanel.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) dymanoid. All rights reserved. +// + +namespace RealTime.UI +{ + using System; + using RealTime.CustomAI; + using SkyTools.Localization; + using SkyTools.Patching; + using UnityEngine; + + /// + /// A customized vehicle info panel that additionally shows the origin building of the owner citizen. + /// + internal sealed class CustomVehicleInfoPanel : RealTimeInfoPanelBase + { + private const string GameInfoPanelName = "(Library) CitizenVehicleWorldInfoPanel"; + private const string GetDriverInstanceMethodName = "GetDriverInstance"; + + private GetDriverInstanceDelegate passengerCarAIGetDriverInstance; + private GetDriverInstanceDelegate bicycleAIGetDriverInstance; + + private CustomVehicleInfoPanel(string panelName, RealTimeResidentAI residentAI, ILocalizationProvider localizationProvider) + : base(panelName, residentAI, localizationProvider) + { + try + { + passengerCarAIGetDriverInstance = FastDelegateFactory.Create>( + typeof(PassengerCarAI), + GetDriverInstanceMethodName, + true); + + bicycleAIGetDriverInstance = FastDelegateFactory.Create>( + typeof(BicycleAI), + GetDriverInstanceMethodName, + true); + } + catch (Exception ex) + { + Debug.LogError($"The 'Snooper' mod failed to obtain at least one of the GetDriverInstance methods. Error message: " + ex); + } + } + + private delegate ushort GetDriverInstanceDelegate(T instance, ushort vehicleId, ref Vehicle vehicle); + + /// Enables the vehicle info panel customization. Can return null on failure. + /// The custom resident AI. + /// The localization provider to use for text translation. + /// An instance of the object that can be used for disabling + /// the customization, or null when the customization fails. + public static CustomVehicleInfoPanel Enable(RealTimeResidentAI residentAI, ILocalizationProvider localizationProvider) + { + if (residentAI == null) + { + throw new ArgumentNullException(nameof(residentAI)); + } + + if (localizationProvider == null) + { + throw new ArgumentNullException(nameof(localizationProvider)); + } + + var result = new CustomVehicleInfoPanel(GameInfoPanelName, residentAI, localizationProvider); + return result.Initialize() ? result : null; + } + + /// Updates the origin building display. + /// The game object instance to get the information from. + public override void UpdateCustomInfo(ref InstanceID instance) + { + ushort instanceId = 0; + try + { + if (passengerCarAIGetDriverInstance == null || bicycleAIGetDriverInstance == null) + { + return; + } + + if (instance.Type != InstanceType.Vehicle || instance.Vehicle == 0) + { + return; + } + + ushort vehicleId = instance.Vehicle; + vehicleId = VehicleManager.instance.m_vehicles.m_buffer[vehicleId].GetFirstVehicle(vehicleId); + if (vehicleId == 0) + { + return; + } + + VehicleInfo vehicleInfo = VehicleManager.instance.m_vehicles.m_buffer[vehicleId].Info; + + try + { + switch (vehicleInfo.m_vehicleAI) + { + case BicycleAI bicycleAI: + instanceId = bicycleAIGetDriverInstance(bicycleAI, vehicleId, ref VehicleManager.instance.m_vehicles.m_buffer[vehicleId]); + break; + + case PassengerCarAI passengerCarAI: + instanceId = passengerCarAIGetDriverInstance(passengerCarAI, vehicleId, ref VehicleManager.instance.m_vehicles.m_buffer[vehicleId]); + break; + + default: + return; + } + } + catch + { + passengerCarAIGetDriverInstance = null; + bicycleAIGetDriverInstance = null; + } + } + finally + { + uint citizenId = instanceId == 0 + ? 0u + : CitizenManager.instance.m_instances.m_buffer[instanceId].m_citizen; + UpdateCitizenInfo(citizenId); + } + } + } +} \ No newline at end of file diff --git a/src/RealTime/UI/RealTimeInfoPanelBase.cs b/src/RealTime/UI/RealTimeInfoPanelBase.cs new file mode 100644 index 00000000..8f5edfc5 --- /dev/null +++ b/src/RealTime/UI/RealTimeInfoPanelBase.cs @@ -0,0 +1,163 @@ +// Copyright (c) dymanoid. All rights reserved. + +namespace RealTime.UI +{ + using System.Text; + using ColossalFramework.UI; + using RealTime.CustomAI; + using SkyTools.Localization; + using SkyTools.UI; + using static Localization.TranslationKeys; + + /// A base class for the customized world info panels. + /// The type of the game world info panel to customize. + internal abstract class RealTimeInfoPanelBase : CustomInfoPanelBase + where T : WorldInfoPanel + { + private const string ComponentId = "RealTimeInfoSchedule"; + private const string AgeEducationLabelName = "AgeEducation"; + private const float LineHeight = 14f; + + private readonly RealTimeResidentAI residentAI; + private readonly ILocalizationProvider localizationProvider; + private UILabel scheduleLabel; + private CitizenSchedule scheduleCopy; + + /// Initializes a new instance of the class. + /// Name of the game's panel object. + /// The custom resident AI. + /// The localization provider to use for text translation. + /// + /// Thrown when or is null. + /// + /// + /// Thrown when is null or an empty string. + /// + protected RealTimeInfoPanelBase(string panelName, RealTimeResidentAI residentAI, ILocalizationProvider localizationProvider) + : base(panelName) + { + this.residentAI = residentAI ?? throw new System.ArgumentNullException(nameof(residentAI)); + this.localizationProvider = localizationProvider ?? throw new System.ArgumentNullException(nameof(localizationProvider)); + } + + /// Disables the custom citizen info panel, if it is enabled. + protected override sealed void DisableCore() + { + if (scheduleLabel == null) + { + return; + } + + ItemsPanel.RemoveUIComponent(scheduleLabel); + UnityEngine.Object.Destroy(scheduleLabel.gameObject); + scheduleLabel = null; + } + + /// Updates the citizen information for the citizen with specified ID. + /// The citizen ID. + protected void UpdateCitizenInfo(uint citizenId) + { + if (citizenId == 0) + { + SetCustomPanelVisibility(scheduleLabel, false); + return; + } + + ref CitizenSchedule schedule = ref residentAI.GetCitizenSchedule(citizenId); + + if (schedule.LastScheduledState == scheduleCopy.LastScheduledState + && schedule.ScheduledStateTime == scheduleCopy.ScheduledStateTime + && schedule.WorkStatus == scheduleCopy.WorkStatus + && schedule.VacationDaysLeft == scheduleCopy.VacationDaysLeft + && schedule.WorkShift == scheduleCopy.WorkShift) + { + return; + } + + if (schedule.LastScheduledState == ResidentState.Ignored) + { + return; + } + + SetCustomPanelVisibility(scheduleLabel, false); + scheduleCopy = schedule; + BuildTextInfo(ref schedule); + } + + /// Builds up the custom UI objects for the info panel. + /// true on success; otherwise, false. + protected override sealed bool InitializeCore() + { + UILabel statusLabel = ItemsPanel.Find(AgeEducationLabelName); + if (statusLabel == null) + { + return false; + } + + scheduleLabel = UIComponentTools.CreateCopy(statusLabel, ItemsPanel, ComponentId); + scheduleLabel.width = 270; + scheduleLabel.zOrder = statusLabel.zOrder + 1; + scheduleLabel.isVisible = false; + return true; + } + + private void BuildTextInfo(ref CitizenSchedule schedule) + { + var info = new StringBuilder(100); + float labelHeight = 0; + if (schedule.LastScheduledState != ResidentState.Unknown) + { + string action = localizationProvider.Translate(ScheduledAction + "." + schedule.LastScheduledState.ToString()); + if (!string.IsNullOrEmpty(action)) + { + info.Append($"{localizationProvider.Translate(ScheduledAction)}: {action}"); + labelHeight += LineHeight; + } + } + + if (schedule.ScheduledStateTime != default) + { + string action = localizationProvider.Translate(NextScheduledAction); + if (!string.IsNullOrEmpty(action)) + { + if (info.Length > 0) + { + info.AppendLine(); + } + + info.Append($"{action}: {schedule.ScheduledStateTime.ToString(localizationProvider.CurrentCulture)}"); + labelHeight += LineHeight; + } + } + + if (schedule.WorkShift != WorkShift.Unemployed) + { + string workShift = localizationProvider.Translate(WorkShiftKey + "." + schedule.WorkShift.ToString()); + if (!string.IsNullOrEmpty(workShift)) + { + if (info.Length > 0) + { + info.AppendLine(); + } + + info.Append(workShift); + labelHeight += LineHeight; + + if (schedule.WorkStatus == WorkStatus.OnVacation) + { + string vacation = localizationProvider.Translate(WorkStatusOnVacation); + if (!string.IsNullOrEmpty(vacation)) + { + info.Append(' '); + info.AppendFormat(vacation, schedule.VacationDaysLeft); + } + } + } + } + + scheduleLabel.height = labelHeight; + scheduleLabel.text = info.ToString(); + SetCustomPanelVisibility(scheduleLabel, info.Length > 0); + } + } +} \ No newline at end of file