Stickers: added some code

This commit is contained in:
NotAKidoS 2024-08-25 06:31:47 -05:00
parent 07d6dff0a9
commit 58079d9390
36 changed files with 2092 additions and 1651 deletions

View file

@ -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<T>() 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>(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<bool> 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<float> 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<string> 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<float> 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<bool> 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<TEnum>(MelonPreferences_Entry<TEnum> entry) where TEnum : Enum
{
MultiSelection multiSelection = new(
entry.DisplayName,
EnumUtils.GetPrettyEnumNames<TEnum>(),
EnumUtils.GetEnumIndex(entry.Value)
)
{
OnOptionUpdated = i => entry.Value = (TEnum)Enum.Parse(typeof(TEnum), Enum.GetNames(typeof(TEnum))[i])
};
return multiSelection;
}
#endregion Melon Preference Extensions
}

View file

@ -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
}

View file

@ -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);
}
}
}

View file

@ -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<string, ImageInfo> _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<string>(stickerFiles);
lock (_cacheLock)
{
if (!_initialClearCacheFolder)
{
_initialClearCacheFolder = true;
DeleteOldIcons(ModSettings.ModName);
}
var keysToRemove = new List<string>();
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<string>(stickerFiles);
lock (_cacheLock)
{
if (!_initialClearCacheFolder)
{
_initialClearCacheFolder = true;
DeleteOldIcons(ModSettings.ModName);
}
var keysToRemove = new List<string>();
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
}
}

View file

@ -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<bool> 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<float> 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<string> 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<float> 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<bool> 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}");
}
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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
}

View file

@ -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
}

View file

@ -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<string> 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<DirectoryInfo> 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<FileInfo> 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
}