From 58079d9390a6aed8d46a3581f4055f9ee1717bd6 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com> Date: Sun, 25 Aug 2024 06:31:47 -0500 Subject: [PATCH] Stickers: added some code --- .../Integrations/BTKUI/BTKUILibExtensions.cs | 103 +++++ Stickers/Integrations/BTKUI/BtkUiAddon.cs | 80 ---- .../BTKUI/BtkUiAddon_CAT_DebugOptions.cs | 15 - .../BTKUI/BtkUiAddon_CAT_StickerSelection.cs | 356 --------------- .../Integrations/BTKUI/BtkuiAddon_Utils.cs | 104 ----- .../BTKUI/UIAddon.Category.DebugOptions.cs | 13 + ...Mod.cs => UIAddon.Category.StickersMod.cs} | 34 +- Stickers/Integrations/BTKUI/UIAddon.Main.cs | 92 ++++ .../BTKUI/UIAddon.Page.PlayerOptions.cs | 53 +++ .../BTKUI/UIAddon.Page.StickerSelect.cs | 270 ++++++++++++ Stickers/Main.cs | 23 +- Stickers/ModSettings.cs | 147 ++----- Stickers/Patches.cs | 18 +- Stickers/Properties/AssemblyInfo.cs | 2 +- Stickers/README.md | 22 +- .../Stickers-magnifying-glass.png | Bin 0 -> 53474 bytes Stickers/Resources/decalery_shaders.assets | Bin 43266 -> 72775 bytes Stickers/Stickers.csproj | 2 + Stickers/Stickers/Enums/StickerKeyBinds.cs | 40 ++ Stickers/Stickers/Enums/StickerSFXType.cs | 9 + .../Networking/ModNetwork.Constants.cs | 16 + .../Stickers/Networking/ModNetwork.Enums.cs | 21 + .../Stickers/Networking/ModNetwork.Events.cs | 18 + .../Stickers/Networking/ModNetwork.Helpers.cs | 18 + .../Stickers/Networking/ModNetwork.Inbound.cs | 222 ++++++++++ .../Stickers/Networking/ModNetwork.Logging.cs | 22 + .../Stickers/Networking/ModNetwork.Main.cs | 48 ++ .../Networking/ModNetwork.Outbound.cs | 189 ++++++++ Stickers/Stickers/Networking/ModNetwork.cs | 414 ------------------ Stickers/Stickers/StickerData.cs | 320 +++++++++----- .../Stickers/StickerSystem.ImageLoading.cs | 152 +++++++ Stickers/Stickers/StickerSystem.Main.cs | 71 +++ .../Stickers/StickerSystem.PlayerCallbacks.cs | 98 +++++ .../StickerSystem.StickerLifecycle.cs | 160 +++++++ Stickers/Stickers/StickerSystem.cs | 406 ----------------- Stickers/Stickers/Utilities/StickerCache.cs | 185 ++++++++ 36 files changed, 2092 insertions(+), 1651 deletions(-) create mode 100644 Stickers/Integrations/BTKUI/BTKUILibExtensions.cs delete mode 100644 Stickers/Integrations/BTKUI/BtkUiAddon.cs delete mode 100644 Stickers/Integrations/BTKUI/BtkUiAddon_CAT_DebugOptions.cs delete mode 100644 Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickerSelection.cs delete mode 100644 Stickers/Integrations/BTKUI/BtkuiAddon_Utils.cs create mode 100644 Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs rename Stickers/Integrations/BTKUI/{BtkUiAddon_CAT_StickersMod.cs => UIAddon.Category.StickersMod.cs} (76%) create mode 100644 Stickers/Integrations/BTKUI/UIAddon.Main.cs create mode 100644 Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs create mode 100644 Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs create mode 100644 Stickers/Resources/Gohsantosadrive_Icons/Stickers-magnifying-glass.png create mode 100644 Stickers/Stickers/Enums/StickerKeyBinds.cs create mode 100644 Stickers/Stickers/Enums/StickerSFXType.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Constants.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Enums.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Events.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Helpers.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Inbound.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Logging.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Main.cs create mode 100644 Stickers/Stickers/Networking/ModNetwork.Outbound.cs delete mode 100644 Stickers/Stickers/Networking/ModNetwork.cs create mode 100644 Stickers/Stickers/StickerSystem.ImageLoading.cs create mode 100644 Stickers/Stickers/StickerSystem.Main.cs create mode 100644 Stickers/Stickers/StickerSystem.PlayerCallbacks.cs create mode 100644 Stickers/Stickers/StickerSystem.StickerLifecycle.cs delete mode 100644 Stickers/Stickers/StickerSystem.cs create mode 100644 Stickers/Stickers/Utilities/StickerCache.cs diff --git a/Stickers/Integrations/BTKUI/BTKUILibExtensions.cs b/Stickers/Integrations/BTKUI/BTKUILibExtensions.cs new file mode 100644 index 0000000..1e16c93 --- /dev/null +++ b/Stickers/Integrations/BTKUI/BTKUILibExtensions.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using BTKUILib; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using BTKUILib.UIObjects.Objects; +using JetBrains.Annotations; +using MelonLoader; +using UnityEngine; + +namespace NAK.Stickers.Integrations; + +[PublicAPI] +public static class BTKUILibExtensions +{ + #region Icon Utils + + public static Stream GetIconStream(string iconName) + { + Assembly assembly = Assembly.GetExecutingAssembly(); + string assemblyName = assembly.GetName().Name; + return assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}"); + } + + #endregion Icon Utils + + #region Enum Utils + + private static class EnumUtils + { + public static string[] GetPrettyEnumNames() where T : Enum + { + return Enum.GetNames(typeof(T)).Select(PrettyFormatEnumName).ToArray(); + } + + private static string PrettyFormatEnumName(string name) + { + // adds spaces before capital letters (excluding the first letter) + return System.Text.RegularExpressions.Regex.Replace(name, "(\\B[A-Z])", " $1"); + } + + public static int GetEnumIndex(T value) where T : Enum + { + return Array.IndexOf(Enum.GetValues(typeof(T)), value); + } + } + + #endregion Enum Utils + + #region Melon Preference Extensions + + public static ToggleButton AddMelonToggle(this Category category, MelonPreferences_Entry entry) + { + ToggleButton toggle = category.AddToggle(entry.DisplayName, entry.Description, entry.Value); + toggle.OnValueUpdated += b => entry.Value = b; + return toggle; + } + + public static SliderFloat AddMelonSlider(this Category category, MelonPreferences_Entry entry, float min, + float max, int decimalPlaces = 2, bool allowReset = true) + { + SliderFloat slider = category.AddSlider(entry.DisplayName, entry.Description, + Mathf.Clamp(entry.Value, min, max), min, max, decimalPlaces, entry.DefaultValue, allowReset); + slider.OnValueUpdated += f => entry.Value = f; + return slider; + } + + public static Button AddMelonStringInput(this Category category, MelonPreferences_Entry entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly) + { + Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle); + button.OnPress += () => QuickMenuAPI.OpenKeyboard(entry.Value, s => entry.Value = s); + return button; + } + + public static Button AddMelonNumberInput(this Category category, MelonPreferences_Entry entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly) + { + Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle); + button.OnPress += () => QuickMenuAPI.OpenNumberInput(entry.DisplayName, entry.Value, f => entry.Value = f); + return button; + } + + public static Category AddMelonCategory(this Page page, MelonPreferences_Entry entry, bool showHeader = true) + { + Category category = page.AddCategory(entry.DisplayName, showHeader, true, !entry.Value); + category.OnCollapse += b => entry.Value = !b; // more intuitive if pref value of true means category open + return category; + } + + public static MultiSelection CreateMelonMultiSelection(MelonPreferences_Entry entry) where TEnum : Enum + { + MultiSelection multiSelection = new( + entry.DisplayName, + EnumUtils.GetPrettyEnumNames(), + EnumUtils.GetEnumIndex(entry.Value) + ) + { + OnOptionUpdated = i => entry.Value = (TEnum)Enum.Parse(typeof(TEnum), Enum.GetNames(typeof(TEnum))[i]) + }; + + return multiSelection; + } + + #endregion Melon Preference Extensions +} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/BtkUiAddon.cs b/Stickers/Integrations/BTKUI/BtkUiAddon.cs deleted file mode 100644 index fafe92b..0000000 --- a/Stickers/Integrations/BTKUI/BtkUiAddon.cs +++ /dev/null @@ -1,80 +0,0 @@ -using ABI_RC.Core.Player; -using BTKUILib; -using BTKUILib.UIObjects; - -namespace NAK.Stickers.Integrations; - -public static partial class BtkUiAddon -{ - private static Page _rootPage; - private static string _rootPageElementID; - - private static bool _isOurTabOpened; - private static Action _onOurTabOpened; - - public static void Initialize() - { - Prepare_Icons(); - Setup_AvatarScaleModTab(); - //Setup_PlayerSelectPage(); - } - - #region Initialization - - private static void Prepare_Icons() - { - // All icons used - https://www.flaticon.com/authors/gohsantosadrive - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-alphabet", GetIconStream("Gohsantosadrive_Icons.Stickers-alphabet.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-eraser", GetIconStream("Gohsantosadrive_Icons.Stickers-eraser.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-folder", GetIconStream("Gohsantosadrive_Icons.Stickers-folder.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-headset", GetIconStream("Gohsantosadrive_Icons.Stickers-headset.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand", GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-pencil", GetIconStream("Gohsantosadrive_Icons.Stickers-pencil.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-puzzle", GetIconStream("Gohsantosadrive_Icons.Stickers-puzzle.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-rubbish-bin", GetIconStream("Gohsantosadrive_Icons.Stickers-rubbish-bin.png")); - } - - private static void Setup_AvatarScaleModTab() - { - _rootPage = new Page(ModSettings.ModName, ModSettings.SM_SettingsCategory, true, "Stickers-puzzle") - { - MenuTitle = ModSettings.SM_SettingsCategory, - MenuSubtitle = "Stickers! Double-click the tab to quickly toggle Sticker Mode.", - }; - - _rootPageElementID = _rootPage.ElementID; - QuickMenuAPI.OnTabChange += OnTabChange; - // QuickMenuAPI.UserJoin += OnUserJoinLeave; - // QuickMenuAPI.UserLeave += OnUserJoinLeave; - // QuickMenuAPI.OnWorldLeave += OnWorldLeave; - - Setup_StickersModCategory(_rootPage); - Setup_StickerSelectionCategory(_rootPage); - Setup_DebugOptionsCategory(_rootPage); - } - - #endregion - - #region Double-Click Place Sticker - - private static DateTime lastTime = DateTime.Now; - - private static void OnTabChange(string newTab, string previousTab) - { - _isOurTabOpened = newTab == _rootPageElementID; - if (_isOurTabOpened) - { - _onOurTabOpened?.Invoke(); - TimeSpan timeDifference = DateTime.Now - lastTime; - if (timeDifference.TotalSeconds <= 0.5) - { - //AvatarScaleManager.Instance.Setting_UniversalScaling = false; - StickerSystem.Instance.IsInStickerMode = !StickerSystem.Instance.IsInStickerMode; - return; - } - } - lastTime = DateTime.Now; - } - - #endregion Double-Click Place Sticker -} diff --git a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_DebugOptions.cs b/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_DebugOptions.cs deleted file mode 100644 index 85f1c14..0000000 --- a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_DebugOptions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using BTKUILib.UIObjects; - -namespace NAK.Stickers.Integrations -{ - public static partial class BtkUiAddon - { - private static void Setup_DebugOptionsCategory(Page page) - { - Category debugCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_DebugCategory); - - AddMelonToggle(ref debugCategory, ModSettings.Debug_NetworkInbound); - AddMelonToggle(ref debugCategory, ModSettings.Debug_NetworkOutbound); - } - } -} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickerSelection.cs b/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickerSelection.cs deleted file mode 100644 index ff50a8a..0000000 --- a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickerSelection.cs +++ /dev/null @@ -1,356 +0,0 @@ -using BTKUILib; -using BTKUILib.UIObjects; -using BTKUILib.UIObjects.Components; -using MTJobSystem; -using NAK.Stickers.Networking; -using NAK.Stickers.Utilities; - -namespace NAK.Stickers.Integrations -{ - public static partial class BtkUiAddon - { - private static readonly object _cacheLock = new(); - - private static bool _initialClearCacheFolder; - private static Category _stickerSelectionCategory; - internal static readonly Dictionary _cachedImages = new(); - - public static string GetBtkUiCachePath(string stickerName) - { - if (_cachedImages.TryGetValue(stickerName, out ImageInfo imageInfo)) - return "coui://uiresources/GameUI/mods/BTKUI/images/" + ModSettings.ModName + "/UserImages/" + imageInfo.cacheName + ".png"; - return string.Empty; - } - - internal class ImageInfo - { - public string filePath; - public string cacheName; // BTKUI cache path - public bool wasFoundThisPass; - public bool hasChanged; - public DateTime lastModified; - public Button button; - } - - #region Setup - - private static void Setup_StickerSelectionCategory(Page page) - { - _onOurTabOpened += UpdateStickerSelectionAsync; - _stickerSelectionCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_SelectionCategory); - ModNetwork.OnTextureOutboundStateChanged += OnSendingTextureOverNetworkChanged; // disable buttons when sending texture over network - StickerSystem.OnImageLoadFailed += OnLoadImageFailed; - GetInitialImageInfo(); - } - - private static void GetInitialImageInfo() - { - Task.Run(() => - { - try - { - string path = StickerSystem.GetStickersFolderPath(); - if (!Directory.Exists(path)) - { - StickerMod.Logger.Warning("Stickers folder not found."); - return; - } - - var stickerFiles = Directory.EnumerateFiles(path, "*.png"); - var currentFiles = new HashSet(stickerFiles); - - lock (_cacheLock) - { - if (!_initialClearCacheFolder) - { - _initialClearCacheFolder = true; - DeleteOldIcons(ModSettings.ModName); - } - - var keysToRemove = new List(); - - foreach (var stickerFile in currentFiles) - { - string stickerName = Path.GetFileNameWithoutExtension(stickerFile); - - if (_cachedImages.TryGetValue(stickerName, out ImageInfo imageInfo)) - { - imageInfo.wasFoundThisPass = true; - DateTime lastModified = File.GetLastWriteTime(stickerFile); - if (lastModified == imageInfo.lastModified) continue; - imageInfo.hasChanged = true; - imageInfo.lastModified = lastModified; - } - else - { - _cachedImages[stickerName] = new ImageInfo - { - filePath = stickerFile, - wasFoundThisPass = true, - hasChanged = true, - lastModified = File.GetLastWriteTime(stickerFile) - }; - } - } - - foreach (var kvp in _cachedImages) - { - var imageName = kvp.Key; - ImageInfo imageInfo = kvp.Value; - - if (!imageInfo.wasFoundThisPass) - { - MainThreadInvoke(() => - { - if (imageInfo.button != null) - { - imageInfo.button.Delete(); - imageInfo.button = null; - } - }); - - if (!string.IsNullOrEmpty(imageInfo.cacheName)) - { - DeleteOldIcon(ModSettings.ModName, imageInfo.cacheName); - imageInfo.cacheName = string.Empty; - } - - keysToRemove.Add(imageName); - } - else if (imageInfo.hasChanged) - { - imageInfo.hasChanged = false; - - if (!string.IsNullOrEmpty(imageInfo.cacheName)) - { - DeleteOldIcon(ModSettings.ModName, imageInfo.cacheName); - imageInfo.cacheName = string.Empty; - } - - MemoryStream imageStream = LoadStreamFromFile(imageInfo.filePath); - if (imageStream == null) continue; - - try - { - if (imageStream.Length > 256 * 1024) - ImageUtility.Resize(ref imageStream, 256, 256); - } - catch (Exception e) - { - StickerMod.Logger.Warning($"Failed to resize image: {e.Message}"); - } - - imageInfo.cacheName = $"{imageName}_{Guid.NewGuid()}"; - - PrepareIconFromMemoryStream(ModSettings.ModName, imageInfo.cacheName, imageStream); - } - - imageInfo.wasFoundThisPass = false; - } - - foreach (var key in keysToRemove) - _cachedImages.Remove(key); - } - } - catch (Exception e) - { - StickerMod.Logger.Error($"Failed to update sticker selection: {e.Message}"); - } - }); - } - - private static void UpdateStickerSelectionAsync() - { - Task.Run(() => - { - try - { - string path = StickerSystem.GetStickersFolderPath(); - if (!Directory.Exists(path)) - { - StickerMod.Logger.Warning("Stickers folder not found."); - return; - } - - var stickerFiles = Directory.EnumerateFiles(path, "*.png"); - var currentFiles = new HashSet(stickerFiles); - - lock (_cacheLock) - { - if (!_initialClearCacheFolder) - { - _initialClearCacheFolder = true; - DeleteOldIcons(ModSettings.ModName); - } - - var keysToRemove = new List(); - - foreach (var stickerFile in currentFiles) - { - string stickerName = Path.GetFileNameWithoutExtension(stickerFile); - - if (_cachedImages.TryGetValue(stickerName, out ImageInfo imageInfo)) - { - imageInfo.wasFoundThisPass = true; - if (imageInfo.button == null) - { - imageInfo.hasChanged = true; - continue; - } - DateTime lastModified = File.GetLastWriteTime(stickerFile); - if (lastModified == imageInfo.lastModified) continue; - imageInfo.hasChanged = true; - imageInfo.lastModified = lastModified; - } - else - { - _cachedImages[stickerName] = new ImageInfo - { - filePath = stickerFile, - wasFoundThisPass = true, - hasChanged = true, - lastModified = File.GetLastWriteTime(stickerFile) - }; - } - } - - foreach (var kvp in _cachedImages) - { - var imageName = kvp.Key; - ImageInfo imageInfo = kvp.Value; - - if (!imageInfo.wasFoundThisPass) - { - MainThreadInvoke(() => - { - if (imageInfo.button != null) - { - imageInfo.button.Delete(); - imageInfo.button = null; - } - }); - - if (!string.IsNullOrEmpty(imageInfo.cacheName)) - { - DeleteOldIcon(ModSettings.ModName, imageInfo.cacheName); - imageInfo.cacheName = string.Empty; - } - - keysToRemove.Add(imageName); - } - else if (imageInfo.hasChanged) - { - imageInfo.hasChanged = false; - - if (!string.IsNullOrEmpty(imageInfo.cacheName)) - { - DeleteOldIcon(ModSettings.ModName, imageInfo.cacheName); - imageInfo.cacheName = string.Empty; - } - - MemoryStream imageStream = LoadStreamFromFile(imageInfo.filePath); - if (imageStream == null) continue; - - try - { - if (imageStream.Length > 256 * 1024) - ImageUtility.Resize(ref imageStream, 256, 256); - } - catch (Exception e) - { - StickerMod.Logger.Warning($"Failed to resize image: {e.Message}"); - } - - imageInfo.cacheName = $"{imageName}_{Guid.NewGuid()}"; - - PrepareIconFromMemoryStream(ModSettings.ModName, imageInfo.cacheName, imageStream); - - MainThreadInvoke(() => - { - if (imageInfo.button != null) - imageInfo.button.ButtonIcon = "UserImages/" + imageInfo.cacheName; - else - { - imageInfo.button = _stickerSelectionCategory.AddButton(imageName, "UserImages/" + imageInfo.cacheName, $"Select {imageName}.", ButtonStyle.TextWithIcon); - imageInfo.button.OnPress += () => OnStickerButtonClick(imageInfo); - } - }); - } - - imageInfo.wasFoundThisPass = false; - } - - foreach (var key in keysToRemove) - _cachedImages.Remove(key); - - MainThreadInvoke(() => - { - _stickerSelectionCategory.CategoryName = $"{ModSettings.SM_SelectionCategory} ({_cachedImages.Count})"; - }); - } - } - catch (Exception e) - { - StickerMod.Logger.Error($"Failed to update sticker selection: {e.Message}"); - } - }); - } - - private static MemoryStream LoadStreamFromFile(string filePath) - { - if (!File.Exists(filePath)) - return null; - - try - { - //StickerMod.Logger.Msg($"Loaded sticker stream from {filePath}"); - - using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read); - - MemoryStream memoryStream = new(); - fileStream.CopyTo(memoryStream); - memoryStream.Position = 0; // Ensure the position is reset before returning - return memoryStream; - } - catch (Exception e) - { - StickerMod.Logger.Warning($"Failed to load stream from {filePath}: {e.Message}"); - return null; - } - } - private static void MainThreadInvoke(Action action) - => MTJobManager.RunOnMainThread("fuck", action.Invoke); - - #endregion Setup - - #region Button Actions - - private static void OnStickerButtonClick(ImageInfo imageInfo) - { - // i wish i could highlight it - StickerSystem.Instance.LoadImage(imageInfo.button.ButtonText); - } - - #endregion Button Actions - - #region Callbacks - - private static void OnSendingTextureOverNetworkChanged(bool isSending) - { - MTJobManager.RunAsyncOnMainThread("fuck2", () => - { - // go through all buttons and disable them - if (_isOurTabOpened && isSending) QuickMenuAPI.ShowAlertToast("Sending Sticker over Mod Network...", 2); - foreach ((_, ImageInfo value) in _cachedImages) value.button.Disabled = isSending; - }); - } - - private static void OnLoadImageFailed(string reason) - { - if (!_isOurTabOpened) return; - QuickMenuAPI.ShowAlertToast(reason, 2); - } - - #endregion Callbacks - } -} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/BtkuiAddon_Utils.cs b/Stickers/Integrations/BTKUI/BtkuiAddon_Utils.cs deleted file mode 100644 index a66fedb..0000000 --- a/Stickers/Integrations/BTKUI/BtkuiAddon_Utils.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Reflection; -using System.Security.Cryptography; -using BTKUILib; -using BTKUILib.UIObjects; -using BTKUILib.UIObjects.Components; -using MelonLoader; -using UnityEngine; - -namespace NAK.Stickers.Integrations; - -public static partial class BtkUiAddon -{ - #region Melon Preference Helpers - - private static ToggleButton AddMelonToggle(ref Category category, MelonPreferences_Entry entry) - { - ToggleButton toggle = category.AddToggle(entry.DisplayName, entry.Description, entry.Value); - toggle.OnValueUpdated += b => entry.Value = b; - return toggle; - } - - private static SliderFloat AddMelonSlider(ref Category category, MelonPreferences_Entry entry, float min, - float max, int decimalPlaces = 2, bool allowReset = true) - { - SliderFloat slider = category.AddSlider(entry.DisplayName, entry.Description, - Mathf.Clamp(entry.Value, min, max), min, max, decimalPlaces, entry.DefaultValue, allowReset); - slider.OnValueUpdated += f => entry.Value = f; - return slider; - } - - private static Button AddMelonStringInput(ref Category category, MelonPreferences_Entry entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly) - { - Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle); - button.OnPress += () => QuickMenuAPI.OpenKeyboard(entry.Value, s => entry.Value = s); - return button; - } - - private static Button AddMelonNumberInput(ref Category category, MelonPreferences_Entry entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly) - { - Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle); - button.OnPress += () => QuickMenuAPI.OpenNumberInput(entry.DisplayName, entry.Value, f => entry.Value = f); - return button; - } - - private static Category AddMelonCategory(ref Page page, MelonPreferences_Entry entry, bool showHeader = true) - { - Category category = page.AddCategory(entry.DisplayName, showHeader, true, entry.Value); - category.OnCollapse += b => entry.Value = b; - return category; - } - - #endregion Melon Preference Helpers - - #region Icon Utils - - private static Stream GetIconStream(string iconName) - { - Assembly assembly = Assembly.GetExecutingAssembly(); - string assemblyName = assembly.GetName().Name; - return assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}"); - } - - #endregion Icon Utils - - public static void PrepareIconFromMemoryStream(string modName, string iconName, MemoryStream destination) - { - if (destination == null) - { - StickerMod.Logger.Error("Mod " + modName + " attempted to prepare " + iconName + " but the resource stream was null! Yell at the mod author to fix this!"); - } - else - { - modName = UIUtils.GetCleanString(modName); - string path1 = @"ChilloutVR_Data\StreamingAssets\Cohtml\UIResources\GameUI\mods\BTKUI\images\" + modName + @"\UserImages"; - if (!Directory.Exists(path1)) Directory.CreateDirectory(path1); - string path2 = path1 + "\\" + iconName + ".png"; - File.WriteAllBytes(path2, destination.ToArray()); - } - } - - private static void DeleteOldIcon(string modName, string iconName) - { - string directoryPath = Path.Combine("ChilloutVR_Data", "StreamingAssets", "Cohtml", "UIResources", "GameUI", "mods", "BTKUI", "images", modName, "UserImages"); - string oldIconPath = Path.Combine(directoryPath, $"{iconName}.png"); - if (!File.Exists(oldIconPath)) - return; - - File.Delete(oldIconPath); - //StickerMod.Logger.Msg($"Deleted old icon: {oldIconPath}"); - } - - private static void DeleteOldIcons(string modName) - { - string directoryPath = Path.Combine("ChilloutVR_Data", "StreamingAssets", "Cohtml", "UIResources", "GameUI", "mods", "BTKUI", "images", modName, "UserImages"); - if (!Directory.Exists(directoryPath)) - return; - - foreach (string file in Directory.EnumerateFiles(directoryPath, "*.png")) - { - File.Delete(file); - //StickerMod.Logger.Msg($"Deleted old icon: {file}"); - } - } -} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs b/Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs new file mode 100644 index 0000000..0a8fc32 --- /dev/null +++ b/Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs @@ -0,0 +1,13 @@ +using BTKUILib.UIObjects; + +namespace NAK.Stickers.Integrations; + +public static partial class BTKUIAddon +{ + private static void Setup_DebugOptionsCategory() + { + Category debugCategory = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_DebugCategory); + debugCategory.AddMelonToggle(ModSettings.Debug_NetworkInbound); + debugCategory.AddMelonToggle(ModSettings.Debug_NetworkOutbound); + } +} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickersMod.cs b/Stickers/Integrations/BTKUI/UIAddon.Category.StickersMod.cs similarity index 76% rename from Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickersMod.cs rename to Stickers/Integrations/BTKUI/UIAddon.Category.StickersMod.cs index 3681fd1..03743bc 100644 --- a/Stickers/Integrations/BTKUI/BtkUiAddon_CAT_StickersMod.cs +++ b/Stickers/Integrations/BTKUI/UIAddon.Category.StickersMod.cs @@ -2,44 +2,24 @@ using BTKUILib.UIObjects; using BTKUILib.UIObjects.Components; using BTKUILib.UIObjects.Objects; -using UnityEngine; namespace NAK.Stickers.Integrations; -public static partial class BtkUiAddon +public static partial class BTKUIAddon { private static Category _ourCategory; private static readonly MultiSelection _sfxSelection = - new( - "Sticker SFX", - new[] { "Little Big Planet", "Source Engine", "None" }, - (int)ModSettings.Entry_SelectedSFX.Value - ) - { - OnOptionUpdated = i => ModSettings.Entry_SelectedSFX.Value = (ModSettings.SFXType)i - }; - + BTKUILibExtensions.CreateMelonMultiSelection(ModSettings.Entry_SelectedSFX); + private static readonly MultiSelection _desktopKeybindSelection = - new( - "Desktop Keybind", - Enum.GetNames(typeof(ModSettings.KeyBind)), - Array.IndexOf(Enum.GetValues(typeof(ModSettings.KeyBind)), ModSettings.Entry_PlaceBinding.Value) - ) - { - OnOptionUpdated = i => - { - string[] options = Enum.GetNames(typeof(ModSettings.KeyBind)); - ModSettings.Entry_PlaceBinding.Value = (ModSettings.KeyBind)Enum.Parse(typeof(ModSettings.KeyBind), options[i]); - } - }; + BTKUILibExtensions.CreateMelonMultiSelection(ModSettings.Entry_PlaceBinding); #region Category Setup - private static void Setup_StickersModCategory(Page page) + private static void Setup_StickersModCategory() { - //_ourCategory = page.AddCategory(ModSettings.Stickers_SettingsCategory, ModSettings.ModName, true, true, false); - _ourCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_SettingsCategory); + _ourCategory = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_SettingsCategory); Button placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon); placeStickersButton.OnPress += OnPlaceStickersButtonClick; @@ -55,6 +35,8 @@ public static partial class BtkUiAddon Button openMultiSelectionButton = _ourCategory.AddButton("Sticker SFX", "Stickers-headset", "Choose the SFX used when a sticker is placed.", ButtonStyle.TextWithIcon); openMultiSelectionButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_sfxSelection); + + _ourCategory.AddMelonToggle(ModSettings.Entry_HapticsOnPlace); ToggleButton toggleDesktopKeybindButton = _ourCategory.AddToggle("Use Desktop Keybind", "Should the Desktop keybind be active.", ModSettings.Entry_UsePlaceBinding.Value); Button openDesktopKeybindButton = _ourCategory.AddButton("Desktop Keybind", "Stickers-alphabet", "Choose the key binding to place stickers.", ButtonStyle.TextWithIcon); diff --git a/Stickers/Integrations/BTKUI/UIAddon.Main.cs b/Stickers/Integrations/BTKUI/UIAddon.Main.cs new file mode 100644 index 0000000..8bce9c0 --- /dev/null +++ b/Stickers/Integrations/BTKUI/UIAddon.Main.cs @@ -0,0 +1,92 @@ +using BTKUILib; +using BTKUILib.UIObjects; +using NAK.Stickers.Networking; +using NAK.Stickers.Utilities; + +namespace NAK.Stickers.Integrations; + +public static partial class BTKUIAddon +{ + private static Page _rootPage; + private static string _rootPageElementID; + + private static bool _isOurTabOpened; + + public static void Initialize() + { + Setup_Icons(); + Setup_StickerModTab(); + Setup_PlayerOptionsPage(); + } + + #region Initialization + + private static void Setup_Icons() + { + // All icons used - https://www.flaticon.com/authors/gohsantosadrive + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-alphabet", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-alphabet.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-eraser", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-eraser.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-folder", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-folder.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-headset", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-headset.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magnifying-glass", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-magnifying-glass.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-pencil", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-pencil.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-puzzle", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-puzzle.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-rubbish-bin", BTKUILibExtensions.GetIconStream("Gohsantosadrive_Icons.Stickers-rubbish-bin.png")); + } + + private static void Setup_StickerModTab() + { + _rootPage = new Page(ModSettings.ModName, ModSettings.SM_SettingsCategory, true, "Stickers-puzzle") + { + MenuTitle = ModSettings.SM_SettingsCategory, + MenuSubtitle = "Stickers! Double-click the tab to quickly toggle Sticker Mode.", + }; + + _rootPageElementID = _rootPage.ElementID; + + QuickMenuAPI.OnTabChange += OnTabChange; + ModNetwork.OnTextureOutboundStateChanged += (isSending) => + { + if (_isOurTabOpened && isSending) QuickMenuAPI.ShowAlertToast("Sending Sticker over Mod Network...", 2); + //_rootPage.Disabled = isSending; // TODO: fix being able to select stickers while sending + }; + + StickerSystem.OnStickerLoaded += (slotIndex, imageRelativePath) => + { + if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast($"Sticker loaded: {imageRelativePath}", 2); + _stickerSelectionButtons[slotIndex].ButtonIcon = StickerCache.GetBtkUiIconName(imageRelativePath); + }; + + StickerSystem.OnStickerLoadFailed += (slotIndex, error) => + { + if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast(error, 3); + }; + + Setup_StickersModCategory(); + Setup_StickerSelectionCategory(); + Setup_DebugOptionsCategory(); + } + + #endregion + + #region Double-Click Place Sticker + + private static DateTime lastTime = DateTime.Now; + + private static void OnTabChange(string newTab, string previousTab) + { + _isOurTabOpened = newTab == _rootPageElementID; + if (!_isOurTabOpened) return; + + TimeSpan timeDifference = DateTime.Now - lastTime; + if (timeDifference.TotalSeconds <= 0.5) + { + StickerSystem.Instance.IsInStickerMode = !StickerSystem.Instance.IsInStickerMode; + return; + } + lastTime = DateTime.Now; + } + + #endregion Double-Click Place Sticker +} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs b/Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs new file mode 100644 index 0000000..f44fc40 --- /dev/null +++ b/Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs @@ -0,0 +1,53 @@ +using BTKUILib; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using NAK.Stickers.Networking; + +namespace NAK.Stickers.Integrations; + +public static partial class BTKUIAddon +{ + #region Setup + + private static ToggleButton _disallowForSessionButton; + + private static void Setup_PlayerOptionsPage() + { + Category category = QuickMenuAPI.PlayerSelectPage.AddCategory(ModSettings.SM_SettingsCategory, ModSettings.ModName); + + //Button identifyButton = category.AddButton("Identify Stickers", "Stickers-magnifying-glass", "Identify this players stickers by making them flash."); + //identifyButton.OnPress += OnPressIdentifyPlayerStickersButton; + + Button clearStickersButton = category.AddButton("Clear Stickers", "Stickers-eraser", "Clear this players stickers."); + clearStickersButton.OnPress += OnPressClearSelectedPlayerStickersButton; + + _disallowForSessionButton = category.AddToggle("Block for Session", "Disallow this player from using stickers for this session. This setting will not persist through restarts.", false); + _disallowForSessionButton.OnValueUpdated += OnToggleDisallowForSessionButton; + QuickMenuAPI.OnPlayerSelected += (_, id) => { _disallowForSessionButton.ToggleValue = ModNetwork.IsPlayerACriminal(id); }; + } + + #endregion Setup + + #region Callbacks + + // private static void OnPressIdentifyPlayerStickersButton() + // { + // if (string.IsNullOrEmpty(QuickMenuAPI.SelectedPlayerID)) return; + // StickerSystem.Instance.OnStickerIdentifyReceived(QuickMenuAPI.SelectedPlayerID); + // } + + private static void OnPressClearSelectedPlayerStickersButton() + { + if (string.IsNullOrEmpty(QuickMenuAPI.SelectedPlayerID)) return; + StickerSystem.Instance.OnStickerClearAllReceived(QuickMenuAPI.SelectedPlayerID); + } + + private static void OnToggleDisallowForSessionButton(bool isOn) + { + if (string.IsNullOrEmpty(QuickMenuAPI.SelectedPlayerID)) return; + ModNetwork.HandleDisallowForSession(QuickMenuAPI.SelectedPlayerID, isOn); + } + + + #endregion Callbacks +} \ No newline at end of file diff --git a/Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs b/Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs new file mode 100644 index 0000000..1874ccb --- /dev/null +++ b/Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs @@ -0,0 +1,270 @@ +using BTKUILib; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using MTJobSystem; +using NAK.Stickers.Utilities; +using UnityEngine; + +namespace NAK.Stickers.Integrations; + +public static partial class BTKUIAddon +{ + #region Constants and Fields + + private static readonly HashSet SUPPORTED_IMAGE_EXTENSIONS = new() { ".png", ".jpg", ".jpeg" }; + + private static Page _ourDirectoryBrowserPage; + + private static Category _fileCategory; + private static Category _folderCategory; + + private static Button[] _fileButtons = new Button[80]; // 100 files, will resize if needed + private static Button[] _folderButtons = new Button[20]; // 20 folders, will resize if needed + + private static readonly Button[] _stickerSelectionButtons = new Button[4]; + private static float _stickerSelectionButtonDoubleClickTime; + + private static DirectoryInfo _curDirectoryInfo; + private static string _initialDirectory; + private static int _curSelectedSticker; + + private static readonly object _isPopulatingLock = new(); + private static bool _isPopulating; + internal static bool IsPopulatingPage { + get { lock (_isPopulatingLock) return _isPopulating; } + private set { lock (_isPopulatingLock) _isPopulating = value; } + } + + #endregion Constants and Fields + + #region Page Setup + + private static void Setup_StickerSelectionCategory() + { + _initialDirectory = StickerSystem.GetStickersFolderPath(); + _curDirectoryInfo = new DirectoryInfo(_initialDirectory); + + // Create page + _ourDirectoryBrowserPage = Page.GetOrCreatePage(ModSettings.ModName, "Directory Browser"); + QuickMenuAPI.AddRootPage(_ourDirectoryBrowserPage); + + // Setup categories + _folderCategory = _ourDirectoryBrowserPage.AddCategory("Subdirectories"); + _fileCategory = _ourDirectoryBrowserPage.AddCategory("Images"); + + SetupFolderButtons(); + SetupFileButtons(); + SetupStickerSelectionButtons(); + + _ourDirectoryBrowserPage.OnPageOpen += OnPageOpen; + _ourDirectoryBrowserPage.OnPageClosed += OnPageClosed; + } + + private static void SetupFolderButtons(int startIndex = 0) + { + for (int i = startIndex; i < _folderButtons.Length; i++) + { + Button button = _folderCategory.AddButton("A", "Stickers-folder", "A"); + button.OnPress += () => + { + if (IsPopulatingPage) return; + _curDirectoryInfo = new DirectoryInfo(Path.Combine(_curDirectoryInfo.FullName, button.ButtonTooltip[5..])); + _ourDirectoryBrowserPage.OpenPage(false, true); + }; + _folderButtons[i] = button; + } + } + + private static void SetupFileButtons(int startIndex = 0) + { + for (int i = startIndex; i < _fileButtons.Length; i++) + { + Button button = _fileCategory.AddButton(string.Empty, "Stickers-folder", "A", ButtonStyle.FullSizeImage); + button.Hidden = true; + button.OnPress += () => + { + string absolutePath = Path.Combine(_curDirectoryInfo.FullName, button.ButtonTooltip[5..]); + string relativePath = Path.GetRelativePath(_initialDirectory, absolutePath); + StickerSystem.Instance.LoadImage(relativePath, _curSelectedSticker); + }; + _fileButtons[i] = button; + } + } + + private static void SetupStickerSelectionButtons() + { + Category stickerSelection = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_SelectionCategory); + + for (int i = 0; i < _stickerSelectionButtons.Length; i++) + { + Button button = stickerSelection.AddButton(string.Empty, "Stickers-puzzle", "Click to select sticker for placement. Double-click or hold to select from Stickers folder.", ButtonStyle.FullSizeImage); + var curIndex = i; + button.OnPress += () => SelectStickerAtSlot(curIndex); + button.OnHeld += () => OpenStickerSelectionForSlot(curIndex); + _stickerSelectionButtons[i] = button; + + // initial setup + button.ButtonIcon = StickerCache.GetBtkUiIconName(ModSettings.Hidden_SelectedStickerNames.Value[i]); + } + } + + #endregion Page Setup + + #region Private Methods + + private static void OnPageOpen() + { + if (IsPopulatingPage) return; // btkui bug, page open is called twice when using OnHeld + IsPopulatingPage = true; + + _ourDirectoryBrowserPage.PageDisplayName = _curDirectoryInfo.Name; + + HideAllButtons(_folderButtons); + HideAllButtons(_fileButtons); + + // Populate the page + Task.Run(PopulateMenuItems); + } + + private static void OnPageClosed() + { + if (_curDirectoryInfo.FullName != _initialDirectory) + _curDirectoryInfo = new DirectoryInfo(Path.Combine(_curDirectoryInfo.FullName, @"..\")); + } + + private static void HideAllButtons(Button[] buttons) + { + foreach (Button button in buttons) + { + if (button == null) break; // Array resized, excess buttons are generating + //if (button.Hidden) break; // Reached the end of the visible buttons + button.Hidden = true; + } + } + + private static void SelectStickerAtSlot(int index) + { + if (_curSelectedSticker != index) + { + _curSelectedSticker = index; + _stickerSelectionButtonDoubleClickTime = 0f; + } + + StickerSystem.Instance.SelectedStickerSlot = index; + + // double-click to open (otherwise just hold) + if (Time.time - _stickerSelectionButtonDoubleClickTime < 0.5f) + { + OpenStickerSelectionForSlot(index); + _stickerSelectionButtonDoubleClickTime = 0f; + return; + } + _stickerSelectionButtonDoubleClickTime = Time.time; + } + + private static void OpenStickerSelectionForSlot(int index) + { + if (IsPopulatingPage) return; + _curSelectedSticker = index; + _curDirectoryInfo = new DirectoryInfo(_initialDirectory); + _ourDirectoryBrowserPage.OpenPage(false, true); + } + + private static void PopulateMenuItems() + { + // StickerMod.Logger.Msg("Populating menu items."); + try + { + var directories = _curDirectoryInfo.GetDirectories(); + var files = _curDirectoryInfo.GetFiles(); + + MTJobManager.RunOnMainThread("PopulateMenuItems", () => + { + // resize the arrays to the max amount of buttons + int difference = directories.Length - _folderButtons.Length; + if (difference > 0) + { + Array.Resize(ref _folderButtons, directories.Length); + SetupFolderButtons(difference); + StickerMod.Logger.Msg($"Resized folder buttons to {directories.Length}"); + } + + difference = files.Length - _fileButtons.Length; + if (difference > 0) + { + Array.Resize(ref _fileButtons, files.Length); + SetupFileButtons(difference); + StickerMod.Logger.Msg($"Resized file buttons to {files.Length}"); + } + + _folderCategory.Hidden = directories.Length == 0; + _folderCategory.CategoryName = $"Subdirectories ({directories.Length})"; + _fileCategory.Hidden = files.Length == 0; + _fileCategory.CategoryName = $"Images ({files.Length})"; + }); + + PopulateFolders(directories); + PopulateFiles(files); + } + catch (Exception e) + { + StickerMod.Logger.Error($"Failed to populate menu items: {e.Message}"); + } + finally + { + IsPopulatingPage = false; + } + } + + private static void PopulateFolders(IReadOnlyList directories) + { + for (int i = 0; i < _folderButtons.Length; i++) + { + Button button = _folderButtons[i]; + if (i >= directories.Count) + break; + + button.ButtonText = directories[i].Name; + button.ButtonTooltip = $"Open {directories[i].Name}"; + MTJobManager.RunOnMainThread("PopulateMenuItems", () => button.Hidden = false); + + if (i <= 16) Thread.Sleep(10); // For the pop-in effect + } + } + + private static void PopulateFiles(IReadOnlyList files) + { + for (int i = 0; i < _fileButtons.Length; i++) + { + Button button = _fileButtons[i]; + if (i >= files.Count) + break; + + FileInfo fileInfo = files[i]; + + if (!SUPPORTED_IMAGE_EXTENSIONS.Contains(fileInfo.Extension.ToLower())) + continue; + + string relativePath = Path.GetRelativePath(_initialDirectory, fileInfo.FullName); + string relativePathWithoutExtension = relativePath[..^fileInfo.Extension.Length]; + + button.ButtonTooltip = $"Load {fileInfo.Name}"; // Do not change "Load " prefix, we extract file name + + if (StickerCache.IsThumbnailAvailable(relativePathWithoutExtension)) + { + button.ButtonIcon = StickerCache.GetBtkUiIconName(relativePath); + } + else + { + button.ButtonIcon = string.Empty; + StickerCache.EnqueueThumbnailGeneration(fileInfo, button); + } + + MTJobManager.RunOnMainThread("PopulateMenuItems", () => button.Hidden = false); + + if (i <= 16) Thread.Sleep(10); // For the pop-in effect + } + } + + #endregion Icon Utils +} \ No newline at end of file diff --git a/Stickers/Main.cs b/Stickers/Main.cs index bb4be55..310e198 100644 --- a/Stickers/Main.cs +++ b/Stickers/Main.cs @@ -22,10 +22,11 @@ public class StickerMod : MelonMod ApplyPatches(typeof(Patches.PlayerSetupPatches)); ApplyPatches(typeof(Patches.ControllerRayPatches)); + ApplyPatches(typeof(Patches.ShaderFilterHelperPatches)); LoadAssetBundle(); - InitializeIntegration(nameof(BTKUILib), BtkUiAddon.Initialize); // quick menu ui + InitializeIntegration(nameof(BTKUILib), BTKUIAddon.Initialize); // quick menu ui } public override void OnUpdate() @@ -36,7 +37,7 @@ public class StickerMod : MelonMod if (!Input.GetKeyDown((KeyCode)ModSettings.Entry_PlaceBinding.Value)) return; - StickerSystem.Instance.PlaceStickerFromTransform(PlayerSetup.Instance.activeCam.transform); + StickerSystem.Instance.PlaceStickerFromControllerRay(PlayerSetup.Instance.activeCam.transform); } public override void OnApplicationQuit() @@ -80,11 +81,13 @@ public class StickerMod : MelonMod private const string SourceSFX_PlayerSprayer = "Assets/Mods/Stickers/Source_sound_player_sprayer.wav"; private const string LittleBigPlanetSFX_StickerPlace = "Assets/Mods/Stickers/LBP_Sticker_Place.wav"; + private const string FactorioSFX_AlertDestroyed = "Assets/Mods/Stickers/Factorio_alert_destroyed.wav"; internal static Shader DecalSimpleShader; internal static Shader DecalWriterShader; internal static AudioClip SourceSFXPlayerSprayer; - internal static AudioClip LittleBigPlanetStickerPlace; + internal static AudioClip LittleBigPlanetSFXStickerPlace; + internal static AudioClip FactorioSFXAlertDestroyed; private void LoadAssetBundle() { @@ -127,14 +130,22 @@ public class StickerMod : MelonMod SourceSFXPlayerSprayer.hideFlags |= HideFlags.DontUnloadUnusedAsset; LoggerInstance.Msg($"Loaded {SourceSFX_PlayerSprayer}!"); - LittleBigPlanetStickerPlace = assetBundle.LoadAsset(LittleBigPlanetSFX_StickerPlace); - if (LittleBigPlanetStickerPlace == null) { + LittleBigPlanetSFXStickerPlace = assetBundle.LoadAsset(LittleBigPlanetSFX_StickerPlace); + if (LittleBigPlanetSFXStickerPlace == null) { LoggerInstance.Error($"Failed to load {LittleBigPlanetSFX_StickerPlace}! Prefab is null!"); return; } - LittleBigPlanetStickerPlace.hideFlags |= HideFlags.DontUnloadUnusedAsset; + LittleBigPlanetSFXStickerPlace.hideFlags |= HideFlags.DontUnloadUnusedAsset; LoggerInstance.Msg($"Loaded {LittleBigPlanetSFX_StickerPlace}!"); + FactorioSFXAlertDestroyed = assetBundle.LoadAsset(FactorioSFX_AlertDestroyed); + if (FactorioSFXAlertDestroyed == null) { + LoggerInstance.Error($"Failed to load {FactorioSFX_AlertDestroyed}! Prefab is null!"); + return; + } + FactorioSFXAlertDestroyed.hideFlags |= HideFlags.DontUnloadUnusedAsset; + LoggerInstance.Msg($"Loaded {FactorioSFX_AlertDestroyed}!"); + // load LoggerInstance.Msg("Asset bundle successfully loaded!"); diff --git a/Stickers/ModSettings.cs b/Stickers/ModSettings.cs index 4ed80fe..8c15b5c 100644 --- a/Stickers/ModSettings.cs +++ b/Stickers/ModSettings.cs @@ -5,13 +5,20 @@ namespace NAK.Stickers; public static class ModSettings { + #region Constants & Category + internal const string ModName = nameof(StickerMod); + internal const string SM_SettingsCategory = "Stickers Mod"; - internal const string SM_SelectionCategory = "Sticker Selection"; + private const string SM_SelectionCategory = "Sticker Selection"; private const string DEBUG_SettingsCategory = "Debug Options"; + internal const int MaxStickerSlots = 4; + private static readonly MelonPreferences_Category Category = MelonPreferences.CreateCategory(ModName); + + #endregion Constants & Category #region Hidden Foldout Entries @@ -28,18 +35,14 @@ public static class ModSettings #region Stickers Mod Settings + internal static readonly MelonPreferences_Entry Entry_HapticsOnPlace = + Category.CreateEntry("haptics_on_place", true, "Haptics On Place", "Enable haptic feedback when placing stickers."); + internal static readonly MelonPreferences_Entry Entry_PlayerUpAlignmentThreshold = Category.CreateEntry("player_up_alignment_threshold", 20f, "Player Up Alignment Threshold", "The threshold the controller roll can be within to align perfectly with the player up vector. Set to 0f to always align to controller up."); internal static readonly MelonPreferences_Entry Entry_SelectedSFX = - Category.CreateEntry("selected_sfx", SFXType.LBP, "Selected SFX", "The SFX used when a sticker is placed."); - - internal enum SFXType - { - LBP, - Source, - None - } + Category.CreateEntry("selected_sfx", SFXType.LittleBigPlanetSticker, "Selected SFX", "The SFX used when a sticker is placed."); internal static readonly MelonPreferences_Entry Entry_UsePlaceBinding = Category.CreateEntry("use_binding", true, "Use Place Binding", "Use the place binding to place stickers."); @@ -47,113 +50,13 @@ public static class ModSettings internal static readonly MelonPreferences_Entry Entry_PlaceBinding = Category.CreateEntry("place_binding", KeyBind.G, "Sticker Bind", "The key binding to place stickers."); - internal enum KeyBind - { - // Alphabetic keys - A = KeyCode.A, // 0x00000061 - B = KeyCode.B, // 0x00000062 - C = KeyCode.C, // 0x00000063 - D = KeyCode.D, // 0x00000064 - E = KeyCode.E, // 0x00000065 - F = KeyCode.F, // 0x00000066 - G = KeyCode.G, // 0x00000067 - H = KeyCode.H, // 0x00000068 - I = KeyCode.I, // 0x00000069 - J = KeyCode.J, // 0x0000006A - K = KeyCode.K, // 0x0000006B - L = KeyCode.L, // 0x0000006C - M = KeyCode.M, // 0x0000006D - N = KeyCode.N, // 0x0000006E - O = KeyCode.O, // 0x0000006F - P = KeyCode.P, // 0x00000070 - Q = KeyCode.Q, // 0x00000071 - R = KeyCode.R, // 0x00000072 - S = KeyCode.S, // 0x00000073 - T = KeyCode.T, // 0x00000074 - U = KeyCode.U, // 0x00000075 - V = KeyCode.V, // 0x00000076 - W = KeyCode.W, // 0x00000077 - X = KeyCode.X, // 0x00000078 - Y = KeyCode.Y, // 0x00000079 - Z = KeyCode.Z, // 0x0000007A - - // Mouse Buttons - Mouse0 = KeyCode.Mouse0, // 0x00000143 - Mouse1 = KeyCode.Mouse1, // 0x00000144 - Mouse2 = KeyCode.Mouse2, // 0x00000145 - Mouse3 = KeyCode.Mouse3, // 0x00000146 - Mouse4 = KeyCode.Mouse4, // 0x00000147 - Mouse5 = KeyCode.Mouse5, // 0x00000148 - Mouse6 = KeyCode.Mouse6, // 0x00000149 - - // Special Characters - // Backspace = KeyCode.Backspace, // 0x00000008 - // Tab = KeyCode.Tab, // 0x00000009 - // Clear = KeyCode.Clear, // 0x0000000C - // Return = KeyCode.Return, // 0x0000000D - // Pause = KeyCode.Pause, // 0x00000013 - // Escape = KeyCode.Escape, // 0x0000001B - // Space = KeyCode.Space, // 0x00000020 - // Exclaim = KeyCode.Exclaim, // 0x00000021 - // DoubleQuote = KeyCode.DoubleQuote, // 0x00000022 - // Hash = KeyCode.Hash, // 0x00000023 - // Dollar = KeyCode.Dollar, // 0x00000024 - // Percent = KeyCode.Percent, // 0x00000025 - // Ampersand = KeyCode.Ampersand, // 0x00000026 - // Quote = KeyCode.Quote, // 0x00000027 - // LeftParen = KeyCode.LeftParen, // 0x00000028 - // RightParen = KeyCode.RightParen, // 0x00000029 - // Asterisk = KeyCode.Asterisk, // 0x0000002A - // Plus = KeyCode.Plus, // 0x0000002B - // Comma = KeyCode.Comma, // 0x0000002C - // Minus = KeyCode.Minus, // 0x0000002D - // Period = KeyCode.Period, // 0x0000002E - // Slash = KeyCode.Slash, // 0x0000002F - // Alpha0 = KeyCode.Alpha0, // 0x00000030 - // Alpha1 = KeyCode.Alpha1, // 0x00000031 - // Alpha2 = KeyCode.Alpha2, // 0x00000032 - // Alpha3 = KeyCode.Alpha3, // 0x00000033 - // Alpha4 = KeyCode.Alpha4, // 0x00000034 - // Alpha5 = KeyCode.Alpha5, // 0x00000035 - // Alpha6 = KeyCode.Alpha6, // 0x00000036 - // Alpha7 = KeyCode.Alpha7, // 0x00000037 - // Alpha8 = KeyCode.Alpha8, // 0x00000038 - // Alpha9 = KeyCode.Alpha9, // 0x00000039 - // Colon = KeyCode.Colon, // 0x0000003A - // Semicolon = KeyCode.Semicolon, // 0x0000003B - // Less = KeyCode.Less, // 0x0000003C - // Equals = KeyCode.Equals, // 0x0000003D - // Greater = KeyCode.Greater, // 0x0000003E - // Question = KeyCode.Question, // 0x0000003F - // At = KeyCode.At, // 0x00000040 - // LeftBracket = KeyCode.LeftBracket, // 0x0000005B - // Backslash = KeyCode.Backslash, // 0x0000005C - // RightBracket = KeyCode.RightBracket, // 0x0000005D - // Caret = KeyCode.Caret, // 0x0000005E - // Underscore = KeyCode.Underscore, // 0x0000005F - // BackQuote = KeyCode.BackQuote, // 0x00000060 - // Delete = KeyCode.Delete // 0x0000007F - } - - internal static readonly MelonPreferences_Entry Hidden_SelectedStickerName = - Category.CreateEntry("hidden_selected_sticker", string.Empty, is_hidden: true, display_name: "Selected Sticker", description: "The currently selected sticker name."); + internal static readonly MelonPreferences_Entry Hidden_SelectedStickerNames = + Category.CreateEntry("selected_sticker_name", Array.Empty(), + display_name: "Selected Sticker Name", + description: "The name of the sticker selected for stickering.", + is_hidden: true); #endregion Stickers Mod Settings - - #region Decalery Settings - - internal static readonly MelonPreferences_Entry Decalery_DecalMode = - Category.CreateEntry("decalery_decal_mode", DecaleryMode.GPU, display_name: "Decal Mode", description: "The mode Decalery should use for decal creation. By default GPU should be used. **Note:** Not all content is marked as readable, so only the GPU modes are expected to work properly on UGC."); - - internal enum DecaleryMode - { - CPU, - GPU, - CPUBurst, - GPUIndirect - } - - #endregion Decalery Settings #region Debug Settings @@ -169,8 +72,15 @@ public static class ModSettings internal static void Initialize() { + // ensure sticker slots are initialized to the correct size + string[] selectedStickerNames = Hidden_SelectedStickerNames.Value; + if (selectedStickerNames.Length != MaxStickerSlots) Array.Resize(ref selectedStickerNames, MaxStickerSlots); + Hidden_SelectedStickerNames.Value = selectedStickerNames; + + foreach (var selectedSticker in selectedStickerNames) + StickerMod.Logger.Msg($"Selected Sticker: {selectedSticker}"); + Entry_PlayerUpAlignmentThreshold.OnEntryValueChanged.Subscribe(OnPlayerUpAlignmentThresholdChanged); - Decalery_DecalMode.OnEntryValueChanged.Subscribe(OnDecaleryDecalModeChanged); } #endregion Initialization @@ -181,13 +91,6 @@ public static class ModSettings { Entry_PlayerUpAlignmentThreshold.Value = Mathf.Clamp(newValue, 0f, 180f); } - - private static void OnDecaleryDecalModeChanged(DecaleryMode oldValue, DecaleryMode newValue) - { - DecalManager.SetPreferredMode((DecalUtils.Mode)newValue, newValue == DecaleryMode.GPUIndirect, 0); - if (newValue != DecaleryMode.GPU) StickerMod.Logger.Warning("Decalery is not set to GPU mode. Expect compatibility issues with user generated content when mesh data is not marked as readable."); - StickerSystem.Instance.CleanupAll(); - } #endregion Setting Changed Callbacks } \ No newline at end of file diff --git a/Stickers/Patches.cs b/Stickers/Patches.cs index d13a10e..717ebb2 100644 --- a/Stickers/Patches.cs +++ b/Stickers/Patches.cs @@ -1,5 +1,7 @@ using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.IO; using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; using HarmonyLib; using UnityEngine; @@ -28,6 +30,20 @@ internal static class ControllerRayPatches if (__instance._hitUIInternal || !__instance._interactDown) return; - StickerSystem.Instance.PlaceStickerFromTransform(__instance.rayDirectionTransform); + StickerSystem.Instance.PlaceStickerFromControllerRay(__instance.rayDirectionTransform, __instance.hand); + } +} + +internal static class ShaderFilterHelperPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(ShaderFilterHelper), nameof(ShaderFilterHelper.SetupFilter))] + private static void Prefix_ShaderFilterHelper_SetupFilter() + { + if (!MetaPort.Instance.settings.GetSettingsBool("ExperimentalShaderLimitEnabled")) + return; + + StickerMod.Logger.Warning("ExperimentalShaderLimitEnabled found to be true. Disabling setting to prevent crashes when spawning stickers!"); + MetaPort.Instance.settings.SetSettingsBool("ExperimentalShaderLimitEnabled", false); } } \ No newline at end of file diff --git a/Stickers/Properties/AssemblyInfo.cs b/Stickers/Properties/AssemblyInfo.cs index 164ca24..994904f 100644 --- a/Stickers/Properties/AssemblyInfo.cs +++ b/Stickers/Properties/AssemblyInfo.cs @@ -27,6 +27,6 @@ using System.Reflection; namespace NAK.Stickers.Properties; internal static class AssemblyInfoParams { - public const string Version = "1.0.0"; + public const string Version = "1.0.1"; public const string Author = "Exterrata & NotAKidoS"; } \ No newline at end of file diff --git a/Stickers/README.md b/Stickers/README.md index cc36730..5db284d 100644 --- a/Stickers/README.md +++ b/Stickers/README.md @@ -1,6 +1,26 @@ # Stickers -Stickers! +Stickers! Allows you to place small images on any surface- synced over Mod Network. + +### How to use +Any image placed in the `UserData/Stickers/` folder will be available to choose from within the BTKUI tab. Once you’ve selected an image, enter Sticker Mode or use the Desktop Binding to start placing the sticker. Remote clients running the mod will automatically request the image data if they do not have it stored locally. + +### Limitations +- Only PNG, JPG, & JPEG images are supported. + - While it would be cool to send gifs, I don't want to abuse Mod Network that much lol. +- Image should be under 256KB in size. +- Image dimensions should be a power of 2 (e.g. 512x512, 1024x1024). + - If the image exceeds the size limit or is not a power of 2 the mod will automatically resize it. + - The automatic resizing may result in loss of quality (or may just fail), so it is recommended to resize the image yourself before placing it in the `UserData/Stickers/` folder. + +## Attributions +- All icons used are by [Gohsantosadrive]() on Flaticon. +- Decal generation system by [Mr F]() on the Unity Asset Store. + +## Notice of partial source-code +This mod is built around a modified version of [Decalery]() from the Unity Asset Store. As such, only partial source code is available in this repository. + +If you are looking for a similar open-source asset to generate decals at runtime, I recommend [Driven Decals (MIT)]() as it is what the mod was built around originally. --- diff --git a/Stickers/Resources/Gohsantosadrive_Icons/Stickers-magnifying-glass.png b/Stickers/Resources/Gohsantosadrive_Icons/Stickers-magnifying-glass.png new file mode 100644 index 0000000000000000000000000000000000000000..c0d49e1697fbac2e365dab8de92d135d4d25968b GIT binary patch literal 53474 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%0Ax$X> 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 literal 0 HcmV?d00001 diff --git a/Stickers/Resources/decalery_shaders.assets b/Stickers/Resources/decalery_shaders.assets index 23dd0145a9a07f25098257a15bd2785499ad4ea8..6535c982552118f4a6779a035dc940efa7d15a6c 100644 GIT binary patch literal 72775 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 Fs=S-O^Y#D$ literal 43266 zcmV)0K+eBaZfSIRMpFO)000OzE_g0@05UK#F)lMMGBai|000000001~0ssI2T>t<8 zod5s=LjV8(00000000000000U0098`00963j|cz&sA2#?00002902zm0%*tp000C- zK|(DwW;0?qI5TE4HDx(wGGR3^I5aXbH!x*3GBGwcWH~f300031ASOTt01r6;5uh$| zWpi(Ja${ux00018000O8003+OBZ0Cb+A*?Hm|1%gu%gPs5sV_e%=!SdK(jA6fEcJq z!ryu#0f}zmVHRCu!H)Bq9~EL*a*O& z3&<_sR~c$vTnD(M#7oKdnYichb>t!h_Yl32kjbVfDotqOfJt?gztTXc=30Ggl;zAI z7JUxpqh5m8>480|SNB7#_ULz;i953SHcx_&3u>g;dQAl?s*v6glyZb<$Oz#UmR8Y_ zw+>AXB723H_&F9s_1VKFqSLH0L|9F17N7XkcWcjo zA*P+GRkZl*bv(r2v^ej0TpFr+t+1Qi`a>7dv4h8LbaT9Ewt(;Rc)S3;5;o?n2ti+O zx!|d!f8#*9;-QsD__}+r1EYqC*QZ1a4(1Lq5{w%W;VwqtiNt{tv8Z zr5_|F7#KzllouY|{OfcgCI2(qH5ecXg_0y)oYerkEYnTEzPK{%`4#|L`A;g!vbzV6 zt=QEBOzrtr!|?b;7z0XOh!dUMtujdXYE%gY+>t^~`Q!)F==)?U`m>@^5!h@vdcU+B3n9Oe;R~o-9cL8($l;8wQJvA7 ziVDj`qR@R7diNPL#(Zw=b4~5Fe90*NW-a~(KLJV864UN2wXbSYielWC1{NLtL1l)8 ziX~z%h+@yAUb!lr9bYZb)1x`N%og`UoFD3eR_NFkz%%uqj?6s7f7|wUG~bj&qNa3? zI@#L@CH;oPC?tOW8Weqr_;gU&{Alt1bx21)=8PPx<#^m=WB(OZ4_w41*-@JqYDJ8; zjD&^kso-}NUhH-(%lN~s%ubi$%vo-}ue^GLfE>#e{EA$~+J7DTC;&sMlT+1q z(NtX{-jnoKv|AjjTJkeuO}m$C?xGIXWHLoBw9tIs)@P!0W63<1e+xwXskbw0UBC&T zp5{(w(7<%CVY?v|<<*g5#_=OWQB)FKL|G;P{AguR`NFX&>KWQ!c}KxPT9cn-&zU;} zFG2$z@z0;6-NJhzq3tsC04fU#Y$=RuaXWBv25Sav*dYt7{|3m){L zK9}X5{Bc@tEz;61MpJp<_gcF!0haZ<`=l1l8lzs@xHqGUR2D)pn~^p(f|!LAnm+{l zxE%-%rL4UwU}-_hwx5LY+Lal&la@8X|`T@R_bTYY`J+`PPEpB^_jxGk9 zINH(~RI{7U*xjtjCSj?r%Sl0*q@nSn z$=FH!9Cx~mC)?s3tAl^_L$^jMO#mq-%~ErUJibE2qorSk74G^>Rb20USWx);vutvJ z)A6~@n57%BbL;dveaL+z{ypjt#lZ<0t6}snCaPS3#M|yHRR3-q1lX47x7Cx&GF}da z{@h*)$Zq0t22cw4LADS+u9%$<5RkKBJbAXI>fd)8BThQ$k}&l9w-Nds=CU9w;`NULnG)Okf}axs`3v zuW`p6$SJ-sa4QG+85ohexUi}oKrD?O)K%Ey71e)HfNTyg=(-J+BRIm+y7a(V<|v@^ zT?GDhnVdooEA(HrbR+s{nG?9%vr%hN`<)iK=Y?8NScfJ^PzXaMtdd&vM8T-r(d5LI@4h4mH6threroPh zIFf%te}h|Io!u9df3XcyB3z6m!|ey5Zly#J?p55HI8>*!&Oxdx=bZ6I7L} z5_yreYic{DDWnI>4;D4J<+tQapXkoxXBdBzmiF>E>LHw4^xp~+x`VuO3?Hg5Z~?;Q z+!@m2(7A9_7!4EasayW5X()}xPGZdkNWvTH@?-LeZiKwV=Ku!1?2HoJn%Am|-E zR;0UsL!W5aRa1z#cO_ZDP-|>oTwB>;yR~mIlaGNilc4w%7+Mubf=Yo+4owDlS=)Q& zb@&Nni6Dm^MW|&c(ueByKcjf{hQesoQ_XR|OVhwLR|iGwXILi+=F-AT(k(I78$@|j zp-Dy1n1ru@pv<13?K*Avyp<*N1zs9QV^j*O9Vgec6>_FPx!dBnj{bli3qOBBku~%K zg&q|p&0%2-QW;|!I|qnx2^|HZ`~4QRjg!}|c-S#K`PVWEF`$fG)wYf0<38oNdk)0) z{e#bSp9_QYpF`rIeq#G}aFjnYfH|up`z&6#l^d-SAthb_A03j^(iFApLlD4q=HnvB zkebnkJ@mzcp8=$tIA=a@H@2}B7w&xR73rr7KP_&N(?|9JwlR|YhdE6yt6WZYA2+29 zT<@A{aRN9I8(L9V8*u!@gS`q@#7>w5ip$4=j*#xEy|*D?^&ITKydt#chE>u^IOid; z{UC8P0q{bB9?M~ITf?!pmTNIC)wPHG&FWbu$(juk^m|Wjv*P3T^m6b@ucyVoE5d=+ zg}|~gX>J!<;yb>wPFeS`f4qbt}m$n*KtLMOg@ntCS-&JLe?{iqJuFT;U z>RO0?_c5za;s(FzCg51uEo5%GbE6{BTn*NpBR?-NVoAi4_2{JYxb}Ua4sclT_X0C1 zb&RLIG>AJ9?sU{l9FcMOV;Ty@z{E1;3i$Ff0im_#(QtnP;Xw1u_w- zE`iJbq%w){cd{9D`0|4?h93JBAmn#C!LE_29i?AcJ~bx48h!N|l*^bD>bqzJgM(KJ zB}{GvtzYy1m}5iQT93X9na<7RD@4q@=s~U0STxxLP?C_;M)#J8b#~oq7HT4e?W_5f z0uC1&@DoKBKBpq?pHcJ|+U#j7gnvZjn3*nkA)U%WyLlLWZ@&8SCrcGXw~Chp5?m`z z7V4XvQu`aa=`3D=RK ztjv21E>7&8=mLkg!7#Uq4r?>_BLLIy3;7DxkF!@21I%Rbf>dlZ2x7M>{b1MU(&^eY z4pzYwa31M6l#z5x+!!8*zVZ$^s>Pp_*E9h9S!W%%RbXd@uA`Y;%3r~yB8{h~Ldr;{ z+$jz&TjKD;8MU=+6oZc9nZhMEDiVhhV>J2ihuvmeAY)~P*=LTnN!pa98c7SJ#rhm) zC_yWUpRbvfgM$UjvMje^%7Bm+TZfykR@fV(i{2XeD(I*24aM z!FRf_o`GpC1za8YEJG<&J4(`-Z-ev_JY31nLz7<$&|kw`&a5#QqMZF6t(sSZ*F0l7 z!UP12Fs_l9KE8~7{~=(^yJyCKUuwf=1Sh2^|) zi@FWBUwkJ^)HiMxp;(YJ*p4f9OSmGk00JBLlBs#F_rj8lUdQyvGmHB4Uz4+Wdbe@M zf0gA?w#TEFwVAgb2*i(DHqn8lV~l!?%9fkYaRP@ud=mKA0PMhy`^0r14xADLht4J~ zw|f8%czf&juPfzGVe*100&>znSGM1{8sifoQ28(!ZH|61SfC^Qd6W zM4_ZT6r4MDV#;PxwThn}%RlsCE=4yQ5HZdP#Y`^{cO%*{yQR~@eV5yjqN=jrfcghb zbTNZ+SpR?}QC#_f${#zezhB?3as|xQEXN^zR2MFT$0QmK81@k&b*5_+)$%G(HKDor z28uc-vv)vCg|ZgG_a;zS)e}aLrlEiP;0ko6cTpV_Z72m+czp&1Lx#Ot+3BrEi6H~f zE0;$-YhxYQVK5YsguzJ9tizGG_xAlkCJBh3BrVX@anAjVno+0DpLg-2oSXg(7LO!L zP(cF)SZWVtDk!NnJv)M#`P*0+V)WHydP_G=qQ=A=imoBP|4^rWH!dn% zF4J)x%A$$9?fOS+EHHQ(6c|gujOPP#+FPFC{9Cc@6K+uS zo_P=hjHb$Ld=+f#O+^6SS;Yf=y?ar0W%yR7p#$7oTq=KFQE{4RaJctvL-!*5>Q613Zaa60}SB zBPxoyvaqdgB=}ZThoFWRtM6V+9-gUZq>Qc5QMjjB?$hP@k_ZG2-Y)Ui;|us_6;>|X z)<%G^n>{EE5(^syzV*|K9}io<`6(5q&eaCVEAm#jORrJd96wPGa%ncJTjhT@2Vh!c z!1uKHy}wW{?CO_=u6b_}hNG^}(ya2P02=_(uzn~)*-&g6eAjISle<8T2+>@W_a0^a z0I6c(M6LL!u+f2=1=$!rOgA^iWs!rQa!qdkNOJA&E)IBBYu`Z#7r@L+9M=!{i6Dl| zh{jcvp_@^IhO_Lby;*4-GGt}M0fmPy$&Ft5XKBxQsg-9%0skCb=`)pPs=+(D(rkgq zZR+>9x^uQg=ZAPdQx(I%X1qj65UO?6mcwekL|qS6_?%=aM?dsK6#429IQEizAZQ%W z?K-18eh+@=OA8^=)=LfBF+i##0f)#rk6)30Q~Y%Vd)rMQ^P&bC#;UjAr8Os0$#a5J z01>u~W6-NWj$;>0$1RTQM`$EFzT)u#LKTc9Fp>t^cla%*On`~gyXcNiZ9F{SI2y?$ zf!K%Q)!Bc?s$;(fuT4_98xfmQC+c(Un7RM{6P^R($e@~MBs*t_d|I{>Kh(jecSr@k z(Rrw@usf%j>~|YtAK~)Sqv)N8)0MRRGhBa<%63=sDtM)lSMh+t8>`7Gn=7&^kFuh% zK&w7Gqcg^Z`wK}($Q^?@D%OZFK%1a+m_E+JR(R=j8*e-8Lty|L%;FHzdBqHs@rQZK z14U-3qs25pTIIy*cVmgw>KLE1-eSG|ij9?w_?>8Mb}hvs*1pPAP@^iN1VBfi z&6jhgfH)j0(EWB0=h(#-YbzjPXC>Z+v;8wTm{YJ5Zl2FE$0HCyy;}O6^lvrC<^p0| z!N!Fjg?(3`#_Rw_(nY}KrIbXEuhR^+L55MwF<+zj1uj+<~0L*3%S zg9ZjP$cy6V4J71`YS)WD^; zG$F*FzSp!Zhxd=gGo z;jJURk`2i#|w;tzzD0xvi zISgZ%C_!EeB=8a#!Vk`Y67{Pe*DSThP;YQ%S4CjhSlwL!PEM`q&@}E`F6zis45~*b1Zo2hl=h?!dk(zP zLCFB~5O62!PVlQH$rQ)4JU}qn2dQ&{vhW{0Jl*PclzGpxo>Wp;HsbGli2I5RJ}$oF z-l)If_QD&L14YODJ?K^mrYCG^?P&bKzVf{bi)^W^y(g%}LG3+S3(BD237TJ$l4?gb zkCfoK8^TdxSuwMuo|m#86ll50_97_MRY0+*&TYV}h_cA0872Wr^j432U(f@x5G>vb z?z879QrcU5p7Ldl68v+jKY%v#e4!_?e1Y3;Gj^{{9{h>nS89)?8|-GpU|NvKiJd!D zS!aj{8Y#zZDi(24iV9uvCi)Q6gMLxFFu#UTQHJ-ATa}v8bc1yFH0t!YARf6a77wqK zVp-J|2f>Px27k%dEnH!tS=*SdM8iE>Jt7f*X_CWPsQ>Dm*FG8lx@Q*tF=&&ah{l`M1)*lI*Y3XQaC^Mmdcw0{~R+fZ_VNyi^Z5x#kkEgL&_MKbyJdBiatOCSjZ z@250CMK4zYML+p5=nY;fZACI=dpaz*S$0zsQ%h z8G~9$H{k;Mm8~kIfVAKOggfibQ%cBlHf!ek0h9=K^4~s{tz^gf}(p@GRHk7y&L#sbrz31Z6~b*1~;x9H;QajT-W; zu*4FHOI23OEb*W75fI(}MrQ%$B<`_D{)jT_f zUyO1?DP5qGlL!&`2@)dVsi55N&XYz8To~MIT*Pl5hMYJ+%_@RoQSRs&N=hjL26a`K-Fo3@=^` zhqjGEpCxAj9tWnm!ZouX-Hly@0;eJKziKjaw zWfOaq4b`OjOi+>9Z=#w&hJd3PR_V$- zr-GT}q`;E)w^EH#-bWOhlDlWtDQ(fG8KTk+wMU^g!>kvqvJCh-v6 zVb>q+oE4s7x_UozUcS#IrU7fC&syY=S>rzb4gihDYQ~AF<`jVqQ+#d~67bg9bPNl- z6*A3RaumJ_2o|TeKcaqV7nx!#5=_q#u{o^UCS1^xx!Y?s&?$3+w@fAREm#i+_EvUtW^FrNP*W-9qQ4ehylBnFsr)tI3=`Dlm zH={{!Th06DV`cicyZBaT;g5IJ8>g=IP**~la{RM!kpipdrcWTRq^5xdn`(~=zhrC! zetlPKvNx!_&ATI2R@13;{V7yMca*sajcKg?p9c!+9CQ96+4Inou98pldwqqMF!SVu z2~k98$rf#T>X;aU_P=?6dqPlf_OeVIDfPN>*vgW*tKH$WMw1KsZ;dQ@3S`Ai5dTlyk| z(8!A+>rtu&iC&~E^fg`3?5QK_S2c-#Q1}ufN8@R5~ zM64aCU>DnV^KU+OpI!ys^}Wo29-@x5Za2b;#BiXdRWb%57(QiscII-&>2CMC=8t;Q z7(HyE`c!kxrWM}#;5Ok%VLiDu(8W*qS>Q6OuZxTjr=a8lX9d2Je-rHOow8OQ- znB%i%X4|XmuXh6=J5&nx{@SQohkZys&%<{C#%vcp9N!2MlYpGhcHXOGWMk5p?xxoB zN-afNlXBnXktdltosOrIbCk7#pY}x49K@K*~Sc~H#afu0WK{QOwgS;(R zG$h&i^YckLsNcG)Jn_xdlR(T;ashcmg$G<1c)U==_!dB=UlHEbT*4Efwuhq(G>}P@ z$JB!8Tlu7D0Fm!BGFV;)QtOy8s}swjuJJRT7TSOSFr(I)L8??kRup0uq78TJaJ)Z^ zp$}|v$?6$4hsrB%-=K$qt;Dqwg^57DbR)A+l~h9+y9-*33N1+0M=kjn@jL7@7JY+Z ziw4SC&SFCbR_~`mnarD;ETUn|G9FnwNvRG9E!G47s{$v#aQx!Bnb&pO{g6_QzYQG* zIfhEBWB^XJvTAzzkrUG%MSOL7IFR$DL3_HcW2e$1C-|eosiZVT!%b{tz|3tM&4U#& z&%bb`HArdm*zzYkMt5vY&AWW&Mpf3Rs|YXEtP=hfpOOS8(x%V4I=mQ~(SFkxbG)c= zCcbyI$MA?9<{X3~zjKLU*_b8sSq~!QC{bW@W*HRewlZjrY7Ck5ta6<|*og{sL6c3X zkHC1XmHv38{yspO$;xbax=|E4;reCHFYXoncrzwORzsWYymR@1b|aw=ns}SBZjg9z zdyBIm#Y7!YW<6YmBS55$Z9&8cS}T?L)yD)n(3U}@vYu6BYMSU|2F=W!kmP``4lzRG z^Yr`SffeO5`44{g#NBcuMUP@q?+gIhBoju)t^Cs2&j5~ysE3muAkdJWJ<=M*Q?SXD z+KbQV^+OkpUC1Ku%2N_(&`P|X?>f0NC^ZM;E@h!6E`^EG`qZn#lIAyWgaTw7p!fl; zIT1e2TlXzI42B;TBJvb(y^YDMnr`Hg#SucNM?*+3X!dAj3{pC~ec0@!X!(&jsU>Wx z>>&*GB=KNw5n9d*dVYIZ6$xP+a}(%Ff}N2FBG}GvtwhY~n!4Exl+6p-xF|Z#JN7KG zgM|gM#)zW9nLfv(UfR8*FNWZO550$i8*CM=iPcH|zuwe~JY4$9PENSyA5Jf$i@R&GlZjZET(i5eYLv@hjvT8h=dPU2IF$eKQU4|M;dm6J zsIg~@q-+$4X*s`Jop7r?XFURpq*UKeF_(6Yof9X1Fg?l}f)=c7(h)u?g%Gk)s$YDK*O209yFM?_K_C8I-CsF>#SeVi2gDG*RCe@tS=uoVnOV@ zCeJ|`lyY^G&Fw$4iASbYgHW>pbWvhDf3(&zZfMY;QCoYhBkE;6FZGEDMp59mFzLY# zEE!Bwk{-XF@en-v_;VZ5K%aX=LxDd%>yVOGwZ6s$J5;ByjMWGzA$KF`yj{|FN9*>} zz5-2j@akR8d^{&Tv$UTWpVt%L{u*B=9_-?n{r>rqR1VOm4?(ezj9qUEpu4!%d+BfX zkwsiM(qb3!_7b(p1*tph+cMs6oE+ICeJd30yKXH<`S}v%u2`0GNk+FM1Ajd2XXOCz zxe&55`M9iM!Fj%I%?8kCo(JsPuE#gPv!+V#omcr@vEc|<8mBPmWuF;=F;v<#LDPKR zmp8l)QsKo3wDmXB!nLK}P{+k}+YFeP`21A^va-c;>Zj4ggdMWg0X*J;OrHME%2}s3 zCCbSszc>In(M=ctyDvoqkV?r1zi)Gxp&8#cP>^cIts6pav+vbXvbb49QPS#?v4g+< zlq1oVdJ-JsPtN4NqT7K^?b%FQdRJwL)A&%JG#O}*3)xH!15 zGr{mVSzG#H4}sa^a@2e&J$K!W7|L_3^)b;V;UMCm*~Raf8*x5kl6)#?^+xKPkq^+t zHME{wCp;av2uQn)lxTxprfb^>tH7{&cSu)JW-ZZGC6#spdjp^vdY;Z*JPTfTN(as{ zM{@CzER2$6X^hs@)aYdk=3|$c5t(46q@HB>t7uNws}hzJys~J51|PD3GV>=qEYswfr2V9KDc$MPU-chauGuz# zA(0hiRt|}o(j1&evUmTC?p#`bQOlf%-TK-?8OUvZXUY8{q0C!Y3&y0OyqgdAg2Ljc zHJP4^!^^UKhnI`aHqr4D|BIh%4Em!eg$@D7kTol>)tjijAC(P009}>qq`fp| zJ4Gr3fR0i)6dJ-gUMyC%jtC_zb0Wr>^ zYaBjkzymV8+_;xu8Of91$q^Y22~IrU2LaTJfyh|f9-KzC^KBFUOP(O`16Xs&rXm)IZP_)_ejGN434Di z=&6vfDHH)o2N(r+TsKJjfPo|{o4}ymPZlx;QO6;E2D98FSr_M}FmSzdK%>+y1ILBT z`q*>{7ChCd8F-c6YlSt*Ei!hmCt+ij9aPs+m)`mf%0SE|2+I%F@_Vundoos0%KWb_ zLKqhbGu1~E$?T=Xlx3My;910qfQvAs9P}WEL5)tB07xXrdm8rX;{PXGx7zMu$6z#F z)-8_)5NIeaHsIg#1ja=w+UTt$mnuKfuXdH7Ig|pDSReRlz!TQ$JauY5Re1FRQ{n*B zTpGc69`M*)P`33THoFHkbq{ubPlOT1j5w^euOy6w391dY-h}($z$;PEDBpZubjHn$u?y4mQB^xg<1EIEe6@6aJL!xZl`N zA#rWPHXzpNyMj!T3JDR7Q$+|%;ORf>HM1Hf7@#cln{TH9wf$MQ7R)*8odzE+AoVH> z@;zyeu1Q{Y$HzqVaLbgV)GyVudk2sX2jX5J*L+Iad*|+8D;?$SkTT{VG4|d*_0#Q- zPz|g|lSN{ygf@rA2$`9B1MRj60H5+n0{uKJPopSz9f*Ye&wf48PshN9tc}7;syTj-)%J$TOEntRi0@Z3%%B{{9L5hgoa?1 zhcw!pZr<&ZFrMb6Pv&JQ=e2K;-vh99S&uBFvJ-*7Cv%uh zl8M}CjX^1q;jE=>kM2`owz9>XfKQLAZEV$I3S+CSk{8%nT&OwPVWl@FeL9=w!7Q3c zeh*QQKU@;@mxV>0XH7TtE}CRX7dG$m-0ypj43jU7T4p#jgm_(ca0FQfTP^Jjke5Y- zh3f=ms`Um5aL6SCYCxWB3(m(tSMP8oFZuzYZ0}w0;CPUqJz-((12uJynkIxOHgD5b z-RAJ8lK6bvBFqHN(w8K7lMtdL-?)bXF(w*&4vHuf|27CN^c&^EwaF-VxDxi!3LdO` zw&PtchnW%kko}%pXbjbvl1TI0+LQvQt87jgobb}!%oevf`D(w{p!Tt_)o$^NL{{q- z4dM*IkmA{1l-q##pra*e$swH*2N;kt^&_NvFre|AH~36~^@)!+vy*53^%s%iOv`oI z*Ck`?zi<|MB}bC-Hnr(e+@;)Bb8&W}o{BgN8Bl=TCD2^bmgmvtF(9FR0$^XGj!xYx zDt=O_h;0zedV!!qT1r?v6Hyx#Itv|;Rw(=P_|$JB+5OO;MDyy5XeG|f&+1SjI8s%= z^!WK%E`|O6Ngc+4$s{nK-cjvURrS1czxF*v%@R&e`qQ_0lCaK;#-3EDMlcx-l5??q zo$holx|DQu&2uPa7ra-Lh{N{|**gBDnVv1O)s%OR@2t-9EdFQkCw z8z`PB7XZOb26d7G)7^UE(?zDVC@oxfU#N5QYz)(Se;m3)oA>!@GZiU$`)8L_nqH97 zi@@maX@Q*4)L6a>SocpFy3E*Me+oqc3pB%{bwlJ z6B%^E!rBTkKn?c-*vNS&5+H~hxV>0fH5r=PVAD0TS&}BJR98$0Sx$}23)95;xtp;y zYgqV|O^iyLVBGtwrRkAVs`dlXo^mH)nNhPbAhBi-()WB4eSZV6J6a4+K2V zHW}Y&IgvDXMQTjO5AMM?#arVp;sfd!iFd z+_V#6V)rgYU^D;<)ug5agM8C^(EEP#aV2v)!*ee^`1L zSD5pRoj*`erjf0vGQqN~>(I5~Q_G_NRGnAbi8=^NKX-90z%5M6g~^0avvhy^#yhZ{ zkk*Bghj9jZtT{8jAJi;rytYpN@I5w7mNpq=W}z@ZWT5FIsC<;Zt#3|S>7F-W)JY1E zHW*46-X5dy&f)W9kXl?REh2MjRmC>@fu*znkD)&)!ymW{b@TiTZ4x}cP@e?XA}r$o znbV9Tt!7$o>?RZGn`K{vPL`1FTFkbtj)h+?o){=~7F_QQDmyOifC9P>{Vu2+A;kq< zD-WlHSHYUh&+8inB~T^3jkDatwy=cUn%C`4we_fGrlRiRk331r$Q=%?1Sq4cZIAIX zM+h7IjI%6twnDNE(mwlN3RuN|+KxJ!Rvo{jqaFlGqJ6#)eKuT^5UTFa4f{q@wMgIb zWN%P0|0=N-vJ45h{K#m-YUlsa1)?MiEfLjJwD;ff2n1JH8MSW|$Q<*SBH2X{nNRtM~`od}m9s(&j;!i<<0taemL^PkB?^Cy7o z*S%7$-H)DHg1~k}2GQfV+1)>56*F-xI)acb?a#TF2sITpZ59r(nzLsj{OZwhM)hI| z$dS>9w(m+V=3dFWuqrKp9#Bn0<1U`{$>ovHlk$1A=S!7s!DLUD<{4OQj11rJww6;K_K;bB3utA`;PBZPV76cv|p*6`N5MP zD05AS6VME+eG1Egxr6yBMIB*Fr#87pHxX*G8hQ3wryoAI zk25k7Yv(mgA;j1GS;UpekfoVWqkj8e_b*Tj$XH$Lg0j-YIzz(a96-CzTZ)NFflrjU zo|tPPd~1_hRvc`U9-ibac)$XSPqeK+nX6l1%OGQ|O<)J_!_#IbZD2L60l~)j|1C z-^NEax^%2i$%{0^-C$@&r&D~o*HIl^=y*28#cs?Q^1gA z!#*5v9zVPnqR$D1O&pRmCQ3gy1{&jjLahcz7r%!anBsH_6{^qSuaGsPgd{MW@MUKI z?KkVZH_CL;g>Qf7V^M-vK94syM-HD4H$^igMuOqv@}jXlRmt$p%px-6)=lscU@>1; zn$2=nuUHP>@I00rd1Z7yZNjqSu($9DAvE%j*1z?NF{N9ee;aquZ}q5(NdWWw6NS59 z>HVb+$&=Cxn{IFOXXtl!Ir2J(O%{i8V{_j$R)bC3P<2>nX(&$=%)?1U?J()grCT#l zo~`Uth0l##u-S%71of)Fx)BGET4GVn70)wTLX6mFl}eM+1oQo@R}`+poUy7I z3r0zuD(-sr69b&}n&}EG+52?3C>9XhYP=8pZ0`2ABs)_r9es@v$sdt8+=LZPBvWty zApSbpz+3bS&@94LKstRCG%9Q^Mi5v-`iJWx+|;>;QOeH^{i@Xd@Wc1ZoAFX2Z_{zf zJG}_SF!;gIVXEZ(*%Pub_fKcbXJMJB&kG-{&ygc2I7Gr@ab^7_%b3aQe&EouvE{~J z3xk5C9u!$+{s6-ny@HY*hiF>FP*DC_Q%s#oU0tIjn`gD=lB*>GNP2Vc51Gjr0QdS_ zxOIM!=#=ctozkM{ju?REcTam9HgPW}=*5)hgy_;x#+{-M9q#}0R|HcDs}z>PD+kcP zf$-QwQT#Wzz$gJN7bAe!#eC>-KF$E@Ewwi7?Wg8-QXfB`<1E(eM>6`t($#!e9m~Ue z!edtVwV8V`70GAs>?*$Dby56O-yF?-aiGc<`>xlLcR_NU00(h)KHF@UFi_+E$PE`w zkcl1&u#852PGw@JkOo!PDBM(Ogo$czSB}t(60OUWX)ON}SD>)KIeE?z?7LavzXQt3 z4zgN2^22&ww!`scRCbYGCVL*8IHr{$RgzTamAk6n`fQaW1ftDiX~_k05BEW$8*twv ziBRct7bXKVb@h|uOxg+^dKtD5H1`Y2L|6%p`t#_V8fE8q|5*7xq?pE0jlZcVT}Z>! z83zs$4QgE+ijmumr|cde283VA0i;$pXe}f!-)OW2r1&ph=oktRyuUk6Wy>Q6*4F^?) zUXL{NHE!a&nvx^T|ts-6{Op$y&1RXU%p zw`q|(2Ytbzm_%&#G<|Bn+vVsd{SOtPE7k*afPCbF^ILmWK(Kp5& z#B-Gxa0^FHu|SuqjSEE!2=N9@r~i==jUT=<=cfC2Ad*BYx3EFFy9YTf$Tv;nN5i1^ z3n`9wmE{4k%OBADfuJXA!gQde$Xx#H2kufypl%-i&!JxMxfMhok-#i%MSaK!|AqXj zax0TWxAsNr1E+1j9CsnX9x!4+69Iw1No$)m%Jy7O|qB#u!#*B_g5$@HOa*^FPeVN9p&h)?23&$B`igCyB7MwKO*C8<|lY1)rwU$>CxHR zk|!-Bgi`BTWKvoKn}_|3&9Vx338SG3Ma@;DLNn9VP28|0K^_jgt$sh0Nq*-e>~hE! z;kvwJChZB>o3{61SJQ6Ny6j5MIqltCHW?BtwND*2#Qp9xqiD9 zXvEoYyGG6R+N%6o^y_M;VxmpX{r~)q>m{wY;Kug?m$l`f1XzcI%RU0SpfR{ku>5q* znpm9xr9rxzHQQZrZ2x2G&(Qk@P^USSm{<%e;dbf#mh{ihggF9@@CWFytRpo?56KDq zNf}s;CC(bBAA_I|(d(81W=|vS@b%S_Tl{6hcF%qh%*?eyB-;@0lN_jl_74z$M+zSk2}#I(_T0b`Tv`CRTZa`TqI`$Fen~Fb-Au z`6;R2-yNh1COXIy)(yfRKxx+;o*kk4y9nPw5D)d0%~{9TFPlmLMLt0$Js;Af>cx!g zn)Z_3lCHy>#tC5*b6#o=Kt^qi;{$XK*mGc$UQ#;Ckhj!SyqLnqrJXFDE;a6SQ2xz6 zB`7R8=D%_PSKmqJs>o%#1Npi@DKNnPGawIq-p(8@RIg(}1sr$#N024mKB1Wo5PbMJ zKvuo({h6Ejr4Czau)>H^9(YIi*;FI-8zfiUQx$R6{V6e*DZ@Nco04tEAkZ;1puS`T@=_bb}PNYckduRk|I#duuJPh6s582tJb^F%JCOX%{KwcHoU85S_! zLpLkWk88`Kc!S0o>0yKKqSrpE^YsIZG?330(o|E;JyA*50DKOBDBWx<4KN)Sbcq}T zspZ7EvH&KH)9dg_1PkDE?~)t41i!hlZV~%lgj9S8AW02aXSWfXW9YYW!LQY2!2PSU z1}GSnCe=f62p-zj&Nq7_z7-t!FojJJb5(CH_;O4?%_JtAi^7seOQYzpLF9AFXQBy# zM;EG0I}?F)#oL|u_)R5%X(GYO-Sw$Bi^Kwy^ zluCZ2DerjLxTaOCsPbxY%KE2APyd%S{r{`>hI}clAr1Zn8ebe$+c|Z#a}hj3n;g1! zEnj3~WN0-I06Rd$zt%(wW&{Tp$hQ%euBSs8aS~TUleUUbNtms)^dY`kUfdMiW)niy z`P^g~rcbdIk*15;2|tNj`Wj)aQ|yXJ+{Za*s@JkE9io8)loB~pC)^{b+diLJ5!CSH zW3_xkSc4DGJ24@|bf^`t#(b_0l;zlizQQ2gk>AR*Gh(}WktNu*f_a{n+?BqBR{)ve zqCt#Kpr>skyC;hdaf;#%@yurGx3&`Kd;7#>c(_BSXf6!mVW2qQT z)<4<#DRIWN?gjb0EoFH9l!c`~2?NruU#DC#EKzfnUa=j%N`ck!Tv;asV_n?27Id#F zJ$GBF%cq_m=#30kfp49xNrqO60QrTItI#3Xa|pAt^Aq=-CRXY@QqG$^`gKmw znUryXZ~;BtHXQi#62j3#BAOA<5VDWCjkvO5tvKY6H~L%YP5MyltE~)vgQHzA*JT%- zsKd@yx`TeNll=B&Ke<8RSa!xp)p#2(GS`g6b*kc*Wu5YCY~4n0aP)wJxw@D=@qLa+ zr<9>gRF2!@7k2Z@k^yPUI*i#vMp|M*k&(Ah>?-3fZO8R$ze5uDVPrr zM;jEecJ0nhO(njXwN(ER+peZ`VlvK1K>8?9iWSeYU$KG&f^fQTtN5oaNu$*ViE&{L zDtg%AbKqM-l5llo{TN1%)62Q_aWM?uJ>F`5&F|%T_N_Xwdh?SdE4-}XSs6gyR3IA6 z6o)xSE)BH9y&70BI>f)7CAi1OVHD#;&&)8tx0~dHsa~M%5&FllRSh-}VBG#lW9^Kq zFYyc0}YrQocaVpvkO-U%~td(x&*xQlS(K3zy zniZ^GC7*FhxNAMI$U>d#93Vzito^^Fdz796>j+G8t{r&*GH^FW&q;B5KD6}d^T3XY zGXuB^(>+c^oqChYH8BcCJxd=sDh)~O0uf;KZ3?1CwxjE0e^?|cF$>|vmYP^PX75fp z%Kdg?6Cq~%i-4j3M~2CgQBXRqiDf21bfA^=yio#r+TsX?78Ke2dqZoB%b)`acq8PM zKscjF#rM4Q;4&&k|*s!~2>AY?FNC%B9{t&{39A>}5iD%s(c^N`c=3*E)9Q^%Dj* z#Vz!(K|(HkZ^Q7@TlxWD;0jCj_nK4{$DzpXEDkKM5Mf zrnMDif7->!!>KT*#mZlu&PAD=M_v!=qOzN8;`(FoFHjph3D@fPuZe4qmmX)auwoZkm;pcs?{Nu%HUu6fJ&5<> z;K5nO2Y!3Pw0EVC#J-gv`e!=0;QOfeI&-EF-Wc3qVFZ_cUNB`eeocmWdyNc7J{bah z#)}*m*izSwZ230rwzE0L&b=2JDh>*64I8ZV$z+i_!c8NiD3E>%=NEASsW>T`DXl&gJxBWY` z4>U+kqEAImN&SoSMekgohCDz?TJVumCV$&wI@&z#i17g1E8=l|g*@s@LIUXFAs$BG z7bIk-q!QXF#bV@|x1ZZW$Q`KX2Mu%Nhw;D$7gi3MOWoYx;Jz>M9_tbSIg*Jh@2VI;hhHM5N_qkRO z{P+sV14Tmh?D7oJ4Lu+OQU(sGH$CZl+uXg%%9=YjPfxs0*ylF4xZN~^d7B%3#iZpp zNm294Ee#%v6`M|~U&Xmbw$VI9G6tyoeV-Im*ABK1?qMDpatUm`>`>~ro)=A%gy(t> z6oj6!nvKmpMITgC62miZ-J~)0;AIBjuBDebuF%O7(0M6nz0?jL$slpk)Q`Lj_ z(X*3qLENNs3N8R+UnZ4cbPv1{h1!xJ#c8xFP5du=AcEW##ysZD3pCj&E9(Q+d<*D( zqd$L)JrL+JKGiPtK`D_oQijooCVX(ca zMcU?L3(ps=g1w2V^YOC1n6!h(hb7V87Kw;esMNbBj>aEAy9ICs$&1rbWx#rWnqoMs zh_3cXVX%}wEQYcXOvX9&hB?a$J%$FAB8%)f(sOjMcY{@;8&%R`s~-D8e?7R(9XR!_ zCP!Y+sZ9|vdK&d&fJWnznXtGhh&P0tLy0&=twd)dLu>7Y_Th^eAVmtj9=}oH`&fiK zMObs$ZJ!p}59zx-D{HXw_<@=OQ_9{!!}1wh<&(>7P75m&x&^$g|BtFLaw7F0&g18w zayVpN=L|9TuDLR18vT2tt|K%(3s_LFKG`i-SBqxQ87S_i@#MR5=V&t~M;93Ui; zFDAq5-^>9f$MAuUaeoO5T4M1178rLKP*iyV>T|wSW7QfSO$k!t+qjZ)sUEzyi?Qz zDx{#(9Q0=Q;Yd1NE4S(sKRQ)v)X`-|&o(G?Zpn6cb8e84A!wpK X?i^o0{wD)AQ zRwYO$PhGGfjJHP6B#2=tHw#*H^I9c#TO$~h&EHp%bhgn%IGZI*aT=!7(A#%7g{}}V zI;1H#UiY;gEau4NTmE#Nq*ohtUwx9CRKS6Chq%^{f2RLrh1%-KgrJL>4PVf1(J@Gt z@FO#BvO6g(SiRXPFGy|qYA>k8*Rc+28TMk8y)*?pd#&~kIY4Cnlj3`G{@53ofdX9X z>+=pRymL8o&zt)*rBQcXjE6zSpsSSF{j3TmACZ(FJS#T8m2vl&uFI z5)4y_^I-`$AiH8Ts=UPavhpz%!}!&|2FT;+klU&m$hAA?lbNt4*Bb{N0QhsBm~NQ& zWHD0o=A1GuE;$#LA_Jr`4ZxF)fLV4H;aRYnjbLEPxkFtjqDQ?Y$3L;kVC287%-zv> z@js@WUXKdrn35qAg(uZ>RH?TLWWw~kAK@x?vxX$dF)hk~6g1-9DmdnMK5@@u4`Q*O zPoh=}A$nWzcjld%SZs0w8OK0R1c=&Jj(XTcQ%#`TgBMx%YiqdGk#BPEfO2$QI-Oy1 z*0t|ozMYg&6)#MY0E**(*@zjba}r|Zip4TfRHyhmjny&kpYhh#jc@lxBY^`7=3lN! z@gv6?%Q$`EwJ!EnTxzp7{=u%SYsJ$$8tj8W##F!%ZRt`)i% zI8M>!7X)pbMyF^dplRzPs_0kO@$U+C=!AfmI@2YK*r$?sHez8}vIe@H`&Y0!SKlYl z*Y*$jB@2u36v!LT5@l8S-7mx-oJHwyo+}-%vZ5j1x{6K=B3Y8?=a_e`GJ6rIlIE0v zGKIg(5beDZ@TqEOx%m;WNN)xie|OAc1FI8Y4^W=NMPJVn?spzla`>q{z)yvllC53) z)%yOmK~zABLJQ=4Pm4+8tytPV{5qJQWaRlX5mEI0m(cZ^_s{mW!6^XZ+H93Ar1E^*8+9LUcV3Da~tJ1*s3T-_6Av=AfARcR!cmby>lBe6QO_j_l_cC4K`_TxE6`=xjrRMeu z)M5_>X?H`HQ9qjCJivgjfvJ<^8J?bEtS$PtGF(5k`R*A(wW zACpMv|5OLbDU4*xTxjw{KRlB6y^8Z+=OfOO2B0RRjPu|&*z$79IsqBBSR43pLJkGm=t3Fi=%^W>BKKbjui!fL)@NEfX2| z=08b}`SEetmNa5f9TB**fD9dY0dx=lBdz;|m7nA=et3iLpvPyl@lp}eH(tcwFc4v3 zxYk^QK(+ImeA(&o3itg@ehTq`DveHPt5M`*l(b!0c!_+z3v8P%^Sq+){-+bvp0Hr24uzGHn!v zA|VQwVrgwVtvF})QrP?;ZK4TysFP0R?C@0?-W1Q+aO$PcuJ-4xw6yQ(DXRGK&Uhv0iUdNU)(5w|poMIr&>o?g2-uO(a+edr zh={2I1NuP`=YMl7kageNp}b=y;P*lnidGlyhT5RnPezlXlaZ`AiL{gP|OUW!vck_G#LgAsgWvs zBXIG^uGRn{fx}ygne_2-!+t(yXB=BkD&))bSR4o>0q{+V8>5s+HsO{#W`yL5_}>84 zSf!qmsXI@u2q!mqOLPaUeND%1`=XYErlAe!>|5cQ?D{7c0AWP_USzTVTmX!m?9r2) zVPNv&Q||J!NJ6^FC?WUW`ey@12ZFtHM?=VXe*$?eWN(U>Np-y(Fdih zWcYY2-YbdOwyqGtVB%oSlvfE28+fd%(+ei1=iL*<-2zr~Bpg4KEl0XQPrSPfPMbRx zNf-(fn*m*zy=K--pcdh=-`>vZo@S{^t@5Jaik%4CjLQeHj81U0RrY^<)wuIIfn2|F~UaxJK|x{ShiPQ?GLi$ zdq$)2pitXr;E&>Pj|6mCqBlZr^&7t3{I7#>KwK~@+t#yD6RUekG#zHp98mW+vfR@y z&29eV{O*=C;yGl!zH(4WEB17}`U1`+XVm7EgrT{Kqm~Mj1eubtJ>dwUI{vr4OG_?S zUxi^m=K7KJj(Yku;%59QF+s*X$S84lYhVyLx?p@Y5EI5Q!4=CuV6u_lP+GbIcmx_1 zKF52Ss9$#lMYyHWtkr%~+1AZ>AkL6_9x~SEJl98mOn30{4IeF5MuCcx1mP_Dm3x2u-q7=B-Oyp~=$ZOCAzQ652tLHDJ9h zY6;~$iureik(nPzFY){a^f8ZAa>{W!lBkdsWmOlxkbI8d`1x#T;cDWY1T!t1_o;o; z?gG?9&52=GS6Te`MZK>Wh(EPghDwHJF~34ZV}`OC>>^v>dAOBgo*sIBs#9Dr3>c<``uzWo7+7 zY||=$T5x26`M?S>@_?}+jLE^R-uO{mLt@i1H#6(0b{eAYTLf=isyjq-{87vttUj@W zJh(OKoimIxI?f1TH=eQL_#>KGN4k*q&BnARw#LdgV6A;h1cuSc8tq}`?SJ%-*{wQu z@iR#pSaOwwd(pC;=%UEB3=0!HnlOFhQdri?fp=4%%_u&wk1<%L(?pnOK54kdMafp4 z(t%<*#uSRz+#(dA)R{AYu0!rU|9GN(b{4V(MeJciK7&hpDpLypB_0nMBg*;YWRf4~ z6Oth&Jyz=3Ts&&nIQVoQ`8Z@OELoc(nwzQ>)&9`~XR?A#;@kDR6aAUa?vg5 zE_Z_|+Oq@^0L-C}!B4MIWYZO_AY?D!H!7sIW+-f!O-7M{W41c2b*g9cB;p)I{Nj-Fro(0uWmiPyT z9A!R(WUGM15g?d#K8s{hAyS!GUC&5v@J_)D*w=nwI4wDu)dvxgQ2M1T!yNhxQ^x~U zN3s^iJPDdfCWUqTTIjXCuyxQ1mo|ho`iR1J%3Ufg_AXo5eH}ng{J_MS${W7r0(ozt z8+py|Mz?UsOo6*44LrKInpZ&#BH|D7EtBfhqe~Em;yOL|1T`o|ezYYPYkUti)^et) zwJE}H3f{HaOLb8APU58&%ztB!jM>P3d)lPGAh#bo(5Bm9UwmBa!zme|wibb0!AiMP zWX~3)cs`+PYEV)~V9Pgjn#xUL?0g%H%?y3d=AY;qHnV1wijc*f+VmS!3gC~bIfYA4|Y0Zn_U2;j5k4ckangqD???)HouR->sr}wRU9a@oj7} z%~+BVDvky%#)h?d*?0s?10a*|iqFdNWD{9R8gC`Xb?GSZSxXSw7_Mager9L$QbBHO zO*^L3yze{1(0$GuP5>32el+f+*cOEiHUK%YG+F=BdSbnBid_&K>RIN40V+4$Pl2_T z7hsxoRJUZ}UiSY?&w9a<3^Rwe9x?tAjI3 zEyFwVbN?3UMTO~RD$s}^^peG_yOAis0IW;@&3N| zD$Tbw%1n&}opq^8{D%n&PBf&&pl>LVWP&R1NBl$9_F(K~aA{=M5byj-(s@Xh{43THJI)3)P zLCUpxVW3~CPoMv=;4oprnW>u?JBHRoy8`BzTd~DV!g$M`QqjzRrqa6<#={q5{!A>>t^iMNS1wiKK;Y^u86W58<)((p%pQR%kHy2-Ojq3`PUKCqPIE|8lbv}X)X)sZpL zEu3RmrQjC`T_IqzZnpf3T(32CF(sK~m{*T9nb2ZqH`%3(Hff%=O!Jhmo=$}n8J_r#8q2QpJfj#XX<|Yw*nPy>L6cG!;e$l4t!)x>j8mZ(rsl9w zqJO)lcnoC`>cu?a7Sw2H2X{u0a7f>U$cSW54W{BXl4eKUOq^o^Fc6o;O%jk{mG{VY z<)z7B=fgdMXT*uhk5sxw0ro$Jjb8i}+k-eAvH?=?Y4(pr@n4vV)a(gw^~K<1Kq%wP zPs*SK8i9HBM0B+~MXVjYCFxsv)TsSGRtV6ZsdbhC8fpvSo>jTpBqwpLvhFBj?=LKg z$Bwv9u5^VPej|8*O1xH21n)OK zz3N?2W(-%;oHc&!4mLy-6_L zpx#KCGH-@sydth-3#n9>BKz+7UG@gK{U3z~uq66pd-}Ifc;$vf6F;cEAlR3dks{SH z_2-!i@dc|}zPv15jegV2K$aatw7k~CoTK6Xz+Q~5j;COF&J?*Y1mzG^5TY!ED+N61 zxc06KOE|GzzPdPlBz=J%&18j+hk(06!me<=8TIjv0vkzXkk74p(q*&od6~9Gm+}9l zf;*0ERFl^?ZvvyGp}Ha|ou9J!g*BZq@}mGA5%K3KCA!ofswk{%Em(dOhXg7%5}+59 z$+s_8Er}-Ttn%wzQ{o`G002Smg>78*G8=h@)b%KYnGhgGrt>pv zEgH%;E$OLPjfQ?79}Y~B8Tcj@Me11V(`y{7cI_JLxViwR)ceVPGyKEQ37HKO@{KPD zw4!6Z>7hH7(a1@gio&qkvJE4mhX-WaPXVl*fF0K6;ao{h(5>ciIJZ9^kA$@q8c2F4 z!ExL(HnPJqc0ylZsIQE6X&~@xSKMaum}abaL_pp=^wAhhgLy@0tQziZApyG*w;KPc zbA`N_sNsSld!lT7AkW|Ma1wyW8*e)+4hMQlg@Kn@848B8MCEf(?(ABD1F@L1Ji#D# z5z(GGGMWJlWZWn|G1x&?z^#Q^txDSiBp&J=V)HFPJ@r%f*-#q6+)u>=MjW&Vw*^=zzZ&d&#{^GZY1jmf;q^+7fL=f(3!5LMeS zcEGR$Ha=_2Cv`CLVU=K8lleUnFxmjpCUmjqN}Sl7`^9z{kyN5blch$TK7iY$rzDHp z=Qm)s2nl_0(|%VFyrhd4Nj^RL9mW1e8jIb-5rbbhYocUe?c~nS+S|zvedYLRzP#8= zo%hq^U^**v9#l0k>feXX^sj_RCbH^qoy3PZnKIY9@C&1T3)VpAB4*Rib8~3<2`Qci zeS?jl5;aHT1_(us+7joY>n3{rT&cefO_Q6u*l3yOOdPLd-WhuNSWC>e%?2H8}`j2L)L z^POi%R!mXDTB}ynvVw2l3R%j3YM!7GA8Y2ZoxG;juxn!%DU0RP8FUeMg{vK^+(D0Y ze^YDhEToY6sVvwRy;@*p;p?0}{0515zCWD>i&C_nU~d{4@e+wINX*QX#E(M0?wc^% zvYHieZCyy&HEvs1-`|K=DB)Xy6JN<6RQT^jz~D-eXD|HL9Yg6E67x~~LGa!1e$Fqz z2(b^(MOLHpm_ROMDa#eHeSqpomI5wbTqq477xl7W`=Xqplp+dS08mwmZm3?%#@HcR zIW$4WGi6&;L_ab{(AyynJcFcNzX zW4|dWIy73e0kj44Jq<`Ae{gNbk@HKHXmCSr=wg2`xJXoz9Nf0^=Y}ml>A>ZdgktRB z)xx=TIC^=kg{6lKKP`PvogB`)l;gF#^ReTMV0a&1rbw@9Ore`;zTB`)s(IQky7cME zq4eymq?7`f84)=5<=thn{B?Kh8PaP7k6hEMaa+q_bb_fMdUG07^iRoR1G}D(jIvfPA&#RRBmLd~hl;i`9HGKS5y!!RRjl)vW~G>&y$!>PI(NV% zoE5gHmQbPW*s--xRMn3iG8NsW9f$|yz)&)jFl!E){~EnqL81v3n*4F)nm8QZQF0rf zm}K;pLQcNHO>v%BU?$#(kkK0Q;55Jks4Uz>XLQ|qVDA!&3rQ6djfafX0}?5*gfRXlJdF51ABo2@$(UghjP%VCn+Ap;A{hWV$0FlLgsZrQ^(SMPH1$`0~xE z9u3C?I7*v|buvU9no$Ghv8ZU1snQ8$SWg>*DMzWyaeSF@vK3k2x68 zJqTf#=Ir4EYoiOXD@sf`;cSsjN(Jjo5P&e(Y8*509~pJwmQ@oveu3o^NgqC>#k+o{ zt}f~)?O*g8_yz?{2M$m6CorRKE(Gid=a?1(REAzxV_p-JOh--v03X~SW@+dRbivSt znuSr1>j-UXT%|$!gcV>gLANaqr`>c2ghW@>k0R`P)VU-ch%k+w3lBN1<8~hAI5x+A zqG=LPDi^R{`TO@A_;&yjeAtu%k!GX8GJ!?RD^qv3 zlPY{1ej7vRhL#;RvMiQCLgxkn26k!9Xe8$&RwhnEmW+>gTo4Z{Tj(}vg z`4e}nRYcySG=vt*RTcqH4_&sCGCIYG+OU9N3z^~#fb}hj1dw6Z|86S zOW?LvRDA!{XNf99G0hONlI}U5pL%9$??%MpWYvwe)lsUs0)C1k5w;lj&!XbP-jEFr z1G*h8mL$By6{~VS$WnTQ%oq&MX6G5=kII)1&r~JhezO8q^pmhWQ7%Tt+J00b53GbZ zaNTVmXq^^FBx#E2L(DItKCPDNekrDQacjk>w1)9ik!u+vnesq|4AYGR8lQA|bQ!vH z-1Yz={A|h{O4F~@`eiIOVH$ipFFv!7v;=ul8@10btaonCqpt&DP)cNkBn!b};Fb4+ znKaBD32R&nb6aBAH|Bm*;R1uCi+Yr@K^HyL^H3*4{&K0odfb0;QoY?<>M11nFnQn4+spH>SwsNqGY zR5z`VI--Lu{W|#E^k{bm$)(pZiOz;kfZ!;XdND5}Iz^m}_odOdD- zMyG~ytGKFA^qk)GljsSVQAhl&Uwp&WQurM+@UzwILtynpk@R>jA~L(&$89JoFT9t> z+29e}!DtPBnNK_?+N%!s8G;F_wW}U~9^NqO0XOa{hV8VEjE6ks;x!y zM`k?P2X93OrXr!4(TQ@|r<)H3xdC9#ba6l4RGcT+ob>o$-w{wv(S|x&n0qhLOv>v_ z)Jy-&wEUr{{&G~CU*Gt7=fy0y$f24D~TefGH=acMh+4M9)c7C zGAnOx{%R^qd>$1C<)q;i0`Y0O#uj!5fKq&KDSq$?XWZ zapzYeeY&&=B)CQ+M(@trXUXY;ARX5x-uK&+W`F_rbe2-Mk1%Cn7T?CwHRe-`bQ-XuZuB8* zV-G~tjMolGW+Qy^Jc`yrw)IvzrE?E!F?@kkm9~glM_oY7WHWydI#yBBoD-Gl41c(e z_9?Lx=|a=y7G-`;#&wyS`<^4yR6`5)q%wjq27jc-S$!ySC<=Za*L%{1Cxk6E3isaI zB$p{W(o{{OUR87L`9cnf9hvcIhEovljLM|PPVBNWn-+9zfL#zBgQli4vjbX5A2Dke zTw)Dlu*tWYgeDCy8LYhNW?3=Fa-Jo}gvPJ4HP6!5WUT?)Hi=*A8lzkSFf27EEVwb{ zsjXjQow5WXC%+D?8z%Pml+3bCfd7MhFP;N5CwTOvR!S4!s`rPyD~ zIWy@mF$_DmVO8?ZW{Vma2B|No zLXrDJjr)|ZUHnrvBHwGyd@NmQ)@~jv?ITg!x+=~Xy!%=U*K(EU$Z+@2jX}!^e$c81 zf%nkW4=+8*5;_yudpA$&mZ1DpCnA(l$;7%eeO%cB%}OoAp?`{^r3}|a{zHu8H(KF! zqjz&D2XLFhj}`JB6FA8@Vux@(Q4?&rFZPXN7P7IgYjw*dt1C93r~lehF7=^r(C%ym zA&sHFfu3N`@C7{UIOh|<+whnLBn^;rlA6Uli)rXZ^P_qI+_Le`iMp>uBB*BOPk&Hf zZd~I51w=8+7qNUVA2T)WPyL~%*zgGbdz8ki*ef+n4W-G|Fc0y5d|4TzZ;+Gs+dHi(5v5 z{-7B)^dAG;qaNj!v6yZ}*f%OcBoI@G8_P3y&y-*UeYJxjW`i3jK1MH0bOsqMzVUR- zg2ja-L`28_Z2zo#Z(73Cuw~X~9D3jdCrK<;IU6R$4^KbY$5&F$T~_USKC=eL=kq6a zy_B{kEH_kOs}1zw72|3`$8Aa~X(t}6+cIr{v*sdS5;)>p3Yr{O-GG42B;0FDI`$~m|}h>*A_c{=<*ts9*B?<)yaeOn^q<%Gfo@W45dQ`|L<=BR) zM|>+Y_*%1KAJMeZnkC2Tl4rAWWh(ZjiAd429$?5Jkn~AeJK_3q(Mcyy-HG}b$xo5} zehzUsG;WQwhGd8NES36=(*?#}vLlHyx=)8EL)oed#1p4Eq9f@Ws(#`TX<4dAeo$qv z5jdEEGf~=#2Jv)Yt)5itN0_~Z2U`dkV|b{cURgn46llb1?EUeN>jHkM7BGzRKQ!)N z7(i)oa8gD+?Xq&GmQjTAYdzu={)xZ`&UtlIM$dmQ|d+xR>4#rE}yRKtJ3-$94mrc@tSWkUAU z3}~r4`Nl8SgeGO8iXzHP4d`~5r@9|iaI_hYE#-pm4qt1Q$Oc?Nhv-? zQkI5=t@8YkqsIggVD@7P`u`T)5${5<1saMap`#JHOx!)lP71X+F&|3<69|~P2gi#E zy;pC$D%PXhlf+-e6e&o+rt%{?7qK{4sQd^B`m3@5R#&J`i~ z^QGabQ=)-+^>j(qQMMo9`4P`FsKh#`e@glyPcdLSLAsP{UE|}$512?_4}fYyL9^DH zD=jebep~?3aZd6!`vTZXtn^L<`Q=dCBy=I(w;@-qd0~qMM8oN1S{Z)4V-W{=JmD!Q zc}|i;o2?oB{$3{zm`wiKAgo}nsp1*X#bDZv^5JMzsOd^{*9-mHHIzpE#U9z?F9!VBIIEYTThK9 z`_4XS2_*n^Az&~L+dv#O|GB>*E`zxs*xRw0TEOB@specpBW4URLWcGZwcy$jE4(V(%<#9PTV{8(aOh_0W zeDaa4Qu6(iG_tl7-Fv>Q=+7d(q3RBeWgQr0*GO|=8V5S`!MJgFAv{nFw z{#AGEpRFxyO*edlA&}45*}QDgLO1S`gO-Y-fe9{&DL<(yWe?z(2$7NLKD22*=f^jB zlXAt=WcY6)Ckvv8yn*cxMyhnIJ_r64gOVjnQM7heCKNxH$i(1IA!MMLlp1TgiKep9 zOO01&lRJ;rV@;j-_LiM>NwPxv=+tZpIq%` zw8M=x(^erUc7Xi=n}q8mdQcPQ)6YLf>Vooz_ex0Q4B0(L*oA<|sCHu&L~(O0lVj`a zY@)DP{478*5={h|dg|j6tP4sRacIBfXa6ZLxY-9!OVsu4k8Rh55Q+%8f-j7}0~dn; z6WNIeV% z-r+EMq02323>gI8hQkO2RNN%zjn1X6xo8k0PcGV3=P)xO9{dh*XYVWAw6t*_^`?3! zEmnyqg{l4UIP1(^0zH$Hjv__93n-#|B!;N^bt`xeV2NpgQEIoat%mvL!VqDZiBdep zep*(xlz_a%;1&5YdzQ0V%2BO*LW3rpR4(+=ZN!$#@4-asI9I52cF&_4fk0gU-P%c% zp-z!q*n?hdkrD7-yEplB$+oF0FHnC|!ZLBiTPwm=C!U(n-Y18j*u68)6=X<-kd|b0 zVp{<7TO#`uG*+lwxL(ala8v;lo0QJBtQ|d>Z}o>TmS*!ycr^HNebpSYQ5CkY+3;If zLjyHNCctVfMPm8VjxW4L`$(8oSv!>v^C`~CTCTK%#vKMJ^Tq&5eLK9abaRkeaVofZ zHlD}&?v{UdZ|VhmjU;L}+~omh(u^(lZt6uvjIz8~|33H_Q`|q9xS6bjKN-tsV11fB zDB_z@m0vdsSio{BX@y7H<3aoS792``I-o?blw=GkEXq1NFN=z5y3muDG@>qSmN)lT zr!4+U3$d%$C*?kDsdXlpM9RCyOPfhvO^ry?H`Hhh5DYkfzgU^?Z$hv}__AL_D)?#% z>@+vX=fKUkF%R_qvea=Un9(Ff9zkDRuJz~~Z(~!|^#@u~s7M3#W?ohpJdeIdvAC~f zX+}|oaCxICSFUe37ija%lnA<(48T}%)HGChKcx`^n;nfR7A2CV5oj22nwY%Rv*%Hb z$nSm^eMt6PyO1qe@Yig{Zg%SC$T~p-zJn3XdH-9e!_!c+Pi^2}_!4d1cVP%9T57$R z)s@Wb^Ino=Q+g)IH=!j4sNVJ!uzU4|K?JmB5W5=W23^TTgD7Y0FYzCwHY=0pk!b!( zygxTn61s{CV9(JLNy+()JA}m^=^_r`H`jzou+NPT4!58QK)@lTKl<6S0Y#|nvT@H` zDq>)qtfV9N(X(TARE?n&C9~Roqz-%YDWxGAeeOmkgokww*h%C5mRIpsE9gX3;T*ey z+2e(Uy2Nm>%XIr?3Q+X(;=F{`;?CI%|@&O;&Mft`2&zwnWP{!5JUOT(pc6=+IQz%%obdzZ6hBN^v+U{ZiC${4x5>k8Ty)RW@eM5o5ef|$d#u@Z%!r78BhO20G-geA6{G}%iH40nhpF|5*RsKkz7nWrdN zQ&_+}I)fR!N7p}+Hiy|m1psT5hg#%sJOjSUKVm_B9TvQ~KdT-IXG(x$CDl+lS6lpC z`fj$tZWE;aBh0@Jmn%u6d)9=V4HBTZ@{&-uf*lw2Oxqo5viKTr*`o@ z@HlB~?t_o~(m@<5u*KDON`&+dGEhrPGg0pid6sErafFIIKkdZ&eJ2PgW9B`84=;i( zeS>iKScbmM&2;#Ik|w{{v^CQs{oxY_5cnA~!_DhecOk^p{T3#lID2T_hCe{rqN`g+ za*)_1qT58EFMf zs_2R^gxG5rM0Xb$9QyOgRao-a-S^Vy zfdS%aG-1X^rn zAOYm|ni&g*A}Nveqm=jws%&KBFLy*VtEz)L@GSRv@@HG=9M8>$=L$u9Y6>Um6@E*h zszmtKPClMc>_iLy(~Ay_9~H_)Efd~ohA8RkFSkX<0BawH>tp%2yN2zUxtRB5+v2F1 ze=OpsWAG8UaDQU)K<$d7Ycosms+M|`0hTCf*D5R(!3arF64{MH(hq{DP7MapTf^2l z)FL>FwIWfl+ow*;s&8eZuauM6S@~Ns$tOj!Y+k5S^7jW|7u``@Qkr!QyB`QR@*NVy zVzBlnn$=GpI@JB+kqPXyyhNJWgQv17 zcLuTh_v%?EQ($eErm#$!Iu%4GF`I?N33hMV@o5*n+Ja_8<@#Y&WEo^P9qe(AP1oU0 zb|lh>1*2we@zLPkKhUPF!yjwiNDgGjF%Xt1wuD{(8BaH*u?@06XmtBxm80~F=aVWv zWUIsAknT0B&G=8|je00#pX1;l9dwr~w6Y5tao$d}w2|xdp}Y!s*z9(2M{7tAW-Z&IN?s3a z0Q|ZzM-p{?P;oPIZHnpFm-Yt+*I225sHjK{u3+dz?-9$2t&~(-h-goe^wTi=6XQz< z%ffdc$>Lz19#%0-D~P%))F!Qc=EFaER^FYf4Am*&FZBfAjl*my+8cl02+qT7-6*g0t@wcS~Y@qKNLheI_Cb*-stW zQB+ID*SN7r1eMpZV(Ai|*#`veJyjV8n$NuU-nR5`k)99eDkIn%%S|6};LY?0EqdI7NRCmb^fK2!?|0){XrawuW|%L3Pl9xSR27wQuX;*Q|C91F zRdT+6wGD`@RQ||gJxYjwXyRt0q33j0mK$swgb@O#|7hO~Tbg=OHz^#X`r2-@kQsf1 zbTD%s`r7z8jLtY;H2Fmu1}>q z5`}S>?Zz+9tG8!nD9CheXM#gaa2?bVn?NLoy`v8#7 zc&A5&`XxLOu|_aVni1*$TWF0NAtbEuK;n|JlL$slu=*{TfkS5^sua6h88cLaHIfqb zCbyPO>qA(p6Nmk!xq33+600;{#USg~_-FRFqnKQn{t&zBw86@2Zm+Fin|!#6PgG$z z1q7N^Q4{^uRG_)EL}g|MK_)%0WccUf?s+D)y{4e?#p&WB z5?4+jMneRU#kGOYxTxZy*6fY=3=9blPKFiPQ%uTZ1`|(lwF@q;U^@wKdLNSsjTo>M z%iFjSMC%)U2uU8(72Cj-mi3V(@UF6d@b~~ws^|LL>uhKAR6}SsVvdbR*IGt2^pq#F zg1Vd}SbLZtW!sFd)6H*`w7Bm-kp&P>kseWJfsK4P6id>V4j~*!k;jHjoqPduqNT4K z>rTffW?_*(431xZa@#Xto(53aeifudrOO;$d&wUdUZj?dm#RHf;wL|kDOom#d^{cv z+QTAg=fXHryCI7aXdkMii!*a)=zVgGjLJNBXtB?l5+Y|`)+&skHmVO*Jy#J1ipj-r zbl-S>Z=SS7SZO3A4$L|7=OAgr$44_$!Wke(a<%RdW03ydfD-v|Gkej3qs}HP(VAj# zNVg`L94Q#>0GNlZYx=hu#BH@Xc76axBZ8!wz#Gi=e!n<`yMJQo-{wVY0e6hWzo*Ot zj@Iyw>v_fzv|W@Q#NrhS+&4Jx;w6HXlOBqAfk*BV#xj_+_C3`Pd;H zf=RVp2^Z0mEYe=IWh;Gp+Xz*QhH1eKS3xE^KSkuF+`E@Zl{2#99~p zjG%!bp zNds6wS{%VO}$sJJ_6=v|c9Qd%!0rz9(J!3tgesgMxAP|q#&t1>uSd5tr z^cgpSIE)f}1kp;1LK)_HQskWKAr5D5bd#4C;cz;WZr>uam-Ra0_KTL-4X&+bMo>LM zVi0|G2kCtpu)(20FvU9wR4IH_dS^-Z(}9C|Tx77wFQ4irb7J#c$^`6RC;Vahf87a> zv-LbgkyJ%ST7|(_yx{LzeCAYxq#(^x1o;(gNB-dt-Yn?LIBEH@dDgQ<+VE49O=GXk2sf3sSA7(=QR~(q(GbFH2jYS*j9@gF$a-hJ-ns4U zmDpVRUBaptQPJj$r{ItT)!R!4afE^nWoUz`2QCmd8GG*JWz-9p)CC-z{Q|D1Obwjw zz?L{$eKkd$iULW@Gz$QVF$~Z+WzPff#u=+v=)J&zK}j6LV-y-TfIgR}5KN8R+9w8h z`|(ecU3EZTWSQ4kbjlF3ER8%!upPIbG~a_e(dqw0uM8k>gb%W1*C(dRrK)ooCuizs zhHkb+aNouSI-@`#yOE^RiLQTo&L_WgH78G6MvAMAE3kptMm2kg67VE=1l>!dipE1N_*|sHRt2{nca= zSwPGFDeS8{8#V+^lSlL@+m1y! zR|>+!37&TS@;`{G3X4)u*x4M=?~~ZDJqEiQFl(GCyB0f)33(v#4~YcOX~Q3YK=u6; zzLhre4-*^hCul*Hu@=~YsKwC@r>qJMGWo3$iq9r3vfY)kq;x%o&LO_~ec% z@t%q*RXv6m$xpiRtgnwY*FAxv(#REpA=4ZODZ6>KMD$aU>dRi!VbrFx(S!ajc{F@2 zwrQio?Y4LC&NE2qWxNqd)nwD9cqcEs+J3wP!5^hF@RUW;_(jtZcZiCbbLc=};>lCC z1D$N9ZX$PcM`hYDyPSIHaWrrXk_R~@EP&B_2!}cw9h>5`jQ=$7;-fHm$Sd9R^iM$j zkaWVJV^Dv*`b5{a5K)%yu6#hk~ecCmNHiL+AnReNR{1fAK8q9>KX>6e%=oIhYm zK{EaDn*%|Icub`SVJ$iAuaiU6lXQXaw>mFGCwGBQcJBlth;1Jqdxwu59-+E@iHpGu z=Pl5IqE3aDUg0pl_PJ`}P)vrHI^HtlB*%_7AcAu(5=YhKVOQTm|NR_mqs;U9z{k$= z=#1gyW}Cx|nMDJ!0Pb4qrFysNBz8zNta3SbF&pX{m6!Hf{~MxD80@Dy>SO5aa`n&J zXV|Uv>(R@ZzdK_L^M%Ite>cOV8V)w7>m_i&x$=gJRKkQtR_*c=>^`AlAZIEmfn4b4w57Frlnq}hTcG}J+quG6pT(txvKXF zUP4hNnNIe;#d&syhD$&3sg<P$m+?@lZak_9qE6%Z zmG?~m4xbj@Gms2G4L9U`+vy?&9nE^XqKH2;-BX;nfMJePnC^zUp7OR}$z9q;NA(c}qeloOokh-$gN^?kuAiN%*iC{X2^( zg0CkB=5coz&Z(OJUz^wpCWd4VeeVpEXSJr&?|YdFtVqaP z8H}`Z1uG|Jj7(j{AU?Pb^!ggf=PQLrPUBIAzT9B+X5(PEdneNU{h`-u^q~*O zFOr`ME7MyzZNxqpR$LjqZ99`K{Kg6R@&ijWQ&!&QGcA4_tNPwNYp26Xuhnv73 z&6g6j;H-D(8JJ1_$-uQT)EKWh=>M7vSG3U0q=?Kv`2$#$)?pgHTp%cAI3Vu>$y zd$e@w0uL|Hq5Trat#t0{ZPP5`wlAnmrdw2F(nL$!X+OYZd_3{V-8OlcUriL$BCANN z#=x0m3`dWpYb^kBX(NAkL*|s@n1uHZxOu4I%R9^Uzya9R7`JliUs3+AVE`yQpfV^j z(eWPufZF(? z%d<;b>5lsK{&HwnjG<@_#v1%P1Y--%7<}%tVErMVjjOH?Ko}kqRtaiJ#mF_UB^DbW z;izU|bNYl&$x9&Q-RoVXfq zvK-&n998d{b)cuOseD-}wQ1IA4mM4kcVZ)5htn+Ai<`;>4%pG8fOZT|-97~$5w*6A zn+PDL0^=%q!)NQTPt!0mwpFGOk$d2*2GO4j>e!@zkg=z(w$W}7&&LBQCg;LsK%m`JC+i9XA3z{GH z?9{AJ;wBJg*u~_i{a<*8#kn>n$Xj8_x36H3Gu82&aknORlrh3CHqbR_Gxb#fkXB4i z#@Oho=Nbkn(RNTWjo;2*P+&03rC}lHpE(dqkEy6h^jNY>xpk`2v4fHOBb6ERQv1>Z6==?YcUQew zbp13#l|Gc~a2KNXSw_w=#igOJ*=YMb{lURY(A*C0{jEylo8Od8*k;__%*N& zl7m_Ng593dW#N$lIf2KaOD={cJ0{9l5TkZGu52UN(BQ#dKU1>#Y5HHKGxdJ>cnyo~ zekn=NZ)&$BkZ2$g6c3v%p8XXk_zo9OahnReH|3FM@fV=L=pYBRFh}h-KMA@3euu({ zAL|+m*&Tbnn^)ZSWDx-&ZbIKr1mb8>;t>EkJpPI=sgw@I$N3zKK+@YNJE(yw>x{Qa zTvXNh5}43OmqnnqIRtR|dOEp@b3*Bsx7PN2#Vc3u7$Lv|-?&@e`Z@$Af(gzCaPYm* z_gd#cB3`2jF#Q+Fo&b%V+;e(bTRkbUiS~AANrZkTBS+Z7?W&wQR{V&Vr`>kXWNLnh zt;WXdcVBnxQOh+DPv5#i7?YD}l0JX_Fr=hOl>N@l^nY7y4)j5Ulq5B{iq`hA+dx?#tirv>o72p2J%h0j%-TZ(2Pq!4Io=07291k0}TT$I3 z0Cqf97Q}t**@S7!D%`Pha~y>iS@^R7>n0`1L0_u%<{tr|%Mc25%3;;%0dK-s9tYuw zeqzEL^jSQP{_5pV^PsNP^f?<5!WyN(S*<|5LSa!D|H=lG4V2^LCnt;KkeAgCC;@QJ zv4J!iw;%?#x-ic|40evlo?Hk_2>cwL*?evvW~4b=HZtC;zjULJ;pHKqsU;vTuT1Hv}t zYk+)vwjzA6;+gM#c!}3nbLLpP)XcKo=JYEN>HXR&P%4Pc$`}vg4%WM1$Up5vStv`> z8*O7;7v4WNC(3NaLiCEjbK^==02PZKbjw~rGfg=Ov+ULh*XLRw&?LyD!4JjIRit$L z&h$FC=4F15jyS{sl+q$O4KtMp?R+97n~drgN>_u3E$E9kqIM~Rrd8gz2&V-#D=@LJ zYjBaIi5Q^L(1^F%6GDTF{LN0ncu)vO-rWM3c(>7}MY7UkF+etMtjxwLbdkBZ>}J7F zWjq}S#>I^RM;9d1+sqjLGKuJbGFGgkvig{%-vKcz5^&*}7Jub*k}Ix{-Nb3DiJ7(= z0LthRCH?W{P|STHhH8rJ*ZSLf7>aN8zN-Fp{Gz$%U)p*FRZh(_{t+L|G3!6F!n01nZ-+bJQNMTtP3>q#zUG0ed-2=^yXbMyDagj!6 zz}LWsAV(&%)i2LJ zU~z5?8PC2wPsW}Rlk5Q_p?`A;?gvk<{&ta}xzhDD%$7~W2tGpWk^a@*NLq~v6kwur z*JPj)1#Nnb<)l8pcd2$gfLhH1Sn2ONFm=EgsZwdMJ707!{7PsLN{1Q{)#sMXD}l*? z-4;tJu->x@2|&a+06dZ^TiTLodi(ofem;Wzlh-CvN$ZT}V$L`HyLgR!ZPiRR@yiO& z-MBM>lrQ&QKUnuQDdVL+)dd5;aD;D$0T0xX@c`d~aS0l$*GPt*BsAM~5oa!tVH+|B zX(wN;vhX#w02Q^-vj5*x?MR$I)||edriM&|Z(vXd62tdB4tD>;tptONRp5P_IHs_e zCnxU^P5(n0m83?4sM6Ufo}A1p#{hOVHZkaPD6Wxwus2Hp`g>BXolk!@P|58FrU(Ar zG1LXsN4eqaHD z^(1$z6EGV%pk}lZw5rFPwL`HTpY{%%{z;^ppr;acSg!tvyUC=*7SX!3(R~O&zy@~J z-=L}c?mbUt+WiBNJDCzmT+~gQk^{^_SL~PU9kX|!6@=6i+{yrg#TS}`4^&AWV-_C&6pc2rb8hu9`Bx`h;U(qR^(;x z|5i3{Q6DriG8y3J`AdL#Ejbi8WIuM!pl|Bjv@cU7)1?Y$46WC>>2@`HhUVuN^AWI0 zt_?J+S*rOR&eU6RfDB>NA|vMUTc6i<0F4)_6t$;3j_~&UvO7zje~!0DF=9!aNA%g?`FbJcpUXrWQK6l@>w& zTh?BnZL^ArxH%K{frF&0gKMCUsUF&{pkKGYoQc_;Bo!E?B#i_;ppj3`ka%8z%mIDi z{Yekb?k2iIM5)hk;`9Fsx;;m;W$$*pi)G$rJ7(FmcLTV5w^8jA|1lL9FimM1f;r9b zQxL%lf9TiwZslV(awH}!zQAFSTd;Vx`X+m@I*P*E7zz3Y7xvq{mIEIeChRZgPWM=?6f}%)pn%}7 z?%p1&oX_D-6kCLkPe;~q(#uM@7_6JqM=<7*E}a>bOZ%Mh29;>q^!PXL1Rx`At#5)K zqm~T_a26CD+L}3m?kV^WfG=<@V<(qfWmP}tKGIqW-8R-lqRoi}LLR&otn#qyiZs7e zc|SOB<4K054u&4OMwv9pJBlW~@VzY;^!FIRz79l{PBCift~^E`p*VPXCSh}&pS!`Z z1eFfX%NY8obF@<|^cLo5Ls51-T2xrcqUuYTV$g^E%75{37Qk$K#jh-sOsXDW=pc5w z2ubFQ9T%b#M2`2GoYO>s9xZ?7;N)azVnKt9T;N6Lymo#QW!bq&fT3}aG?TuYyWxwq z6llhr))wX6A!#Hat6aho)PV##x;Y{B`#_7PfVos;*W_}h7RQM9pvfJ)Tj|>r?>htP z?_6k3@7mql?@5x?CzPH@4H?X(H}3i&OmBw3FHg}RGXxIHHo{kKsd=`lF5IKvmP7(` z1JAGiEY5$D0gdtVu-yGG3ce1T)`@|)7nRv+WK|CBLzWoHuEsZKblu2>>EoX7CeK(d zOVVG71s*oT>+Uslo*BiGgu>nb&$!L{ixxt^|(XB=oNHLb3OPOC3V+`>&<1%N* zMCtmS?EU~BjyU1<9H+59R%ydc`2!v!u(LRTqLil!p!igaCP0V9b}_E2bprg10Kpx> z0v%0+DeTn6??n?w2IN<=q|%xbI!fb-6A;h!^=Cl9`aXoZH!%x6twm?pw056npUVHz zHYj)%_b%IGoUwV&6jUDTHa0!uQfGAIC;`Dro+ev`tB2II2`?knY%=yfkzP0|{KnTj zcoP~n!y%#b6!TSo!T>|mqacf! z=M2cE+uJp^k$KbUf`Lg?=3ovNM;1gui3Yp?a+?}Kr$uD)36}|Zot);#K(6KoFPiU- zbB_qoiUtq0Y1c~$uBBkr9U|w5+PjHAz&4xXB zSg&6W{aGU*QR2t+C#zg#|Je-fU`o9PY>Z8`RQ+=pOOJ!Cffi$eNu&89^6T=60!Jfz zN<7;=j`WXc+kQj<_9HEc^Ax|(kj^i~*W`q?esIcm8iMAH!(dqsWPgm z?HZm|O`Uu+Z%Y?ve@~+ri}m0Or31?8)zo{S4#!tq-_wVx()~olhkK>SQ@2^-S@#hE z%%bm@H^wi`C+h$iXHmw&esC@;%(fHTs;&mAcplkbw`eHFxNPZFS2r3LVo(8I*J?RDmvi6+yR}TRft37He2~QPO17NYvNAg?zb6#h1mA zI1TKdIAN^LRceUv7~LDq6pQP#_qu2t}?C&$o(43Lu+_l*xf3cq}3SF7a1<7zfJS#aNhPu@5nRU zv6r*+BAvqsSg!{^lERIND66Ryl3?X#N=GDxe6xCk()6{E(5iTQkTUBzPXn*%jGb%D2&#l_{mL+!rLRj$k-K9HfFM*JQ?Ewx#ER>qZ60zc&MOH>w%7aEzH>17r%K4 z@|H+jN1{C#rHo>rS^{jbI>n#6NSd&)}; zC!_k=c?)n&kfq*ZV)M*`WJ;bOKQ``MOUKJhBJ=8ETE`oQolt;EJ1gfv>sL}_!1q`> zScY~Psl#=ayB!G-UK`NpwM2qx4IIuLKOFl>!b|cGdCwkf;u0REQfv946|p4z$4%37 zOyVSjnF!zc?kRh1d$Z)6gdj|-4yDfTTLX7c2rWC|www9=LxOEHB0eT~+r!ni2bN00 zlfDSo7dpYsMNHNz&BdnvG$B?0*cH^MZ#k+-wrlqCVnRowNVcw;u27uH>W#me9n%46 zGTP)qdPAvfUTCGG%s>zANs}f*cr99mlEdrR+xEP%LYSVC0(_ZBEv&lUcD~p8% zF<@^fS0G)7C+dFzTp-q1he;gpx6F1c3AiPHi)I6QZ~!aOAFnP0mEz*o#XX~Qb}DNm z-=FGFuYP&LAZ*4O8@^AWCXuF#C*MFGQFhrk#P@0O_S@@3pKOUl)rI@)bvGl#>L3vd zATXZ}x|TtruVC2xQ;6YAJ2=l)t>0k~shtWjwL1{g;8LiAYYDA#jVb~;Nn2hO+=P!? z#)x%^0+xxhpCu?dz-1e8g1K(DPz!o5^^dtVwSWfv2RBm^?15W^FK^ W->~}~JDQ-Lg)n{lsf+*q21_1z1(_@W diff --git a/Stickers/Stickers.csproj b/Stickers/Stickers.csproj index a587521..19f88ea 100644 --- a/Stickers/Stickers.csproj +++ b/Stickers/Stickers.csproj @@ -29,6 +29,8 @@ + + diff --git a/Stickers/Stickers/Enums/StickerKeyBinds.cs b/Stickers/Stickers/Enums/StickerKeyBinds.cs new file mode 100644 index 0000000..9de4b42 --- /dev/null +++ b/Stickers/Stickers/Enums/StickerKeyBinds.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace NAK.Stickers; + +internal enum KeyBind +{ + A = KeyCode.A, + B = KeyCode.B, + C = KeyCode.C, + D = KeyCode.D, + E = KeyCode.E, + F = KeyCode.F, + G = KeyCode.G, + H = KeyCode.H, + I = KeyCode.I, + J = KeyCode.J, + K = KeyCode.K, + L = KeyCode.L, + M = KeyCode.M, + N = KeyCode.N, + O = KeyCode.O, + P = KeyCode.P, + Q = KeyCode.Q, + R = KeyCode.R, + S = KeyCode.S, + T = KeyCode.T, + U = KeyCode.U, + V = KeyCode.V, + W = KeyCode.W, + X = KeyCode.X, + Y = KeyCode.Y, + Z = KeyCode.Z, + Mouse0 = KeyCode.Mouse0, + Mouse1 = KeyCode.Mouse1, + Mouse2 = KeyCode.Mouse2, + Mouse3 = KeyCode.Mouse3, + Mouse4 = KeyCode.Mouse4, + Mouse5 = KeyCode.Mouse5, + Mouse6 = KeyCode.Mouse6, +} \ No newline at end of file diff --git a/Stickers/Stickers/Enums/StickerSFXType.cs b/Stickers/Stickers/Enums/StickerSFXType.cs new file mode 100644 index 0000000..e116c9c --- /dev/null +++ b/Stickers/Stickers/Enums/StickerSFXType.cs @@ -0,0 +1,9 @@ +namespace NAK.Stickers; + +internal enum SFXType +{ + LittleBigPlanetSticker, + SourceEngineSpray, + FactorioAlertDestroyed, + None +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Constants.cs b/Stickers/Stickers/Networking/ModNetwork.Constants.cs new file mode 100644 index 0000000..2d58a94 --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Constants.cs @@ -0,0 +1,16 @@ +using NAK.Stickers.Properties; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Constants + + internal const int MaxTextureSize = 1024 * 256; // 256KB + + private const string ModId = $"MelonMod.NAK.Stickers_v{AssemblyInfoParams.Version}"; + private const int ChunkSize = 1024; // roughly 1KB per ModNetworkMessage + private const int MaxChunkCount = MaxTextureSize / ChunkSize; + + #endregion Constants +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Enums.cs b/Stickers/Stickers/Networking/ModNetwork.Enums.cs new file mode 100644 index 0000000..a3c9e96 --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Enums.cs @@ -0,0 +1,21 @@ +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Enums + + // Remote clients will request textures from the sender if they receive PlaceSticker with a textureHash they don't have + + private enum MessageType : byte + { + PlaceSticker = 0, // stickerSlot, textureHash, position, forward, up + ClearSticker = 1, // stickerSlot + ClearAllStickers = 2, // none + StartTexture = 3, // stickerSlot, textureHash, chunkCount, width, height + SendTexture = 4, // chunkIdx, chunkData + EndTexture = 5, // none + RequestTexture = 6 // stickerSlot, textureHash + } + + #endregion Enums +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Events.cs b/Stickers/Stickers/Networking/ModNetwork.Events.cs new file mode 100644 index 0000000..cf414a8 --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Events.cs @@ -0,0 +1,18 @@ +using MTJobSystem; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Events + + public static Action OnTextureOutboundStateChanged; + + private static void InvokeTextureOutboundStateChanged(bool isSending) + { + MTJobManager.RunOnMainThread("ModNetwork.InvokeTextureOutboundStateChanged", + () => OnTextureOutboundStateChanged?.Invoke(isSending)); + } + + #endregion Events +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Helpers.cs b/Stickers/Stickers/Networking/ModNetwork.Helpers.cs new file mode 100644 index 0000000..04e173d --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Helpers.cs @@ -0,0 +1,18 @@ +using ABI_RC.Core.Networking; +using DarkRift; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Private Methods + + private static bool IsConnectedToGameNetwork() + { + return NetworkManager.Instance != null + && NetworkManager.Instance.GameNetwork != null + && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected; + } + + #endregion Private Methods +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Inbound.cs b/Stickers/Stickers/Networking/ModNetwork.Inbound.cs new file mode 100644 index 0000000..fbada5b --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Inbound.cs @@ -0,0 +1,222 @@ +using ABI_RC.Core.Savior; +using ABI_RC.Systems.ModNetwork; +using NAK.Stickers.Utilities; +using UnityEngine; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Inbound Buffers + + private static readonly Dictionary _textureChunkBuffers = new(); + private static readonly Dictionary _receivedChunkCounts = new(); + private static readonly Dictionary _expectedChunkCounts = new(); + private static readonly Dictionary _textureMetadata = new(); + + #endregion Inbound Buffers + + #region Inbound Methods + + private static void HandleMessageReceived(ModNetworkMessage msg) + { + string sender = msg.Sender; + msg.Read(out byte msgTypeRaw); + + if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw)) + return; + + if (_disallowedForSession.Contains(sender)) + return; // ignore messages from disallowed users + + if (MetaPort.Instance.blockedUserIds.Contains(sender)) + return; // ignore messages from blocked users + + LoggerInbound($"Received message from {msg.Sender}, Type: {(MessageType)msgTypeRaw}"); + + switch ((MessageType)msgTypeRaw) + { + case MessageType.PlaceSticker: + HandlePlaceSticker(msg); + break; + + case MessageType.ClearSticker: + HandleClearSticker(msg); + break; + + case MessageType.ClearAllStickers: + HandleClearAllStickers(msg); + break; + + case MessageType.StartTexture: + HandleStartTexture(msg); + break; + + case MessageType.SendTexture: + HandleSendTexture(msg); + break; + + case MessageType.EndTexture: + HandleEndTexture(msg); + break; + + case MessageType.RequestTexture: + HandleRequestTexture(msg); + break; + + default: + LoggerInbound($"Invalid message type received: {msgTypeRaw}"); + break; + } + } + + private static void HandlePlaceSticker(ModNetworkMessage msg) + { + msg.Read(out int stickerSlot); + msg.Read(out Guid textureHash); + msg.Read(out Vector3 position); + msg.Read(out Vector3 forward); + msg.Read(out Vector3 up); + + if (!StickerSystem.Instance.HasTextureHash(msg.Sender, textureHash)) + SendRequestTexture(stickerSlot, textureHash); + + StickerSystem.Instance.OnStickerPlaceReceived(msg.Sender, stickerSlot, position, forward, up); + } + + private static void HandleClearSticker(ModNetworkMessage msg) + { + msg.Read(out int stickerSlot); + StickerSystem.Instance.OnStickerClearReceived(msg.Sender, stickerSlot); + } + + private static void HandleClearAllStickers(ModNetworkMessage msg) + { + StickerSystem.Instance.OnStickerClearAllReceived(msg.Sender); + } + + private static void HandleStartTexture(ModNetworkMessage msg) + { + string sender = msg.Sender; + msg.Read(out int stickerSlot); + msg.Read(out Guid textureHash); + msg.Read(out int chunkCount); + msg.Read(out int width); + msg.Read(out int height); + + if (_textureChunkBuffers.ContainsKey(sender)) + { + LoggerInbound($"Received StartTexture message from {sender} while still receiving texture data!"); + return; + } + + if (StickerSystem.Instance.HasTextureHash(sender, textureHash)) + { + LoggerInbound($"Received StartTexture message from {sender} with existing texture hash {textureHash}, skipping texture data."); + return; + } + + if (chunkCount > MaxChunkCount) + { + LoggerInbound($"Received StartTexture message from {sender} with too many chunks: {chunkCount}", true); + return; + } + + _textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)]; + _receivedChunkCounts[sender] = 0; + _expectedChunkCounts[sender] = chunkCount; + _textureMetadata[sender] = (stickerSlot, textureHash, width, height); + + LoggerInbound($"Received StartTexture message from {sender}: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Resolution: {width}x{height}"); + } + + private static void HandleSendTexture(ModNetworkMessage msg) + { + string sender = msg.Sender; + msg.Read(out int chunkIdx); + msg.Read(out byte[] chunkData); + + if (!_textureChunkBuffers.TryGetValue(sender, out var buffer)) + return; + + int startIndex = chunkIdx * ChunkSize; + Array.Copy(chunkData, 0, buffer, startIndex, chunkData.Length); + + _receivedChunkCounts[sender]++; + if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender]) + return; + + (int stickerSlot, Guid Hash, int Width, int Height) metadata = _textureMetadata[sender]; + + // All chunks received, reassemble texture + _textureChunkBuffers.Remove(sender); + _receivedChunkCounts.Remove(sender); + _expectedChunkCounts.Remove(sender); + _textureMetadata.Remove(sender); + + // Validate image + if (!ImageUtility.IsValidImage(buffer)) + { + LoggerInbound($"[Inbound] Received texture data is not a valid image from {sender}!", true); + return; + } + + // Validate data TODO: fix hash??????? + (Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer); + if (metadata.Width != width + || metadata.Height != height) + { + LoggerInbound($"Received texture data does not match metadata! Expected: {metadata.Hash} ({metadata.Width}x{metadata.Height}), received: {imageHash} ({width}x{height})", true); + return; + } + + Texture2D texture = new(1,1); + texture.LoadImage(buffer); + texture.Compress(true); + + StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture, metadata.stickerSlot); + + LoggerInbound($"All chunks received and texture reassembled from {sender}. " + + $"Texture size: {metadata.Width}x{metadata.Height}"); + } + + private static void HandleEndTexture(ModNetworkMessage msg) + { + string sender = msg.Sender; + if (!_textureChunkBuffers.ContainsKey(sender)) + return; + + LoggerInbound($"Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received."); + + _textureChunkBuffers.Remove(sender); + _receivedChunkCounts.Remove(sender); + _expectedChunkCounts.Remove(sender); + _textureMetadata.Remove(sender); + } + + private static void HandleRequestTexture(ModNetworkMessage msg) + { + string sender = msg.Sender; + msg.Read(out int stickerSlot); + msg.Read(out Guid textureHash); + + if (!_isSubscribedToModNetwork || IsSendingTexture) + return; + + if (stickerSlot < 0 || stickerSlot >= _textureStorage.Length) + { + LoggerInbound($"Received RequestTexture message from {sender} with invalid slot {stickerSlot}!"); + return; + } + + if (_textureStorage[stickerSlot].textureHash != textureHash) + { + LoggerInbound($"Received RequestTexture message from {sender} with invalid texture hash {textureHash} for slot {stickerSlot}!"); + return; + } + + SendTexture(stickerSlot); + } + + #endregion Inbound Methods +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Logging.cs b/Stickers/Stickers/Networking/ModNetwork.Logging.cs new file mode 100644 index 0000000..60a7a6f --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Logging.cs @@ -0,0 +1,22 @@ +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Network Logging + + private static void LoggerInbound(string message, bool isWarning = false) + { + if (!ModSettings.Debug_NetworkInbound.Value) return; + if (isWarning) StickerMod.Logger.Warning("[Inbound] " + message); + else StickerMod.Logger.Msg("[Inbound] " + message); + } + + private static void LoggerOutbound(string message, bool isWarning = false) + { + if (!ModSettings.Debug_NetworkOutbound.Value) return; + if (isWarning) StickerMod.Logger.Warning("[Outbound] " + message); + else StickerMod.Logger.Msg("[Outbound] " + message); + } + + #endregion Network Logging +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Main.cs b/Stickers/Stickers/Networking/ModNetwork.Main.cs new file mode 100644 index 0000000..64f21b9 --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Main.cs @@ -0,0 +1,48 @@ +using ABI_RC.Systems.ModNetwork; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Mod Network Internals + + public static bool IsSendingTexture { get; private set; } + private static bool _isSubscribedToModNetwork; + + internal static void Subscribe() + { + ModNetworkManager.Subscribe(ModId, HandleMessageReceived); + + _isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId); + if (!_isSubscribedToModNetwork) StickerMod.Logger.Error("Failed to subscribe to Mod Network! This should not happen."); + } + + #endregion Mod Network Internals + + #region Disallow For Session + + private static readonly HashSet _disallowedForSession = new(); + + public static bool IsPlayerACriminal(string playerID) + { + return _disallowedForSession.Contains(playerID); + } + + public static void HandleDisallowForSession(string playerID, bool isOn) + { + if (string.IsNullOrEmpty(playerID)) return; + + if (isOn) + { + _disallowedForSession.Add(playerID); + StickerMod.Logger.Msg($"Player {playerID} has been disallowed from using stickers for this session."); + } + else + { + _disallowedForSession.Remove(playerID); + StickerMod.Logger.Msg($"Player {playerID} has been allowed to use stickers again."); + } + } + + #endregion Disallow For Session +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.Outbound.cs b/Stickers/Stickers/Networking/ModNetwork.Outbound.cs new file mode 100644 index 0000000..fd16aa5 --- /dev/null +++ b/Stickers/Stickers/Networking/ModNetwork.Outbound.cs @@ -0,0 +1,189 @@ +using ABI_RC.Systems.ModNetwork; +using NAK.Stickers.Utilities; +using UnityEngine; + +namespace NAK.Stickers.Networking; + +public static partial class ModNetwork +{ + #region Texture Data + + private static readonly (byte[] textureData, Guid textureHash, int width, int height)[] _textureStorage = new (byte[], Guid, int, int)[ModSettings.MaxStickerSlots]; + + public static bool SetTexture(int stickerSlot, byte[] imageBytes) + { + if (imageBytes == null + || imageBytes.Length == 0 + || imageBytes.Length > MaxTextureSize) + return false; + + (Guid hashGuid, int width, int height) = ImageUtility.ExtractImageInfo(imageBytes); + if (_textureStorage[stickerSlot].textureHash == hashGuid) + { + LoggerOutbound($"Texture data is the same as the current texture for slot {stickerSlot}: {hashGuid}"); + return false; + } + + _textureStorage[stickerSlot] = (imageBytes, hashGuid, width, height); + LoggerOutbound($"Set texture data for slot {stickerSlot}, metadata: {hashGuid} ({width}x{height})"); + + SendTexture(stickerSlot); + + return true; + } + + #endregion Texture Data + + #region Outbound Methods + + public static void SendPlaceSticker(int stickerSlot, Vector3 position, Vector3 forward, Vector3 up) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.PlaceSticker); + modMsg.Write(stickerSlot); + modMsg.Write(_textureStorage[stickerSlot].textureHash); + modMsg.Write(position); + modMsg.Write(forward); + modMsg.Write(up); + modMsg.Send(); + + LoggerOutbound($"PlaceSticker: Slot: {stickerSlot}, Hash: {_textureStorage[stickerSlot].textureHash}, Position: {position}, Forward: {forward}, Up: {up}"); + } + + public static void SendClearSticker(int stickerSlot) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.ClearSticker); + modMsg.Write(stickerSlot); + modMsg.Send(); + + LoggerOutbound($"ClearSticker: Slot: {stickerSlot}"); + } + + public static void SendClearAllStickers() + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.ClearAllStickers); + modMsg.Send(); + + LoggerOutbound("ClearAllStickers"); + } + + public static void SendStartTexture(int stickerSlot, Guid textureHash, int chunkCount, int width, int height) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.StartTexture); + modMsg.Write(stickerSlot); + modMsg.Write(textureHash); + modMsg.Write(chunkCount); + modMsg.Write(width); + modMsg.Write(height); + modMsg.Send(); + + LoggerOutbound($"StartTexture: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Size: {width}x{height}"); + } + + public static void SendTextureChunk(int chunkIdx, byte[] chunkData) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.SendTexture); + modMsg.Write(chunkIdx); + modMsg.Write(chunkData); + modMsg.Send(); + + LoggerOutbound($"SendTextureChunk: Index: {chunkIdx}, Size: {chunkData.Length} bytes"); + } + + public static void SendEndTexture() + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.EndTexture); + modMsg.Send(); + + LoggerOutbound("EndTexture"); + } + + public static void SendRequestTexture(int stickerSlot, Guid textureHash) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)MessageType.RequestTexture); + modMsg.Write(stickerSlot); + modMsg.Write(textureHash); + modMsg.Send(); + + LoggerOutbound($"RequestTexture: Slot: {stickerSlot}, Hash: {textureHash}"); + } + + public static void SendTexture(int stickerSlot) + { + if (!IsConnectedToGameNetwork() || IsSendingTexture) + return; + + IsSendingTexture = true; + + Task.Run(() => + { + try + { + var textureData = _textureStorage[stickerSlot].textureData; + var textureHash = _textureStorage[stickerSlot].textureHash; + var width = _textureStorage[stickerSlot].width; + var height = _textureStorage[stickerSlot].height; + int totalChunks = Mathf.CeilToInt(textureData.Length / (float)ChunkSize); + if (totalChunks > MaxChunkCount) + { + LoggerOutbound($"Texture data too large to send for slot {stickerSlot}: {textureData.Length} bytes, {totalChunks} chunks", true); + return; + } + + LoggerOutbound($"Sending texture for slot {stickerSlot}: {textureData.Length} bytes, Chunks: {totalChunks}, Resolution: {width}x{height}"); + + SendStartTexture(stickerSlot, textureHash, totalChunks, width, height); + + for (int i = 0; i < textureData.Length; i += ChunkSize) + { + int size = Mathf.Min(ChunkSize, textureData.Length - i); + byte[] chunk = new byte[size]; + Array.Copy(textureData, i, chunk, 0, size); + + SendTextureChunk(i / ChunkSize, chunk); + Thread.Sleep(5); // Simulate network latency + } + + SendEndTexture(); + } + catch (Exception e) + { + LoggerOutbound($"Failed to send texture for slot {stickerSlot}: {e}", true); + } + finally + { + IsSendingTexture = false; + InvokeTextureOutboundStateChanged(false); + } + }); + } + + #endregion Outbound Methods +} \ No newline at end of file diff --git a/Stickers/Stickers/Networking/ModNetwork.cs b/Stickers/Stickers/Networking/ModNetwork.cs deleted file mode 100644 index 46d9c2a..0000000 --- a/Stickers/Stickers/Networking/ModNetwork.cs +++ /dev/null @@ -1,414 +0,0 @@ -using ABI_RC.Core.Networking; -using ABI_RC.Systems.ModNetwork; -using DarkRift; -using NAK.Stickers.Properties; -using NAK.Stickers.Utilities; -using UnityEngine; - -namespace NAK.Stickers.Networking -{ - public static class ModNetwork - { - #region Configuration - - private static bool Debug_NetworkInbound => ModSettings.Debug_NetworkInbound.Value; - private static bool Debug_NetworkOutbound => ModSettings.Debug_NetworkOutbound.Value; - - #endregion Configuration - - #region Constants - - internal const int MaxTextureSize = 1024 * 256; // 256KB - - private const string ModId = $"MelonMod.NAK.Stickers_v{AssemblyInfoParams.Version}"; - private const int ChunkSize = 1024; // roughly 1KB per ModNetworkMessage - private const int MaxChunkCount = MaxTextureSize / ChunkSize; - - #endregion Constants - - #region Enums - - private enum MessageType : byte - { - PlaceSticker = 0, - ClearStickers = 1, - StartTexture = 2, - SendTexture = 3, - EndTexture = 4, - RequestTexture = 5 - } - - #endregion Enums - - #region Mod Network Internals - - internal static Action OnTextureOutboundStateChanged; - - internal static bool IsSendingTexture { get; private set; } - private static bool _isSubscribedToModNetwork; - - private static byte[] _ourTextureData; - private static (Guid hashGuid, int Width, int Height) _ourTextureMetadata; - - private static readonly Dictionary _textureChunkBuffers = new(); - private static readonly Dictionary _receivedChunkCounts = new(); - private static readonly Dictionary _expectedChunkCounts = new(); - private static readonly Dictionary _textureMetadata = new(); - - internal static void Subscribe() - { - ModNetworkManager.Subscribe(ModId, OnMessageReceived); - - _isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId); - if (!_isSubscribedToModNetwork) - StickerMod.Logger.Error("Failed to subscribe to Mod Network!"); - } - - public static void PlaceSticker(Vector3 position, Vector3 forward, Vector3 up) - { - if (!_isSubscribedToModNetwork) - return; - - SendStickerPlace(_ourTextureMetadata.hashGuid, position, forward, up); - } - - public static void ClearStickers() - { - if (!_isSubscribedToModNetwork) - return; - - SendMessage(MessageType.ClearStickers); - } - - public static bool SetTexture(byte[] imageBytes) - { - if (imageBytes == null - || imageBytes.Length == 0 - || imageBytes.Length > MaxTextureSize) - return false; - - (Guid hashGuid, int width, int height) = ImageUtility.ExtractImageInfo(imageBytes); - if (_ourTextureMetadata.hashGuid == hashGuid) - { - StickerMod.Logger.Msg($"[ModNetwork] Texture data is the same as the current texture: {hashGuid}"); - return false; - } - - _ourTextureData = imageBytes; - _ourTextureMetadata = (hashGuid, width, height); - StickerMod.Logger.Msg($"[ModNetwork] Set texture metadata for networking: {hashGuid} ({width}x{height})"); - return true; - } - - public static void SendTexture() - { - if (!IsConnectedToGameNetwork()) - return; - - if (!_isSubscribedToModNetwork || IsSendingTexture) - return; - - if (_ourTextureData == null - || _ourTextureMetadata.hashGuid == Guid.Empty) - return; // no texture to send - - IsSendingTexture = true; - - // Send each chunk of the texture data - Task.Run(() => - { - try - { - if (Debug_NetworkOutbound) - StickerMod.Logger.Msg("[ModNetwork] Sending texture to network"); - - var textureData = _ourTextureData; - (Guid hash, int Width, int Height) textureMetadata = _ourTextureMetadata; - int totalChunks = Mathf.CeilToInt(textureData.Length / (float)ChunkSize); - if (totalChunks > MaxChunkCount) - { - StickerMod.Logger.Error($"[ModNetwork] Texture data too large to send: {textureData.Length} bytes, {totalChunks} chunks"); - return; - } - - if (Debug_NetworkOutbound) - StickerMod.Logger.Msg($"[ModNetwork] Texture data length: {textureData.Length}, total chunks: {totalChunks}, width: {textureMetadata.Width}, height: {textureMetadata.Height}"); - - SendStartTexture(textureMetadata.hash, totalChunks, textureMetadata.Width, textureMetadata.Height); - - for (int i = 0; i < textureData.Length; i += ChunkSize) - { - int size = Mathf.Min(ChunkSize, textureData.Length - i); - byte[] chunk = new byte[size]; - Array.Copy(textureData, i, chunk, 0, size); - - SendTextureChunk(chunk, i / ChunkSize); - Thread.Sleep(5); - } - } - catch (Exception e) - { - if (Debug_NetworkOutbound) StickerMod.Logger.Error($"[ModNetwork] Failed to send texture to network: {e}"); - } - finally - { - IsSendingTexture = false; - SendMessage(MessageType.EndTexture); - } - }); - } - - private static void SendStickerPlace(Guid textureHash, Vector3 position, Vector3 forward, Vector3 up) - { - if (!IsConnectedToGameNetwork()) - return; - - using ModNetworkMessage modMsg = new(ModId); - modMsg.Write((byte)MessageType.PlaceSticker); - modMsg.Write(textureHash); - modMsg.Write(position); - modMsg.Write(forward); - modMsg.Write(up); - modMsg.Send(); - - if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] PlaceSticker: Hash: {textureHash}, Position: {position}, Forward: {forward}, Up: {up}"); - } - - private static void SendStartTexture(Guid hash, int chunkCount, int width, int height) - { - OnTextureOutboundStateChanged?.Invoke(true); - - if (!IsConnectedToGameNetwork()) - return; - - using ModNetworkMessage modMsg = new(ModId); - modMsg.Write((byte)MessageType.StartTexture); - modMsg.Write(hash); - modMsg.Write(chunkCount); - modMsg.Write(width); - modMsg.Write(height); - modMsg.Send(); - - if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] StartTexture sent with {chunkCount} chunks, width: {width}, height: {height}"); - } - - private static void SendTextureChunk(byte[] chunk, int chunkIdx) - { - if (!IsConnectedToGameNetwork()) - return; - - using ModNetworkMessage modMsg = new(ModId); - modMsg.Write((byte)MessageType.SendTexture); - modMsg.Write(chunkIdx); - modMsg.Write(chunk); - modMsg.Send(); - - if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] Sent texture chunk {chunkIdx + 1}"); - } - - private static void SendMessage(MessageType messageType) - { - OnTextureOutboundStateChanged?.Invoke(false); - - if (!IsConnectedToGameNetwork()) - return; - - using ModNetworkMessage modMsg = new(ModId); - modMsg.Write((byte)messageType); - modMsg.Send(); - - if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] MessageType: {messageType}"); - } - - private static void SendTextureRequest(string sender) - { - if (!IsConnectedToGameNetwork()) - return; - - using ModNetworkMessage modMsg = new(ModId); - modMsg.Write((byte)MessageType.RequestTexture); - modMsg.Write(sender); - modMsg.Send(); - - if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] RequestTexture sent to {sender}"); - } - - private static void OnMessageReceived(ModNetworkMessage msg) - { - msg.Read(out byte msgTypeRaw); - - if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw)) - return; - - if (Debug_NetworkInbound) - StickerMod.Logger.Msg($"[Inbound] Sender: {msg.Sender}, MessageType: {(MessageType)msgTypeRaw}"); - - switch ((MessageType)msgTypeRaw) - { - case MessageType.PlaceSticker: - msg.Read(out Guid hash); - msg.Read(out Vector3 receivedPosition); - msg.Read(out Vector3 receivedForward); - msg.Read(out Vector3 receivedUp); - OnStickerPlaceReceived(msg.Sender, hash, receivedPosition, receivedForward, receivedUp); - break; - - case MessageType.ClearStickers: - OnStickersClearReceived(msg.Sender); - break; - - case MessageType.StartTexture: - msg.Read(out Guid startHash); - msg.Read(out int chunkCount); - msg.Read(out int width); - msg.Read(out int height); - OnStartTextureReceived(msg.Sender, startHash, chunkCount, width, height); - break; - - case MessageType.SendTexture: - msg.Read(out int chunkIdx); - msg.Read(out byte[] textureChunk); - OnTextureChunkReceived(msg.Sender, textureChunk, chunkIdx); - break; - - case MessageType.EndTexture: - OnEndTextureReceived(msg.Sender); - break; - - case MessageType.RequestTexture: - OnTextureRequestReceived(msg.Sender); - break; - - default: - if (Debug_NetworkInbound) StickerMod.Logger.Error($"[ModNetwork] Invalid message type received from: {msg.Sender}"); - break; - } - } - - #endregion Mod Network Internals - - #region Private Methods - - private static bool IsConnectedToGameNetwork() - { - return NetworkManager.Instance != null - && NetworkManager.Instance.GameNetwork != null - && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected; - } - - private static void OnStickerPlaceReceived(string sender, Guid textureHash, Vector3 position, Vector3 forward, Vector3 up) - { - Guid localHash = StickerSystem.Instance.GetPlayerStickerTextureHash(sender); - if (localHash != textureHash && textureHash != Guid.Empty) SendTextureRequest(sender); - StickerSystem.Instance.OnPlayerStickerPlace(sender, position, forward, up); - } - - private static void OnStickersClearReceived(string sender) - { - StickerSystem.Instance.OnPlayerStickersClear(sender); - } - - private static void OnStartTextureReceived(string sender, Guid hash, int chunkCount, int width, int height) - { - if (_textureChunkBuffers.ContainsKey(sender)) - { - if (Debug_NetworkInbound) StickerMod.Logger.Warning($"[Inbound] Received StartTexture message from {sender} while still receiving texture data!"); - return; - } - - Guid oldHash = StickerSystem.Instance.GetPlayerStickerTextureHash(sender); - if (oldHash == hash) - { - if (Debug_NetworkInbound) - StickerMod.Logger.Msg($"[Inbound] Received StartTexture message from {sender} with existing texture hash {hash}, skipping texture data."); - return; - } - - if (chunkCount > MaxChunkCount) - { - StickerMod.Logger.Error($"[Inbound] Received StartTexture message from {sender} with too many chunks: {chunkCount}"); - return; - } - - _textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)]; - _receivedChunkCounts[sender] = 0; - _expectedChunkCounts[sender] = chunkCount; - _textureMetadata[sender] = (hash, width, height); - - if (Debug_NetworkInbound) - StickerMod.Logger.Msg($"[Inbound] StartTexture received from {sender} with hash: {hash} chunk count: {chunkCount}, width: {width}, height: {height}"); - } - - private static void OnTextureChunkReceived(string sender, byte[] chunk, int chunkIdx) - { - if (!_textureChunkBuffers.TryGetValue(sender, out var buffer)) - return; - - int startIndex = chunkIdx * ChunkSize; - Array.Copy(chunk, 0, buffer, startIndex, chunk.Length); - - _receivedChunkCounts[sender]++; - if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender]) - return; - - (Guid Hash, int Width, int Height) metadata = _textureMetadata[sender]; - - // All chunks received, reassemble texture - _textureChunkBuffers.Remove(sender); - _receivedChunkCounts.Remove(sender); - _expectedChunkCounts.Remove(sender); - _textureMetadata.Remove(sender); - - // Validate image - if (!ImageUtility.IsValidImage(buffer)) - { - StickerMod.Logger.Error($"[Inbound] Received texture data is not a valid image from {sender}!"); - return; - } - - // Validate data TODO: fix hash??????? - (Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer); - if (metadata.Width != width - || metadata.Height != height) - { - StickerMod.Logger.Error($"[Inbound] Received texture data does not match metadata! Expected: {metadata.Hash} ({metadata.Width}x{metadata.Height}), received: {imageHash} ({width}x{height})"); - return; - } - - Texture2D texture = new(1,1); - texture.LoadImage(buffer); - texture.Compress(true); - - StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture); - - if (Debug_NetworkInbound) StickerMod.Logger.Msg($"[Inbound] All chunks received and texture reassembled from {sender}. " + - $"Texture size: {metadata.Width}x{metadata.Height}"); - } - - private static void OnEndTextureReceived(string sender) - { - if (!_textureChunkBuffers.ContainsKey(sender)) - return; - - if (Debug_NetworkInbound) StickerMod.Logger.Error($"[Inbound] Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received."); - - _textureChunkBuffers.Remove(sender); - _receivedChunkCounts.Remove(sender); - _expectedChunkCounts.Remove(sender); - _textureMetadata.Remove(sender); - } - - private static void OnTextureRequestReceived(string sender) - { - if (!_isSubscribedToModNetwork || IsSendingTexture) - return; - - if (_ourTextureData != null && _ourTextureMetadata.hashGuid != Guid.Empty) - SendTexture(); - else - StickerMod.Logger.Warning($"[Inbound] Received texture request from {sender}, but no texture is set!"); - } - - #endregion Private Methods - } -} \ No newline at end of file diff --git a/Stickers/Stickers/StickerData.cs b/Stickers/Stickers/StickerData.cs index d1a0448..670c7d8 100644 --- a/Stickers/Stickers/StickerData.cs +++ b/Stickers/Stickers/StickerData.cs @@ -2,131 +2,213 @@ using UnityEngine; using Object = UnityEngine.Object; -namespace NAK.Stickers; - -public class StickerData +namespace NAK.Stickers { - private const float DECAL_SIZE = 0.25f; - - public float DeathTime; // when a remote player leaves, we need to kill their stickers - - public Guid TextureHash; - public float LastPlacedTime; - - private Vector3 _lastPlacedPosition = Vector3.zero; - - public readonly bool IsLocal; - private readonly DecalType _decal; - private readonly DecalSpawner _spawner; - private readonly AudioSource _audioSource; - - public StickerData(bool isLocal) + public class StickerData { - IsLocal = isLocal; + private const float DECAL_SIZE = 0.25f; + + private static readonly int s_EmissionStrengthID = Shader.PropertyToID("_EmissionStrength"); + + public float DeathTime; // when a remote player leaves, we need to kill their stickers + public float LastPlacedTime; + private Vector3 _lastPlacedPosition = Vector3.zero; + + public readonly bool IsLocal; + private readonly DecalType _decal; + private readonly DecalSpawner[] _decalSpawners; - _decal = ScriptableObject.CreateInstance(); - _decal.decalSettings = new DecalSpawner.InitData + private readonly Guid[] _textureHashes; + private readonly Material[] _materials; + private readonly AudioSource _audioSource; + + public StickerData(bool isLocal, int decalSpawnersCount) { - material = new Material(StickerMod.DecalSimpleShader) + IsLocal = isLocal; + + _decal = ScriptableObject.CreateInstance(); + _decalSpawners = new DecalSpawner[decalSpawnersCount]; + _materials = new Material[decalSpawnersCount]; + _textureHashes = new Guid[decalSpawnersCount]; + + for (int i = 0; i < decalSpawnersCount; i++) { - //color = new Color(Random.value, Random.value, Random.value, 1f), - }, - useShaderReplacement = false, - inheritMaterialProperties = false, - inheritMaterialPropertyBlock = false, - }; - - _spawner = DecalManager.GetSpawner(_decal.decalSettings, 4096, 1024); - - _audioSource = new GameObject("StickerAudioSource").AddComponent(); - _audioSource.spatialBlend = 1f; - _audioSource.volume = 0.5f; - _audioSource.playOnAwake = false; - _audioSource.loop = false; - _audioSource.rolloffMode = AudioRolloffMode.Logarithmic; - _audioSource.maxDistance = 5f; - _audioSource.minDistance = 1f; - _audioSource.outputAudioMixerGroup = RootLogic.Instance.propSfx; // props are close enough to stickers - - if (isLocal) Object.DontDestroyOnLoad(_audioSource.gameObject); // keep audio source through world transitions - } - - public void SetTexture(Guid textureHash, Texture2D texture) - { - if (texture == null) StickerMod.Logger.Warning("Assigning null texture to StickerData!"); - - TextureHash = textureHash; - - texture.wrapMode = TextureWrapMode.Clamp; // noachi said to do, prevents white edges - texture.filterMode = texture.width > 64 || texture.height > 64 - ? FilterMode.Bilinear // smear it cause its fat - : FilterMode.Point; // my minecraft skin looked shit + _materials[i] = new Material(StickerMod.DecalSimpleShader); + _decal.decalSettings = new DecalSpawner.InitData + { + material = _materials[i], + useShaderReplacement = false, + inheritMaterialProperties = false, + inheritMaterialPropertyBlock = false, + }; + _decalSpawners[i] = DecalManager.GetSpawner(_decal.decalSettings, 4096, 1024); + } - if (IsLocal) StickerMod.Logger.Msg($"Set texture filter mode to: {texture.filterMode}"); - - Material material = _decal.decalSettings.material; - - // TODO: fix - if (material.mainTexture != null) Object.Destroy(material.mainTexture); - material.mainTexture = texture; - } - - public void Place(RaycastHit hit, Vector3 forwardDirection, Vector3 upDirection) - { - Transform rootObject = null; - if (hit.rigidbody != null) rootObject = hit.rigidbody.transform; - - _lastPlacedPosition = hit.point; - LastPlacedTime = Time.time; - - // todo: add decal to queue - _spawner.AddDecal( - _lastPlacedPosition, Quaternion.LookRotation(forwardDirection, upDirection), - hit.collider.gameObject, - DECAL_SIZE, DECAL_SIZE, 1f, 1f, 0f, rootObject); - } - - public void Clear() - { - // BUG?: Release does not clear dictionary's, clearing them myself so we can hit same objects again - // maybe it was intended to recycle groups- but rn no work like that - _spawner.Release(); - _spawner.staticGroups.Clear(); - _spawner.movableGroups.Clear(); - } - - public void Cleanup() - { - Clear(); - - // no leaking textures or mats - Material material = _decal.decalSettings.material; - Object.Destroy(material.mainTexture); - Object.Destroy(material); - Object.Destroy(_decal); - } - - public void PlayAudio() - { - _audioSource.transform.position = _lastPlacedPosition; - switch (ModSettings.Entry_SelectedSFX.Value) - { - case ModSettings.SFXType.Source: - _audioSource.PlayOneShot(StickerMod.SourceSFXPlayerSprayer); - break; - case ModSettings.SFXType.LBP: - _audioSource.PlayOneShot(StickerMod.LittleBigPlanetStickerPlace); - break; - case ModSettings.SFXType.None: - break; - default: - throw new ArgumentOutOfRangeException(); + _audioSource = new GameObject("StickerAudioSource").AddComponent(); + _audioSource.spatialBlend = 1f; + _audioSource.volume = 0.5f; + _audioSource.playOnAwake = false; + _audioSource.loop = false; + _audioSource.rolloffMode = AudioRolloffMode.Logarithmic; + _audioSource.maxDistance = 5f; + _audioSource.minDistance = 1f; + _audioSource.outputAudioMixerGroup = RootLogic.Instance.propSfx; // props are close enough to stickers + if (isLocal) Object.DontDestroyOnLoad(_audioSource.gameObject); // keep audio source through world transitions } - } - - public void SetAlpha(float alpha) - { - Material material = _decal.decalSettings.material; - material.color = new Color(material.color.r, material.color.g, material.color.b, alpha); + + public Guid GetTextureHash(int spawnerIndex = 0) + { + if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length) + { + StickerMod.Logger.Warning("Invalid spawner index!"); + return Guid.Empty; + } + + return _textureHashes[spawnerIndex]; + } + + public bool CheckHasTextureHash(Guid textureHash) + { + foreach (Guid hash in _textureHashes) if (hash == textureHash) return true; + return false; + } + + public void SetTexture(Guid textureHash, Texture2D texture, int spawnerIndex = 0) + { + if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length) + { + StickerMod.Logger.Warning("Invalid spawner index!"); + return; + } + + if (texture == null) + { + StickerMod.Logger.Warning("Assigning null texture to StickerData!"); + return; + } + + _textureHashes[spawnerIndex] = textureHash; + + texture.wrapMode = TextureWrapMode.Clamp; // prevents white edges + texture.filterMode = texture.width > 64 || texture.height > 64 + ? FilterMode.Bilinear // smear it cause its fat + : FilterMode.Point; // my minecraft skin looked shit + + if (IsLocal) StickerMod.Logger.Msg($"Set texture filter mode to: {texture.filterMode}"); + + Material material = _materials[spawnerIndex]; + + // Destroy the previous texture to avoid memory leaks + if (material.mainTexture != null) Object.Destroy(material.mainTexture); + material.mainTexture = texture; + } + + public void Place(RaycastHit hit, Vector3 forwardDirection, Vector3 upDirection, int spawnerIndex = 0) + { + if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length) + { + StickerMod.Logger.Warning("Invalid spawner index!"); + return; + } + + Transform rootObject = null; + if (hit.rigidbody != null) rootObject = hit.rigidbody.transform; + + _lastPlacedPosition = hit.point; + LastPlacedTime = Time.time; + + // Add decal to the specified spawner + _decalSpawners[spawnerIndex].AddDecal( + _lastPlacedPosition, Quaternion.LookRotation(forwardDirection, upDirection), + hit.collider.gameObject, + DECAL_SIZE, DECAL_SIZE, 1f, 1f, 0f, rootObject); + } + + public void Clear() + { + foreach (DecalSpawner spawner in _decalSpawners) + { + spawner.Release(); + spawner.staticGroups.Clear(); + spawner.movableGroups.Clear(); + } + } + + public void Clear(int spawnerIndex) + { + if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length) + { + StickerMod.Logger.Warning("Invalid spawner index!"); + return; + } + + _decalSpawners[spawnerIndex].Release(); + _decalSpawners[spawnerIndex].staticGroups.Clear(); + _decalSpawners[spawnerIndex].movableGroups.Clear(); + } + + public void Cleanup() + { + for (int i = 0; i < _decalSpawners.Length; i++) + { + _decalSpawners[i].Release(); + _decalSpawners[i].staticGroups.Clear(); + _decalSpawners[i].movableGroups.Clear(); + + // Clean up textures and materials + if (_materials[i].mainTexture != null) Object.Destroy(_materials[i].mainTexture); + Object.Destroy(_materials[i]); + } + + Object.Destroy(_decal); + } + + public void PlayAudio() + { + _audioSource.transform.position = _lastPlacedPosition; + switch (ModSettings.Entry_SelectedSFX.Value) + { + case SFXType.SourceEngineSpray: + _audioSource.PlayOneShot(StickerMod.SourceSFXPlayerSprayer); + break; + case SFXType.LittleBigPlanetSticker: + _audioSource.PlayOneShot(StickerMod.LittleBigPlanetSFXStickerPlace); + break; + case SFXType.FactorioAlertDestroyed: + _audioSource.PlayOneShot(StickerMod.FactorioSFXAlertDestroyed); + break; + case SFXType.None: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void SetAlpha(float alpha) + { + foreach (Material material in _materials) + { + Color color = material.color; + color.a = alpha; + material.color = color; + } + } + + #region shitty identify + + public void Identify() + { + Color color = Color.HSVToRGB(Time.time % 1f, 1f, 1f); + foreach (Material material in _materials) + material.color = color; // cycle rainbow + } + + public void ResetIdentify() + { + foreach (Material material in _materials) + material.color = Color.white; + } + + #endregion shitty identify } } \ No newline at end of file diff --git a/Stickers/Stickers/StickerSystem.ImageLoading.cs b/Stickers/Stickers/StickerSystem.ImageLoading.cs new file mode 100644 index 0000000..4f5fce4 --- /dev/null +++ b/Stickers/Stickers/StickerSystem.ImageLoading.cs @@ -0,0 +1,152 @@ +using System.Diagnostics; +using MTJobSystem; +using NAK.Stickers.Networking; +using NAK.Stickers.Utilities; +using UnityEngine; + +namespace NAK.Stickers; + +public partial class StickerSystem +{ + #region Actions + + public static event Action OnStickerLoaded; + public static event Action OnStickerLoadFailed; + + private static void InvokeOnImageLoaded(int slotIndex, string imageName) + => MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoaded", + () => OnStickerLoaded?.Invoke(slotIndex, imageName)); + + private static void InvokeOnImageLoadFailed(int slotIndex, string errorMessage) + => MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoadFailed", + () => OnStickerLoadFailed?.Invoke(slotIndex, errorMessage)); + + #endregion Actions + + #region Image Loading + + private static readonly string s_StickersFolderPath = Path.GetFullPath(Application.dataPath + "/../UserData/Stickers/"); + private readonly bool[] _isLoadingImage = new bool[ModSettings.MaxStickerSlots]; + + private void LoadAllImagesAtStartup() + { + string[] selectedStickers = ModSettings.Hidden_SelectedStickerNames.Value; + for (int i = 0; i < ModSettings.MaxStickerSlots; i++) + { + if (i >= selectedStickers.Length || string.IsNullOrEmpty(selectedStickers[i])) continue; + LoadImage(selectedStickers[i], i); + } + } + + public void LoadImage(string imageName, int slotIndex) + { + if (string.IsNullOrEmpty(imageName) || slotIndex < 0 || slotIndex >= _isLoadingImage.Length) + return; + + if (_isLoadingImage[slotIndex]) return; + _isLoadingImage[slotIndex] = true; + + Task.Run(() => + { + try + { + if (!TryLoadImage(imageName, slotIndex, out string errorMessage)) + throw new Exception(errorMessage); + } + catch (Exception ex) + { + //StickerMod.Logger.Error($"Failed to load sticker for slot {slotIndex}: {ex.Message}"); + InvokeOnImageLoadFailed(slotIndex, ex.Message); + } + finally + { + _isLoadingImage[slotIndex] = false; + } + }); + } + + private bool TryLoadImage(string imageName, int slotIndex, out string errorMessage) + { + errorMessage = string.Empty; + + if (ModNetwork.IsSendingTexture) + { + //StickerMod.Logger.Warning("A texture is currently being sent over the network. Cannot load a new image yet."); + errorMessage = "A texture is currently being sent over the network. Cannot load a new image yet."; + return false; + } + + if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath); + + string imagePath = Path.Combine(s_StickersFolderPath, imageName); + FileInfo fileInfo = new(imagePath); + if (!fileInfo.Exists) + { + //StickerMod.Logger.Warning($"Target image does not exist on disk. Path: {imagePath}"); + errorMessage = "Target image does not exist on disk."; + return false; + } + + var bytes = File.ReadAllBytes(imagePath); + + if (!ImageUtility.IsValidImage(bytes)) + { + //StickerMod.Logger.Error("File is not a valid image or is corrupt."); + errorMessage = "File is not a valid image or is corrupt."; + return false; + } + + //StickerMod.Logger.Msg("Loaded image from disk. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); + + if (bytes.Length > ModNetwork.MaxTextureSize) + { + ImageUtility.Resize(ref bytes, 256, 256); + //StickerMod.Logger.Warning("File ate too many cheeseburgers. Attempting experimental resize. Notice: this may cause filesize to increase."); + //StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); + } + + if (ImageUtility.ResizeToNearestPowerOfTwo(ref bytes)) + { + //StickerMod.Logger.Warning("Image resolution was not a power of two. Attempting experimental resize. Notice: this may cause filesize to increase."); + //StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); + } + + if (bytes.Length > ModNetwork.MaxTextureSize) + { + //StickerMod.Logger.Error("File is still too large. Aborting. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); + //StickerMod.Logger.Msg("Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two."); + errorMessage = "File is still too large. Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two."; + return false; + } + + //StickerMod.Logger.Msg("Image successfully loaded."); + + MTJobManager.RunOnMainThread("StickersSystem.LoadImage", () => + { + ModSettings.Hidden_SelectedStickerNames.Value[slotIndex] = imageName; + SetTextureSelf(bytes, slotIndex); + + InvokeOnImageLoaded(slotIndex, imageName); + + if (!IsInStickerMode) return; + IsInStickerMode = false; + IsInStickerMode = true; + }); + + return true; + } + + public static void OpenStickersFolder() + { + if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath); + Process.Start(s_StickersFolderPath); + } + + public static string GetStickersFolderPath() + { + if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath); + return s_StickersFolderPath; + } + + #endregion Image Loading +} \ No newline at end of file diff --git a/Stickers/Stickers/StickerSystem.Main.cs b/Stickers/Stickers/StickerSystem.Main.cs new file mode 100644 index 0000000..e609fed --- /dev/null +++ b/Stickers/Stickers/StickerSystem.Main.cs @@ -0,0 +1,71 @@ +using ABI_RC.Core.UI; +using ABI_RC.Systems.GameEventSystem; +using NAK.Stickers.Utilities; +using UnityEngine; + +namespace NAK.Stickers; + +public partial class StickerSystem +{ + #region Singleton + + public static StickerSystem Instance { get; private set; } + + public static void Initialize() + { + if (Instance != null) + return; + + Instance = new StickerSystem(); + + // configure decalery + DecalManager.SetPreferredMode(DecalUtils.Mode.GPU, false, 0); + + // ensure cache folder exists + if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath); + + // listen for game events + CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart); + } + + #endregion Singleton + + #region Data + + private int _selectedStickerSlot; + public int SelectedStickerSlot + { + get => _selectedStickerSlot; + set + { + _selectedStickerSlot = Mathf.Clamp(value, 0, ModSettings.MaxStickerSlots - 1); + IsInStickerMode = IsInStickerMode; // refresh sticker mode + } + } + + private bool _isInStickerMode; + public bool IsInStickerMode + { + get => _isInStickerMode; + set + { + _isInStickerMode = value; + if (_isInStickerMode) CohtmlHud.Instance.SelectPropToSpawn( + StickerCache.GetCohtmlResourcesPath(SelectedStickerName), + Path.GetFileNameWithoutExtension(SelectedStickerName), + "Sticker selected for stickering:"); + else CohtmlHud.Instance.ClearPropToSpawn(); + } + } + + private string SelectedStickerName => ModSettings.Hidden_SelectedStickerNames.Value[_selectedStickerSlot]; + + private const float StickerKillTime = 30f; + private const float StickerCooldown = 0.2f; + private readonly Dictionary _playerStickers = new(); + private const string PlayerLocalId = "_PLAYERLOCAL"; + + private readonly List _deadStickerPool = new(); // for cleanup on player leave + + #endregion Data +} \ No newline at end of file diff --git a/Stickers/Stickers/StickerSystem.PlayerCallbacks.cs b/Stickers/Stickers/StickerSystem.PlayerCallbacks.cs new file mode 100644 index 0000000..f361e0a --- /dev/null +++ b/Stickers/Stickers/StickerSystem.PlayerCallbacks.cs @@ -0,0 +1,98 @@ +using ABI_RC.Core.IO; +using ABI_RC.Core.Networking.IO.Instancing; +using ABI_RC.Core.Player; +using ABI_RC.Systems.GameEventSystem; +using UnityEngine; + +namespace NAK.Stickers; + +public partial class StickerSystem +{ + #region Player Callbacks + + private void OnPlayerSetupStart() + { + CVRGameEventSystem.World.OnUnload.AddListener(_ => Instance.CleanupAllButSelf()); + CVRGameEventSystem.Instance.OnConnected.AddListener((_) => { if (!Instances.IsReconnecting) Instance.ClearStickersSelf(); }); + + CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined); + CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft); + SchedulerSystem.AddJob(Instance.OnOccasionalUpdate, 10f, 1f); + LoadAllImagesAtStartup(); + } + + private void OnPlayerJoined(CVRPlayerEntity playerEntity) + { + if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData)) + return; + + stickerData.DeathTime = -1f; + stickerData.SetAlpha(1f); + _deadStickerPool.Remove(stickerData); + } + + private void OnPlayerLeft(CVRPlayerEntity playerEntity) + { + if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData)) + return; + + stickerData.DeathTime = Time.time + StickerKillTime; + stickerData.SetAlpha(1f); + _deadStickerPool.Add(stickerData); + } + + private void OnOccasionalUpdate() + { + if (_deadStickerPool.Count == 0) + return; + + for (var i = _deadStickerPool.Count - 1; i >= 0; i--) + { + float currentTime = Time.time; + StickerData stickerData = _deadStickerPool[i]; + if (stickerData == null) + { + _deadStickerPool.RemoveAt(i); + continue; + } + + if (stickerData.DeathTime < 0f) + continue; + + if (currentTime < stickerData.DeathTime) + { + stickerData.SetAlpha(Mathf.Lerp(0f, 1f, (stickerData.DeathTime - currentTime) / StickerKillTime)); + continue; + } + + _playerStickers.Remove(_playerStickers.First(x => x.Value == stickerData).Key); + _deadStickerPool.RemoveAt(i); + stickerData.Cleanup(); + } + } + + #endregion Player Callbacks + + #region Player Callbacks + + public void OnStickerPlaceReceived(string playerId, int stickerSlot, Vector3 position, Vector3 forward, Vector3 up) + => AttemptPlaceSticker(playerId, position, forward, up, alignWithNormal: true, stickerSlot); + + public void OnStickerClearReceived(string playerId, int stickerSlot) + => ClearStickersForPlayer(playerId, stickerSlot); + + public void OnStickerClearAllReceived(string playerId) + => ClearStickersForPlayer(playerId); + + public void OnStickerIdentifyReceived(string playerId) + { + if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) + return; + + // todo: make prettier (idk shaders) + SchedulerSystem.AddJob(() => stickerData.Identify(), 0f, 0.1f, 30); + SchedulerSystem.AddJob(() => stickerData.ResetIdentify(), 4f, 1f, 1); + } + + #endregion Player Callbacks +} diff --git a/Stickers/Stickers/StickerSystem.StickerLifecycle.cs b/Stickers/Stickers/StickerSystem.StickerLifecycle.cs new file mode 100644 index 0000000..a24ebd8 --- /dev/null +++ b/Stickers/Stickers/StickerSystem.StickerLifecycle.cs @@ -0,0 +1,160 @@ +using ABI_RC.Core; +using ABI_RC.Core.Player; +using ABI_RC.Systems.InputManagement; +using NAK.Stickers.Networking; +using UnityEngine; + +namespace NAK.Stickers; + +public partial class StickerSystem +{ + #region Sticker Lifecycle + + private StickerData GetOrCreateStickerData(string playerId) + { + if (_playerStickers.TryGetValue(playerId, out StickerData stickerData)) + return stickerData; + + stickerData = new StickerData(playerId == PlayerLocalId, ModSettings.MaxStickerSlots); + _playerStickers[playerId] = stickerData; + return stickerData; + } + + public void PlaceStickerFromControllerRay(Transform transform, CVRHand hand = CVRHand.Left) + { + Vector3 controllerForward = transform.forward; + Vector3 controllerUp = transform.up; + Vector3 playerUp = PlayerSetup.Instance.transform.up; + + // extracting angle of controller ray on forward axis + Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized; + Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized; + float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp); + + float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value; + Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold) + // leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop + ? Vector3.Slerp(controllerUp, playerUp, 0.99f) + : controllerUp; + + if (!PlaceStickerSelf(transform.position, transform.forward, targetUp)) + return; + + // do haptic if not lame + if (!ModSettings.Entry_HapticsOnPlace.Value) return; + CVRInputManager.Instance.Vibrate(0f, 0.1f, 10f, 0.1f, hand); + } + + private bool PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true) + { + if (!AttemptPlaceSticker(PlayerLocalId, position, forward, up, alignWithNormal, SelectedStickerSlot)) + return false; // failed + + // placed, now network + ModNetwork.SendPlaceSticker(SelectedStickerSlot, position, forward, up); + return true; + } + + private bool AttemptPlaceSticker(string playerId, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true, int stickerSlot = 0) + { + StickerData stickerData = GetOrCreateStickerData(playerId); + if (Time.time - stickerData.LastPlacedTime < StickerCooldown) + return false; + + // Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal + const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15)); + if (!Physics.Raycast(position, forward, out RaycastHit hit, + 10f, LayerMask, QueryTriggerInteraction.Ignore)) + return false; + + stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot); + stickerData.PlayAudio(); + return true; + } + + public void ClearStickersSelf() + { + ClearStickersForPlayer(PlayerLocalId); + ModNetwork.SendClearAllStickers(); + } + + private void ClearStickersForPlayer(string playerId) + { + if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) + return; + + stickerData.Clear(); + } + + private void ClearStickersForPlayer(string playerId, int stickerSlot) + { + if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) + return; + + stickerData.Clear(stickerSlot); + } + + private void SetTextureSelf(byte[] imageBytes, int stickerSlot = 0) + { + Texture2D texture = new(1, 1); // placeholder + texture.LoadImage(imageBytes); + texture.Compress(true); // noachi said to do + + OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture, stickerSlot); + ModNetwork.SetTexture(stickerSlot, imageBytes); + } + + public void ClearAllStickers() + { + foreach (StickerData stickerData in _playerStickers.Values) + stickerData.Clear(); + + ModNetwork.SendClearAllStickers(); + } + + public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture, int stickerSlot = 0) + { + StickerData stickerData = GetOrCreateStickerData(playerId); + stickerData.SetTexture(textureHash, texture, stickerSlot); + } + + public bool HasTextureHash(string playerId, Guid textureHash) + { + StickerData stickerData = GetOrCreateStickerData(playerId); + return stickerData.CheckHasTextureHash(textureHash); + } + + public void CleanupAll() + { + foreach ((_, StickerData data) in _playerStickers) + data.Cleanup(); + + _playerStickers.Clear(); + } + + public void CleanupAllButSelf() + { + StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId); + + foreach ((_, StickerData data) in _playerStickers) + { + if (data.IsLocal) data.Clear(); + else data.Cleanup(); + } + + _playerStickers.Clear(); + _playerStickers[PlayerLocalId] = localStickerData; + } + + public void SelectStickerSlot(int stickerSlot) + { + SelectedStickerSlot = Mathf.Clamp(stickerSlot, 0, ModSettings.MaxStickerSlots - 1); + } + + public int GetCurrentStickerSlot() + { + return SelectedStickerSlot; + } + + #endregion Sticker Lifecycle +} diff --git a/Stickers/Stickers/StickerSystem.cs b/Stickers/Stickers/StickerSystem.cs deleted file mode 100644 index e0176ed..0000000 --- a/Stickers/Stickers/StickerSystem.cs +++ /dev/null @@ -1,406 +0,0 @@ -using System.Diagnostics; -using ABI_RC.Core.IO; -using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; -using ABI_RC.Core.UI; -using ABI_RC.Systems.GameEventSystem; -using MTJobSystem; -using NAK.Stickers.Integrations; -using NAK.Stickers.Networking; -using NAK.Stickers.Utilities; -using UnityEngine; - -namespace NAK.Stickers; - -public class StickerSystem -{ - #region Singleton - - public static StickerSystem Instance { get; private set; } - - public static void Initialize() - { - if (Instance != null) - return; - - Instance = new StickerSystem(); - - // ensure cache folder exists - if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath); - - // configure Decalery - ModSettings.DecaleryMode selectedMode = ModSettings.Decalery_DecalMode.Value; - DecalManager.SetPreferredMode((DecalUtils.Mode)selectedMode, selectedMode == ModSettings.DecaleryMode.GPUIndirect, 0); - if (selectedMode != ModSettings.DecaleryMode.GPU) StickerMod.Logger.Warning("Decalery is not set to GPU mode. Expect compatibility issues with user generated content when mesh data is not marked as readable."); - - // listen for game events - CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart); - } - - #endregion Singleton - - #region Actions - - public static Action OnImageLoadFailed; - - private void InvokeOnImageLoadFailed(string errorMessage) - { - MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoadFailed", () => OnImageLoadFailed?.Invoke(errorMessage)); - } - - #endregion Actions - - #region Data - - private bool _isInStickerMode; - public bool IsInStickerMode - { - get => _isInStickerMode; - set - { - _isInStickerMode = value; - if (_isInStickerMode) CohtmlHud.Instance.SelectPropToSpawn(BtkUiAddon.GetBtkUiCachePath(ModSettings.Hidden_SelectedStickerName.Value), ModSettings.Hidden_SelectedStickerName.Value, "Sticker selected for stickering:"); - else CohtmlHud.Instance.ClearPropToSpawn(); - } - } - - private const float StickerKillTime = 30f; - private const float StickerCooldown = 0.2f; - private readonly Dictionary _playerStickers = new(); - private const string PlayerLocalId = "_PLAYERLOCAL"; - - private readonly List _deadStickerPool = new(); // for cleanup on player leave - - #endregion Data - - #region Game Events - - private void OnPlayerSetupStart() - { - CVRGameEventSystem.World.OnUnload.AddListener(_ => Instance.CleanupAllButSelf()); - CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined); - CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft); - SchedulerSystem.AddJob(Instance.OnOccasionalUpdate, 10f, 1f); - - LoadImage(ModSettings.Hidden_SelectedStickerName.Value); - } - - private void OnPlayerJoined(CVRPlayerEntity playerEntity) - { - if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData)) - return; - - stickerData.DeathTime = -1f; - stickerData.SetAlpha(1f); - _deadStickerPool.Remove(stickerData); - } - - private void OnPlayerLeft(CVRPlayerEntity playerEntity) - { - if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData)) - return; - - stickerData.DeathTime = Time.time + StickerKillTime; - stickerData.SetAlpha(1f); - _deadStickerPool.Add(stickerData); - } - - private void OnOccasionalUpdate() - { - if (_deadStickerPool.Count == 0) - return; - - for (var i = _deadStickerPool.Count - 1; i >= 0; i--) - { - float currentTime = Time.time; - StickerData stickerData = _deadStickerPool[i]; - if (stickerData.DeathTime < 0f) - continue; - - if (currentTime < stickerData.DeathTime) - { - stickerData.SetAlpha(Mathf.Lerp(0f, 1f, (stickerData.DeathTime - currentTime) / StickerKillTime)); - continue; - } - - _playerStickers.Remove(_playerStickers.First(x => x.Value == stickerData).Key); - _deadStickerPool.RemoveAt(i); - stickerData.Cleanup(); - } - } - - #endregion Game Events - - #region Local Player - - public void PlaceStickerFromTransform(Transform transform) - { - Vector3 controllerForward = transform.forward; - Vector3 controllerUp = transform.up; - Vector3 playerUp = PlayerSetup.Instance.transform.up; - - // extracting angle of controller ray on forward axis - Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized; - Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized; - float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp); - - float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value; - Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold) - // leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop - ? Vector3.Slerp(controllerUp, playerUp, 0.99f) - : controllerUp; - - PlaceStickerSelf(transform.position, transform.forward, targetUp); - } - - /// Place own sticker. Network if successful. - private void PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true) - { - if (!AttemptPlaceSticker(PlayerLocalId, position, forward, up, alignWithNormal)) - return; // failed - - // placed, now network - ModNetwork.PlaceSticker(position, forward, up); - } - - /// Clear own stickers. Network if successful. - public void ClearStickersSelf() - { - OnPlayerStickersClear(PlayerLocalId); - ModNetwork.ClearStickers(); - } - - /// Set own Texture2D for sticker. Network if cusses. - private void SetTextureSelf(byte[] imageBytes) - { - Texture2D texture = new(1, 1); // placeholder - texture.LoadImage(imageBytes); - texture.Compress(true); // noachi said to do - - OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture); - if (ModNetwork.SetTexture(imageBytes)) ModNetwork.SendTexture(); - } - - #endregion Local Player - - #region Public Methods - - /// When a player wants to place a sticker. - public void OnPlayerStickerPlace(string playerId, Vector3 position, Vector3 forward, Vector3 up) - => AttemptPlaceSticker(playerId, position, forward, up); - - /// When a player wants to clear their stickers. - public void OnPlayerStickersClear(string playerId) - => ClearStickersForPlayer(playerId); - - /// Clear all stickers from all players. - public void ClearAllStickers() - { - foreach (StickerData stickerData in _playerStickers.Values) - stickerData.Clear(); - - ModNetwork.ClearStickers(); - } - - public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture) - { - StickerData stickerData = GetOrCreateStickerData(playerId); - stickerData.SetTexture(textureHash, texture); - } - - public Guid GetPlayerStickerTextureHash(string playerId) - { - StickerData stickerData = GetOrCreateStickerData(playerId); - return stickerData.TextureHash; - } - - public void CleanupAll() - { - foreach ((_, StickerData data) in _playerStickers) - data.Cleanup(); - - _playerStickers.Clear(); - } - - public void CleanupAllButSelf() - { - StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId); - - foreach ((_, StickerData data) in _playerStickers) - { - if (data.IsLocal) data.Clear(); - else data.Cleanup(); - } - - _playerStickers.Clear(); - _playerStickers[PlayerLocalId] = localStickerData; - } - - - #endregion Public Methods - - #region Private Methods - - private StickerData GetOrCreateStickerData(string playerId) - { - if (_playerStickers.TryGetValue(playerId, out StickerData stickerData)) - return stickerData; - - stickerData = new StickerData(playerId == PlayerLocalId); - _playerStickers[playerId] = stickerData; - return stickerData; - } - - private bool AttemptPlaceSticker(string playerId, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true) - { - StickerData stickerData = GetOrCreateStickerData(playerId); - if (Time.time - stickerData.LastPlacedTime < StickerCooldown) - return false; - - // Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal - const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15)); - if (!Physics.Raycast(position, forward, out RaycastHit hit, - 10f, LayerMask, QueryTriggerInteraction.Ignore)) - return false; - - stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up); - stickerData.PlayAudio(); - return true; - } - - private void ClearStickersForPlayer(string playerId) - { - if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) - return; - - stickerData.Clear(); - } - - private void ReleaseStickerData(string playerId) - { - if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) - return; - - stickerData.Cleanup(); - _playerStickers.Remove(playerId); - } - - #endregion Private Methods - - #region Image Loading - - private static readonly string s_StickersSourcePath = Application.dataPath + "/../UserData/Stickers/"; - - private bool _isLoadingImage; - - public void LoadImage(string imageName) - { - if (string.IsNullOrEmpty(imageName)) - return; - - if (_isLoadingImage) return; - _isLoadingImage = true; - - Task.Run(() => - { - try - { - if (!TryLoadImage(imageName, out string errorMessage)) - { - InvokeOnImageLoadFailed(errorMessage); - } - } - catch (Exception ex) - { - InvokeOnImageLoadFailed(ex.Message); - } - finally - { - _isLoadingImage = false; - } - }); - } - - private bool TryLoadImage(string imageName, out string errorMessage) - { - errorMessage = string.Empty; - - if (ModNetwork.IsSendingTexture) - { - StickerMod.Logger.Warning("A texture is currently being sent over the network. Cannot load a new image yet."); - errorMessage = "A texture is currently being sent over the network. Cannot load a new image yet."; - return false; - } - - if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath); - - string imagePath = Path.Combine(s_StickersSourcePath, imageName + ".png"); - FileInfo fileInfo = new(imagePath); - if (!fileInfo.Exists) - { - StickerMod.Logger.Warning($"Target image does not exist on disk. Path: {imagePath}"); - errorMessage = "Target image does not exist on disk."; - return false; - } - - var bytes = File.ReadAllBytes(imagePath); - - if (!ImageUtility.IsValidImage(bytes)) - { - StickerMod.Logger.Error("File is not a valid image or is corrupt."); - errorMessage = "File is not a valid image or is corrupt."; - return false; - } - - StickerMod.Logger.Msg("Loaded image from disk. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); - - if (bytes.Length > ModNetwork.MaxTextureSize) - { - ImageUtility.Resize(ref bytes, 256, 256); - StickerMod.Logger.Warning("File ate too many cheeseburgers. Attempting experimental resize. Notice: this may cause filesize to increase."); - StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); - } - - if (ImageUtility.ResizeToNearestPowerOfTwo(ref bytes)) - { - StickerMod.Logger.Warning("Image resolution was not a power of two. Attempting experimental resize. Notice: this may cause filesize to increase."); - StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); - } - - if (bytes.Length > ModNetwork.MaxTextureSize) - { - StickerMod.Logger.Error("File is still too large. Aborting. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)"); - StickerMod.Logger.Msg("Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two."); - errorMessage = "File is still too large. Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two."; - return false; - } - - StickerMod.Logger.Msg("Image successfully loaded."); - - MTJobManager.RunOnMainThread("StickersSystem.LoadImage", () => - { - ModSettings.Hidden_SelectedStickerName.Value = imageName; - SetTextureSelf(bytes); - - if (!IsInStickerMode) return; - IsInStickerMode = false; - IsInStickerMode = true; - }); - - return true; - } - - public static void OpenStickersFolder() - { - if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath); - Process.Start(s_StickersSourcePath); - } - - public static string GetStickersFolderPath() - { - if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath); - return s_StickersSourcePath; - } - - #endregion Image Loading -} \ No newline at end of file diff --git a/Stickers/Stickers/Utilities/StickerCache.cs b/Stickers/Stickers/Utilities/StickerCache.cs new file mode 100644 index 0000000..d01b59e --- /dev/null +++ b/Stickers/Stickers/Utilities/StickerCache.cs @@ -0,0 +1,185 @@ +using BTKUILib.UIObjects.Components; +using MTJobSystem; +using NAK.Stickers.Integrations; +using System.Collections.Concurrent; +using BTKUILib; +using UnityEngine; + +namespace NAK.Stickers.Utilities; + +public static class StickerCache +{ + #region Constants and Fields + + private static readonly string CohtmlResourcesPath = Path.Combine("coui://", "uiresources", "GameUI", "mods", "BTKUI", "images", ModSettings.ModName, "UserImages"); + private static readonly string ThumbnailPath = Path.Combine(Application.dataPath, "StreamingAssets", "Cohtml", "UIResources", "GameUI", "mods", "BTKUI", "images", ModSettings.ModName, "UserImages"); + + private static readonly ConcurrentQueue<(FileInfo, Button)> _filesToGenerateThumbnails = new(); + private static readonly HashSet _filesBeingProcessed = new(); + + private static readonly object _isGeneratingThumbnailsLock = new(); + private static bool _isGeneratingThumbnails; + private static bool IsGeneratingThumbnails { + get { lock (_isGeneratingThumbnailsLock) return _isGeneratingThumbnails; } + set { lock (_isGeneratingThumbnailsLock) { _isGeneratingThumbnails = value; } } + } + + #endregion Constants and Fields + + #region Public Methods + + public static string GetBtkUiIconName(string relativePath) + { + if (string.IsNullOrEmpty(relativePath)) return "Stickers-puzzle"; // default icon when shit fucked + + string relativePathWithoutExtension = relativePath[..^Path.GetExtension(relativePath).Length]; + return "UserImages/" + relativePathWithoutExtension.Replace('\\', '/'); + } + + public static string GetCohtmlResourcesPath(string relativePath) + { + if (string.IsNullOrEmpty(relativePath)) return string.Empty; + + string relativePathWithoutExtension = relativePath[..^Path.GetExtension(relativePath).Length]; + string cachePath = Path.Combine(CohtmlResourcesPath, relativePathWithoutExtension + ".png"); + + // Normalize path + cachePath = cachePath.Replace('\\', '/'); + return cachePath; + } + + public static bool IsThumbnailAvailable(string relativePathWithoutExtension) + { + if (string.IsNullOrEmpty(relativePathWithoutExtension)) return false; + + string thumbnailImagePath = Path.Combine(ThumbnailPath, relativePathWithoutExtension + ".png"); + return File.Exists(thumbnailImagePath); + } + + public static void EnqueueThumbnailGeneration(FileInfo fileInfo, Button button) + { + lock (_filesBeingProcessed) + { + if (!_filesBeingProcessed.Add(fileInfo.FullName)) + return; + + _filesToGenerateThumbnails.Enqueue((fileInfo, button)); + MTJobManager.RunOnMainThread("StartGeneratingThumbnailsIfNeeded", StartGeneratingThumbnailsIfNeeded); + } + } + + #endregion Public Methods + + #region Private Methods + + private static void StartGeneratingThumbnailsIfNeeded() + { + if (IsGeneratingThumbnails) return; + IsGeneratingThumbnails = true; + + StickerMod.Logger.Msg("Starting thumbnail generation task."); + + Task.Run(() => + { + try + { + int generatedThumbnails = 0; + + while (BTKUIAddon.IsPopulatingPage || _filesToGenerateThumbnails.Count > 0) + { + if (!_filesToGenerateThumbnails.TryDequeue(out (FileInfo, Button) fileInfo)) continue; + + bool success = GenerateThumbnail(fileInfo.Item1); + if (success && fileInfo.Item2.ButtonTooltip[5..] == fileInfo.Item1.Name) + { + var iconPath = GetBtkUiIconName(Path.GetRelativePath(StickerSystem.GetStickersFolderPath(), fileInfo.Item1.FullName)); + fileInfo.Item2.ButtonIcon = iconPath; + } + + lock (_filesBeingProcessed) + { + _filesBeingProcessed.Remove(fileInfo.Item1.FullName); + } + + generatedThumbnails++; + } + + StickerMod.Logger.Msg($"Finished thumbnail generation for {generatedThumbnails} files."); + } + catch (Exception e) + { + StickerMod.Logger.Error($"Failed to generate thumbnails: {e.Message}"); + } + finally + { + IsGeneratingThumbnails = false; + } + }); + } + + private static bool GenerateThumbnail(FileSystemInfo fileInfo) + { + string relativePath = Path.GetRelativePath(StickerSystem.GetStickersFolderPath(), fileInfo.FullName); + string relativePathWithoutExtension = relativePath[..^fileInfo.Extension.Length]; + string thumbnailDirectory = Path.GetDirectoryName(Path.Combine(ThumbnailPath, relativePathWithoutExtension + ".png")); + if (thumbnailDirectory == null) + return false; + + if (!Directory.Exists(thumbnailDirectory)) Directory.CreateDirectory(thumbnailDirectory); + + MemoryStream imageStream = LoadStreamFromFile(fileInfo.FullName); + if (imageStream == null) return false; + + try + { + ImageUtility.Resize(ref imageStream, 128, 128); + } + catch (Exception e) + { + StickerMod.Logger.Warning($"Failed to resize image: {e.Message}"); + imageStream.Dispose(); + return false; + } + + PrepareIconFromMemoryStream(ModSettings.ModName, relativePathWithoutExtension, imageStream); + imageStream.Dispose(); + return true; + } + + private static void PrepareIconFromMemoryStream(string modName, string iconPath, MemoryStream destination) + { + if (destination == null) + { + StickerMod.Logger.Error("Mod " + modName + " attempted to prepare " + iconPath + " but the resource stream was null! Yell at the mod author to fix this!"); + } + else + { + iconPath = UIUtils.GetCleanString(iconPath); + iconPath = Path.Combine(ThumbnailPath, iconPath); + File.WriteAllBytes(iconPath + ".png", destination.ToArray()); + } + } + + private static MemoryStream LoadStreamFromFile(string filePath) + { + if (!File.Exists(filePath)) + return null; + + try + { + using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read); + + MemoryStream memoryStream = new(); + fileStream.CopyTo(memoryStream); + memoryStream.Position = 0; // Ensure the position is reset before returning + return memoryStream; + } + catch (Exception e) + { + StickerMod.Logger.Warning($"Failed to load stream from {filePath}: {e.Message}"); + return null; + } + } + + #endregion Private Methods +} \ No newline at end of file