This commit is contained in:
NotAKidoS 2024-08-22 20:53:37 -05:00
parent 61f1a884d2
commit 475593bc1d
25 changed files with 2172 additions and 0 deletions

View file

@ -0,0 +1,80 @@
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

@ -0,0 +1,15 @@
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

@ -0,0 +1,356 @@
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

@ -0,0 +1,105 @@
using BTKUILib;
using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components;
using BTKUILib.UIObjects.Objects;
using UnityEngine;
namespace NAK.Stickers.Integrations;
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
};
private static readonly MultiSelection _desktopKeybindSelection =
new(
"Desktop Keybind",
Enum.GetNames(typeof(KeyCode)),
(int)ModSettings.Entry_PlaceBinding.Value
)
{
OnOptionUpdated = i =>
{
if (Enum.GetValues(typeof(KeyCode)) is string[] options) // inefficient but works
ModSettings.Entry_PlaceBinding.Value = (KeyCode)Enum.Parse(typeof(KeyCode), options[i]);
}
};
#region Category Setup
private static void Setup_StickersModCategory(Page page)
{
//_ourCategory = page.AddCategory(ModSettings.Stickers_SettingsCategory, ModSettings.ModName, true, true, false);
_ourCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_SettingsCategory);
Button placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon);
placeStickersButton.OnPress += OnPlaceStickersButtonClick;
Button clearSelfStickersButton = _ourCategory.AddButton("Clear Self", "Stickers-eraser", "Clear own stickers.", ButtonStyle.TextWithIcon);
clearSelfStickersButton.OnPress += OnClearSelfStickersButtonClick;
Button clearAllStickersButton = _ourCategory.AddButton("Clear All", "Stickers-rubbish-bin", "Clear all stickers.", ButtonStyle.TextWithIcon);
clearAllStickersButton.OnPress += OnClearAllStickersButtonClick;
Button openStickersFolderButton = _ourCategory.AddButton("Open Stickers Folder", "Stickers-folder", "Open UserData/Stickers folder in explorer. If above 256kb your image will automatically be downscaled for networking reasons.", ButtonStyle.TextWithIcon);
openStickersFolderButton.OnPress += OnOpenStickersFolderButtonClick;
Button openMultiSelectionButton = _ourCategory.AddButton("Sticker SFX", "Stickers-headset", "Choose the SFX used when a sticker is placed.", ButtonStyle.TextWithIcon);
openMultiSelectionButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_sfxSelection);
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);
openDesktopKeybindButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_desktopKeybindSelection);
toggleDesktopKeybindButton.OnValueUpdated += (b) =>
{
ModSettings.Entry_UsePlaceBinding.Value = b;
openDesktopKeybindButton.Disabled = !b;
};
//AddMelonToggle(ref _ourCategory, ModSettings.Entry_UsePlaceBinding);
}
#endregion Category Setup
#region Button Actions
private static void OnPlaceStickersButtonClick()
{
if (!_isOurTabOpened) return;
string mode = StickerSystem.Instance.IsInStickerMode ? "Exiting" : "Entering";
QuickMenuAPI.ShowAlertToast($"{mode} sticker placement mode...", 2);
StickerSystem.Instance.IsInStickerMode = !StickerSystem.Instance.IsInStickerMode;
}
private static void OnClearSelfStickersButtonClick()
{
if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Clearing own stickers in world...", 2);
StickerSystem.Instance.ClearStickersSelf();
}
private static void OnClearAllStickersButtonClick()
{
if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Clearing all stickers in world...", 2);
StickerSystem.Instance.ClearAllStickers();
}
private static void OnOpenStickersFolderButtonClick()
{
if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Opening Stickers folder in Explorer...", 2);
StickerSystem.OpenStickersFolder();
}
#endregion Button Actions
}

View file

@ -0,0 +1,104 @@
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}");
}
}
}