Normalize JSON encoding

This commit is contained in:
NotAKidoS 2025-12-28 20:52:46 -06:00
parent ab46b2505f
commit 5046c2259d
8 changed files with 862 additions and 861 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
*.json text eol=lf

View file

@ -1,4 +1,4 @@
{ {
"_id": -1, "_id": -1,
"name": "ShowPlayerInSelfMirror", "name": "ShowPlayerInSelfMirror",
"modversion": "1.0.0", "modversion": "1.0.0",

View file

@ -1,122 +1,122 @@
using BTKUILib; using BTKUILib;
using BTKUILib.UIObjects; using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components; using BTKUILib.UIObjects.Components;
using BTKUILib.UIObjects.Objects; using BTKUILib.UIObjects.Objects;
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 Button _placeStickersButton; private static Button _placeStickersButton;
private static readonly MultiSelection _sfxSelection = private static readonly MultiSelection _sfxSelection =
MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_SelectedSFX); MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_SelectedSFX);
private static readonly MultiSelection _desktopKeybindSelection = private static readonly MultiSelection _desktopKeybindSelection =
MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_PlaceBinding); MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_PlaceBinding);
private static readonly MultiSelection _tabDoubleClickSelection = private static readonly MultiSelection _tabDoubleClickSelection =
MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_TabDoubleClick); MultiSelection.CreateMultiSelectionFromMelonPref(ModSettings.Entry_TabDoubleClick);
#region Category Setup #region Category Setup
private static void Setup_StickersModCategory() private static void Setup_StickersModCategory()
{ {
_ourCategory = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_SettingsCategory); _ourCategory = _rootPage.AddMelonCategory(ModSettings.Hidden_Foldout_SettingsCategory);
_placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon); _placeStickersButton = _ourCategory.AddButton("Place Stickers", "Stickers-magic-wand", "Place stickers via raycast.", ButtonStyle.TextWithIcon);
_placeStickersButton.OnPress += OnPlaceStickersButtonClick; _placeStickersButton.OnPress += OnPlaceStickersButtonClick;
Button clearSelfStickersButton = _ourCategory.AddButton("Clear Self", "Stickers-eraser", "Clear own stickers.", ButtonStyle.TextWithIcon); Button clearSelfStickersButton = _ourCategory.AddButton("Clear Self", "Stickers-eraser", "Clear own stickers.", ButtonStyle.TextWithIcon);
clearSelfStickersButton.OnPress += OnClearSelfStickersButtonClick; clearSelfStickersButton.OnPress += OnClearSelfStickersButtonClick;
Button clearAllStickersButton = _ourCategory.AddButton("Clear All", "Stickers-rubbish-bin", "Clear all stickers.", ButtonStyle.TextWithIcon); Button clearAllStickersButton = _ourCategory.AddButton("Clear All", "Stickers-rubbish-bin", "Clear all stickers.", ButtonStyle.TextWithIcon);
clearAllStickersButton.OnPress += OnClearAllStickersButtonClick; clearAllStickersButton.OnPress += OnClearAllStickersButtonClick;
Button openStickersFolderButton = _ourCategory.AddButton("Open Stickers Folder", "Stickers-folder", "Open UserData/Stickers folder in explorer. If above 256kb your image will automatically be downscaled for networking reasons.", ButtonStyle.TextWithIcon); Button openStickersFolderButton = _ourCategory.AddButton("Open Stickers Folder", "Stickers-folder", "Open UserData/Stickers folder in explorer. If above 256kb your image will automatically be downscaled for networking reasons.", ButtonStyle.TextWithIcon);
openStickersFolderButton.OnPress += OnOpenStickersFolderButtonClick; openStickersFolderButton.OnPress += OnOpenStickersFolderButtonClick;
Button openStickerSFXButton = _ourCategory.AddButton("Sticker SFX", "Stickers-headset", "Choose the SFX used when a sticker is placed.", ButtonStyle.TextWithIcon); Button openStickerSFXButton = _ourCategory.AddButton("Sticker SFX", "Stickers-headset", "Choose the SFX used when a sticker is placed.", ButtonStyle.TextWithIcon);
openStickerSFXButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_sfxSelection); openStickerSFXButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_sfxSelection);
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);
openDesktopKeybindButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_desktopKeybindSelection); openDesktopKeybindButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_desktopKeybindSelection);
toggleDesktopKeybindButton.OnValueUpdated += (b) => toggleDesktopKeybindButton.OnValueUpdated += (b) =>
{ {
ModSettings.Entry_UsePlaceBinding.Value = b; ModSettings.Entry_UsePlaceBinding.Value = b;
openDesktopKeybindButton.Disabled = !b; openDesktopKeybindButton.Disabled = !b;
}; };
Button openTabDoubleClickButton = _ourCategory.AddButton("Tab Double Click", "Stickers-mouse", "Choose the action to perform when double clicking the Stickers tab.", ButtonStyle.TextWithIcon); Button openTabDoubleClickButton = _ourCategory.AddButton("Tab Double Click", "Stickers-mouse", "Choose the action to perform when double clicking the Stickers tab.", ButtonStyle.TextWithIcon);
openTabDoubleClickButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_tabDoubleClickSelection); openTabDoubleClickButton.OnPress += () => QuickMenuAPI.OpenMultiSelect(_tabDoubleClickSelection);
} }
#endregion Category Setup #endregion Category Setup
#region Button Actions #region Button Actions
private static void OnPlaceStickersButtonClick() private static void OnPlaceStickersButtonClick()
{ {
if (!_isOurTabOpened) return; if (!_isOurTabOpened) return;
if (StickerSystem.Instance.IsRestrictedInstance) if (StickerSystem.Instance.IsRestrictedInstance)
{ {
QuickMenuAPI.ShowAlertToast("Stickers are not allowed in this world!", 2); QuickMenuAPI.ShowAlertToast("Stickers are not allowed in this world!", 2);
return; return;
} }
string mode = StickerSystem.Instance.IsInStickerMode ? "Exiting" : "Entering"; string mode = StickerSystem.Instance.IsInStickerMode ? "Exiting" : "Entering";
QuickMenuAPI.ShowAlertToast($"{mode} sticker placement mode...", 2); QuickMenuAPI.ShowAlertToast($"{mode} sticker placement mode...", 2);
StickerSystem.Instance.IsInStickerMode = !StickerSystem.Instance.IsInStickerMode; StickerSystem.Instance.IsInStickerMode = !StickerSystem.Instance.IsInStickerMode;
} }
private static void OnClearSelfStickersButtonClick() private static void OnClearSelfStickersButtonClick()
{ {
if (!_isOurTabOpened) return; if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Clearing own stickers in world...", 2); QuickMenuAPI.ShowAlertToast("Clearing own stickers in world...", 2);
StickerSystem.Instance.ClearStickersSelf(); StickerSystem.Instance.ClearStickersSelf();
} }
private static void OnClearAllStickersButtonClick() private static void OnClearAllStickersButtonClick()
{ {
if (!_isOurTabOpened) return; if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Clearing all stickers in world...", 2); QuickMenuAPI.ShowAlertToast("Clearing all stickers in world...", 2);
StickerSystem.Instance.ClearAllStickers(); StickerSystem.Instance.ClearAllStickers();
} }
private static void OnOpenStickersFolderButtonClick() private static void OnOpenStickersFolderButtonClick()
{ {
if (!_isOurTabOpened) return; if (!_isOurTabOpened) return;
QuickMenuAPI.ShowAlertToast("Opening Stickers folder in Explorer...", 2); QuickMenuAPI.ShowAlertToast("Opening Stickers folder in Explorer...", 2);
StickerSystem.OpenStickersFolder(); StickerSystem.OpenStickersFolder();
} }
public static void OnStickerRestrictionUpdated(bool isRestricted = false) //TODO: add Icon changing, Bono needs to expose the value first. public static void OnStickerRestrictionUpdated(bool isRestricted = false) //TODO: add Icon changing, Bono needs to expose the value first.
{ {
if (_rootPage == null || _placeStickersButton == null) if (_rootPage == null || _placeStickersButton == null)
return; return;
if (isRestricted) if (isRestricted)
{ {
_rootPage.MenuSubtitle = "Stickers... are sadly disabled in this world."; _rootPage.MenuSubtitle = "Stickers... are sadly disabled in this world.";
_placeStickersButton.Disabled = true; _placeStickersButton.Disabled = true;
_placeStickersButton.ButtonText = "Stickers Disabled"; _placeStickersButton.ButtonText = "Stickers Disabled";
_placeStickersButton.ButtonTooltip = "This world is not allowing Stickers."; _placeStickersButton.ButtonTooltip = "This world is not allowing Stickers.";
_placeStickersButton.ButtonIcon = "Stickers-magic-wand-broken"; _placeStickersButton.ButtonIcon = "Stickers-magic-wand-broken";
return; return;
} }
_rootPage.MenuSubtitle = "Stickers! Double-click the tab to quickly toggle Sticker Mode."; _rootPage.MenuSubtitle = "Stickers! Double-click the tab to quickly toggle Sticker Mode.";
_placeStickersButton.Disabled = false; _placeStickersButton.Disabled = false;
_placeStickersButton.ButtonText = "Place Stickers"; _placeStickersButton.ButtonText = "Place Stickers";
_placeStickersButton.ButtonTooltip = "Place stickers via raycast."; _placeStickersButton.ButtonTooltip = "Place stickers via raycast.";
_placeStickersButton.ButtonIcon = "Stickers-magic-wand"; _placeStickersButton.ButtonIcon = "Stickers-magic-wand";
} }
#endregion Button Actions #endregion Button Actions
} }

View file

@ -1,116 +1,116 @@
using BTKUILib; using BTKUILib;
using BTKUILib.UIObjects; using BTKUILib.UIObjects;
using NAK.Stickers.Networking; using NAK.Stickers.Networking;
using NAK.Stickers.Utilities; using NAK.Stickers.Utilities;
using System.Reflection; using System.Reflection;
namespace NAK.Stickers.Integrations; namespace NAK.Stickers.Integrations;
public static partial class BTKUIAddon public static partial class BTKUIAddon
{ {
private static Page _rootPage; private static Page _rootPage;
private static string _rootPageElementID; private static string _rootPageElementID;
private static bool _isOurTabOpened; private static bool _isOurTabOpened;
public static void Initialize() public static void Initialize()
{ {
Setup_Icons(); Setup_Icons();
Setup_StickerModTab(); Setup_StickerModTab();
Setup_PlayerOptionsPage(); Setup_PlayerOptionsPage();
} }
#region Setup #region Setup
private static void Setup_Icons() private static void Setup_Icons()
{ {
Assembly assembly = Assembly.GetExecutingAssembly(); Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyName = assembly.GetName().Name; string assemblyName = assembly.GetName().Name;
// All icons used - https://www.flaticon.com/authors/gohsantosadrive // 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-alphabet", GetIconStream("Gohsantosadrive_Icons.Stickers-alphabet.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-eraser", GetIconStream("Gohsantosadrive_Icons.Stickers-eraser.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-folder", GetIconStream("Gohsantosadrive_Icons.Stickers-folder.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-headset", GetIconStream("Gohsantosadrive_Icons.Stickers-headset.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-headset", GetIconStream("Gohsantosadrive_Icons.Stickers-headset.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magnifying-glass", GetIconStream("Gohsantosadrive_Icons.Stickers-magnifying-glass.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magnifying-glass", GetIconStream("Gohsantosadrive_Icons.Stickers-magnifying-glass.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand", GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand", GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand-broken", GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand-broken.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-magic-wand-broken", GetIconStream("Gohsantosadrive_Icons.Stickers-magic-wand-broken.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-mouse", GetIconStream("Gohsantosadrive_Icons.Stickers-mouse.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-mouse", GetIconStream("Gohsantosadrive_Icons.Stickers-mouse.png"));
//QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-pencil", GetIconStream("Gohsantosadrive_Icons.Stickers-pencil.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-puzzle", GetIconStream("Gohsantosadrive_Icons.Stickers-puzzle.png"));
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-puzzle-disabled", GetIconStream("Gohsantosadrive_Icons.Stickers-puzzle-disabled.png")); // disabled Sticker Puzzle QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-puzzle-disabled", GetIconStream("Gohsantosadrive_Icons.Stickers-puzzle-disabled.png")); // disabled Sticker Puzzle
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-rubbish-bin", GetIconStream("Gohsantosadrive_Icons.Stickers-rubbish-bin.png")); QuickMenuAPI.PrepareIcon(ModSettings.ModName, "Stickers-rubbish-bin", GetIconStream("Gohsantosadrive_Icons.Stickers-rubbish-bin.png"));
return; return;
Stream GetIconStream(string iconName) => assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}"); Stream GetIconStream(string iconName) => assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}");
} }
private static void Setup_StickerModTab() private static void Setup_StickerModTab()
{ {
_rootPage = new Page(ModSettings.ModName, ModSettings.SM_SettingsCategory, true, "Stickers-Puzzle") // sticker icon will be left blank as it is updated on world join, AFTER Icon value is exposed.. _rootPage = new Page(ModSettings.ModName, ModSettings.SM_SettingsCategory, true, "Stickers-Puzzle") // sticker icon will be left blank as it is updated on world join, AFTER Icon value is exposed..
{ {
MenuTitle = ModSettings.SM_SettingsCategory + $" (Network Version v{ModNetwork.NetworkVersion})", MenuTitle = ModSettings.SM_SettingsCategory + $" (Network Version v{ModNetwork.NetworkVersion})",
MenuSubtitle = "", // left this blank as it is defined when the world loads MenuSubtitle = "", // left this blank as it is defined when the world loads
}; };
_rootPageElementID = _rootPage.ElementID; _rootPageElementID = _rootPage.ElementID;
QuickMenuAPI.OnTabChange += OnTabChange; QuickMenuAPI.OnTabChange += OnTabChange;
ModNetwork.OnTextureOutboundStateChanged += (isSending) => ModNetwork.OnTextureOutboundStateChanged += (isSending) =>
{ {
if (_isOurTabOpened && isSending) QuickMenuAPI.ShowAlertToast("Sending Sticker over Mod Network...", 2); if (_isOurTabOpened && isSending) QuickMenuAPI.ShowAlertToast("Sending Sticker over Mod Network...", 2);
//_rootPage.Disabled = isSending; // TODO: fix being able to select stickers while sending //_rootPage.Disabled = isSending; // TODO: fix being able to select stickers while sending
}; };
StickerSystem.OnStickerLoaded += (slotIndex, imageRelativePath) => StickerSystem.OnStickerLoaded += (slotIndex, imageRelativePath) =>
{ {
if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast($"Sticker loaded: {imageRelativePath}", 2); if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast($"Sticker loaded: {imageRelativePath}", 2);
_stickerSelectionButtons[slotIndex].ButtonIcon = StickerCache.GetBtkUiIconName(imageRelativePath); _stickerSelectionButtons[slotIndex].ButtonIcon = StickerCache.GetBtkUiIconName(imageRelativePath);
}; };
StickerSystem.OnStickerLoadFailed += (slotIndex, error) => StickerSystem.OnStickerLoadFailed += (slotIndex, error) =>
{ {
if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast(error, 3); if (_isOurTabOpened) QuickMenuAPI.ShowAlertToast(error, 3);
}; };
Setup_StickersModCategory(); Setup_StickersModCategory();
Setup_StickerSelectionCategory(); Setup_StickerSelectionCategory();
Setup_OtherOptionsCategory(); Setup_OtherOptionsCategory();
} }
#endregion Setup #endregion Setup
#region Double-Click Place Sticker #region Double-Click Place Sticker
private static DateTime lastTime = DateTime.Now; private static DateTime lastTime = DateTime.Now;
private static void OnTabChange(string newTab, string previousTab) private static void OnTabChange(string newTab, string previousTab)
{ {
_isOurTabOpened = newTab == _rootPageElementID; _isOurTabOpened = newTab == _rootPageElementID;
if (!_isOurTabOpened) return; if (!_isOurTabOpened) return;
TimeSpan timeDifference = DateTime.Now - lastTime; TimeSpan timeDifference = DateTime.Now - lastTime;
if (timeDifference.TotalSeconds <= 0.5) if (timeDifference.TotalSeconds <= 0.5)
{ {
switch (ModSettings.Entry_TabDoubleClick.Value) switch (ModSettings.Entry_TabDoubleClick.Value)
{ {
default: default:
case TabDoubleClick.ToggleStickerMode: case TabDoubleClick.ToggleStickerMode:
OnPlaceStickersButtonClick(); OnPlaceStickersButtonClick();
break; break;
case TabDoubleClick.ClearAllStickers: case TabDoubleClick.ClearAllStickers:
OnClearAllStickersButtonClick(); OnClearAllStickersButtonClick();
break; break;
case TabDoubleClick.ClearSelfStickers: case TabDoubleClick.ClearSelfStickers:
OnClearSelfStickersButtonClick(); OnClearSelfStickersButtonClick();
break; break;
case TabDoubleClick.None: case TabDoubleClick.None:
break; break;
} }
return; return;
} }
lastTime = DateTime.Now; lastTime = DateTime.Now;
} }
#endregion Double-Click Place Sticker #endregion Double-Click Place Sticker
} }

View file

@ -1,32 +1,32 @@
using NAK.Stickers.Properties; using NAK.Stickers.Properties;
using MelonLoader; using MelonLoader;
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)] [assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] [assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] [assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.Stickers))] [assembly: AssemblyTitle(nameof(NAK.Stickers))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)] [assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.Stickers))] [assembly: AssemblyProduct(nameof(NAK.Stickers))]
[assembly: MelonInfo( [assembly: MelonInfo(
typeof(NAK.Stickers.StickerMod), typeof(NAK.Stickers.StickerMod),
nameof(NAK.Stickers), nameof(NAK.Stickers),
AssemblyInfoParams.Version, AssemblyInfoParams.Version,
AssemblyInfoParams.Author, AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/Stickers" downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/Stickers"
)] )]
[assembly: MelonGame("ChilloutVR", "ChilloutVR")] [assembly: MelonGame("ChilloutVR", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] [assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink [assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red [assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll] [assembly: HarmonyDontPatchAll]
namespace NAK.Stickers.Properties; namespace NAK.Stickers.Properties;
internal static class AssemblyInfoParams internal static class AssemblyInfoParams
{ {
public const string Version = "1.1.1"; public const string Version = "1.1.1";
public const string Author = "NotAKidoS"; public const string Author = "NotAKidoS";
} }

View file

@ -1,260 +1,260 @@
using ABI_RC.Core.Networking.IO.Social; using ABI_RC.Core.Networking.IO.Social;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using ABI_RC.Core.Savior; using ABI_RC.Core.Savior;
using ABI_RC.Systems.ModNetwork; using ABI_RC.Systems.ModNetwork;
using NAK.Stickers.Utilities; using NAK.Stickers.Utilities;
using UnityEngine; using UnityEngine;
namespace NAK.Stickers.Networking; namespace NAK.Stickers.Networking;
public static partial class ModNetwork public static partial class ModNetwork
{ {
#region Inbound Buffers #region Inbound Buffers
private static readonly Dictionary<string, byte[]> _textureChunkBuffers = new(); private static readonly Dictionary<string, byte[]> _textureChunkBuffers = new();
private static readonly Dictionary<string, int> _receivedChunkCounts = new(); private static readonly Dictionary<string, int> _receivedChunkCounts = new();
private static readonly Dictionary<string, int> _expectedChunkCounts = new(); private static readonly Dictionary<string, int> _expectedChunkCounts = new();
private static readonly Dictionary<string, (int stickerSlot, Guid Hash, int Width, int Height)> _textureMetadata = new(); private static readonly Dictionary<string, (int stickerSlot, Guid Hash, int Width, int Height)> _textureMetadata = new();
#endregion Inbound Buffers #endregion Inbound Buffers
#region Reset Method #region Reset Method
public static void Reset() public static void Reset()
{ {
_textureChunkBuffers.Clear(); _textureChunkBuffers.Clear();
_receivedChunkCounts.Clear(); _receivedChunkCounts.Clear();
_expectedChunkCounts.Clear(); _expectedChunkCounts.Clear();
_textureMetadata.Clear(); _textureMetadata.Clear();
LoggerInbound("ModNetwork inbound buffers and metadata have been reset."); LoggerInbound("ModNetwork inbound buffers and metadata have been reset.");
} }
#endregion Reset Method #endregion Reset Method
#region Inbound Methods #region Inbound Methods
private static bool ShouldReceiveFromSender(string sender) private static bool ShouldReceiveFromSender(string sender)
{ {
if (_disallowedForSession.Contains(sender)) if (_disallowedForSession.Contains(sender))
return false; // ignore messages from disallowed users return false; // ignore messages from disallowed users
if (MetaPort.Instance.blockedUserIds.Contains(sender)) if (MetaPort.Instance.blockedUserIds.Contains(sender))
return false; // ignore messages from blocked users return false; // ignore messages from blocked users
if (ModSettings.Entry_FriendsOnly.Value && !Friends.FriendsWith(sender)) if (ModSettings.Entry_FriendsOnly.Value && !Friends.FriendsWith(sender))
return false; // ignore messages from non-friends if friends only is enabled return false; // ignore messages from non-friends if friends only is enabled
if (StickerSystem.Instance.IsRestrictedInstance) // ignore messages from users when the world is restricted. This also includes older or modified version of Stickers mod. if (StickerSystem.Instance.IsRestrictedInstance) // ignore messages from users when the world is restricted. This also includes older or modified version of Stickers mod.
return false; return false;
if (!CVRPlayerManager.Instance.GetPlayerPuppetMaster(sender, out PuppetMaster _)) if (!CVRPlayerManager.Instance.GetPlayerPuppetMaster(sender, out PuppetMaster _))
return false; // ignore messages from players that don't exist return false; // ignore messages from players that don't exist
return true; return true;
} }
private static void HandleMessageReceived(ModNetworkMessage msg) private static void HandleMessageReceived(ModNetworkMessage msg)
{ {
try try
{ {
string sender = msg.Sender; string sender = msg.Sender;
msg.Read(out byte msgTypeRaw); msg.Read(out byte msgTypeRaw);
if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw)) if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
return; return;
if (!ShouldReceiveFromSender(sender)) if (!ShouldReceiveFromSender(sender))
return; return;
LoggerInbound($"Received message from {msg.Sender}, Type: {(MessageType)msgTypeRaw}"); LoggerInbound($"Received message from {msg.Sender}, Type: {(MessageType)msgTypeRaw}");
switch ((MessageType)msgTypeRaw) switch ((MessageType)msgTypeRaw)
{ {
case MessageType.PlaceSticker: case MessageType.PlaceSticker:
HandlePlaceSticker(msg); HandlePlaceSticker(msg);
break; break;
case MessageType.ClearSticker: case MessageType.ClearSticker:
HandleClearSticker(msg); HandleClearSticker(msg);
break; break;
case MessageType.ClearAllStickers: case MessageType.ClearAllStickers:
HandleClearAllStickers(msg); HandleClearAllStickers(msg);
break; break;
case MessageType.StartTexture: case MessageType.StartTexture:
HandleStartTexture(msg); HandleStartTexture(msg);
break; break;
case MessageType.SendTexture: case MessageType.SendTexture:
HandleSendTexture(msg); HandleSendTexture(msg);
break; break;
case MessageType.EndTexture: case MessageType.EndTexture:
HandleEndTexture(msg); HandleEndTexture(msg);
break; break;
case MessageType.RequestTexture: case MessageType.RequestTexture:
HandleRequestTexture(msg); HandleRequestTexture(msg);
break; break;
default: default:
LoggerInbound($"Invalid message type received: {msgTypeRaw}"); LoggerInbound($"Invalid message type received: {msgTypeRaw}");
break; break;
} }
} }
catch (Exception e) catch (Exception e)
{ {
LoggerInbound($"Error handling message from {msg.Sender}: {e.Message}", true); LoggerInbound($"Error handling message from {msg.Sender}: {e.Message}", true);
} }
} }
private static void HandlePlaceSticker(ModNetworkMessage msg) private static void HandlePlaceSticker(ModNetworkMessage msg)
{ {
msg.Read(out int stickerSlot); msg.Read(out int stickerSlot);
msg.Read(out Guid textureHash); msg.Read(out Guid textureHash);
msg.Read(out Vector3 position); msg.Read(out Vector3 position);
msg.Read(out Vector3 forward); msg.Read(out Vector3 forward);
msg.Read(out Vector3 up); msg.Read(out Vector3 up);
msg.Read(out int size); msg.Read(out int size);
msg.Read(out float opacity); msg.Read(out float opacity);
if (!StickerSystem.Instance.HasTextureHash(msg.Sender, textureHash)) if (!StickerSystem.Instance.HasTextureHash(msg.Sender, textureHash))
{ {
SendRequestTexture(stickerSlot, textureHash); SendRequestTexture(stickerSlot, textureHash);
StickerSystem.Instance.ClearStickersForPlayer(msg.Sender, stickerSlot); // Ensure no exploit StickerSystem.Instance.ClearStickersForPlayer(msg.Sender, stickerSlot); // Ensure no exploit
} }
StickerSystem.Instance.OnStickerPlaceReceived(msg.Sender, stickerSlot, position, forward, up, (StickerSize)size, opacity); StickerSystem.Instance.OnStickerPlaceReceived(msg.Sender, stickerSlot, position, forward, up, (StickerSize)size, opacity);
} }
private static void HandleClearSticker(ModNetworkMessage msg) private static void HandleClearSticker(ModNetworkMessage msg)
{ {
msg.Read(out int stickerSlot); msg.Read(out int stickerSlot);
StickerSystem.Instance.OnStickerClearReceived(msg.Sender, stickerSlot); StickerSystem.Instance.OnStickerClearReceived(msg.Sender, stickerSlot);
} }
private static void HandleClearAllStickers(ModNetworkMessage msg) private static void HandleClearAllStickers(ModNetworkMessage msg)
{ {
StickerSystem.Instance.OnStickerClearAllReceived(msg.Sender); StickerSystem.Instance.OnStickerClearAllReceived(msg.Sender);
} }
private static void HandleStartTexture(ModNetworkMessage msg) private static void HandleStartTexture(ModNetworkMessage msg)
{ {
string sender = msg.Sender; string sender = msg.Sender;
msg.Read(out int stickerSlot); msg.Read(out int stickerSlot);
msg.Read(out Guid textureHash); msg.Read(out Guid textureHash);
msg.Read(out int chunkCount); msg.Read(out int chunkCount);
msg.Read(out int width); msg.Read(out int width);
msg.Read(out int height); msg.Read(out int height);
if (_textureChunkBuffers.ContainsKey(sender)) if (_textureChunkBuffers.ContainsKey(sender))
{ {
LoggerInbound($"Received StartTexture message from {sender} while still receiving texture data!"); LoggerInbound($"Received StartTexture message from {sender} while still receiving texture data!");
return; return;
} }
if (StickerSystem.Instance.HasTextureHash(sender, textureHash)) if (StickerSystem.Instance.HasTextureHash(sender, textureHash))
{ {
LoggerInbound($"Received StartTexture message from {sender} with existing texture hash {textureHash}, skipping texture data."); LoggerInbound($"Received StartTexture message from {sender} with existing texture hash {textureHash}, skipping texture data.");
return; return;
} }
if (chunkCount > MaxChunkCount) if (chunkCount > MaxChunkCount)
{ {
LoggerInbound($"Received StartTexture message from {sender} with too many chunks: {chunkCount}", true); LoggerInbound($"Received StartTexture message from {sender} with too many chunks: {chunkCount}", true);
return; return;
} }
_textureMetadata[sender] = (stickerSlot, textureHash, width, height); _textureMetadata[sender] = (stickerSlot, textureHash, width, height);
_textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)]; _textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)];
_expectedChunkCounts[sender] = Mathf.Clamp(chunkCount, 0, MaxChunkCount); _expectedChunkCounts[sender] = Mathf.Clamp(chunkCount, 0, MaxChunkCount);
_receivedChunkCounts[sender] = 0; _receivedChunkCounts[sender] = 0;
LoggerInbound($"Received StartTexture message from {sender}: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Resolution: {width}x{height}"); LoggerInbound($"Received StartTexture message from {sender}: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Resolution: {width}x{height}");
} }
private static void HandleSendTexture(ModNetworkMessage msg) private static void HandleSendTexture(ModNetworkMessage msg)
{ {
string sender = msg.Sender; string sender = msg.Sender;
msg.Read(out int chunkIdx); msg.Read(out int chunkIdx);
msg.Read(out byte[] chunkData); msg.Read(out byte[] chunkData);
if (!_textureChunkBuffers.TryGetValue(sender, out var buffer)) if (!_textureChunkBuffers.TryGetValue(sender, out var buffer))
return; return;
int startIndex = chunkIdx * ChunkSize; int startIndex = chunkIdx * ChunkSize;
Array.Copy(chunkData, 0, buffer, startIndex, chunkData.Length); Array.Copy(chunkData, 0, buffer, startIndex, chunkData.Length);
_receivedChunkCounts[sender]++; _receivedChunkCounts[sender]++;
if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender]) if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender])
return; return;
(int stickerSlot, Guid Hash, int Width, int Height) metadata = _textureMetadata[sender]; (int stickerSlot, Guid Hash, int Width, int Height) metadata = _textureMetadata[sender];
// All chunks received, reassemble texture // All chunks received, reassemble texture
_textureChunkBuffers.Remove(sender); _textureChunkBuffers.Remove(sender);
_receivedChunkCounts.Remove(sender); _receivedChunkCounts.Remove(sender);
_expectedChunkCounts.Remove(sender); _expectedChunkCounts.Remove(sender);
_textureMetadata.Remove(sender); _textureMetadata.Remove(sender);
// Validate image // Validate image
if (!ImageUtility.IsValidImage(buffer)) if (!ImageUtility.IsValidImage(buffer))
{ {
LoggerInbound($"[Inbound] Received texture data is not a valid image from {sender}!", true); LoggerInbound($"[Inbound] Received texture data is not a valid image from {sender}!", true);
return; return;
} }
// Validate data TODO: fix hash??????? // Validate data TODO: fix hash???????
(Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer); (Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer);
if (metadata.Width != width if (metadata.Width != width
|| metadata.Height != height) || 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); LoggerInbound($"Received texture data does not match metadata! Expected: {metadata.Hash} ({metadata.Width}x{metadata.Height}), received: {imageHash} ({width}x{height})", true);
return; return;
} }
Texture2D texture = new(1,1); Texture2D texture = new(1,1);
texture.LoadImage(buffer); texture.LoadImage(buffer);
texture.Compress(true); texture.Compress(true);
StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture, metadata.stickerSlot); StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture, metadata.stickerSlot);
LoggerInbound($"All chunks received and texture reassembled from {sender}. " + LoggerInbound($"All chunks received and texture reassembled from {sender}. " +
$"Texture size: {metadata.Width}x{metadata.Height}"); $"Texture size: {metadata.Width}x{metadata.Height}");
} }
private static void HandleEndTexture(ModNetworkMessage msg) private static void HandleEndTexture(ModNetworkMessage msg)
{ {
string sender = msg.Sender; string sender = msg.Sender;
if (!_textureChunkBuffers.ContainsKey(sender)) if (!_textureChunkBuffers.ContainsKey(sender))
return; return;
LoggerInbound($"Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received."); LoggerInbound($"Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received.");
_textureChunkBuffers.Remove(sender); _textureChunkBuffers.Remove(sender);
_receivedChunkCounts.Remove(sender); _receivedChunkCounts.Remove(sender);
_expectedChunkCounts.Remove(sender); _expectedChunkCounts.Remove(sender);
_textureMetadata.Remove(sender); _textureMetadata.Remove(sender);
} }
private static void HandleRequestTexture(ModNetworkMessage msg) private static void HandleRequestTexture(ModNetworkMessage msg)
{ {
string sender = msg.Sender; string sender = msg.Sender;
msg.Read(out int stickerSlot); msg.Read(out int stickerSlot);
msg.Read(out Guid textureHash); msg.Read(out Guid textureHash);
if (!_isSubscribedToModNetwork || IsSendingTexture) if (!_isSubscribedToModNetwork || IsSendingTexture)
return; return;
if (stickerSlot < 0 || stickerSlot >= _textureStorage.Length) if (stickerSlot < 0 || stickerSlot >= _textureStorage.Length)
{ {
LoggerInbound($"Received RequestTexture message from {sender} with invalid slot {stickerSlot}!"); LoggerInbound($"Received RequestTexture message from {sender} with invalid slot {stickerSlot}!");
return; return;
} }
if (_textureStorage[stickerSlot].textureHash != textureHash) if (_textureStorage[stickerSlot].textureHash != textureHash)
{ {
LoggerInbound($"Received RequestTexture message from {sender} with invalid texture hash {textureHash} for slot {stickerSlot}!"); LoggerInbound($"Received RequestTexture message from {sender} with invalid texture hash {textureHash} for slot {stickerSlot}!");
return; return;
} }
SendTexture(stickerSlot); SendTexture(stickerSlot);
} }
#endregion Inbound Methods #endregion Inbound Methods
} }

View file

@ -1,140 +1,140 @@
using ABI_RC.Core.IO; using ABI_RC.Core.IO;
using ABI_RC.Core.Networking.IO.Instancing; using ABI_RC.Core.Networking.IO.Instancing;
using ABI_RC.Core.UI; using ABI_RC.Core.UI;
using ABI_RC.Systems.GameEventSystem; using ABI_RC.Systems.GameEventSystem;
using NAK.Stickers.Networking; using NAK.Stickers.Networking;
using NAK.Stickers.Utilities; using NAK.Stickers.Utilities;
using ABI.CCK.Components; using ABI.CCK.Components;
using NAK.Stickers.Integrations; using NAK.Stickers.Integrations;
namespace NAK.Stickers; namespace NAK.Stickers;
public partial class StickerSystem public partial class StickerSystem
{ {
#region Singleton #region Singleton
public static StickerSystem Instance { get; private set; } public static StickerSystem Instance { get; private set; }
public static void Initialize() public static void Initialize()
{ {
if (Instance != null) if (Instance != null)
return; return;
Instance = new StickerSystem(); Instance = new StickerSystem();
// configure decalery // configure decalery
DecalManager.SetPreferredMode(DecalUtils.Mode.GPU, false, 0); DecalManager.SetPreferredMode(DecalUtils.Mode.GPU, false, 0);
// ensure cache folder exists // ensure cache folder exists
EnsureStickersFolderExists(); EnsureStickersFolderExists();
// listen for game events // listen for game events
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart); CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart);
} }
#endregion Singleton #endregion Singleton
#region Callback Registration #region Callback Registration
private void OnPlayerSetupStart() private void OnPlayerSetupStart()
{ {
// TODO: this can be spammed by world author toggling CVRWorld.enabled state // TODO: this can be spammed by world author toggling CVRWorld.enabled state
CVRGameEventSystem.World.OnLoad.AddListener(_ => OnWorldLoad()); CVRGameEventSystem.World.OnLoad.AddListener(_ => OnWorldLoad());
CVRGameEventSystem.World.OnUnload.AddListener(_ => OnWorldUnload()); CVRGameEventSystem.World.OnUnload.AddListener(_ => OnWorldUnload());
CVRGameEventSystem.Instance.OnConnected.AddListener((_) => { if (!Instances.IsReconnecting) OnInitialConnection(); }); CVRGameEventSystem.Instance.OnConnected.AddListener((_) => { if (!Instances.IsReconnecting) OnInitialConnection(); });
CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined); CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined);
CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft); CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft);
BetterScheduleSystem.AddJob(Instance.OnUpdate, 10f, -1); BetterScheduleSystem.AddJob(Instance.OnUpdate, 10f, -1);
LoadAllImagesAtStartup(); LoadAllImagesAtStartup();
} }
#endregion Callback Registration #endregion Callback Registration
#region Game Events #region Game Events
private void OnInitialConnection() private void OnInitialConnection()
{ {
ClearStickersSelf(); // clear stickers on remotes just in case we rejoined ClearStickersSelf(); // clear stickers on remotes just in case we rejoined
ModNetwork.Reset(); // reset network buffers and metadata ModNetwork.Reset(); // reset network buffers and metadata
} }
private void OnWorldLoad() private void OnWorldLoad()
{ {
CVRDataStore worldDS = CVRWorld.Instance.DataStore; CVRDataStore worldDS = CVRWorld.Instance.DataStore;
// IsRestrictedInstance = worldDS && worldDS.GetValue<bool>("StickersMod-ForceDisable"); // IsRestrictedInstance = worldDS && worldDS.GetValue<bool>("StickersMod-ForceDisable");
if (IsRestrictedInstance) StickerMod.Logger.Msg("Stickers are restricted by the world author."); if (IsRestrictedInstance) StickerMod.Logger.Msg("Stickers are restricted by the world author.");
BTKUIAddon.OnStickerRestrictionUpdated(IsRestrictedInstance); BTKUIAddon.OnStickerRestrictionUpdated(IsRestrictedInstance);
} }
private void OnWorldUnload() private void OnWorldUnload()
{ {
IsRestrictedInstance = false; IsRestrictedInstance = false;
CleanupAllButSelf(); CleanupAllButSelf();
} }
#endregion Game Events #endregion Game Events
#region Data #region Data
// private bool _isEnabled = true; // private bool _isEnabled = true;
// //
// public bool IsEnabled // public bool IsEnabled
// { // {
// get => _isEnabled; // get => _isEnabled;
// set // set
// { // {
// if (_isEnabled == value) // if (_isEnabled == value)
// return; // return;
// //
// _isEnabled = value; // _isEnabled = value;
// if (!_isEnabled) ClearAllStickers(); // if (!_isEnabled) ClearAllStickers();
// ModNetwork.IsEnabled = _isEnabled; // ModNetwork.IsEnabled = _isEnabled;
// } // }
// } // }
public bool IsRestrictedInstance { get; internal set; } public bool IsRestrictedInstance { get; internal set; }
private string SelectedStickerName => ModSettings.Hidden_SelectedStickerNames.Value[_selectedStickerSlot]; private string SelectedStickerName => ModSettings.Hidden_SelectedStickerNames.Value[_selectedStickerSlot];
private const float StickerKillTime = 30f; private const float StickerKillTime = 30f;
private const float StickerCooldown = 0.2f; private const float StickerCooldown = 0.2f;
private readonly Dictionary<string, StickerData> _playerStickers = new(); private readonly Dictionary<string, StickerData> _playerStickers = new();
internal const string PlayerLocalId = "_PLAYERLOCAL"; internal const string PlayerLocalId = "_PLAYERLOCAL";
private int _selectedStickerSlot; private int _selectedStickerSlot;
public int SelectedStickerSlot public int SelectedStickerSlot
{ {
get => _selectedStickerSlot; get => _selectedStickerSlot;
set set
{ {
_selectedStickerSlot = value < 0 ? ModSettings.MaxStickerSlots - 1 : value % ModSettings.MaxStickerSlots; _selectedStickerSlot = value < 0 ? ModSettings.MaxStickerSlots - 1 : value % ModSettings.MaxStickerSlots;
IsInStickerMode = IsInStickerMode; // refresh sticker mode IsInStickerMode = IsInStickerMode; // refresh sticker mode
} }
} }
private bool _isInStickerMode; private bool _isInStickerMode;
public bool IsInStickerMode public bool IsInStickerMode
{ {
get => _isInStickerMode; get => _isInStickerMode;
set set
{ {
_isInStickerMode = value && !IsRestrictedInstance; // ensure cannot enter when restricted _isInStickerMode = value && !IsRestrictedInstance; // ensure cannot enter when restricted
if (_isInStickerMode) if (_isInStickerMode)
{ {
CohtmlHud.Instance.SelectPropToSpawn( CohtmlHud.Instance.SelectPropToSpawn(
StickerCache.GetCohtmlResourcesPath(SelectedStickerName), StickerCache.GetCohtmlResourcesPath(SelectedStickerName),
Path.GetFileNameWithoutExtension(SelectedStickerName), Path.GetFileNameWithoutExtension(SelectedStickerName),
"Sticker selected for stickering:"); "Sticker selected for stickering:");
} }
else else
{ {
CohtmlHud.Instance.ClearPropToSpawn(); CohtmlHud.Instance.ClearPropToSpawn();
ClearStickerPreview(); ClearStickerPreview();
} }
} }
} }
#endregion Data #endregion Data
} }

View file

@ -1,195 +1,195 @@
using ABI_RC.Core; using ABI_RC.Core;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using ABI_RC.Systems.InputManagement; using ABI_RC.Systems.InputManagement;
using NAK.Stickers.Networking; using NAK.Stickers.Networking;
using UnityEngine; using UnityEngine;
namespace NAK.Stickers; namespace NAK.Stickers;
public partial class StickerSystem public partial class StickerSystem
{ {
#region Sticker Lifecycle #region Sticker Lifecycle
private StickerData GetOrCreateStickerData(string playerId) private StickerData GetOrCreateStickerData(string playerId)
{ {
if (_playerStickers.TryGetValue(playerId, out StickerData stickerData)) if (_playerStickers.TryGetValue(playerId, out StickerData stickerData))
return stickerData; return stickerData;
stickerData = new StickerData(playerId, ModSettings.MaxStickerSlots); stickerData = new StickerData(playerId, ModSettings.MaxStickerSlots);
_playerStickers[playerId] = stickerData; _playerStickers[playerId] = stickerData;
return stickerData; return stickerData;
} }
public void PlaceStickerFromControllerRay(Transform transform, CVRHand hand = CVRHand.Left, bool isPreview = false) public void PlaceStickerFromControllerRay(Transform transform, CVRHand hand = CVRHand.Left, bool isPreview = false)
{ {
Vector3 controllerForward = transform.forward; Vector3 controllerForward = transform.forward;
Vector3 controllerUp = transform.up; Vector3 controllerUp = transform.up;
Vector3 playerUp = PlayerSetup.Instance.transform.up; Vector3 playerUp = PlayerSetup.Instance.transform.up;
// extracting angle of controller ray on forward axis // extracting angle of controller ray on forward axis
Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized; Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized;
Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized; Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized;
float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp); float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp);
float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value; float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value;
Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold) Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold)
// leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop // leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop
? Vector3.Slerp(controllerUp, playerUp, 0.99f) ? Vector3.Slerp(controllerUp, playerUp, 0.99f)
: controllerUp; : controllerUp;
if (isPreview) if (isPreview)
{ {
PlaceStickerPreview(transform.position, controllerForward, targetUp); PlaceStickerPreview(transform.position, controllerForward, targetUp);
return; return;
} }
if (!PlaceStickerSelf(transform.position, transform.forward, targetUp)) if (!PlaceStickerSelf(transform.position, transform.forward, targetUp))
return; return;
CVRInputManager.Instance.Vibrate(0f, 0.1f, 10f, 0.1f, hand); CVRInputManager.Instance.Vibrate(0f, 0.1f, 10f, 0.1f, hand);
} }
private bool PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true) private bool PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true)
{ {
if (!AttemptPlaceSticker(PlayerLocalId, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value, position, forward, up, alignWithNormal, SelectedStickerSlot)) if (!AttemptPlaceSticker(PlayerLocalId, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value, position, forward, up, alignWithNormal, SelectedStickerSlot))
return false; // failed return false; // failed
// placed, now network // placed, now network
ModNetwork.SendPlaceSticker(SelectedStickerSlot, position, forward, up, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value); ModNetwork.SendPlaceSticker(SelectedStickerSlot, position, forward, up, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value);
return true; return true;
} }
private bool AttemptPlaceSticker(string playerId, StickerSize size, float opacity, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true, int stickerSlot = 0, bool isPreview = false) private bool AttemptPlaceSticker(string playerId, StickerSize size, float opacity, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true, int stickerSlot = 0, bool isPreview = false)
{ {
// if the world contained a gameobject with the [DisableStickers] name and restricted the instance disable stickers! // if the world contained a gameobject with the [DisableStickers] name and restricted the instance disable stickers!
if (IsRestrictedInstance) if (IsRestrictedInstance)
return false; return false;
StickerData stickerData = GetOrCreateStickerData(playerId); StickerData stickerData = GetOrCreateStickerData(playerId);
if (Time.time - stickerData.LastPlacedTime < StickerCooldown) if (Time.time - stickerData.LastPlacedTime < StickerCooldown)
return false; return false;
// Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal // Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal
const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15)); const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15));
if (!Physics.Raycast(position, forward, out RaycastHit hit, if (!Physics.Raycast(position, forward, out RaycastHit hit,
10f, LayerMask, QueryTriggerInteraction.Ignore)) 10f, LayerMask, QueryTriggerInteraction.Ignore))
return false; return false;
// if gameobject name starts with [NoSticker] then don't place sticker // if gameobject name starts with [NoSticker] then don't place sticker
if (hit.transform.gameObject.name.StartsWith("[NoSticker]")) if (hit.transform.gameObject.name.StartsWith("[NoSticker]"))
return false; return false;
if (isPreview) if (isPreview)
{ {
stickerData.PlacePreview(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot, size); stickerData.PlacePreview(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot, size);
return true; return true;
} }
stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot, size, opacity); stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot, size, opacity);
stickerData.PlayAudio(); stickerData.PlayAudio();
return true; return true;
} }
public void ClearStickersSelf() public void ClearStickersSelf()
{ {
ClearStickersForPlayer(PlayerLocalId); ClearStickersForPlayer(PlayerLocalId);
ModNetwork.SendClearAllStickers(); ModNetwork.SendClearAllStickers();
} }
public void ClearStickerSelf(int stickerSlot) public void ClearStickerSelf(int stickerSlot)
{ {
ClearStickersForPlayer(PlayerLocalId, stickerSlot); ClearStickersForPlayer(PlayerLocalId, stickerSlot);
ModNetwork.SendClearSticker(stickerSlot); ModNetwork.SendClearSticker(stickerSlot);
} }
private void ClearStickersForPlayer(string playerId) private void ClearStickersForPlayer(string playerId)
{ {
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
return; return;
stickerData.Clear(); stickerData.Clear();
} }
public void ClearStickersForPlayer(string playerId, int stickerSlot) public void ClearStickersForPlayer(string playerId, int stickerSlot)
{ {
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData)) if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
return; return;
stickerData.Clear(stickerSlot); stickerData.Clear(stickerSlot);
} }
private void SetTextureSelf(byte[] imageBytes, int stickerSlot = 0) private void SetTextureSelf(byte[] imageBytes, int stickerSlot = 0)
{ {
Texture2D texture = new(1, 1); // placeholder Texture2D texture = new(1, 1); // placeholder
texture.LoadImage(imageBytes); texture.LoadImage(imageBytes);
texture.Compress(true); // noachi said to do texture.Compress(true); // noachi said to do
ClearStickerSelf(stickerSlot); // clear placed stickers in-scene so we can't replace an entire wall at once ClearStickerSelf(stickerSlot); // clear placed stickers in-scene so we can't replace an entire wall at once
OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture, stickerSlot); OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture, stickerSlot);
ModNetwork.SetTexture(stickerSlot, imageBytes); ModNetwork.SetTexture(stickerSlot, imageBytes);
} }
public void ClearAllStickers() public void ClearAllStickers()
{ {
foreach (StickerData stickerData in _playerStickers.Values) foreach (StickerData stickerData in _playerStickers.Values)
stickerData.Clear(); stickerData.Clear();
ModNetwork.SendClearAllStickers(); ModNetwork.SendClearAllStickers();
} }
public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture, int stickerSlot = 0) public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture, int stickerSlot = 0)
{ {
StickerData stickerData = GetOrCreateStickerData(playerId); StickerData stickerData = GetOrCreateStickerData(playerId);
stickerData.SetTexture(textureHash, texture, stickerSlot); stickerData.SetTexture(textureHash, texture, stickerSlot);
} }
public bool HasTextureHash(string playerId, Guid textureHash) public bool HasTextureHash(string playerId, Guid textureHash)
{ {
StickerData stickerData = GetOrCreateStickerData(playerId); StickerData stickerData = GetOrCreateStickerData(playerId);
return stickerData.CheckHasTextureHash(textureHash); return stickerData.CheckHasTextureHash(textureHash);
} }
public void CleanupAll() public void CleanupAll()
{ {
foreach ((_, StickerData data) in _playerStickers) foreach ((_, StickerData data) in _playerStickers)
data.Cleanup(); data.Cleanup();
_playerStickers.Clear(); _playerStickers.Clear();
} }
public void CleanupAllButSelf() public void CleanupAllButSelf()
{ {
StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId); StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId);
foreach ((_, StickerData data) in _playerStickers) foreach ((_, StickerData data) in _playerStickers)
{ {
if (data == localStickerData) data.Clear(); if (data == localStickerData) data.Clear();
else data.Cleanup(); else data.Cleanup();
} }
_playerStickers.Clear(); _playerStickers.Clear();
_playerStickers[PlayerLocalId] = localStickerData; _playerStickers[PlayerLocalId] = localStickerData;
} }
public void PlaceStickerPreview(Vector3 position, Vector3 forward, Vector3 up) public void PlaceStickerPreview(Vector3 position, Vector3 forward, Vector3 up)
{ {
AttemptPlaceSticker(PlayerLocalId, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value, position, forward, up, true, SelectedStickerSlot, true); AttemptPlaceSticker(PlayerLocalId, ModSettings.Entry_StickerSize.Value, ModSettings.Entry_StickerOpacity.Value, position, forward, up, true, SelectedStickerSlot, true);
} }
public void UpdateStickerPreview() public void UpdateStickerPreview()
{ {
if (!IsInStickerMode) return; if (!IsInStickerMode) return;
StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId); StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId);
localStickerData.ClearPreview(); // clear prior frames sticker preview localStickerData.ClearPreview(); // clear prior frames sticker preview
localStickerData.UpdatePreview(SelectedStickerSlot); localStickerData.UpdatePreview(SelectedStickerSlot);
} }
public void ClearStickerPreview() public void ClearStickerPreview()
{ {
StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId); StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId);
localStickerData.ClearPreview(); localStickerData.ClearPreview();
} }
#endregion Sticker Lifecycle #endregion Sticker Lifecycle
} }