mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-04 07:19:22 +00:00
Stickers: added some code
This commit is contained in:
parent
07d6dff0a9
commit
58079d9390
36 changed files with 2092 additions and 1651 deletions
40
Stickers/Stickers/Enums/StickerKeyBinds.cs
Normal file
40
Stickers/Stickers/Enums/StickerKeyBinds.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
internal enum KeyBind
|
||||
{
|
||||
A = KeyCode.A,
|
||||
B = KeyCode.B,
|
||||
C = KeyCode.C,
|
||||
D = KeyCode.D,
|
||||
E = KeyCode.E,
|
||||
F = KeyCode.F,
|
||||
G = KeyCode.G,
|
||||
H = KeyCode.H,
|
||||
I = KeyCode.I,
|
||||
J = KeyCode.J,
|
||||
K = KeyCode.K,
|
||||
L = KeyCode.L,
|
||||
M = KeyCode.M,
|
||||
N = KeyCode.N,
|
||||
O = KeyCode.O,
|
||||
P = KeyCode.P,
|
||||
Q = KeyCode.Q,
|
||||
R = KeyCode.R,
|
||||
S = KeyCode.S,
|
||||
T = KeyCode.T,
|
||||
U = KeyCode.U,
|
||||
V = KeyCode.V,
|
||||
W = KeyCode.W,
|
||||
X = KeyCode.X,
|
||||
Y = KeyCode.Y,
|
||||
Z = KeyCode.Z,
|
||||
Mouse0 = KeyCode.Mouse0,
|
||||
Mouse1 = KeyCode.Mouse1,
|
||||
Mouse2 = KeyCode.Mouse2,
|
||||
Mouse3 = KeyCode.Mouse3,
|
||||
Mouse4 = KeyCode.Mouse4,
|
||||
Mouse5 = KeyCode.Mouse5,
|
||||
Mouse6 = KeyCode.Mouse6,
|
||||
}
|
9
Stickers/Stickers/Enums/StickerSFXType.cs
Normal file
9
Stickers/Stickers/Enums/StickerSFXType.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace NAK.Stickers;
|
||||
|
||||
internal enum SFXType
|
||||
{
|
||||
LittleBigPlanetSticker,
|
||||
SourceEngineSpray,
|
||||
FactorioAlertDestroyed,
|
||||
None
|
||||
}
|
16
Stickers/Stickers/Networking/ModNetwork.Constants.cs
Normal file
16
Stickers/Stickers/Networking/ModNetwork.Constants.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using NAK.Stickers.Properties;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Constants
|
||||
|
||||
internal const int MaxTextureSize = 1024 * 256; // 256KB
|
||||
|
||||
private const string ModId = $"MelonMod.NAK.Stickers_v{AssemblyInfoParams.Version}";
|
||||
private const int ChunkSize = 1024; // roughly 1KB per ModNetworkMessage
|
||||
private const int MaxChunkCount = MaxTextureSize / ChunkSize;
|
||||
|
||||
#endregion Constants
|
||||
}
|
21
Stickers/Stickers/Networking/ModNetwork.Enums.cs
Normal file
21
Stickers/Stickers/Networking/ModNetwork.Enums.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Enums
|
||||
|
||||
// Remote clients will request textures from the sender if they receive PlaceSticker with a textureHash they don't have
|
||||
|
||||
private enum MessageType : byte
|
||||
{
|
||||
PlaceSticker = 0, // stickerSlot, textureHash, position, forward, up
|
||||
ClearSticker = 1, // stickerSlot
|
||||
ClearAllStickers = 2, // none
|
||||
StartTexture = 3, // stickerSlot, textureHash, chunkCount, width, height
|
||||
SendTexture = 4, // chunkIdx, chunkData
|
||||
EndTexture = 5, // none
|
||||
RequestTexture = 6 // stickerSlot, textureHash
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
}
|
18
Stickers/Stickers/Networking/ModNetwork.Events.cs
Normal file
18
Stickers/Stickers/Networking/ModNetwork.Events.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using MTJobSystem;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Events
|
||||
|
||||
public static Action<bool> OnTextureOutboundStateChanged;
|
||||
|
||||
private static void InvokeTextureOutboundStateChanged(bool isSending)
|
||||
{
|
||||
MTJobManager.RunOnMainThread("ModNetwork.InvokeTextureOutboundStateChanged",
|
||||
() => OnTextureOutboundStateChanged?.Invoke(isSending));
|
||||
}
|
||||
|
||||
#endregion Events
|
||||
}
|
18
Stickers/Stickers/Networking/ModNetwork.Helpers.cs
Normal file
18
Stickers/Stickers/Networking/ModNetwork.Helpers.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using ABI_RC.Core.Networking;
|
||||
using DarkRift;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Private Methods
|
||||
|
||||
private static bool IsConnectedToGameNetwork()
|
||||
{
|
||||
return NetworkManager.Instance != null
|
||||
&& NetworkManager.Instance.GameNetwork != null
|
||||
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
}
|
222
Stickers/Stickers/Networking/ModNetwork.Inbound.cs
Normal file
222
Stickers/Stickers/Networking/ModNetwork.Inbound.cs
Normal file
|
@ -0,0 +1,222 @@
|
|||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Inbound Buffers
|
||||
|
||||
private static readonly Dictionary<string, byte[]> _textureChunkBuffers = new();
|
||||
private static readonly Dictionary<string, int> _receivedChunkCounts = new();
|
||||
private static readonly Dictionary<string, int> _expectedChunkCounts = new();
|
||||
private static readonly Dictionary<string, (int stickerSlot, Guid Hash, int Width, int Height)> _textureMetadata = new();
|
||||
|
||||
#endregion Inbound Buffers
|
||||
|
||||
#region Inbound Methods
|
||||
|
||||
private static void HandleMessageReceived(ModNetworkMessage msg)
|
||||
{
|
||||
string sender = msg.Sender;
|
||||
msg.Read(out byte msgTypeRaw);
|
||||
|
||||
if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
|
||||
return;
|
||||
|
||||
if (_disallowedForSession.Contains(sender))
|
||||
return; // ignore messages from disallowed users
|
||||
|
||||
if (MetaPort.Instance.blockedUserIds.Contains(sender))
|
||||
return; // ignore messages from blocked users
|
||||
|
||||
LoggerInbound($"Received message from {msg.Sender}, Type: {(MessageType)msgTypeRaw}");
|
||||
|
||||
switch ((MessageType)msgTypeRaw)
|
||||
{
|
||||
case MessageType.PlaceSticker:
|
||||
HandlePlaceSticker(msg);
|
||||
break;
|
||||
|
||||
case MessageType.ClearSticker:
|
||||
HandleClearSticker(msg);
|
||||
break;
|
||||
|
||||
case MessageType.ClearAllStickers:
|
||||
HandleClearAllStickers(msg);
|
||||
break;
|
||||
|
||||
case MessageType.StartTexture:
|
||||
HandleStartTexture(msg);
|
||||
break;
|
||||
|
||||
case MessageType.SendTexture:
|
||||
HandleSendTexture(msg);
|
||||
break;
|
||||
|
||||
case MessageType.EndTexture:
|
||||
HandleEndTexture(msg);
|
||||
break;
|
||||
|
||||
case MessageType.RequestTexture:
|
||||
HandleRequestTexture(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
LoggerInbound($"Invalid message type received: {msgTypeRaw}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandlePlaceSticker(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out int stickerSlot);
|
||||
msg.Read(out Guid textureHash);
|
||||
msg.Read(out Vector3 position);
|
||||
msg.Read(out Vector3 forward);
|
||||
msg.Read(out Vector3 up);
|
||||
|
||||
if (!StickerSystem.Instance.HasTextureHash(msg.Sender, textureHash))
|
||||
SendRequestTexture(stickerSlot, textureHash);
|
||||
|
||||
StickerSystem.Instance.OnStickerPlaceReceived(msg.Sender, stickerSlot, position, forward, up);
|
||||
}
|
||||
|
||||
private static void HandleClearSticker(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out int stickerSlot);
|
||||
StickerSystem.Instance.OnStickerClearReceived(msg.Sender, stickerSlot);
|
||||
}
|
||||
|
||||
private static void HandleClearAllStickers(ModNetworkMessage msg)
|
||||
{
|
||||
StickerSystem.Instance.OnStickerClearAllReceived(msg.Sender);
|
||||
}
|
||||
|
||||
private static void HandleStartTexture(ModNetworkMessage msg)
|
||||
{
|
||||
string sender = msg.Sender;
|
||||
msg.Read(out int stickerSlot);
|
||||
msg.Read(out Guid textureHash);
|
||||
msg.Read(out int chunkCount);
|
||||
msg.Read(out int width);
|
||||
msg.Read(out int height);
|
||||
|
||||
if (_textureChunkBuffers.ContainsKey(sender))
|
||||
{
|
||||
LoggerInbound($"Received StartTexture message from {sender} while still receiving texture data!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (StickerSystem.Instance.HasTextureHash(sender, textureHash))
|
||||
{
|
||||
LoggerInbound($"Received StartTexture message from {sender} with existing texture hash {textureHash}, skipping texture data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunkCount > MaxChunkCount)
|
||||
{
|
||||
LoggerInbound($"Received StartTexture message from {sender} with too many chunks: {chunkCount}", true);
|
||||
return;
|
||||
}
|
||||
|
||||
_textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)];
|
||||
_receivedChunkCounts[sender] = 0;
|
||||
_expectedChunkCounts[sender] = chunkCount;
|
||||
_textureMetadata[sender] = (stickerSlot, textureHash, width, height);
|
||||
|
||||
LoggerInbound($"Received StartTexture message from {sender}: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Resolution: {width}x{height}");
|
||||
}
|
||||
|
||||
private static void HandleSendTexture(ModNetworkMessage msg)
|
||||
{
|
||||
string sender = msg.Sender;
|
||||
msg.Read(out int chunkIdx);
|
||||
msg.Read(out byte[] chunkData);
|
||||
|
||||
if (!_textureChunkBuffers.TryGetValue(sender, out var buffer))
|
||||
return;
|
||||
|
||||
int startIndex = chunkIdx * ChunkSize;
|
||||
Array.Copy(chunkData, 0, buffer, startIndex, chunkData.Length);
|
||||
|
||||
_receivedChunkCounts[sender]++;
|
||||
if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender])
|
||||
return;
|
||||
|
||||
(int stickerSlot, Guid Hash, int Width, int Height) metadata = _textureMetadata[sender];
|
||||
|
||||
// All chunks received, reassemble texture
|
||||
_textureChunkBuffers.Remove(sender);
|
||||
_receivedChunkCounts.Remove(sender);
|
||||
_expectedChunkCounts.Remove(sender);
|
||||
_textureMetadata.Remove(sender);
|
||||
|
||||
// Validate image
|
||||
if (!ImageUtility.IsValidImage(buffer))
|
||||
{
|
||||
LoggerInbound($"[Inbound] Received texture data is not a valid image from {sender}!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data TODO: fix hash???????
|
||||
(Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer);
|
||||
if (metadata.Width != width
|
||||
|| metadata.Height != height)
|
||||
{
|
||||
LoggerInbound($"Received texture data does not match metadata! Expected: {metadata.Hash} ({metadata.Width}x{metadata.Height}), received: {imageHash} ({width}x{height})", true);
|
||||
return;
|
||||
}
|
||||
|
||||
Texture2D texture = new(1,1);
|
||||
texture.LoadImage(buffer);
|
||||
texture.Compress(true);
|
||||
|
||||
StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture, metadata.stickerSlot);
|
||||
|
||||
LoggerInbound($"All chunks received and texture reassembled from {sender}. " +
|
||||
$"Texture size: {metadata.Width}x{metadata.Height}");
|
||||
}
|
||||
|
||||
private static void HandleEndTexture(ModNetworkMessage msg)
|
||||
{
|
||||
string sender = msg.Sender;
|
||||
if (!_textureChunkBuffers.ContainsKey(sender))
|
||||
return;
|
||||
|
||||
LoggerInbound($"Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received.");
|
||||
|
||||
_textureChunkBuffers.Remove(sender);
|
||||
_receivedChunkCounts.Remove(sender);
|
||||
_expectedChunkCounts.Remove(sender);
|
||||
_textureMetadata.Remove(sender);
|
||||
}
|
||||
|
||||
private static void HandleRequestTexture(ModNetworkMessage msg)
|
||||
{
|
||||
string sender = msg.Sender;
|
||||
msg.Read(out int stickerSlot);
|
||||
msg.Read(out Guid textureHash);
|
||||
|
||||
if (!_isSubscribedToModNetwork || IsSendingTexture)
|
||||
return;
|
||||
|
||||
if (stickerSlot < 0 || stickerSlot >= _textureStorage.Length)
|
||||
{
|
||||
LoggerInbound($"Received RequestTexture message from {sender} with invalid slot {stickerSlot}!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_textureStorage[stickerSlot].textureHash != textureHash)
|
||||
{
|
||||
LoggerInbound($"Received RequestTexture message from {sender} with invalid texture hash {textureHash} for slot {stickerSlot}!");
|
||||
return;
|
||||
}
|
||||
|
||||
SendTexture(stickerSlot);
|
||||
}
|
||||
|
||||
#endregion Inbound Methods
|
||||
}
|
22
Stickers/Stickers/Networking/ModNetwork.Logging.cs
Normal file
22
Stickers/Stickers/Networking/ModNetwork.Logging.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Network Logging
|
||||
|
||||
private static void LoggerInbound(string message, bool isWarning = false)
|
||||
{
|
||||
if (!ModSettings.Debug_NetworkInbound.Value) return;
|
||||
if (isWarning) StickerMod.Logger.Warning("[Inbound] " + message);
|
||||
else StickerMod.Logger.Msg("[Inbound] " + message);
|
||||
}
|
||||
|
||||
private static void LoggerOutbound(string message, bool isWarning = false)
|
||||
{
|
||||
if (!ModSettings.Debug_NetworkOutbound.Value) return;
|
||||
if (isWarning) StickerMod.Logger.Warning("[Outbound] " + message);
|
||||
else StickerMod.Logger.Msg("[Outbound] " + message);
|
||||
}
|
||||
|
||||
#endregion Network Logging
|
||||
}
|
48
Stickers/Stickers/Networking/ModNetwork.Main.cs
Normal file
48
Stickers/Stickers/Networking/ModNetwork.Main.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using ABI_RC.Systems.ModNetwork;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Mod Network Internals
|
||||
|
||||
public static bool IsSendingTexture { get; private set; }
|
||||
private static bool _isSubscribedToModNetwork;
|
||||
|
||||
internal static void Subscribe()
|
||||
{
|
||||
ModNetworkManager.Subscribe(ModId, HandleMessageReceived);
|
||||
|
||||
_isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId);
|
||||
if (!_isSubscribedToModNetwork) StickerMod.Logger.Error("Failed to subscribe to Mod Network! This should not happen.");
|
||||
}
|
||||
|
||||
#endregion Mod Network Internals
|
||||
|
||||
#region Disallow For Session
|
||||
|
||||
private static readonly HashSet<string> _disallowedForSession = new();
|
||||
|
||||
public static bool IsPlayerACriminal(string playerID)
|
||||
{
|
||||
return _disallowedForSession.Contains(playerID);
|
||||
}
|
||||
|
||||
public static void HandleDisallowForSession(string playerID, bool isOn)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerID)) return;
|
||||
|
||||
if (isOn)
|
||||
{
|
||||
_disallowedForSession.Add(playerID);
|
||||
StickerMod.Logger.Msg($"Player {playerID} has been disallowed from using stickers for this session.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_disallowedForSession.Remove(playerID);
|
||||
StickerMod.Logger.Msg($"Player {playerID} has been allowed to use stickers again.");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Disallow For Session
|
||||
}
|
189
Stickers/Stickers/Networking/ModNetwork.Outbound.cs
Normal file
189
Stickers/Stickers/Networking/ModNetwork.Outbound.cs
Normal file
|
@ -0,0 +1,189 @@
|
|||
using ABI_RC.Systems.ModNetwork;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers.Networking;
|
||||
|
||||
public static partial class ModNetwork
|
||||
{
|
||||
#region Texture Data
|
||||
|
||||
private static readonly (byte[] textureData, Guid textureHash, int width, int height)[] _textureStorage = new (byte[], Guid, int, int)[ModSettings.MaxStickerSlots];
|
||||
|
||||
public static bool SetTexture(int stickerSlot, byte[] imageBytes)
|
||||
{
|
||||
if (imageBytes == null
|
||||
|| imageBytes.Length == 0
|
||||
|| imageBytes.Length > MaxTextureSize)
|
||||
return false;
|
||||
|
||||
(Guid hashGuid, int width, int height) = ImageUtility.ExtractImageInfo(imageBytes);
|
||||
if (_textureStorage[stickerSlot].textureHash == hashGuid)
|
||||
{
|
||||
LoggerOutbound($"Texture data is the same as the current texture for slot {stickerSlot}: {hashGuid}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_textureStorage[stickerSlot] = (imageBytes, hashGuid, width, height);
|
||||
LoggerOutbound($"Set texture data for slot {stickerSlot}, metadata: {hashGuid} ({width}x{height})");
|
||||
|
||||
SendTexture(stickerSlot);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion Texture Data
|
||||
|
||||
#region Outbound Methods
|
||||
|
||||
public static void SendPlaceSticker(int stickerSlot, Vector3 position, Vector3 forward, Vector3 up)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.PlaceSticker);
|
||||
modMsg.Write(stickerSlot);
|
||||
modMsg.Write(_textureStorage[stickerSlot].textureHash);
|
||||
modMsg.Write(position);
|
||||
modMsg.Write(forward);
|
||||
modMsg.Write(up);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound($"PlaceSticker: Slot: {stickerSlot}, Hash: {_textureStorage[stickerSlot].textureHash}, Position: {position}, Forward: {forward}, Up: {up}");
|
||||
}
|
||||
|
||||
public static void SendClearSticker(int stickerSlot)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.ClearSticker);
|
||||
modMsg.Write(stickerSlot);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound($"ClearSticker: Slot: {stickerSlot}");
|
||||
}
|
||||
|
||||
public static void SendClearAllStickers()
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.ClearAllStickers);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound("ClearAllStickers");
|
||||
}
|
||||
|
||||
public static void SendStartTexture(int stickerSlot, Guid textureHash, int chunkCount, int width, int height)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.StartTexture);
|
||||
modMsg.Write(stickerSlot);
|
||||
modMsg.Write(textureHash);
|
||||
modMsg.Write(chunkCount);
|
||||
modMsg.Write(width);
|
||||
modMsg.Write(height);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound($"StartTexture: Slot: {stickerSlot}, Hash: {textureHash}, Chunks: {chunkCount}, Size: {width}x{height}");
|
||||
}
|
||||
|
||||
public static void SendTextureChunk(int chunkIdx, byte[] chunkData)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.SendTexture);
|
||||
modMsg.Write(chunkIdx);
|
||||
modMsg.Write(chunkData);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound($"SendTextureChunk: Index: {chunkIdx}, Size: {chunkData.Length} bytes");
|
||||
}
|
||||
|
||||
public static void SendEndTexture()
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.EndTexture);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound("EndTexture");
|
||||
}
|
||||
|
||||
public static void SendRequestTexture(int stickerSlot, Guid textureHash)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.RequestTexture);
|
||||
modMsg.Write(stickerSlot);
|
||||
modMsg.Write(textureHash);
|
||||
modMsg.Send();
|
||||
|
||||
LoggerOutbound($"RequestTexture: Slot: {stickerSlot}, Hash: {textureHash}");
|
||||
}
|
||||
|
||||
public static void SendTexture(int stickerSlot)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork() || IsSendingTexture)
|
||||
return;
|
||||
|
||||
IsSendingTexture = true;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var textureData = _textureStorage[stickerSlot].textureData;
|
||||
var textureHash = _textureStorage[stickerSlot].textureHash;
|
||||
var width = _textureStorage[stickerSlot].width;
|
||||
var height = _textureStorage[stickerSlot].height;
|
||||
int totalChunks = Mathf.CeilToInt(textureData.Length / (float)ChunkSize);
|
||||
if (totalChunks > MaxChunkCount)
|
||||
{
|
||||
LoggerOutbound($"Texture data too large to send for slot {stickerSlot}: {textureData.Length} bytes, {totalChunks} chunks", true);
|
||||
return;
|
||||
}
|
||||
|
||||
LoggerOutbound($"Sending texture for slot {stickerSlot}: {textureData.Length} bytes, Chunks: {totalChunks}, Resolution: {width}x{height}");
|
||||
|
||||
SendStartTexture(stickerSlot, textureHash, totalChunks, width, height);
|
||||
|
||||
for (int i = 0; i < textureData.Length; i += ChunkSize)
|
||||
{
|
||||
int size = Mathf.Min(ChunkSize, textureData.Length - i);
|
||||
byte[] chunk = new byte[size];
|
||||
Array.Copy(textureData, i, chunk, 0, size);
|
||||
|
||||
SendTextureChunk(i / ChunkSize, chunk);
|
||||
Thread.Sleep(5); // Simulate network latency
|
||||
}
|
||||
|
||||
SendEndTexture();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoggerOutbound($"Failed to send texture for slot {stickerSlot}: {e}", true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSendingTexture = false;
|
||||
InvokeTextureOutboundStateChanged(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion Outbound Methods
|
||||
}
|
|
@ -1,414 +0,0 @@
|
|||
using ABI_RC.Core.Networking;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
using DarkRift;
|
||||
using NAK.Stickers.Properties;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers.Networking
|
||||
{
|
||||
public static class ModNetwork
|
||||
{
|
||||
#region Configuration
|
||||
|
||||
private static bool Debug_NetworkInbound => ModSettings.Debug_NetworkInbound.Value;
|
||||
private static bool Debug_NetworkOutbound => ModSettings.Debug_NetworkOutbound.Value;
|
||||
|
||||
#endregion Configuration
|
||||
|
||||
#region Constants
|
||||
|
||||
internal const int MaxTextureSize = 1024 * 256; // 256KB
|
||||
|
||||
private const string ModId = $"MelonMod.NAK.Stickers_v{AssemblyInfoParams.Version}";
|
||||
private const int ChunkSize = 1024; // roughly 1KB per ModNetworkMessage
|
||||
private const int MaxChunkCount = MaxTextureSize / ChunkSize;
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Enums
|
||||
|
||||
private enum MessageType : byte
|
||||
{
|
||||
PlaceSticker = 0,
|
||||
ClearStickers = 1,
|
||||
StartTexture = 2,
|
||||
SendTexture = 3,
|
||||
EndTexture = 4,
|
||||
RequestTexture = 5
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
|
||||
#region Mod Network Internals
|
||||
|
||||
internal static Action<bool> OnTextureOutboundStateChanged;
|
||||
|
||||
internal static bool IsSendingTexture { get; private set; }
|
||||
private static bool _isSubscribedToModNetwork;
|
||||
|
||||
private static byte[] _ourTextureData;
|
||||
private static (Guid hashGuid, int Width, int Height) _ourTextureMetadata;
|
||||
|
||||
private static readonly Dictionary<string, byte[]> _textureChunkBuffers = new();
|
||||
private static readonly Dictionary<string, int> _receivedChunkCounts = new();
|
||||
private static readonly Dictionary<string, int> _expectedChunkCounts = new();
|
||||
private static readonly Dictionary<string, (Guid Hash, int Width, int Height)> _textureMetadata = new();
|
||||
|
||||
internal static void Subscribe()
|
||||
{
|
||||
ModNetworkManager.Subscribe(ModId, OnMessageReceived);
|
||||
|
||||
_isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId);
|
||||
if (!_isSubscribedToModNetwork)
|
||||
StickerMod.Logger.Error("Failed to subscribe to Mod Network!");
|
||||
}
|
||||
|
||||
public static void PlaceSticker(Vector3 position, Vector3 forward, Vector3 up)
|
||||
{
|
||||
if (!_isSubscribedToModNetwork)
|
||||
return;
|
||||
|
||||
SendStickerPlace(_ourTextureMetadata.hashGuid, position, forward, up);
|
||||
}
|
||||
|
||||
public static void ClearStickers()
|
||||
{
|
||||
if (!_isSubscribedToModNetwork)
|
||||
return;
|
||||
|
||||
SendMessage(MessageType.ClearStickers);
|
||||
}
|
||||
|
||||
public static bool SetTexture(byte[] imageBytes)
|
||||
{
|
||||
if (imageBytes == null
|
||||
|| imageBytes.Length == 0
|
||||
|| imageBytes.Length > MaxTextureSize)
|
||||
return false;
|
||||
|
||||
(Guid hashGuid, int width, int height) = ImageUtility.ExtractImageInfo(imageBytes);
|
||||
if (_ourTextureMetadata.hashGuid == hashGuid)
|
||||
{
|
||||
StickerMod.Logger.Msg($"[ModNetwork] Texture data is the same as the current texture: {hashGuid}");
|
||||
return false;
|
||||
}
|
||||
|
||||
_ourTextureData = imageBytes;
|
||||
_ourTextureMetadata = (hashGuid, width, height);
|
||||
StickerMod.Logger.Msg($"[ModNetwork] Set texture metadata for networking: {hashGuid} ({width}x{height})");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void SendTexture()
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
if (!_isSubscribedToModNetwork || IsSendingTexture)
|
||||
return;
|
||||
|
||||
if (_ourTextureData == null
|
||||
|| _ourTextureMetadata.hashGuid == Guid.Empty)
|
||||
return; // no texture to send
|
||||
|
||||
IsSendingTexture = true;
|
||||
|
||||
// Send each chunk of the texture data
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Debug_NetworkOutbound)
|
||||
StickerMod.Logger.Msg("[ModNetwork] Sending texture to network");
|
||||
|
||||
var textureData = _ourTextureData;
|
||||
(Guid hash, int Width, int Height) textureMetadata = _ourTextureMetadata;
|
||||
int totalChunks = Mathf.CeilToInt(textureData.Length / (float)ChunkSize);
|
||||
if (totalChunks > MaxChunkCount)
|
||||
{
|
||||
StickerMod.Logger.Error($"[ModNetwork] Texture data too large to send: {textureData.Length} bytes, {totalChunks} chunks");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Debug_NetworkOutbound)
|
||||
StickerMod.Logger.Msg($"[ModNetwork] Texture data length: {textureData.Length}, total chunks: {totalChunks}, width: {textureMetadata.Width}, height: {textureMetadata.Height}");
|
||||
|
||||
SendStartTexture(textureMetadata.hash, totalChunks, textureMetadata.Width, textureMetadata.Height);
|
||||
|
||||
for (int i = 0; i < textureData.Length; i += ChunkSize)
|
||||
{
|
||||
int size = Mathf.Min(ChunkSize, textureData.Length - i);
|
||||
byte[] chunk = new byte[size];
|
||||
Array.Copy(textureData, i, chunk, 0, size);
|
||||
|
||||
SendTextureChunk(chunk, i / ChunkSize);
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Error($"[ModNetwork] Failed to send texture to network: {e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSendingTexture = false;
|
||||
SendMessage(MessageType.EndTexture);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void SendStickerPlace(Guid textureHash, Vector3 position, Vector3 forward, Vector3 up)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.PlaceSticker);
|
||||
modMsg.Write(textureHash);
|
||||
modMsg.Write(position);
|
||||
modMsg.Write(forward);
|
||||
modMsg.Write(up);
|
||||
modMsg.Send();
|
||||
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] PlaceSticker: Hash: {textureHash}, Position: {position}, Forward: {forward}, Up: {up}");
|
||||
}
|
||||
|
||||
private static void SendStartTexture(Guid hash, int chunkCount, int width, int height)
|
||||
{
|
||||
OnTextureOutboundStateChanged?.Invoke(true);
|
||||
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.StartTexture);
|
||||
modMsg.Write(hash);
|
||||
modMsg.Write(chunkCount);
|
||||
modMsg.Write(width);
|
||||
modMsg.Write(height);
|
||||
modMsg.Send();
|
||||
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] StartTexture sent with {chunkCount} chunks, width: {width}, height: {height}");
|
||||
}
|
||||
|
||||
private static void SendTextureChunk(byte[] chunk, int chunkIdx)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.SendTexture);
|
||||
modMsg.Write(chunkIdx);
|
||||
modMsg.Write(chunk);
|
||||
modMsg.Send();
|
||||
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] Sent texture chunk {chunkIdx + 1}");
|
||||
}
|
||||
|
||||
private static void SendMessage(MessageType messageType)
|
||||
{
|
||||
OnTextureOutboundStateChanged?.Invoke(false);
|
||||
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)messageType);
|
||||
modMsg.Send();
|
||||
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] MessageType: {messageType}");
|
||||
}
|
||||
|
||||
private static void SendTextureRequest(string sender)
|
||||
{
|
||||
if (!IsConnectedToGameNetwork())
|
||||
return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModId);
|
||||
modMsg.Write((byte)MessageType.RequestTexture);
|
||||
modMsg.Write(sender);
|
||||
modMsg.Send();
|
||||
|
||||
if (Debug_NetworkOutbound) StickerMod.Logger.Msg($"[Outbound] RequestTexture sent to {sender}");
|
||||
}
|
||||
|
||||
private static void OnMessageReceived(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out byte msgTypeRaw);
|
||||
|
||||
if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
|
||||
return;
|
||||
|
||||
if (Debug_NetworkInbound)
|
||||
StickerMod.Logger.Msg($"[Inbound] Sender: {msg.Sender}, MessageType: {(MessageType)msgTypeRaw}");
|
||||
|
||||
switch ((MessageType)msgTypeRaw)
|
||||
{
|
||||
case MessageType.PlaceSticker:
|
||||
msg.Read(out Guid hash);
|
||||
msg.Read(out Vector3 receivedPosition);
|
||||
msg.Read(out Vector3 receivedForward);
|
||||
msg.Read(out Vector3 receivedUp);
|
||||
OnStickerPlaceReceived(msg.Sender, hash, receivedPosition, receivedForward, receivedUp);
|
||||
break;
|
||||
|
||||
case MessageType.ClearStickers:
|
||||
OnStickersClearReceived(msg.Sender);
|
||||
break;
|
||||
|
||||
case MessageType.StartTexture:
|
||||
msg.Read(out Guid startHash);
|
||||
msg.Read(out int chunkCount);
|
||||
msg.Read(out int width);
|
||||
msg.Read(out int height);
|
||||
OnStartTextureReceived(msg.Sender, startHash, chunkCount, width, height);
|
||||
break;
|
||||
|
||||
case MessageType.SendTexture:
|
||||
msg.Read(out int chunkIdx);
|
||||
msg.Read(out byte[] textureChunk);
|
||||
OnTextureChunkReceived(msg.Sender, textureChunk, chunkIdx);
|
||||
break;
|
||||
|
||||
case MessageType.EndTexture:
|
||||
OnEndTextureReceived(msg.Sender);
|
||||
break;
|
||||
|
||||
case MessageType.RequestTexture:
|
||||
OnTextureRequestReceived(msg.Sender);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (Debug_NetworkInbound) StickerMod.Logger.Error($"[ModNetwork] Invalid message type received from: {msg.Sender}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Mod Network Internals
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private static bool IsConnectedToGameNetwork()
|
||||
{
|
||||
return NetworkManager.Instance != null
|
||||
&& NetworkManager.Instance.GameNetwork != null
|
||||
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
|
||||
}
|
||||
|
||||
private static void OnStickerPlaceReceived(string sender, Guid textureHash, Vector3 position, Vector3 forward, Vector3 up)
|
||||
{
|
||||
Guid localHash = StickerSystem.Instance.GetPlayerStickerTextureHash(sender);
|
||||
if (localHash != textureHash && textureHash != Guid.Empty) SendTextureRequest(sender);
|
||||
StickerSystem.Instance.OnPlayerStickerPlace(sender, position, forward, up);
|
||||
}
|
||||
|
||||
private static void OnStickersClearReceived(string sender)
|
||||
{
|
||||
StickerSystem.Instance.OnPlayerStickersClear(sender);
|
||||
}
|
||||
|
||||
private static void OnStartTextureReceived(string sender, Guid hash, int chunkCount, int width, int height)
|
||||
{
|
||||
if (_textureChunkBuffers.ContainsKey(sender))
|
||||
{
|
||||
if (Debug_NetworkInbound) StickerMod.Logger.Warning($"[Inbound] Received StartTexture message from {sender} while still receiving texture data!");
|
||||
return;
|
||||
}
|
||||
|
||||
Guid oldHash = StickerSystem.Instance.GetPlayerStickerTextureHash(sender);
|
||||
if (oldHash == hash)
|
||||
{
|
||||
if (Debug_NetworkInbound)
|
||||
StickerMod.Logger.Msg($"[Inbound] Received StartTexture message from {sender} with existing texture hash {hash}, skipping texture data.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chunkCount > MaxChunkCount)
|
||||
{
|
||||
StickerMod.Logger.Error($"[Inbound] Received StartTexture message from {sender} with too many chunks: {chunkCount}");
|
||||
return;
|
||||
}
|
||||
|
||||
_textureChunkBuffers[sender] = new byte[Mathf.Clamp(chunkCount * ChunkSize, 0, MaxTextureSize)];
|
||||
_receivedChunkCounts[sender] = 0;
|
||||
_expectedChunkCounts[sender] = chunkCount;
|
||||
_textureMetadata[sender] = (hash, width, height);
|
||||
|
||||
if (Debug_NetworkInbound)
|
||||
StickerMod.Logger.Msg($"[Inbound] StartTexture received from {sender} with hash: {hash} chunk count: {chunkCount}, width: {width}, height: {height}");
|
||||
}
|
||||
|
||||
private static void OnTextureChunkReceived(string sender, byte[] chunk, int chunkIdx)
|
||||
{
|
||||
if (!_textureChunkBuffers.TryGetValue(sender, out var buffer))
|
||||
return;
|
||||
|
||||
int startIndex = chunkIdx * ChunkSize;
|
||||
Array.Copy(chunk, 0, buffer, startIndex, chunk.Length);
|
||||
|
||||
_receivedChunkCounts[sender]++;
|
||||
if (_receivedChunkCounts[sender] < _expectedChunkCounts[sender])
|
||||
return;
|
||||
|
||||
(Guid Hash, int Width, int Height) metadata = _textureMetadata[sender];
|
||||
|
||||
// All chunks received, reassemble texture
|
||||
_textureChunkBuffers.Remove(sender);
|
||||
_receivedChunkCounts.Remove(sender);
|
||||
_expectedChunkCounts.Remove(sender);
|
||||
_textureMetadata.Remove(sender);
|
||||
|
||||
// Validate image
|
||||
if (!ImageUtility.IsValidImage(buffer))
|
||||
{
|
||||
StickerMod.Logger.Error($"[Inbound] Received texture data is not a valid image from {sender}!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data TODO: fix hash???????
|
||||
(Guid imageHash, int width, int height) = ImageUtility.ExtractImageInfo(buffer);
|
||||
if (metadata.Width != width
|
||||
|| metadata.Height != height)
|
||||
{
|
||||
StickerMod.Logger.Error($"[Inbound] Received texture data does not match metadata! Expected: {metadata.Hash} ({metadata.Width}x{metadata.Height}), received: {imageHash} ({width}x{height})");
|
||||
return;
|
||||
}
|
||||
|
||||
Texture2D texture = new(1,1);
|
||||
texture.LoadImage(buffer);
|
||||
texture.Compress(true);
|
||||
|
||||
StickerSystem.Instance.OnPlayerStickerTextureReceived(sender, metadata.Hash, texture);
|
||||
|
||||
if (Debug_NetworkInbound) StickerMod.Logger.Msg($"[Inbound] All chunks received and texture reassembled from {sender}. " +
|
||||
$"Texture size: {metadata.Width}x{metadata.Height}");
|
||||
}
|
||||
|
||||
private static void OnEndTextureReceived(string sender)
|
||||
{
|
||||
if (!_textureChunkBuffers.ContainsKey(sender))
|
||||
return;
|
||||
|
||||
if (Debug_NetworkInbound) StickerMod.Logger.Error($"[Inbound] Received EndTexture message without all chunks received from {sender}! Only {_receivedChunkCounts[sender]} out of {_expectedChunkCounts[sender]} received.");
|
||||
|
||||
_textureChunkBuffers.Remove(sender);
|
||||
_receivedChunkCounts.Remove(sender);
|
||||
_expectedChunkCounts.Remove(sender);
|
||||
_textureMetadata.Remove(sender);
|
||||
}
|
||||
|
||||
private static void OnTextureRequestReceived(string sender)
|
||||
{
|
||||
if (!_isSubscribedToModNetwork || IsSendingTexture)
|
||||
return;
|
||||
|
||||
if (_ourTextureData != null && _ourTextureMetadata.hashGuid != Guid.Empty)
|
||||
SendTexture();
|
||||
else
|
||||
StickerMod.Logger.Warning($"[Inbound] Received texture request from {sender}, but no texture is set!");
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
}
|
||||
}
|
|
@ -2,131 +2,213 @@
|
|||
using UnityEngine;
|
||||
using 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
|
||||
}
|
||||
}
|
152
Stickers/Stickers/StickerSystem.ImageLoading.cs
Normal file
152
Stickers/Stickers/StickerSystem.ImageLoading.cs
Normal file
|
@ -0,0 +1,152 @@
|
|||
using System.Diagnostics;
|
||||
using MTJobSystem;
|
||||
using NAK.Stickers.Networking;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public partial class StickerSystem
|
||||
{
|
||||
#region Actions
|
||||
|
||||
public static event Action<int, string> OnStickerLoaded;
|
||||
public static event Action<int, string> OnStickerLoadFailed;
|
||||
|
||||
private static void InvokeOnImageLoaded(int slotIndex, string imageName)
|
||||
=> MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoaded",
|
||||
() => OnStickerLoaded?.Invoke(slotIndex, imageName));
|
||||
|
||||
private static void InvokeOnImageLoadFailed(int slotIndex, string errorMessage)
|
||||
=> MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoadFailed",
|
||||
() => OnStickerLoadFailed?.Invoke(slotIndex, errorMessage));
|
||||
|
||||
#endregion Actions
|
||||
|
||||
#region Image Loading
|
||||
|
||||
private static readonly string s_StickersFolderPath = Path.GetFullPath(Application.dataPath + "/../UserData/Stickers/");
|
||||
private readonly bool[] _isLoadingImage = new bool[ModSettings.MaxStickerSlots];
|
||||
|
||||
private void LoadAllImagesAtStartup()
|
||||
{
|
||||
string[] selectedStickers = ModSettings.Hidden_SelectedStickerNames.Value;
|
||||
for (int i = 0; i < ModSettings.MaxStickerSlots; i++)
|
||||
{
|
||||
if (i >= selectedStickers.Length || string.IsNullOrEmpty(selectedStickers[i])) continue;
|
||||
LoadImage(selectedStickers[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadImage(string imageName, int slotIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageName) || slotIndex < 0 || slotIndex >= _isLoadingImage.Length)
|
||||
return;
|
||||
|
||||
if (_isLoadingImage[slotIndex]) return;
|
||||
_isLoadingImage[slotIndex] = true;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!TryLoadImage(imageName, slotIndex, out string errorMessage))
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//StickerMod.Logger.Error($"Failed to load sticker for slot {slotIndex}: {ex.Message}");
|
||||
InvokeOnImageLoadFailed(slotIndex, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingImage[slotIndex] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryLoadImage(string imageName, int slotIndex, out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
|
||||
if (ModNetwork.IsSendingTexture)
|
||||
{
|
||||
//StickerMod.Logger.Warning("A texture is currently being sent over the network. Cannot load a new image yet.");
|
||||
errorMessage = "A texture is currently being sent over the network. Cannot load a new image yet.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath);
|
||||
|
||||
string imagePath = Path.Combine(s_StickersFolderPath, imageName);
|
||||
FileInfo fileInfo = new(imagePath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
//StickerMod.Logger.Warning($"Target image does not exist on disk. Path: {imagePath}");
|
||||
errorMessage = "Target image does not exist on disk.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(imagePath);
|
||||
|
||||
if (!ImageUtility.IsValidImage(bytes))
|
||||
{
|
||||
//StickerMod.Logger.Error("File is not a valid image or is corrupt.");
|
||||
errorMessage = "File is not a valid image or is corrupt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
//StickerMod.Logger.Msg("Loaded image from disk. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
|
||||
if (bytes.Length > ModNetwork.MaxTextureSize)
|
||||
{
|
||||
ImageUtility.Resize(ref bytes, 256, 256);
|
||||
//StickerMod.Logger.Warning("File ate too many cheeseburgers. Attempting experimental resize. Notice: this may cause filesize to increase.");
|
||||
//StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
}
|
||||
|
||||
if (ImageUtility.ResizeToNearestPowerOfTwo(ref bytes))
|
||||
{
|
||||
//StickerMod.Logger.Warning("Image resolution was not a power of two. Attempting experimental resize. Notice: this may cause filesize to increase.");
|
||||
//StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
}
|
||||
|
||||
if (bytes.Length > ModNetwork.MaxTextureSize)
|
||||
{
|
||||
//StickerMod.Logger.Error("File is still too large. Aborting. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
//StickerMod.Logger.Msg("Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two.");
|
||||
errorMessage = "File is still too large. Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two.";
|
||||
return false;
|
||||
}
|
||||
|
||||
//StickerMod.Logger.Msg("Image successfully loaded.");
|
||||
|
||||
MTJobManager.RunOnMainThread("StickersSystem.LoadImage", () =>
|
||||
{
|
||||
ModSettings.Hidden_SelectedStickerNames.Value[slotIndex] = imageName;
|
||||
SetTextureSelf(bytes, slotIndex);
|
||||
|
||||
InvokeOnImageLoaded(slotIndex, imageName);
|
||||
|
||||
if (!IsInStickerMode) return;
|
||||
IsInStickerMode = false;
|
||||
IsInStickerMode = true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void OpenStickersFolder()
|
||||
{
|
||||
if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath);
|
||||
Process.Start(s_StickersFolderPath);
|
||||
}
|
||||
|
||||
public static string GetStickersFolderPath()
|
||||
{
|
||||
if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath);
|
||||
return s_StickersFolderPath;
|
||||
}
|
||||
|
||||
#endregion Image Loading
|
||||
}
|
71
Stickers/Stickers/StickerSystem.Main.cs
Normal file
71
Stickers/Stickers/StickerSystem.Main.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using ABI_RC.Core.UI;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public partial class StickerSystem
|
||||
{
|
||||
#region Singleton
|
||||
|
||||
public static StickerSystem Instance { get; private set; }
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Instance != null)
|
||||
return;
|
||||
|
||||
Instance = new StickerSystem();
|
||||
|
||||
// configure decalery
|
||||
DecalManager.SetPreferredMode(DecalUtils.Mode.GPU, false, 0);
|
||||
|
||||
// ensure cache folder exists
|
||||
if (!Directory.Exists(s_StickersFolderPath)) Directory.CreateDirectory(s_StickersFolderPath);
|
||||
|
||||
// listen for game events
|
||||
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart);
|
||||
}
|
||||
|
||||
#endregion Singleton
|
||||
|
||||
#region Data
|
||||
|
||||
private int _selectedStickerSlot;
|
||||
public int SelectedStickerSlot
|
||||
{
|
||||
get => _selectedStickerSlot;
|
||||
set
|
||||
{
|
||||
_selectedStickerSlot = Mathf.Clamp(value, 0, ModSettings.MaxStickerSlots - 1);
|
||||
IsInStickerMode = IsInStickerMode; // refresh sticker mode
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isInStickerMode;
|
||||
public bool IsInStickerMode
|
||||
{
|
||||
get => _isInStickerMode;
|
||||
set
|
||||
{
|
||||
_isInStickerMode = value;
|
||||
if (_isInStickerMode) CohtmlHud.Instance.SelectPropToSpawn(
|
||||
StickerCache.GetCohtmlResourcesPath(SelectedStickerName),
|
||||
Path.GetFileNameWithoutExtension(SelectedStickerName),
|
||||
"Sticker selected for stickering:");
|
||||
else CohtmlHud.Instance.ClearPropToSpawn();
|
||||
}
|
||||
}
|
||||
|
||||
private string SelectedStickerName => ModSettings.Hidden_SelectedStickerNames.Value[_selectedStickerSlot];
|
||||
|
||||
private const float StickerKillTime = 30f;
|
||||
private const float StickerCooldown = 0.2f;
|
||||
private readonly Dictionary<string, StickerData> _playerStickers = new();
|
||||
private const string PlayerLocalId = "_PLAYERLOCAL";
|
||||
|
||||
private readonly List<StickerData> _deadStickerPool = new(); // for cleanup on player leave
|
||||
|
||||
#endregion Data
|
||||
}
|
98
Stickers/Stickers/StickerSystem.PlayerCallbacks.cs
Normal file
98
Stickers/Stickers/StickerSystem.PlayerCallbacks.cs
Normal file
|
@ -0,0 +1,98 @@
|
|||
using ABI_RC.Core.IO;
|
||||
using ABI_RC.Core.Networking.IO.Instancing;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public partial class StickerSystem
|
||||
{
|
||||
#region Player Callbacks
|
||||
|
||||
private void OnPlayerSetupStart()
|
||||
{
|
||||
CVRGameEventSystem.World.OnUnload.AddListener(_ => Instance.CleanupAllButSelf());
|
||||
CVRGameEventSystem.Instance.OnConnected.AddListener((_) => { if (!Instances.IsReconnecting) Instance.ClearStickersSelf(); });
|
||||
|
||||
CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined);
|
||||
CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft);
|
||||
SchedulerSystem.AddJob(Instance.OnOccasionalUpdate, 10f, 1f);
|
||||
LoadAllImagesAtStartup();
|
||||
}
|
||||
|
||||
private void OnPlayerJoined(CVRPlayerEntity playerEntity)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.DeathTime = -1f;
|
||||
stickerData.SetAlpha(1f);
|
||||
_deadStickerPool.Remove(stickerData);
|
||||
}
|
||||
|
||||
private void OnPlayerLeft(CVRPlayerEntity playerEntity)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.DeathTime = Time.time + StickerKillTime;
|
||||
stickerData.SetAlpha(1f);
|
||||
_deadStickerPool.Add(stickerData);
|
||||
}
|
||||
|
||||
private void OnOccasionalUpdate()
|
||||
{
|
||||
if (_deadStickerPool.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = _deadStickerPool.Count - 1; i >= 0; i--)
|
||||
{
|
||||
float currentTime = Time.time;
|
||||
StickerData stickerData = _deadStickerPool[i];
|
||||
if (stickerData == null)
|
||||
{
|
||||
_deadStickerPool.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stickerData.DeathTime < 0f)
|
||||
continue;
|
||||
|
||||
if (currentTime < stickerData.DeathTime)
|
||||
{
|
||||
stickerData.SetAlpha(Mathf.Lerp(0f, 1f, (stickerData.DeathTime - currentTime) / StickerKillTime));
|
||||
continue;
|
||||
}
|
||||
|
||||
_playerStickers.Remove(_playerStickers.First(x => x.Value == stickerData).Key);
|
||||
_deadStickerPool.RemoveAt(i);
|
||||
stickerData.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Player Callbacks
|
||||
|
||||
#region Player Callbacks
|
||||
|
||||
public void OnStickerPlaceReceived(string playerId, int stickerSlot, Vector3 position, Vector3 forward, Vector3 up)
|
||||
=> AttemptPlaceSticker(playerId, position, forward, up, alignWithNormal: true, stickerSlot);
|
||||
|
||||
public void OnStickerClearReceived(string playerId, int stickerSlot)
|
||||
=> ClearStickersForPlayer(playerId, stickerSlot);
|
||||
|
||||
public void OnStickerClearAllReceived(string playerId)
|
||||
=> ClearStickersForPlayer(playerId);
|
||||
|
||||
public void OnStickerIdentifyReceived(string playerId)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
// todo: make prettier (idk shaders)
|
||||
SchedulerSystem.AddJob(() => stickerData.Identify(), 0f, 0.1f, 30);
|
||||
SchedulerSystem.AddJob(() => stickerData.ResetIdentify(), 4f, 1f, 1);
|
||||
}
|
||||
|
||||
#endregion Player Callbacks
|
||||
}
|
160
Stickers/Stickers/StickerSystem.StickerLifecycle.cs
Normal file
160
Stickers/Stickers/StickerSystem.StickerLifecycle.cs
Normal file
|
@ -0,0 +1,160 @@
|
|||
using ABI_RC.Core;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Systems.InputManagement;
|
||||
using NAK.Stickers.Networking;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public partial class StickerSystem
|
||||
{
|
||||
#region Sticker Lifecycle
|
||||
|
||||
private StickerData GetOrCreateStickerData(string playerId)
|
||||
{
|
||||
if (_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return stickerData;
|
||||
|
||||
stickerData = new StickerData(playerId == PlayerLocalId, ModSettings.MaxStickerSlots);
|
||||
_playerStickers[playerId] = stickerData;
|
||||
return stickerData;
|
||||
}
|
||||
|
||||
public void PlaceStickerFromControllerRay(Transform transform, CVRHand hand = CVRHand.Left)
|
||||
{
|
||||
Vector3 controllerForward = transform.forward;
|
||||
Vector3 controllerUp = transform.up;
|
||||
Vector3 playerUp = PlayerSetup.Instance.transform.up;
|
||||
|
||||
// extracting angle of controller ray on forward axis
|
||||
Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized;
|
||||
Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized;
|
||||
float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp);
|
||||
|
||||
float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value;
|
||||
Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold)
|
||||
// leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop
|
||||
? Vector3.Slerp(controllerUp, playerUp, 0.99f)
|
||||
: controllerUp;
|
||||
|
||||
if (!PlaceStickerSelf(transform.position, transform.forward, targetUp))
|
||||
return;
|
||||
|
||||
// do haptic if not lame
|
||||
if (!ModSettings.Entry_HapticsOnPlace.Value) return;
|
||||
CVRInputManager.Instance.Vibrate(0f, 0.1f, 10f, 0.1f, hand);
|
||||
}
|
||||
|
||||
private bool PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true)
|
||||
{
|
||||
if (!AttemptPlaceSticker(PlayerLocalId, position, forward, up, alignWithNormal, SelectedStickerSlot))
|
||||
return false; // failed
|
||||
|
||||
// placed, now network
|
||||
ModNetwork.SendPlaceSticker(SelectedStickerSlot, position, forward, up);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool AttemptPlaceSticker(string playerId, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true, int stickerSlot = 0)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
if (Time.time - stickerData.LastPlacedTime < StickerCooldown)
|
||||
return false;
|
||||
|
||||
// Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal
|
||||
const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15));
|
||||
if (!Physics.Raycast(position, forward, out RaycastHit hit,
|
||||
10f, LayerMask, QueryTriggerInteraction.Ignore))
|
||||
return false;
|
||||
|
||||
stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up, stickerSlot);
|
||||
stickerData.PlayAudio();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearStickersSelf()
|
||||
{
|
||||
ClearStickersForPlayer(PlayerLocalId);
|
||||
ModNetwork.SendClearAllStickers();
|
||||
}
|
||||
|
||||
private void ClearStickersForPlayer(string playerId)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.Clear();
|
||||
}
|
||||
|
||||
private void ClearStickersForPlayer(string playerId, int stickerSlot)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.Clear(stickerSlot);
|
||||
}
|
||||
|
||||
private void SetTextureSelf(byte[] imageBytes, int stickerSlot = 0)
|
||||
{
|
||||
Texture2D texture = new(1, 1); // placeholder
|
||||
texture.LoadImage(imageBytes);
|
||||
texture.Compress(true); // noachi said to do
|
||||
|
||||
OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture, stickerSlot);
|
||||
ModNetwork.SetTexture(stickerSlot, imageBytes);
|
||||
}
|
||||
|
||||
public void ClearAllStickers()
|
||||
{
|
||||
foreach (StickerData stickerData in _playerStickers.Values)
|
||||
stickerData.Clear();
|
||||
|
||||
ModNetwork.SendClearAllStickers();
|
||||
}
|
||||
|
||||
public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture, int stickerSlot = 0)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
stickerData.SetTexture(textureHash, texture, stickerSlot);
|
||||
}
|
||||
|
||||
public bool HasTextureHash(string playerId, Guid textureHash)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
return stickerData.CheckHasTextureHash(textureHash);
|
||||
}
|
||||
|
||||
public void CleanupAll()
|
||||
{
|
||||
foreach ((_, StickerData data) in _playerStickers)
|
||||
data.Cleanup();
|
||||
|
||||
_playerStickers.Clear();
|
||||
}
|
||||
|
||||
public void CleanupAllButSelf()
|
||||
{
|
||||
StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId);
|
||||
|
||||
foreach ((_, StickerData data) in _playerStickers)
|
||||
{
|
||||
if (data.IsLocal) data.Clear();
|
||||
else data.Cleanup();
|
||||
}
|
||||
|
||||
_playerStickers.Clear();
|
||||
_playerStickers[PlayerLocalId] = localStickerData;
|
||||
}
|
||||
|
||||
public void SelectStickerSlot(int stickerSlot)
|
||||
{
|
||||
SelectedStickerSlot = Mathf.Clamp(stickerSlot, 0, ModSettings.MaxStickerSlots - 1);
|
||||
}
|
||||
|
||||
public int GetCurrentStickerSlot()
|
||||
{
|
||||
return SelectedStickerSlot;
|
||||
}
|
||||
|
||||
#endregion Sticker Lifecycle
|
||||
}
|
|
@ -1,406 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using ABI_RC.Core.IO;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Core.UI;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using MTJobSystem;
|
||||
using NAK.Stickers.Integrations;
|
||||
using NAK.Stickers.Networking;
|
||||
using NAK.Stickers.Utilities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public class StickerSystem
|
||||
{
|
||||
#region Singleton
|
||||
|
||||
public static StickerSystem Instance { get; private set; }
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Instance != null)
|
||||
return;
|
||||
|
||||
Instance = new StickerSystem();
|
||||
|
||||
// ensure cache folder exists
|
||||
if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath);
|
||||
|
||||
// configure Decalery
|
||||
ModSettings.DecaleryMode selectedMode = ModSettings.Decalery_DecalMode.Value;
|
||||
DecalManager.SetPreferredMode((DecalUtils.Mode)selectedMode, selectedMode == ModSettings.DecaleryMode.GPUIndirect, 0);
|
||||
if (selectedMode != ModSettings.DecaleryMode.GPU) StickerMod.Logger.Warning("Decalery is not set to GPU mode. Expect compatibility issues with user generated content when mesh data is not marked as readable.");
|
||||
|
||||
// listen for game events
|
||||
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart);
|
||||
}
|
||||
|
||||
#endregion Singleton
|
||||
|
||||
#region Actions
|
||||
|
||||
public static Action<string> OnImageLoadFailed;
|
||||
|
||||
private void InvokeOnImageLoadFailed(string errorMessage)
|
||||
{
|
||||
MTJobManager.RunOnMainThread("StickersSystem.InvokeOnImageLoadFailed", () => OnImageLoadFailed?.Invoke(errorMessage));
|
||||
}
|
||||
|
||||
#endregion Actions
|
||||
|
||||
#region Data
|
||||
|
||||
private bool _isInStickerMode;
|
||||
public bool IsInStickerMode
|
||||
{
|
||||
get => _isInStickerMode;
|
||||
set
|
||||
{
|
||||
_isInStickerMode = value;
|
||||
if (_isInStickerMode) CohtmlHud.Instance.SelectPropToSpawn(BtkUiAddon.GetBtkUiCachePath(ModSettings.Hidden_SelectedStickerName.Value), ModSettings.Hidden_SelectedStickerName.Value, "Sticker selected for stickering:");
|
||||
else CohtmlHud.Instance.ClearPropToSpawn();
|
||||
}
|
||||
}
|
||||
|
||||
private const float StickerKillTime = 30f;
|
||||
private const float StickerCooldown = 0.2f;
|
||||
private readonly Dictionary<string, StickerData> _playerStickers = new();
|
||||
private const string PlayerLocalId = "_PLAYERLOCAL";
|
||||
|
||||
private readonly List<StickerData> _deadStickerPool = new(); // for cleanup on player leave
|
||||
|
||||
#endregion Data
|
||||
|
||||
#region Game Events
|
||||
|
||||
private void OnPlayerSetupStart()
|
||||
{
|
||||
CVRGameEventSystem.World.OnUnload.AddListener(_ => Instance.CleanupAllButSelf());
|
||||
CVRGameEventSystem.Player.OnJoinEntity.AddListener(Instance.OnPlayerJoined);
|
||||
CVRGameEventSystem.Player.OnLeaveEntity.AddListener(Instance.OnPlayerLeft);
|
||||
SchedulerSystem.AddJob(Instance.OnOccasionalUpdate, 10f, 1f);
|
||||
|
||||
LoadImage(ModSettings.Hidden_SelectedStickerName.Value);
|
||||
}
|
||||
|
||||
private void OnPlayerJoined(CVRPlayerEntity playerEntity)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.DeathTime = -1f;
|
||||
stickerData.SetAlpha(1f);
|
||||
_deadStickerPool.Remove(stickerData);
|
||||
}
|
||||
|
||||
private void OnPlayerLeft(CVRPlayerEntity playerEntity)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerEntity.Uuid, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.DeathTime = Time.time + StickerKillTime;
|
||||
stickerData.SetAlpha(1f);
|
||||
_deadStickerPool.Add(stickerData);
|
||||
}
|
||||
|
||||
private void OnOccasionalUpdate()
|
||||
{
|
||||
if (_deadStickerPool.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = _deadStickerPool.Count - 1; i >= 0; i--)
|
||||
{
|
||||
float currentTime = Time.time;
|
||||
StickerData stickerData = _deadStickerPool[i];
|
||||
if (stickerData.DeathTime < 0f)
|
||||
continue;
|
||||
|
||||
if (currentTime < stickerData.DeathTime)
|
||||
{
|
||||
stickerData.SetAlpha(Mathf.Lerp(0f, 1f, (stickerData.DeathTime - currentTime) / StickerKillTime));
|
||||
continue;
|
||||
}
|
||||
|
||||
_playerStickers.Remove(_playerStickers.First(x => x.Value == stickerData).Key);
|
||||
_deadStickerPool.RemoveAt(i);
|
||||
stickerData.Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Game Events
|
||||
|
||||
#region Local Player
|
||||
|
||||
public void PlaceStickerFromTransform(Transform transform)
|
||||
{
|
||||
Vector3 controllerForward = transform.forward;
|
||||
Vector3 controllerUp = transform.up;
|
||||
Vector3 playerUp = PlayerSetup.Instance.transform.up;
|
||||
|
||||
// extracting angle of controller ray on forward axis
|
||||
Vector3 projectedControllerUp = Vector3.ProjectOnPlane(controllerUp, controllerForward).normalized;
|
||||
Vector3 projectedPlayerUp = Vector3.ProjectOnPlane(playerUp, controllerForward).normalized;
|
||||
float angle = Vector3.Angle(projectedControllerUp, projectedPlayerUp);
|
||||
|
||||
float angleThreshold = ModSettings.Entry_PlayerUpAlignmentThreshold.Value;
|
||||
Vector3 targetUp = (angleThreshold != 0f && angle <= angleThreshold)
|
||||
// leave 0.01% of the controller up vector to prevent issues with alignment on floor & ceiling in Desktop
|
||||
? Vector3.Slerp(controllerUp, playerUp, 0.99f)
|
||||
: controllerUp;
|
||||
|
||||
PlaceStickerSelf(transform.position, transform.forward, targetUp);
|
||||
}
|
||||
|
||||
/// Place own sticker. Network if successful.
|
||||
private void PlaceStickerSelf(Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true)
|
||||
{
|
||||
if (!AttemptPlaceSticker(PlayerLocalId, position, forward, up, alignWithNormal))
|
||||
return; // failed
|
||||
|
||||
// placed, now network
|
||||
ModNetwork.PlaceSticker(position, forward, up);
|
||||
}
|
||||
|
||||
/// Clear own stickers. Network if successful.
|
||||
public void ClearStickersSelf()
|
||||
{
|
||||
OnPlayerStickersClear(PlayerLocalId);
|
||||
ModNetwork.ClearStickers();
|
||||
}
|
||||
|
||||
/// Set own Texture2D for sticker. Network if cusses.
|
||||
private void SetTextureSelf(byte[] imageBytes)
|
||||
{
|
||||
Texture2D texture = new(1, 1); // placeholder
|
||||
texture.LoadImage(imageBytes);
|
||||
texture.Compress(true); // noachi said to do
|
||||
|
||||
OnPlayerStickerTextureReceived(PlayerLocalId, Guid.Empty, texture);
|
||||
if (ModNetwork.SetTexture(imageBytes)) ModNetwork.SendTexture();
|
||||
}
|
||||
|
||||
#endregion Local Player
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// When a player wants to place a sticker.
|
||||
public void OnPlayerStickerPlace(string playerId, Vector3 position, Vector3 forward, Vector3 up)
|
||||
=> AttemptPlaceSticker(playerId, position, forward, up);
|
||||
|
||||
/// When a player wants to clear their stickers.
|
||||
public void OnPlayerStickersClear(string playerId)
|
||||
=> ClearStickersForPlayer(playerId);
|
||||
|
||||
/// Clear all stickers from all players.
|
||||
public void ClearAllStickers()
|
||||
{
|
||||
foreach (StickerData stickerData in _playerStickers.Values)
|
||||
stickerData.Clear();
|
||||
|
||||
ModNetwork.ClearStickers();
|
||||
}
|
||||
|
||||
public void OnPlayerStickerTextureReceived(string playerId, Guid textureHash, Texture2D texture)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
stickerData.SetTexture(textureHash, texture);
|
||||
}
|
||||
|
||||
public Guid GetPlayerStickerTextureHash(string playerId)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
return stickerData.TextureHash;
|
||||
}
|
||||
|
||||
public void CleanupAll()
|
||||
{
|
||||
foreach ((_, StickerData data) in _playerStickers)
|
||||
data.Cleanup();
|
||||
|
||||
_playerStickers.Clear();
|
||||
}
|
||||
|
||||
public void CleanupAllButSelf()
|
||||
{
|
||||
StickerData localStickerData = GetOrCreateStickerData(PlayerLocalId);
|
||||
|
||||
foreach ((_, StickerData data) in _playerStickers)
|
||||
{
|
||||
if (data.IsLocal) data.Clear();
|
||||
else data.Cleanup();
|
||||
}
|
||||
|
||||
_playerStickers.Clear();
|
||||
_playerStickers[PlayerLocalId] = localStickerData;
|
||||
}
|
||||
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private StickerData GetOrCreateStickerData(string playerId)
|
||||
{
|
||||
if (_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return stickerData;
|
||||
|
||||
stickerData = new StickerData(playerId == PlayerLocalId);
|
||||
_playerStickers[playerId] = stickerData;
|
||||
return stickerData;
|
||||
}
|
||||
|
||||
private bool AttemptPlaceSticker(string playerId, Vector3 position, Vector3 forward, Vector3 up, bool alignWithNormal = true)
|
||||
{
|
||||
StickerData stickerData = GetOrCreateStickerData(playerId);
|
||||
if (Time.time - stickerData.LastPlacedTime < StickerCooldown)
|
||||
return false;
|
||||
|
||||
// Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal
|
||||
const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15));
|
||||
if (!Physics.Raycast(position, forward, out RaycastHit hit,
|
||||
10f, LayerMask, QueryTriggerInteraction.Ignore))
|
||||
return false;
|
||||
|
||||
stickerData.Place(hit, alignWithNormal ? -hit.normal : forward, up);
|
||||
stickerData.PlayAudio();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ClearStickersForPlayer(string playerId)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.Clear();
|
||||
}
|
||||
|
||||
private void ReleaseStickerData(string playerId)
|
||||
{
|
||||
if (!_playerStickers.TryGetValue(playerId, out StickerData stickerData))
|
||||
return;
|
||||
|
||||
stickerData.Cleanup();
|
||||
_playerStickers.Remove(playerId);
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
|
||||
#region Image Loading
|
||||
|
||||
private static readonly string s_StickersSourcePath = Application.dataPath + "/../UserData/Stickers/";
|
||||
|
||||
private bool _isLoadingImage;
|
||||
|
||||
public void LoadImage(string imageName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageName))
|
||||
return;
|
||||
|
||||
if (_isLoadingImage) return;
|
||||
_isLoadingImage = true;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!TryLoadImage(imageName, out string errorMessage))
|
||||
{
|
||||
InvokeOnImageLoadFailed(errorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
InvokeOnImageLoadFailed(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingImage = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryLoadImage(string imageName, out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
|
||||
if (ModNetwork.IsSendingTexture)
|
||||
{
|
||||
StickerMod.Logger.Warning("A texture is currently being sent over the network. Cannot load a new image yet.");
|
||||
errorMessage = "A texture is currently being sent over the network. Cannot load a new image yet.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath);
|
||||
|
||||
string imagePath = Path.Combine(s_StickersSourcePath, imageName + ".png");
|
||||
FileInfo fileInfo = new(imagePath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
StickerMod.Logger.Warning($"Target image does not exist on disk. Path: {imagePath}");
|
||||
errorMessage = "Target image does not exist on disk.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(imagePath);
|
||||
|
||||
if (!ImageUtility.IsValidImage(bytes))
|
||||
{
|
||||
StickerMod.Logger.Error("File is not a valid image or is corrupt.");
|
||||
errorMessage = "File is not a valid image or is corrupt.";
|
||||
return false;
|
||||
}
|
||||
|
||||
StickerMod.Logger.Msg("Loaded image from disk. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
|
||||
if (bytes.Length > ModNetwork.MaxTextureSize)
|
||||
{
|
||||
ImageUtility.Resize(ref bytes, 256, 256);
|
||||
StickerMod.Logger.Warning("File ate too many cheeseburgers. Attempting experimental resize. Notice: this may cause filesize to increase.");
|
||||
StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
}
|
||||
|
||||
if (ImageUtility.ResizeToNearestPowerOfTwo(ref bytes))
|
||||
{
|
||||
StickerMod.Logger.Warning("Image resolution was not a power of two. Attempting experimental resize. Notice: this may cause filesize to increase.");
|
||||
StickerMod.Logger.Msg("Resized image. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
}
|
||||
|
||||
if (bytes.Length > ModNetwork.MaxTextureSize)
|
||||
{
|
||||
StickerMod.Logger.Error("File is still too large. Aborting. Size in KB: " + bytes.Length / 1024 + " (" + bytes.Length + " bytes)");
|
||||
StickerMod.Logger.Msg("Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two.");
|
||||
errorMessage = "File is still too large. Please resize the image manually to be smaller than " + ModNetwork.MaxTextureSize / 1024 + " KB and round resolution to nearest power of two.";
|
||||
return false;
|
||||
}
|
||||
|
||||
StickerMod.Logger.Msg("Image successfully loaded.");
|
||||
|
||||
MTJobManager.RunOnMainThread("StickersSystem.LoadImage", () =>
|
||||
{
|
||||
ModSettings.Hidden_SelectedStickerName.Value = imageName;
|
||||
SetTextureSelf(bytes);
|
||||
|
||||
if (!IsInStickerMode) return;
|
||||
IsInStickerMode = false;
|
||||
IsInStickerMode = true;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void OpenStickersFolder()
|
||||
{
|
||||
if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath);
|
||||
Process.Start(s_StickersSourcePath);
|
||||
}
|
||||
|
||||
public static string GetStickersFolderPath()
|
||||
{
|
||||
if (!Directory.Exists(s_StickersSourcePath)) Directory.CreateDirectory(s_StickersSourcePath);
|
||||
return s_StickersSourcePath;
|
||||
}
|
||||
|
||||
#endregion Image Loading
|
||||
}
|
185
Stickers/Stickers/Utilities/StickerCache.cs
Normal file
185
Stickers/Stickers/Utilities/StickerCache.cs
Normal file
|
@ -0,0 +1,185 @@
|
|||
using BTKUILib.UIObjects.Components;
|
||||
using MTJobSystem;
|
||||
using NAK.Stickers.Integrations;
|
||||
using System.Collections.Concurrent;
|
||||
using BTKUILib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.Stickers.Utilities;
|
||||
|
||||
public static class StickerCache
|
||||
{
|
||||
#region Constants and Fields
|
||||
|
||||
private static readonly string CohtmlResourcesPath = Path.Combine("coui://", "uiresources", "GameUI", "mods", "BTKUI", "images", ModSettings.ModName, "UserImages");
|
||||
private static readonly string ThumbnailPath = Path.Combine(Application.dataPath, "StreamingAssets", "Cohtml", "UIResources", "GameUI", "mods", "BTKUI", "images", ModSettings.ModName, "UserImages");
|
||||
|
||||
private static readonly ConcurrentQueue<(FileInfo, Button)> _filesToGenerateThumbnails = new();
|
||||
private static readonly HashSet<string> _filesBeingProcessed = new();
|
||||
|
||||
private static readonly object _isGeneratingThumbnailsLock = new();
|
||||
private static bool _isGeneratingThumbnails;
|
||||
private static bool IsGeneratingThumbnails {
|
||||
get { lock (_isGeneratingThumbnailsLock) return _isGeneratingThumbnails; }
|
||||
set { lock (_isGeneratingThumbnailsLock) { _isGeneratingThumbnails = value; } }
|
||||
}
|
||||
|
||||
#endregion Constants and Fields
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public static string GetBtkUiIconName(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath)) return "Stickers-puzzle"; // default icon when shit fucked
|
||||
|
||||
string relativePathWithoutExtension = relativePath[..^Path.GetExtension(relativePath).Length];
|
||||
return "UserImages/" + relativePathWithoutExtension.Replace('\\', '/');
|
||||
}
|
||||
|
||||
public static string GetCohtmlResourcesPath(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath)) return string.Empty;
|
||||
|
||||
string relativePathWithoutExtension = relativePath[..^Path.GetExtension(relativePath).Length];
|
||||
string cachePath = Path.Combine(CohtmlResourcesPath, relativePathWithoutExtension + ".png");
|
||||
|
||||
// Normalize path
|
||||
cachePath = cachePath.Replace('\\', '/');
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
public static bool IsThumbnailAvailable(string relativePathWithoutExtension)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePathWithoutExtension)) return false;
|
||||
|
||||
string thumbnailImagePath = Path.Combine(ThumbnailPath, relativePathWithoutExtension + ".png");
|
||||
return File.Exists(thumbnailImagePath);
|
||||
}
|
||||
|
||||
public static void EnqueueThumbnailGeneration(FileInfo fileInfo, Button button)
|
||||
{
|
||||
lock (_filesBeingProcessed)
|
||||
{
|
||||
if (!_filesBeingProcessed.Add(fileInfo.FullName))
|
||||
return;
|
||||
|
||||
_filesToGenerateThumbnails.Enqueue((fileInfo, button));
|
||||
MTJobManager.RunOnMainThread("StartGeneratingThumbnailsIfNeeded", StartGeneratingThumbnailsIfNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private static void StartGeneratingThumbnailsIfNeeded()
|
||||
{
|
||||
if (IsGeneratingThumbnails) return;
|
||||
IsGeneratingThumbnails = true;
|
||||
|
||||
StickerMod.Logger.Msg("Starting thumbnail generation task.");
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
int generatedThumbnails = 0;
|
||||
|
||||
while (BTKUIAddon.IsPopulatingPage || _filesToGenerateThumbnails.Count > 0)
|
||||
{
|
||||
if (!_filesToGenerateThumbnails.TryDequeue(out (FileInfo, Button) fileInfo)) continue;
|
||||
|
||||
bool success = GenerateThumbnail(fileInfo.Item1);
|
||||
if (success && fileInfo.Item2.ButtonTooltip[5..] == fileInfo.Item1.Name)
|
||||
{
|
||||
var iconPath = GetBtkUiIconName(Path.GetRelativePath(StickerSystem.GetStickersFolderPath(), fileInfo.Item1.FullName));
|
||||
fileInfo.Item2.ButtonIcon = iconPath;
|
||||
}
|
||||
|
||||
lock (_filesBeingProcessed)
|
||||
{
|
||||
_filesBeingProcessed.Remove(fileInfo.Item1.FullName);
|
||||
}
|
||||
|
||||
generatedThumbnails++;
|
||||
}
|
||||
|
||||
StickerMod.Logger.Msg($"Finished thumbnail generation for {generatedThumbnails} files.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
StickerMod.Logger.Error($"Failed to generate thumbnails: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsGeneratingThumbnails = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool GenerateThumbnail(FileSystemInfo fileInfo)
|
||||
{
|
||||
string relativePath = Path.GetRelativePath(StickerSystem.GetStickersFolderPath(), fileInfo.FullName);
|
||||
string relativePathWithoutExtension = relativePath[..^fileInfo.Extension.Length];
|
||||
string thumbnailDirectory = Path.GetDirectoryName(Path.Combine(ThumbnailPath, relativePathWithoutExtension + ".png"));
|
||||
if (thumbnailDirectory == null)
|
||||
return false;
|
||||
|
||||
if (!Directory.Exists(thumbnailDirectory)) Directory.CreateDirectory(thumbnailDirectory);
|
||||
|
||||
MemoryStream imageStream = LoadStreamFromFile(fileInfo.FullName);
|
||||
if (imageStream == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
ImageUtility.Resize(ref imageStream, 128, 128);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
StickerMod.Logger.Warning($"Failed to resize image: {e.Message}");
|
||||
imageStream.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
PrepareIconFromMemoryStream(ModSettings.ModName, relativePathWithoutExtension, imageStream);
|
||||
imageStream.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PrepareIconFromMemoryStream(string modName, string iconPath, MemoryStream destination)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
StickerMod.Logger.Error("Mod " + modName + " attempted to prepare " + iconPath + " but the resource stream was null! Yell at the mod author to fix this!");
|
||||
}
|
||||
else
|
||||
{
|
||||
iconPath = UIUtils.GetCleanString(iconPath);
|
||||
iconPath = Path.Combine(ThumbnailPath, iconPath);
|
||||
File.WriteAllBytes(iconPath + ".png", destination.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static MemoryStream LoadStreamFromFile(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read);
|
||||
|
||||
MemoryStream memoryStream = new();
|
||||
fileStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0; // Ensure the position is reset before returning
|
||||
return memoryStream;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
StickerMod.Logger.Warning($"Failed to load stream from {filePath}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue