From 9d56090033a5e7577d424ae9c4f17dc53093ce4b Mon Sep 17 00:00:00 2001 From: jonathan-robertson Date: Sun, 2 Jul 2023 12:20:07 -0500 Subject: [PATCH 1/4] improve time trigger frequency --- CHANGELOG.md | 4 +++ ModInfo.xml | 2 +- src/Patches/GameManager.cs | 55 -------------------------------------- src/Patches/World.cs | 43 +++++++++++++++++++++++++++++ src/src.csproj | 2 +- 5 files changed, 49 insertions(+), 57 deletions(-) delete mode 100644 src/Patches/GameManager.cs create mode 100644 src/Patches/World.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 275c692..fca0882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update trader restock on login - update trader restock ui to days remaining +## [1.0.2] - 2023-07-02 + +- improve time trigger frequency + ## [1.0.1] - 2023-06-30 - update to support a21 b324 (stable) diff --git a/ModInfo.xml b/ModInfo.xml index 7dc3ea1..cb7ded4 100644 --- a/ModInfo.xml +++ b/ModInfo.xml @@ -1,7 +1,7 @@ - + diff --git a/src/Patches/GameManager.cs b/src/Patches/GameManager.cs deleted file mode 100644 index c258268..0000000 --- a/src/Patches/GameManager.cs +++ /dev/null @@ -1,55 +0,0 @@ -using HarmonyLib; -using System; - -namespace DaysRemaining.Patches -{ - [HarmonyPatch(typeof(GameManager), "SetWorldTime")] - internal class GameManager_SetWorldTime_Patches - { - private static readonly ModLog _log = new ModLog(); - - public static void Postfix() - { - try - { - var players = GameManager.Instance.World.Players.list; - for (var i = 0; i < players.Count; i++) - { - if (Helpers.TryGetClientInfo(players[i].entityId, out var clientInfo)) - { - Helpers.SetExpirationDaysRemaining(clientInfo, players[i]); - } - } - } - catch (Exception e) - { - _log.Error("Postfix", e); - } - } - } - - [HarmonyPatch(typeof(GameManager), "updateTimeOfDay")] - internal class GameManager_updateTimeOfDay_Patches - { - private static readonly ModLog _log = new ModLog(); - - public static void Postfix() - { - try - { - var players = GameManager.Instance.World.Players.list; - for (var i = 0; i < players.Count; i++) - { - if (Helpers.TryGetClientInfo(players[i].entityId, out var clientInfo)) - { - Helpers.SetExpirationDaysRemaining(clientInfo, players[i]); - } - } - } - catch (Exception e) - { - _log.Error("Postfix", e); - } - } - } -} diff --git a/src/Patches/World.cs b/src/Patches/World.cs new file mode 100644 index 0000000..0af86ca --- /dev/null +++ b/src/Patches/World.cs @@ -0,0 +1,43 @@ +using HarmonyLib; +using System; + +namespace DaysRemaining.Patches +{ + [HarmonyPatch(typeof(World), "SetTime")] + internal class World_SetTime_Patches + { + private static readonly ModLog _log = new ModLog(); + + private static int prevMinute = -1; + + /// + /// Fire when game updates time of day on its own or when admin updates game time via `st` command such that it produces a minute change. + /// + /// New time the world was just set to. + public static void Postfix(ulong _time) + { + try + { + var curMinute = GameUtils.WorldTimeToMinutes(_time); + if (curMinute == prevMinute) + { + return; + } + prevMinute = curMinute; + + var players = GameManager.Instance.World.Players.list; + for (var i = 0; i < players.Count; i++) + { + if (Helpers.TryGetClientInfo(players[i].entityId, out var clientInfo)) + { + Helpers.SetExpirationDaysRemaining(clientInfo, players[i]); + } + } + } + catch (Exception e) + { + _log.Error("Postfix", e); + } + } + } +} diff --git a/src/src.csproj b/src/src.csproj index abad236..e1d1486 100644 --- a/src/src.csproj +++ b/src/src.csproj @@ -65,8 +65,8 @@ - + From 0695fd7433d2cbb199546f3b57af79b38f631cd1 Mon Sep 17 00:00:00 2001 From: jonathan-robertson Date: Sun, 2 Jul 2023 14:48:49 -0500 Subject: [PATCH 2/4] improve remaining rental time calculations --- CHANGELOG.md | 1 + src/ModApi.cs | 22 +++++++-- src/Patches/NetPackageTileEntity.cs | 5 +- src/Patches/World.cs | 43 ----------------- src/Utilities/DayMonitor.cs | 72 +++++++++++++++++++++++++++++ src/{ => Utilities}/Helpers.cs | 29 +----------- src/{ => Utilities}/ModLog.cs | 2 +- src/src.csproj | 6 +-- 8 files changed, 99 insertions(+), 81 deletions(-) delete mode 100644 src/Patches/World.cs create mode 100644 src/Utilities/DayMonitor.cs rename src/{ => Utilities}/Helpers.cs (61%) rename src/{ => Utilities}/ModLog.cs (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fca0882..c42ff51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.2] - 2023-07-02 +- improve remaining rental time calculations - improve time trigger frequency ## [1.0.1] - 2023-06-30 diff --git a/src/ModApi.cs b/src/ModApi.cs index 0846730..058fcbb 100644 --- a/src/ModApi.cs +++ b/src/ModApi.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using DaysRemaining.Utilities; +using HarmonyLib; using System; using System.Reflection; @@ -8,7 +9,7 @@ internal class ModApi : IModApi { private static readonly ModLog _log = new ModLog(); - public static bool DebugMode { get; set; } = false; // TODO: disable before release + public static bool DebugMode { get; set; } = true; // TODO: disable before release public void InitMod(Mod _modInstance) { @@ -19,13 +20,26 @@ public void InitMod(Mod _modInstance) // TODO: on rental agreement changed, calculate and send cvar ModEvents.PlayerSpawnedInWorld.RegisterHandler(OnPlayerSpawnedInWorld); + ModEvents.GameUpdate.RegisterHandler(OnGameUpdate); + } + + private void OnGameUpdate() + { + try + { + DayMonitor.OnGameUpdate(); + } + catch (Exception e) + { + _log.Error($"OnGameUpdate", e); + } } private void OnPlayerSpawnedInWorld(ClientInfo _cInfo, RespawnType _respawnReason, Vector3i _pos) { try { - if (_cInfo == null) // is client local? + if (_cInfo == null) // local client { // TODO: var ppId = ((_cInfo != null) ? _cInfo.InternalId : null) ?? PlatformManager.InternalLocalUserIdentifier; return; @@ -36,7 +50,7 @@ private void OnPlayerSpawnedInWorld(ClientInfo _cInfo, RespawnType _respawnReaso case RespawnType.JoinMultiplayer: if (GameManager.Instance.World.Players.dict.TryGetValue(_cInfo.entityId, out var player)) { - Helpers.SetExpirationDaysRemaining(_cInfo, player); + DayMonitor.SetExpirationDaysRemaining(_cInfo, player); } return; } diff --git a/src/Patches/NetPackageTileEntity.cs b/src/Patches/NetPackageTileEntity.cs index cb75cd9..23addc2 100644 --- a/src/Patches/NetPackageTileEntity.cs +++ b/src/Patches/NetPackageTileEntity.cs @@ -1,4 +1,5 @@ -using HarmonyLib; +using DaysRemaining.Utilities; +using HarmonyLib; using System; namespace DaysRemaining.Patches @@ -31,7 +32,7 @@ public static void Postfix(TileEntity _te, Vector3i ___teWorldPos) clientInfo.latestPlayerData.rentalEndDay = tileEntityVendingMachine.RentalEndDay; // close the open trader (vending machine) window because the cvar cannot auto-refresh its ui value on its own clientInfo.SendPackage(NetPackageManager.GetPackage().Setup("xui close trader", true)); - Helpers.SetExpirationDaysRemaining(clientInfo, player); + DayMonitor.SetExpirationDaysRemaining(clientInfo, player); } } } diff --git a/src/Patches/World.cs b/src/Patches/World.cs deleted file mode 100644 index 0af86ca..0000000 --- a/src/Patches/World.cs +++ /dev/null @@ -1,43 +0,0 @@ -using HarmonyLib; -using System; - -namespace DaysRemaining.Patches -{ - [HarmonyPatch(typeof(World), "SetTime")] - internal class World_SetTime_Patches - { - private static readonly ModLog _log = new ModLog(); - - private static int prevMinute = -1; - - /// - /// Fire when game updates time of day on its own or when admin updates game time via `st` command such that it produces a minute change. - /// - /// New time the world was just set to. - public static void Postfix(ulong _time) - { - try - { - var curMinute = GameUtils.WorldTimeToMinutes(_time); - if (curMinute == prevMinute) - { - return; - } - prevMinute = curMinute; - - var players = GameManager.Instance.World.Players.list; - for (var i = 0; i < players.Count; i++) - { - if (Helpers.TryGetClientInfo(players[i].entityId, out var clientInfo)) - { - Helpers.SetExpirationDaysRemaining(clientInfo, players[i]); - } - } - } - catch (Exception e) - { - _log.Error("Postfix", e); - } - } - } -} diff --git a/src/Utilities/DayMonitor.cs b/src/Utilities/DayMonitor.cs new file mode 100644 index 0000000..e79c531 --- /dev/null +++ b/src/Utilities/DayMonitor.cs @@ -0,0 +1,72 @@ +using DaysRemaining.Patches; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DaysRemaining.Utilities +{ + internal class DayMonitor + { + private const string CVAR_VENDING_EXPIRATION = "daysRemainingVendingExpiration"; + private const string BUFF_VENDING_EXPIRATION = "buffDaysRemainingVendingExpiration"; + + internal static int CurrentDay { get; private set; } = -1; + + private static readonly ModLog _log = new ModLog(); + + internal static void OnGameUpdate() + { + if (IsNewDay(out var day)) + { + CurrentDay = day; + SetExpirationDaysRemaining(); + } + } + + internal static void SetExpirationDaysRemaining() + { + var players = GameManager.Instance.World.Players.list; + for (var i = 0; i < players.Count; i++) + { + if (Helpers.TryGetClientInfo(players[i].entityId, out var clientInfo)) + { + SetExpirationDaysRemaining(clientInfo, players[i]); + } + } + } + + /// + /// Call this to update the player's client-side data related to vending expiration date. + /// + /// ClientInfo containing the current rental information. + /// EntityPlayer to update. + /// Would've loved to use rentalEndTime here, but it isn't reported to the server (just rentalEndDay). Maybe rentalEndTime is deprecated? + internal static void SetExpirationDaysRemaining(ClientInfo clientInfo, EntityPlayer player) + { + if (clientInfo == null || player == null) + { + _log.Warn($"ClientInfo and EntityPlayer params must not be null; ClientInfo {(clientInfo != null ? "exists" : "does not exist")}, EntityPlayer {(player != null ? "exists" : "does not exist")}."); + return; + } + + if (clientInfo.latestPlayerData.rentedVMPosition == Vector3i.zero) + { + return; // player does not have a vending machine rental + } + var daysRemaining = Math.Max(clientInfo.latestPlayerData.rentalEndDay - GameUtils.WorldTimeToDays(GameManager.Instance.World.worldTime), 0); + if (daysRemaining != player.GetCVar(CVAR_VENDING_EXPIRATION)) + { + player.SetCVar(CVAR_VENDING_EXPIRATION, daysRemaining); + _ = player.Buffs.AddBuff(BUFF_VENDING_EXPIRATION); + } + } + + private static bool IsNewDay(out int day) + { + day = GameUtils.WorldTimeToDays(GameManager.Instance.World.worldTime); + return day != CurrentDay; + } + } +} diff --git a/src/Helpers.cs b/src/Utilities/Helpers.cs similarity index 61% rename from src/Helpers.cs rename to src/Utilities/Helpers.cs index ba54d8e..cd8d622 100644 --- a/src/Helpers.cs +++ b/src/Utilities/Helpers.cs @@ -1,12 +1,9 @@ using System; -namespace DaysRemaining +namespace DaysRemaining.Utilities { internal class Helpers { - private const string CVAR_VENDING_EXPIRATION = "daysRemainingVendingExpiration"; - private const string BUFF_VENDING_EXPIRATION = "buffDaysRemainingVendingExpiration"; - private static readonly ModLog _log = new ModLog(); public static bool TryGetTileEntityVendingMachine(Vector3i blockPos, out TileEntityVendingMachine tileEntityVendingMachine) @@ -64,29 +61,5 @@ public static bool TryGetVendingMachineRentalData(Vector3i blockPos, out Platfor rentalEndDay = tileEntityVendingMachine.RentalEndDay; return true; } - - /// - /// Call this to update the player's client-side data related to vending expiration date. - /// - /// ClientInfo containing the current rental information. - /// EntityPlayer to update. - public static void SetExpirationDaysRemaining(ClientInfo clientInfo, EntityPlayer player) - { - if (clientInfo == null || player == null) - { - _log.Warn($"ClientInfo and EntityPlayer params must not be null; ClientInfo {(clientInfo != null ? "exists" : "does not exist")}, EntityPlayer {(player != null ? "exists" : "does not exist")}."); - return; - } - if (clientInfo.latestPlayerData.rentalEndDay == 0) - { - return; - } - var daysRemaining = Math.Max(clientInfo.latestPlayerData.rentalEndDay - GameUtils.WorldTimeToDays(GameManager.Instance.World.worldTime), 0); - if (daysRemaining != player.GetCVar(CVAR_VENDING_EXPIRATION)) - { - player.SetCVar(CVAR_VENDING_EXPIRATION, daysRemaining); - _ = player.Buffs.AddBuff(BUFF_VENDING_EXPIRATION); - } - } } } diff --git a/src/ModLog.cs b/src/Utilities/ModLog.cs similarity index 97% rename from src/ModLog.cs rename to src/Utilities/ModLog.cs index f5e09d1..41881b4 100644 --- a/src/ModLog.cs +++ b/src/Utilities/ModLog.cs @@ -1,6 +1,6 @@ using System; -namespace DaysRemaining +namespace DaysRemaining.Utilities { internal class ModLog { diff --git a/src/src.csproj b/src/src.csproj index e1d1486..80d4456 100644 --- a/src/src.csproj +++ b/src/src.csproj @@ -62,11 +62,11 @@ - - + + + - From c637bef9ce48307eee55170774f51966c2e177a3 Mon Sep 17 00:00:00 2001 From: jonathan-robertson Date: Sun, 2 Jul 2023 14:49:04 -0500 Subject: [PATCH 3/4] fix notification color --- CHANGELOG.md | 1 + Config/Localization.txt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42ff51..a75b215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.2] - 2023-07-02 +- fix notification color - improve remaining rental time calculations - improve time trigger frequency diff --git a/Config/Localization.txt b/Config/Localization.txt index 3caa1db..0933c41 100644 --- a/Config/Localization.txt +++ b/Config/Localization.txt @@ -1,5 +1,5 @@ Key,File,Type,UsedInMainMenu,NoTranslate,english,Context / Alternate Text,german,latam,french,italian,japanese,koreana,polish,brazilian,russian,turkish,schinese,tchinese,spanish buffDaysRemainingVendingExpirationFormat,UI,XUI,,?,{0:0} Days,,{0:0} Tage,{0:0} Días,{0:0} Jours,{0:0} Giorni,{0:0} 日,{0:0} 일,{0:0} Dni,{0:0} Dias,{0:0} Дней,{0:0} Gün,{0:0} 天数,{0:0} 天,{0:0} Días buffDaysRemainingVendingExpirationName,buffs,Buff,,x,Vending Machine,,Automat,Máquina expendedora,Distributeur automatique,Distributore automatico,自動販売機,자판기,Automat sprzedający,Máquina de Vendas,Торговый автомат,Otomat,自动贩卖机,自動販賣機,Máquina expendedora -buffDaysRemainingVendingExpirationDesc,buffs,Buff,,x,Your vending machine rental will expire in [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] days.,,Ihre Automatenmiete läuft in [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] Tagen ab.,El alquiler de su máquina expendedora caducará en [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] días.,Votre location de distributeur expirera dans [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] jours.,Il noleggio del tuo distributore automatico scadrà tra [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] giorni.,自動販売機のレンタルは [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 日後に期限切れになります.,자판기 대여가 [007fff]{cvar(daysRemainingVendingExpiration:0)}일 후에 만료됩니다.,Wypożyczenie automatu wygaśnie za [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] dni.,O aluguel da máquina de venda automática expira em [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] dias.,Срок аренды вашего торгового автомата истекает через [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] дней.,Otomat kiralama süreniz [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] gün içinde sona erecek.,您的自动售货机租赁将在 [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,您的自動售貨機租賃將在 [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,El alquiler de su máquina expendedora caducará en [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] días. -buffDaysRemainingVendingExpirationTooltip,buffs,Buff,,x,Your vending machine rental will expire in [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] days.,,Ihre Automatenmiete läuft in [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] Tagen ab.,El alquiler de su máquina expendedora caducará en [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] días.,Votre location de distributeur expirera dans [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] jours.,Il noleggio del tuo distributore automatico scadrà tra [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] giorni.,自動販売機のレンタルは [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 日後に期限切れになります.,자판기 대여가 [007fff]{cvar(daysRemainingVendingExpiration:0)}일 후에 만료됩니다.,Wypożyczenie automatu wygaśnie za [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] dni.,O aluguel da máquina de venda automática expira em [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] dias.,Срок аренды вашего торгового автомата истекает через [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] дней.,Otomat kiralama süreniz [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] gün içinde sona erecek.,您的自动售货机租赁将在 [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,您的自動售貨機租賃將在 [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,El alquiler de su máquina expendedora caducará en [007fff]{cvar(daysRemainingVendingExpiration:0)}[-] días. +buffDaysRemainingVendingExpirationDesc,buffs,Buff,,x,Your vending machine rental will expire in [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] days.,,Ihre Automatenmiete läuft in [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] Tagen ab.,El alquiler de su máquina expendedora caducará en [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] días.,Votre location de distributeur expirera dans [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] jours.,Il noleggio del tuo distributore automatico scadrà tra [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] giorni.,自動販売機のレンタルは [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 日後に期限切れになります.,자판기 대여가 [ffff00]{cvar(daysRemainingVendingExpiration:0)}일 후에 만료됩니다.,Wypożyczenie automatu wygaśnie za [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] dni.,O aluguel da máquina de venda automática expira em [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] dias.,Срок аренды вашего торгового автомата истекает через [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] дней.,Otomat kiralama süreniz [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] gün içinde sona erecek.,您的自动售货机租赁将在 [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,您的自動售貨機租賃將在 [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,El alquiler de su máquina expendedora caducará en [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] días. +buffDaysRemainingVendingExpirationTooltip,buffs,Buff,,x,Your vending machine rental will expire in [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] days.,,Ihre Automatenmiete läuft in [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] Tagen ab.,El alquiler de su máquina expendedora caducará en [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] días.,Votre location de distributeur expirera dans [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] jours.,Il noleggio del tuo distributore automatico scadrà tra [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] giorni.,自動販売機のレンタルは [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 日後に期限切れになります.,자판기 대여가 [ffff00]{cvar(daysRemainingVendingExpiration:0)}일 후에 만료됩니다.,Wypożyczenie automatu wygaśnie za [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] dni.,O aluguel da máquina de venda automática expira em [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] dias.,Срок аренды вашего торгового автомата истекает через [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] дней.,Otomat kiralama süreniz [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] gün içinde sona erecek.,您的自动售货机租赁将在 [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,您的自動售貨機租賃將在 [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] 天后到期.,El alquiler de su máquina expendedora caducará en [ffff00]{cvar(daysRemainingVendingExpiration:0)}[-] días. From bcff7f2a1707995195cbbc51801de265dd5aa17c Mon Sep 17 00:00:00 2001 From: jonathan-robertson Date: Sun, 2 Jul 2023 14:50:42 -0500 Subject: [PATCH 4/4] update binary --- DaysRemaining.dll | Bin 10752 -> 10752 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/DaysRemaining.dll b/DaysRemaining.dll index 6763dd2902f1b17eb348f6842b6d68092f0d60b8..4860789584a24388f43fbca2404e1a041ab7783f 100644 GIT binary patch delta 4740 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 B{-Xc@ delta 4658 zcmcIneQ;FO6+h>`&%DHC_ietj#7#nA&6hzzPyrzjAwYmgB4#ATXu^kI8#V;Nkh}$y zQV=CPP0I*v!Eqd|T5VKD#|libr87>)bgb1WrCP0m?X&|rRhic5@7_%y!SYA{=$pLX zJzw|SbI&>VzD=?**|>YV{->h_5A;10;fMR4UdGdW<_Si0CQ-yM5*2AaH6lF$xoD(%BS1&%E(0dkL!XtCosG4UhvVfCjV~({u?i4nRh^X>k}^eQiIhyd4Xz+o1$q2R zKu@(ad^i?FHY*Aeufv<@ma@bVkXmda$kRm?BLt6@izV_@5(?4)s(7`MsvBMxjWm@s}bt70URvfX?dxvFjx}4bVFs zAigni^vuo~QhEcR%tQHbJnOIv9o?%{ij%YTf~(6$^wCV%b}RFM%N5Af;)kdS`|8-n8Leg!K4L(8*m4r?sbtvPN%WDJ+t5N?Fa)`86y* z1b8y=uJ<3b!!vTR!IEC02QdCv(o5GR*4(?QQ(BF~HQb23mNYG;4QO83R3&qp>d67mqOwOC0U{E*G)QN6&enmMi35d zz(yR}fh{`pDK38u^RbLW+tJGom8d0K3_0N}CYW?W^qUT?M3fFaBP&j?3aNb;_5u36 z{yRt}(rIWDaJrb@QVT3s_Swqw8%p9}%MSfntujTP8#<~N8lH>jRtkp-mC<4((4>!{ zaOhqY?`%j8)AyD992XkdbR6;k)!K65>+Mol?t<2#6o;h zY{EAm+`p7AjUv>L+)1ATJAo7FE&YB`L=7TEZxLRI#`6v#6}uEZrf|Pl5``b?r-XP? zFzr6iJuftRVTjYsL&BiLL);eQh_L9r^W3jR8htUuJxPNion{sho_^Y*pAZ2WTMWjd zZMoCYEdXPfYKg8`dg5=UmL)>~}D}`y2 ztOISMDdtwnqL&rdZ*SvMB8OgAoY!wZ%6|iUOtGKv?R-Y$(pQSxObhr+kw>?UQ*KnC z5n8RdRG<;sq_}=cvp*IQ`f8LVmCAX;mKlm0w%o3`ZQhX6-J?wRoi}AILJP-VSiX=N z6_;`<(G_?fv+2{0qd+Iw2|-E-=xhd+)SG8 zA?nkVA>IMb5hrMzI-An1XLE9pe+gJAr-NVtfglVxEPgSH!=8hxHS* zmt__Y@RhdUy?jo$`Dw1_FVxo>;N8eciHn;_kM_HTSfy@Ndm6FB+-s>S$|S*DiI7 zSI%kOxME4`x;s|;kLNz+FUXtczmb>lMWk)uC;7|RUm3Z^UyxrF7_)@pNbEXViKDl{ zzb}6r{zpBrC*zq{RvulvFL$B0<=m!S{xZO%7w-F4Gcjy<4Z5?nWa@ zZgQ8xK4IEc6dF5Jt6S{W>CiRVS#EMKyUG0;exvKAn>=7f*==^43^~{ai9jG?X1N*c z+H#?Q+p0y$)-1NuNC-Fiw1?Lq4vY=R>CXu>9hJ4yG1InfJ<1sYL(a>P+MlOK%~0}a zz!pMeh%i2+2sxB|6D|X4kuV7}V38h>5BzWduGwvsv38-bZgLRuA>P3Z*eBoD+-5Jd zHdLDq=8s?`EtiYr1|j*7M~RukBy-c^wx-G1e-XCZA`_qpM~Y4(s4z>}X{C$`2!df& z!MVYJr9-uuF%3CtwdItS%^YTHNQ|t7Ns5uL%+!#ZVHs|n+k^>NSThk|knwBLaIlRt zvLeYd0g>9(*QqHH`_TQ%Z_bMa`17+xV$FR&SQ?@@i#9F&<=R`sD; z4X*sb%TJDJXIeU<&;8@kd4(Axj{?!^Z<_?iTy!1=o{R2ee@b!4ZJT-FNcKuP+EHbr0^`nUVt4gGDE1HV)_(wflj)%V