Stickers: added some code

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

View file

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

View file

@ -0,0 +1,9 @@
namespace NAK.Stickers;
internal enum SFXType
{
LittleBigPlanetSticker,
SourceEngineSpray,
FactorioAlertDestroyed,
None
}

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

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

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

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

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

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

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

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

View file

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

View file

@ -2,131 +2,213 @@
using UnityEngine;
using Object = UnityEngine.Object;
namespace NAK.Stickers;
public class StickerData
namespace NAK.Stickers
{
private const float DECAL_SIZE = 0.25f;
public float DeathTime; // when a remote player leaves, we need to kill their stickers
public Guid TextureHash;
public float LastPlacedTime;
private Vector3 _lastPlacedPosition = Vector3.zero;
public readonly bool IsLocal;
private readonly DecalType _decal;
private readonly DecalSpawner _spawner;
private readonly AudioSource _audioSource;
public StickerData(bool isLocal)
public class StickerData
{
IsLocal = isLocal;
private const float DECAL_SIZE = 0.25f;
private static readonly int s_EmissionStrengthID = Shader.PropertyToID("_EmissionStrength");
public float DeathTime; // when a remote player leaves, we need to kill their stickers
public float LastPlacedTime;
private Vector3 _lastPlacedPosition = Vector3.zero;
public readonly bool IsLocal;
private readonly DecalType _decal;
private readonly DecalSpawner[] _decalSpawners;
_decal = ScriptableObject.CreateInstance<DecalType>();
_decal.decalSettings = new DecalSpawner.InitData
private readonly Guid[] _textureHashes;
private readonly Material[] _materials;
private readonly AudioSource _audioSource;
public StickerData(bool isLocal, int decalSpawnersCount)
{
material = new Material(StickerMod.DecalSimpleShader)
IsLocal = isLocal;
_decal = ScriptableObject.CreateInstance<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),
},
useShaderReplacement = false,
inheritMaterialProperties = false,
inheritMaterialPropertyBlock = false,
};
_spawner = 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
_materials[i] = new Material(StickerMod.DecalSimpleShader);
_decal.decalSettings = new DecalSpawner.InitData
{
material = _materials[i],
useShaderReplacement = false,
inheritMaterialProperties = false,
inheritMaterialPropertyBlock = false,
};
_decalSpawners[i] = DecalManager.GetSpawner(_decal.decalSettings, 4096, 1024);
}
if (IsLocal) StickerMod.Logger.Msg($"Set texture filter mode to: {texture.filterMode}");
Material material = _decal.decalSettings.material;
// TODO: fix
if (material.mainTexture != null) Object.Destroy(material.mainTexture);
material.mainTexture = texture;
}
public void Place(RaycastHit hit, Vector3 forwardDirection, Vector3 upDirection)
{
Transform rootObject = null;
if (hit.rigidbody != null) rootObject = hit.rigidbody.transform;
_lastPlacedPosition = hit.point;
LastPlacedTime = Time.time;
// todo: add decal to queue
_spawner.AddDecal(
_lastPlacedPosition, Quaternion.LookRotation(forwardDirection, upDirection),
hit.collider.gameObject,
DECAL_SIZE, DECAL_SIZE, 1f, 1f, 0f, rootObject);
}
public void Clear()
{
// BUG?: Release does not clear dictionary's, clearing them myself so we can hit same objects again
// maybe it was intended to recycle groups- but rn no work like that
_spawner.Release();
_spawner.staticGroups.Clear();
_spawner.movableGroups.Clear();
}
public void Cleanup()
{
Clear();
// no leaking textures or mats
Material material = _decal.decalSettings.material;
Object.Destroy(material.mainTexture);
Object.Destroy(material);
Object.Destroy(_decal);
}
public void PlayAudio()
{
_audioSource.transform.position = _lastPlacedPosition;
switch (ModSettings.Entry_SelectedSFX.Value)
{
case ModSettings.SFXType.Source:
_audioSource.PlayOneShot(StickerMod.SourceSFXPlayerSprayer);
break;
case ModSettings.SFXType.LBP:
_audioSource.PlayOneShot(StickerMod.LittleBigPlanetStickerPlace);
break;
case ModSettings.SFXType.None:
break;
default:
throw new ArgumentOutOfRangeException();
_audioSource = new GameObject("StickerAudioSource").AddComponent<AudioSource>();
_audioSource.spatialBlend = 1f;
_audioSource.volume = 0.5f;
_audioSource.playOnAwake = false;
_audioSource.loop = false;
_audioSource.rolloffMode = AudioRolloffMode.Logarithmic;
_audioSource.maxDistance = 5f;
_audioSource.minDistance = 1f;
_audioSource.outputAudioMixerGroup = RootLogic.Instance.propSfx; // props are close enough to stickers
if (isLocal) Object.DontDestroyOnLoad(_audioSource.gameObject); // keep audio source through world transitions
}
}
public void SetAlpha(float alpha)
{
Material material = _decal.decalSettings.material;
material.color = new Color(material.color.r, material.color.g, material.color.b, alpha);
public Guid GetTextureHash(int spawnerIndex = 0)
{
if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length)
{
StickerMod.Logger.Warning("Invalid spawner index!");
return Guid.Empty;
}
return _textureHashes[spawnerIndex];
}
public bool CheckHasTextureHash(Guid textureHash)
{
foreach (Guid hash in _textureHashes) if (hash == textureHash) return true;
return false;
}
public void SetTexture(Guid textureHash, Texture2D texture, int spawnerIndex = 0)
{
if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length)
{
StickerMod.Logger.Warning("Invalid spawner index!");
return;
}
if (texture == null)
{
StickerMod.Logger.Warning("Assigning null texture to StickerData!");
return;
}
_textureHashes[spawnerIndex] = textureHash;
texture.wrapMode = TextureWrapMode.Clamp; // prevents white edges
texture.filterMode = texture.width > 64 || texture.height > 64
? FilterMode.Bilinear // smear it cause its fat
: FilterMode.Point; // my minecraft skin looked shit
if (IsLocal) StickerMod.Logger.Msg($"Set texture filter mode to: {texture.filterMode}");
Material material = _materials[spawnerIndex];
// Destroy the previous texture to avoid memory leaks
if (material.mainTexture != null) Object.Destroy(material.mainTexture);
material.mainTexture = texture;
}
public void Place(RaycastHit hit, Vector3 forwardDirection, Vector3 upDirection, int spawnerIndex = 0)
{
if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length)
{
StickerMod.Logger.Warning("Invalid spawner index!");
return;
}
Transform rootObject = null;
if (hit.rigidbody != null) rootObject = hit.rigidbody.transform;
_lastPlacedPosition = hit.point;
LastPlacedTime = Time.time;
// Add decal to the specified spawner
_decalSpawners[spawnerIndex].AddDecal(
_lastPlacedPosition, Quaternion.LookRotation(forwardDirection, upDirection),
hit.collider.gameObject,
DECAL_SIZE, DECAL_SIZE, 1f, 1f, 0f, rootObject);
}
public void Clear()
{
foreach (DecalSpawner spawner in _decalSpawners)
{
spawner.Release();
spawner.staticGroups.Clear();
spawner.movableGroups.Clear();
}
}
public void Clear(int spawnerIndex)
{
if (spawnerIndex < 0 || spawnerIndex >= _decalSpawners.Length)
{
StickerMod.Logger.Warning("Invalid spawner index!");
return;
}
_decalSpawners[spawnerIndex].Release();
_decalSpawners[spawnerIndex].staticGroups.Clear();
_decalSpawners[spawnerIndex].movableGroups.Clear();
}
public void Cleanup()
{
for (int i = 0; i < _decalSpawners.Length; i++)
{
_decalSpawners[i].Release();
_decalSpawners[i].staticGroups.Clear();
_decalSpawners[i].movableGroups.Clear();
// Clean up textures and materials
if (_materials[i].mainTexture != null) Object.Destroy(_materials[i].mainTexture);
Object.Destroy(_materials[i]);
}
Object.Destroy(_decal);
}
public void PlayAudio()
{
_audioSource.transform.position = _lastPlacedPosition;
switch (ModSettings.Entry_SelectedSFX.Value)
{
case SFXType.SourceEngineSpray:
_audioSource.PlayOneShot(StickerMod.SourceSFXPlayerSprayer);
break;
case SFXType.LittleBigPlanetSticker:
_audioSource.PlayOneShot(StickerMod.LittleBigPlanetSFXStickerPlace);
break;
case SFXType.FactorioAlertDestroyed:
_audioSource.PlayOneShot(StickerMod.FactorioSFXAlertDestroyed);
break;
case SFXType.None:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public void SetAlpha(float alpha)
{
foreach (Material material in _materials)
{
Color color = material.color;
color.a = alpha;
material.color = color;
}
}
#region shitty identify
public void Identify()
{
Color color = Color.HSVToRGB(Time.time % 1f, 1f, 1f);
foreach (Material material in _materials)
material.color = color; // cycle rainbow
}
public void ResetIdentify()
{
foreach (Material material in _materials)
material.color = Color.white;
}
#endregion shitty identify
}
}

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

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

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

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

View file

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

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