mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
Stickers: added some code
This commit is contained in:
parent
07d6dff0a9
commit
58079d9390
36 changed files with 2092 additions and 1651 deletions
103
Stickers/Integrations/BTKUI/BTKUILibExtensions.cs
Normal file
103
Stickers/Integrations/BTKUI/BTKUILibExtensions.cs
Normal 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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
13
Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs
Normal file
13
Stickers/Integrations/BTKUI/UIAddon.Category.DebugOptions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,44 +2,24 @@
|
||||||
using BTKUILib.UIObjects;
|
using BTKUILib.UIObjects;
|
||||||
using BTKUILib.UIObjects.Components;
|
using BTKUILib.UIObjects.Components;
|
||||||
using BTKUILib.UIObjects.Objects;
|
using BTKUILib.UIObjects.Objects;
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace NAK.Stickers.Integrations;
|
namespace NAK.Stickers.Integrations;
|
||||||
|
|
||||||
public static partial class BtkUiAddon
|
public static partial class BTKUIAddon
|
||||||
{
|
{
|
||||||
private static Category _ourCategory;
|
private static Category _ourCategory;
|
||||||
|
|
||||||
private static readonly MultiSelection _sfxSelection =
|
private static readonly MultiSelection _sfxSelection =
|
||||||
new(
|
BTKUILibExtensions.CreateMelonMultiSelection(ModSettings.Entry_SelectedSFX);
|
||||||
"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 =
|
private static readonly MultiSelection _desktopKeybindSelection =
|
||||||
new(
|
BTKUILibExtensions.CreateMelonMultiSelection(ModSettings.Entry_PlaceBinding);
|
||||||
"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]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#region Category Setup
|
#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 = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_SettingsCategory);
|
||||||
_ourCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_SettingsCategory);
|
|
||||||
|
|
||||||
Button placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon);
|
Button placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon);
|
||||||
placeStickersButton.OnPress += OnPlaceStickersButtonClick;
|
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);
|
Button openMultiSelectionButton = _ourCategory.AddButton("Sticker SFX", "Stickers-headset", "Choose the SFX used when a sticker is placed.", ButtonStyle.TextWithIcon);
|
||||||
openMultiSelectionButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_sfxSelection);
|
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);
|
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);
|
Button openDesktopKeybindButton = _ourCategory.AddButton("Desktop Keybind", "Stickers-alphabet", "Choose the key binding to place stickers.", ButtonStyle.TextWithIcon);
|
92
Stickers/Integrations/BTKUI/UIAddon.Main.cs
Normal file
92
Stickers/Integrations/BTKUI/UIAddon.Main.cs
Normal 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
|
||||||
|
}
|
53
Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs
Normal file
53
Stickers/Integrations/BTKUI/UIAddon.Page.PlayerOptions.cs
Normal 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
|
||||||
|
}
|
270
Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs
Normal file
270
Stickers/Integrations/BTKUI/UIAddon.Page.StickerSelect.cs
Normal 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
|
||||||
|
}
|
|
@ -22,10 +22,11 @@ public class StickerMod : MelonMod
|
||||||
|
|
||||||
ApplyPatches(typeof(Patches.PlayerSetupPatches));
|
ApplyPatches(typeof(Patches.PlayerSetupPatches));
|
||||||
ApplyPatches(typeof(Patches.ControllerRayPatches));
|
ApplyPatches(typeof(Patches.ControllerRayPatches));
|
||||||
|
ApplyPatches(typeof(Patches.ShaderFilterHelperPatches));
|
||||||
|
|
||||||
LoadAssetBundle();
|
LoadAssetBundle();
|
||||||
|
|
||||||
InitializeIntegration(nameof(BTKUILib), BtkUiAddon.Initialize); // quick menu ui
|
InitializeIntegration(nameof(BTKUILib), BTKUIAddon.Initialize); // quick menu ui
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnUpdate()
|
public override void OnUpdate()
|
||||||
|
@ -36,7 +37,7 @@ public class StickerMod : MelonMod
|
||||||
if (!Input.GetKeyDown((KeyCode)ModSettings.Entry_PlaceBinding.Value))
|
if (!Input.GetKeyDown((KeyCode)ModSettings.Entry_PlaceBinding.Value))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
StickerSystem.Instance.PlaceStickerFromTransform(PlayerSetup.Instance.activeCam.transform);
|
StickerSystem.Instance.PlaceStickerFromControllerRay(PlayerSetup.Instance.activeCam.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnApplicationQuit()
|
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 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 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 DecalSimpleShader;
|
||||||
internal static Shader DecalWriterShader;
|
internal static Shader DecalWriterShader;
|
||||||
internal static AudioClip SourceSFXPlayerSprayer;
|
internal static AudioClip SourceSFXPlayerSprayer;
|
||||||
internal static AudioClip LittleBigPlanetStickerPlace;
|
internal static AudioClip LittleBigPlanetSFXStickerPlace;
|
||||||
|
internal static AudioClip FactorioSFXAlertDestroyed;
|
||||||
|
|
||||||
private void LoadAssetBundle()
|
private void LoadAssetBundle()
|
||||||
{
|
{
|
||||||
|
@ -127,14 +130,22 @@ public class StickerMod : MelonMod
|
||||||
SourceSFXPlayerSprayer.hideFlags |= HideFlags.DontUnloadUnusedAsset;
|
SourceSFXPlayerSprayer.hideFlags |= HideFlags.DontUnloadUnusedAsset;
|
||||||
LoggerInstance.Msg($"Loaded {SourceSFX_PlayerSprayer}!");
|
LoggerInstance.Msg($"Loaded {SourceSFX_PlayerSprayer}!");
|
||||||
|
|
||||||
LittleBigPlanetStickerPlace = assetBundle.LoadAsset<AudioClip>(LittleBigPlanetSFX_StickerPlace);
|
LittleBigPlanetSFXStickerPlace = assetBundle.LoadAsset<AudioClip>(LittleBigPlanetSFX_StickerPlace);
|
||||||
if (LittleBigPlanetStickerPlace == null) {
|
if (LittleBigPlanetSFXStickerPlace == null) {
|
||||||
LoggerInstance.Error($"Failed to load {LittleBigPlanetSFX_StickerPlace}! Prefab is null!");
|
LoggerInstance.Error($"Failed to load {LittleBigPlanetSFX_StickerPlace}! Prefab is null!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LittleBigPlanetStickerPlace.hideFlags |= HideFlags.DontUnloadUnusedAsset;
|
LittleBigPlanetSFXStickerPlace.hideFlags |= HideFlags.DontUnloadUnusedAsset;
|
||||||
LoggerInstance.Msg($"Loaded {LittleBigPlanetSFX_StickerPlace}!");
|
LoggerInstance.Msg($"Loaded {LittleBigPlanetSFX_StickerPlace}!");
|
||||||
|
|
||||||
|
FactorioSFXAlertDestroyed = assetBundle.LoadAsset<AudioClip>(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
|
// load
|
||||||
|
|
||||||
LoggerInstance.Msg("Asset bundle successfully loaded!");
|
LoggerInstance.Msg("Asset bundle successfully loaded!");
|
||||||
|
|
|
@ -5,13 +5,20 @@ namespace NAK.Stickers;
|
||||||
|
|
||||||
public static class ModSettings
|
public static class ModSettings
|
||||||
{
|
{
|
||||||
|
#region Constants & Category
|
||||||
|
|
||||||
internal const string ModName = nameof(StickerMod);
|
internal const string ModName = nameof(StickerMod);
|
||||||
|
|
||||||
internal const string SM_SettingsCategory = "Stickers Mod";
|
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";
|
private const string DEBUG_SettingsCategory = "Debug Options";
|
||||||
|
|
||||||
|
internal const int MaxStickerSlots = 4;
|
||||||
|
|
||||||
private static readonly MelonPreferences_Category Category =
|
private static readonly MelonPreferences_Category Category =
|
||||||
MelonPreferences.CreateCategory(ModName);
|
MelonPreferences.CreateCategory(ModName);
|
||||||
|
|
||||||
|
#endregion Constants & Category
|
||||||
|
|
||||||
#region Hidden Foldout Entries
|
#region Hidden Foldout Entries
|
||||||
|
|
||||||
|
@ -28,18 +35,14 @@ public static class ModSettings
|
||||||
|
|
||||||
#region Stickers Mod Settings
|
#region Stickers Mod Settings
|
||||||
|
|
||||||
|
internal static readonly MelonPreferences_Entry<bool> Entry_HapticsOnPlace =
|
||||||
|
Category.CreateEntry("haptics_on_place", true, "Haptics On Place", "Enable haptic feedback when placing stickers.");
|
||||||
|
|
||||||
internal static readonly MelonPreferences_Entry<float> Entry_PlayerUpAlignmentThreshold =
|
internal static readonly MelonPreferences_Entry<float> 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.");
|
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<SFXType> Entry_SelectedSFX =
|
internal static readonly MelonPreferences_Entry<SFXType> Entry_SelectedSFX =
|
||||||
Category.CreateEntry("selected_sfx", SFXType.LBP, "Selected SFX", "The SFX used when a sticker is placed.");
|
Category.CreateEntry("selected_sfx", SFXType.LittleBigPlanetSticker, "Selected SFX", "The SFX used when a sticker is placed.");
|
||||||
|
|
||||||
internal enum SFXType
|
|
||||||
{
|
|
||||||
LBP,
|
|
||||||
Source,
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static readonly MelonPreferences_Entry<bool> Entry_UsePlaceBinding =
|
internal static readonly MelonPreferences_Entry<bool> Entry_UsePlaceBinding =
|
||||||
Category.CreateEntry("use_binding", true, "Use Place Binding", "Use the place binding to place stickers.");
|
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<KeyBind> Entry_PlaceBinding =
|
internal static readonly MelonPreferences_Entry<KeyBind> Entry_PlaceBinding =
|
||||||
Category.CreateEntry("place_binding", KeyBind.G, "Sticker Bind", "The key binding to place stickers.");
|
Category.CreateEntry("place_binding", KeyBind.G, "Sticker Bind", "The key binding to place stickers.");
|
||||||
|
|
||||||
internal enum KeyBind
|
internal static readonly MelonPreferences_Entry<string[]> Hidden_SelectedStickerNames =
|
||||||
{
|
Category.CreateEntry("selected_sticker_name", Array.Empty<string>(),
|
||||||
// Alphabetic keys
|
display_name: "Selected Sticker Name",
|
||||||
A = KeyCode.A, // 0x00000061
|
description: "The name of the sticker selected for stickering.",
|
||||||
B = KeyCode.B, // 0x00000062
|
is_hidden: true);
|
||||||
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<string> Hidden_SelectedStickerName =
|
|
||||||
Category.CreateEntry("hidden_selected_sticker", string.Empty, is_hidden: true, display_name: "Selected Sticker", description: "The currently selected sticker name.");
|
|
||||||
|
|
||||||
#endregion Stickers Mod Settings
|
#endregion Stickers Mod Settings
|
||||||
|
|
||||||
#region Decalery Settings
|
|
||||||
|
|
||||||
internal static readonly MelonPreferences_Entry<DecaleryMode> 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
|
#region Debug Settings
|
||||||
|
|
||||||
|
@ -169,8 +72,15 @@ public static class ModSettings
|
||||||
|
|
||||||
internal static void Initialize()
|
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);
|
Entry_PlayerUpAlignmentThreshold.OnEntryValueChanged.Subscribe(OnPlayerUpAlignmentThresholdChanged);
|
||||||
Decalery_DecalMode.OnEntryValueChanged.Subscribe(OnDecaleryDecalModeChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion Initialization
|
#endregion Initialization
|
||||||
|
@ -181,13 +91,6 @@ public static class ModSettings
|
||||||
{
|
{
|
||||||
Entry_PlayerUpAlignmentThreshold.Value = Mathf.Clamp(newValue, 0f, 180f);
|
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
|
#endregion Setting Changed Callbacks
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
using ABI_RC.Core.InteractionSystem;
|
using ABI_RC.Core.InteractionSystem;
|
||||||
|
using ABI_RC.Core.IO;
|
||||||
using ABI_RC.Core.Player;
|
using ABI_RC.Core.Player;
|
||||||
|
using ABI_RC.Core.Savior;
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
@ -28,6 +30,20 @@ internal static class ControllerRayPatches
|
||||||
if (__instance._hitUIInternal || !__instance._interactDown)
|
if (__instance._hitUIInternal || !__instance._interactDown)
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,6 @@ using System.Reflection;
|
||||||
namespace NAK.Stickers.Properties;
|
namespace NAK.Stickers.Properties;
|
||||||
internal static class AssemblyInfoParams
|
internal static class AssemblyInfoParams
|
||||||
{
|
{
|
||||||
public const string Version = "1.0.0";
|
public const string Version = "1.0.1";
|
||||||
public const string Author = "Exterrata & NotAKidoS";
|
public const string Author = "Exterrata & NotAKidoS";
|
||||||
}
|
}
|
|
@ -1,6 +1,26 @@
|
||||||
# Stickers
|
# 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](<https://www.flaticon.com/authors/gohsantosadrive>) on Flaticon.
|
||||||
|
- Decal generation system by [Mr F](<https://assetstore.unity.com/publishers/37453>) on the Unity Asset Store.
|
||||||
|
|
||||||
|
## Notice of partial source-code
|
||||||
|
This mod is built around a modified version of [Decalery](<https://assetstore.unity.com/packages/tools/level-design/decalery-293468>) 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)](<https://github.com/Anatta336/driven-decals>) as it is what the mod was built around originally.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
@ -29,6 +29,8 @@
|
||||||
<EmbeddedResource Include="Resources\Gohsantosadrive_Icons\Stickers-rubbish-bin.png" />
|
<EmbeddedResource Include="Resources\Gohsantosadrive_Icons\Stickers-rubbish-bin.png" />
|
||||||
<None Remove="Resources\Gohsantosadrive_Icons\Stickers-alphabet.png" />
|
<None Remove="Resources\Gohsantosadrive_Icons\Stickers-alphabet.png" />
|
||||||
<EmbeddedResource Include="Resources\Gohsantosadrive_Icons\Stickers-alphabet.png" />
|
<EmbeddedResource Include="Resources\Gohsantosadrive_Icons\Stickers-alphabet.png" />
|
||||||
|
<None Remove="Resources\Gohsantosadrive_Icons\Stickers-magnifying-glass.png" />
|
||||||
|
<EmbeddedResource Include="Resources\Gohsantosadrive_Icons\Stickers-magnifying-glass.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="BTKUILib">
|
<Reference Include="BTKUILib">
|
||||||
|
|
40
Stickers/Stickers/Enums/StickerKeyBinds.cs
Normal file
40
Stickers/Stickers/Enums/StickerKeyBinds.cs
Normal file
|
@ -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,
|
||||||
|
}
|
9
Stickers/Stickers/Enums/StickerSFXType.cs
Normal file
9
Stickers/Stickers/Enums/StickerSFXType.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace NAK.Stickers;
|
||||||
|
|
||||||
|
internal enum SFXType
|
||||||
|
{
|
||||||
|
LittleBigPlanetSticker,
|
||||||
|
SourceEngineSpray,
|
||||||
|
FactorioAlertDestroyed,
|
||||||
|
None
|
||||||
|
}
|
16
Stickers/Stickers/Networking/ModNetwork.Constants.cs
Normal file
16
Stickers/Stickers/Networking/ModNetwork.Constants.cs
Normal file
|
@ -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
|
||||||
|
}
|
21
Stickers/Stickers/Networking/ModNetwork.Enums.cs
Normal file
21
Stickers/Stickers/Networking/ModNetwork.Enums.cs
Normal file
|
@ -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
|
||||||
|
}
|
18
Stickers/Stickers/Networking/ModNetwork.Events.cs
Normal file
18
Stickers/Stickers/Networking/ModNetwork.Events.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using MTJobSystem;
|
||||||
|
|
||||||
|
namespace NAK.Stickers.Networking;
|
||||||
|
|
||||||
|
public static partial class ModNetwork
|
||||||
|
{
|
||||||
|
#region Events
|
||||||
|
|
||||||
|
public static Action<bool> OnTextureOutboundStateChanged;
|
||||||
|
|
||||||
|
private static void InvokeTextureOutboundStateChanged(bool isSending)
|
||||||
|
{
|
||||||
|
MTJobManager.RunOnMainThread("ModNetwork.InvokeTextureOutboundStateChanged",
|
||||||
|
() => OnTextureOutboundStateChanged?.Invoke(isSending));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Events
|
||||||
|
}
|
18
Stickers/Stickers/Networking/ModNetwork.Helpers.cs
Normal file
18
Stickers/Stickers/Networking/ModNetwork.Helpers.cs
Normal file
|
@ -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
|
||||||
|
}
|
222
Stickers/Stickers/Networking/ModNetwork.Inbound.cs
Normal file
222
Stickers/Stickers/Networking/ModNetwork.Inbound.cs
Normal file
|
@ -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<string, byte[]> _textureChunkBuffers = new();
|
||||||
|
private static readonly Dictionary<string, int> _receivedChunkCounts = new();
|
||||||
|
private static readonly Dictionary<string, int> _expectedChunkCounts = new();
|
||||||
|
private static readonly Dictionary<string, (int stickerSlot, Guid Hash, int Width, int Height)> _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
|
||||||
|
}
|
22
Stickers/Stickers/Networking/ModNetwork.Logging.cs
Normal file
22
Stickers/Stickers/Networking/ModNetwork.Logging.cs
Normal file
|
@ -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
|
||||||
|
}
|
48
Stickers/Stickers/Networking/ModNetwork.Main.cs
Normal file
48
Stickers/Stickers/Networking/ModNetwork.Main.cs
Normal file
|
@ -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<string> _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
|
||||||
|
}
|
189
Stickers/Stickers/Networking/ModNetwork.Outbound.cs
Normal file
189
Stickers/Stickers/Networking/ModNetwork.Outbound.cs
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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<bool> 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<string, byte[]> _textureChunkBuffers = new();
|
|
||||||
private static readonly Dictionary<string, int> _receivedChunkCounts = new();
|
|
||||||
private static readonly Dictionary<string, int> _expectedChunkCounts = new();
|
|
||||||
private static readonly Dictionary<string, (Guid Hash, int Width, int Height)> _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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,131 +2,213 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Object = UnityEngine.Object;
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
namespace NAK.Stickers;
|
namespace NAK.Stickers
|
||||||
|
|
||||||
public class StickerData
|
|
||||||
{
|
{
|
||||||
private const float DECAL_SIZE = 0.25f;
|
public class StickerData
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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<DecalType>();
|
private readonly Guid[] _textureHashes;
|
||||||
_decal.decalSettings = new DecalSpawner.InitData
|
private readonly Material[] _materials;
|
||||||
|
private readonly AudioSource _audioSource;
|
||||||
|
|
||||||
|
public StickerData(bool isLocal, int decalSpawnersCount)
|
||||||
{
|
{
|
||||||
material = new Material(StickerMod.DecalSimpleShader)
|
IsLocal = isLocal;
|
||||||
|
|
||||||
|
_decal = ScriptableObject.CreateInstance<DecalType>();
|
||||||
|
_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),
|
_materials[i] = new Material(StickerMod.DecalSimpleShader);
|
||||||
},
|
_decal.decalSettings = new DecalSpawner.InitData
|
||||||
useShaderReplacement = false,
|
{
|
||||||
inheritMaterialProperties = false,
|
material = _materials[i],
|
||||||
inheritMaterialPropertyBlock = false,
|
useShaderReplacement = false,
|
||||||
};
|
inheritMaterialProperties = false,
|
||||||
|
inheritMaterialPropertyBlock = false,
|
||||||
_spawner = DecalManager.GetSpawner(_decal.decalSettings, 4096, 1024);
|
};
|
||||||
|
_decalSpawners[i] = DecalManager.GetSpawner(_decal.decalSettings, 4096, 1024);
|
||||||
_audioSource = new GameObject("StickerAudioSource").AddComponent<AudioSource>();
|
}
|
||||||
_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
|
|
||||||
|
|
||||||
if (IsLocal) StickerMod.Logger.Msg($"Set texture filter mode to: {texture.filterMode}");
|
_audioSource = new GameObject("StickerAudioSource").AddComponent<AudioSource>();
|
||||||
|
_audioSource.spatialBlend = 1f;
|
||||||
Material material = _decal.decalSettings.material;
|
_audioSource.volume = 0.5f;
|
||||||
|
_audioSource.playOnAwake = false;
|
||||||
// TODO: fix
|
_audioSource.loop = false;
|
||||||
if (material.mainTexture != null) Object.Destroy(material.mainTexture);
|
_audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
|
||||||
material.mainTexture = texture;
|
_audioSource.maxDistance = 5f;
|
||||||
}
|
_audioSource.minDistance = 1f;
|
||||||
|
_audioSource.outputAudioMixerGroup = RootLogic.Instance.propSfx; // props are close enough to stickers
|
||||||
public void Place(RaycastHit hit, Vector3 forwardDirection, Vector3 upDirection)
|
if (isLocal) Object.DontDestroyOnLoad(_audioSource.gameObject); // keep audio source through world transitions
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public Guid GetTextureHash(int spawnerIndex = 0)
|
||||||
public void SetAlpha(float alpha)
|
{
|
||||||
{
|
if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length)
|
||||||
Material material = _decal.decalSettings.material;
|
{
|
||||||
material.color = new Color(material.color.r, material.color.g, material.color.b, alpha);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
152
Stickers/Stickers/StickerSystem.ImageLoading.cs
Normal file
152
Stickers/Stickers/StickerSystem.ImageLoading.cs
Normal file
|
@ -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<int, string> OnStickerLoaded;
|
||||||
|
public static event Action<int, string> 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
|
||||||
|
}
|
71
Stickers/Stickers/StickerSystem.Main.cs
Normal file
71
Stickers/Stickers/StickerSystem.Main.cs
Normal file
|
@ -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<string, StickerData> _playerStickers = new();
|
||||||
|
private const string PlayerLocalId = "_PLAYERLOCAL";
|
||||||
|
|
||||||
|
private readonly List<StickerData> _deadStickerPool = new(); // for cleanup on player leave
|
||||||
|
|
||||||
|
#endregion Data
|
||||||
|
}
|
98
Stickers/Stickers/StickerSystem.PlayerCallbacks.cs
Normal file
98
Stickers/Stickers/StickerSystem.PlayerCallbacks.cs
Normal file
|
@ -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
|
||||||
|
}
|
160
Stickers/Stickers/StickerSystem.StickerLifecycle.cs
Normal file
160
Stickers/Stickers/StickerSystem.StickerLifecycle.cs
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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<string> 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<string, StickerData> _playerStickers = new();
|
|
||||||
private const string PlayerLocalId = "_PLAYERLOCAL";
|
|
||||||
|
|
||||||
private readonly List<StickerData> _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
|
|
||||||
}
|
|
185
Stickers/Stickers/Utilities/StickerCache.cs
Normal file
185
Stickers/Stickers/Utilities/StickerCache.cs
Normal file
|
@ -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<string> _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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue