mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-03 23:09:22 +00:00
stickers
This commit is contained in:
parent
61f1a884d2
commit
475593bc1d
25 changed files with 2172 additions and 0 deletions
414
Stickers/Stickers/Networking/ModNetwork.cs
Normal file
414
Stickers/Stickers/Networking/ModNetwork.cs
Normal file
|
@ -0,0 +1,414 @@
|
|||
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
|
||||
}
|
||||
}
|
132
Stickers/Stickers/StickerData.cs
Normal file
132
Stickers/Stickers/StickerData.cs
Normal file
|
@ -0,0 +1,132 @@
|
|||
using ABI_RC.Core;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace NAK.Stickers;
|
||||
|
||||
public class StickerData
|
||||
{
|
||||
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)
|
||||
{
|
||||
IsLocal = isLocal;
|
||||
|
||||
_decal = ScriptableObject.CreateInstance<DecalType>();
|
||||
_decal.decalSettings = new DecalSpawner.InitData
|
||||
{
|
||||
material = new Material(StickerMod.DecalSimpleShader)
|
||||
{
|
||||
//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
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAlpha(float alpha)
|
||||
{
|
||||
Material material = _decal.decalSettings.material;
|
||||
material.color = new Color(material.color.r, material.color.g, material.color.b, alpha);
|
||||
}
|
||||
}
|
406
Stickers/Stickers/StickerSystem.cs
Normal file
406
Stickers/Stickers/StickerSystem.cs
Normal file
|
@ -0,0 +1,406 @@
|
|||
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
|
||||
}
|
171
Stickers/Stickers/Utilities/ImageUtility.cs
Normal file
171
Stickers/Stickers/Utilities/ImageUtility.cs
Normal file
|
@ -0,0 +1,171 @@
|
|||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NAK.Stickers.Utilities;
|
||||
|
||||
// i do not know shit about images
|
||||
// TODO: optimize Image usage, attempt create, reuse when valid
|
||||
|
||||
public static class ImageUtility
|
||||
{
|
||||
public static bool IsValidImage(byte[] image)
|
||||
{
|
||||
try
|
||||
{
|
||||
using MemoryStream stream = new(image);
|
||||
using Image img = Image.FromStream(stream);
|
||||
return img.Width > 0 && img.Height > 0;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
StickerMod.Logger.Error($"[ImageUtility] Failed to validate image: {e}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Resize(ref byte[] image, int width, int height)
|
||||
{
|
||||
MemoryStream stream = new(image);
|
||||
Resize(ref stream, width, height);
|
||||
|
||||
image = stream.ToArray();
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
public static void Resize(ref MemoryStream stream, int width, int height)
|
||||
{
|
||||
using Image source = Image.FromStream(stream);
|
||||
using Bitmap bitmap = new(width, height);
|
||||
Rectangle destRect = new(0, 0, width, height);
|
||||
|
||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
||||
{
|
||||
graphics.CompositingMode = CompositingMode.SourceCopy;
|
||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
using (ImageAttributes wrapMode = new())
|
||||
{
|
||||
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
|
||||
graphics.DrawImage(source, destRect, 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, wrapMode);
|
||||
}
|
||||
}
|
||||
|
||||
stream.Dispose();
|
||||
stream = new MemoryStream();
|
||||
SaveBitmapPreservingFormat(bitmap, stream);
|
||||
}
|
||||
|
||||
private static void SaveBitmapPreservingFormat(Bitmap bitmap, Stream stream)
|
||||
{
|
||||
bool hasTransparency = ImageHasTransparency(bitmap);
|
||||
if (!hasTransparency)
|
||||
{
|
||||
ImageCodecInfo jpegEncoder = ImageCodecInfo.GetImageEncoders().First(x => x.FormatID == ImageFormat.Jpeg.Guid);
|
||||
EncoderParameters jpegEncoderParameters = new(1)
|
||||
{
|
||||
Param = new[]
|
||||
{
|
||||
new EncoderParameter(Encoder.Quality, 80L) // basically, fuck you, get smaller
|
||||
}
|
||||
};
|
||||
bitmap.Save(stream, jpegEncoder, jpegEncoderParameters);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImageCodecInfo pngEncoder = ImageCodecInfo.GetImageEncoders().First(x => x.FormatID == ImageFormat.Png.Guid);
|
||||
EncoderParameters pngEncoderParameters = new(1)
|
||||
{
|
||||
Param = new[]
|
||||
{
|
||||
//new EncoderParameter(Encoder.Quality, 50L),
|
||||
new EncoderParameter(Encoder.Compression, (long)EncoderValue.CompressionLZW), // PNG compression
|
||||
new EncoderParameter(Encoder.ColorDepth, bitmap.PixelFormat == PixelFormat.Format32bppArgb ? 32L : 24L),
|
||||
new EncoderParameter(Encoder.ScanMethod, (long)EncoderValue.ScanMethodInterlaced),
|
||||
}
|
||||
};
|
||||
bitmap.Save(stream, pngEncoder, pngEncoderParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ImageHasTransparency(Bitmap bitmap)
|
||||
{
|
||||
for (int y = 0; y < bitmap.Height; y++)
|
||||
for (int x = 0; x < bitmap.Width; x++)
|
||||
if (bitmap.GetPixel(x, y).A < 255)
|
||||
return true;
|
||||
|
||||
return false; // no transparency found
|
||||
}
|
||||
|
||||
public static bool IsPowerOfTwo(byte[] image)
|
||||
{
|
||||
using MemoryStream stream = new(image);
|
||||
using Image source = Image.FromStream(stream);
|
||||
return IsPowerOfTwo(source.Width) && IsPowerOfTwo(source.Height);
|
||||
}
|
||||
|
||||
private static bool IsPowerOfTwo(int value)
|
||||
{
|
||||
return (value > 0) && (value & (value - 1)) == 0;
|
||||
}
|
||||
|
||||
public static int NearestPowerOfTwo(int value)
|
||||
{
|
||||
return (int)Math.Pow(2, Math.Ceiling(Math.Log(value, 2)));
|
||||
}
|
||||
|
||||
public static int FlooredPowerOfTwo(int value)
|
||||
{
|
||||
return (int)Math.Pow(2, Math.Floor(Math.Log(value, 2)));
|
||||
}
|
||||
|
||||
public static bool ResizeToNearestPowerOfTwo(ref byte[] image)
|
||||
{
|
||||
using MemoryStream stream = new(image);
|
||||
using Image source = Image.FromStream(stream);
|
||||
|
||||
if (IsPowerOfTwo(source.Width) && IsPowerOfTwo(source.Height))
|
||||
return false; // already power of two
|
||||
|
||||
int newWidth = NearestPowerOfTwo(source.Width);
|
||||
int newHeight = NearestPowerOfTwo(source.Height);
|
||||
Resize(ref image, newWidth, newHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
// making the assumption that the above could potentially put an image over my filesize limit
|
||||
public static bool ResizeToFlooredPowerOfTwo(byte[] image)
|
||||
{
|
||||
using MemoryStream stream = new(image);
|
||||
using Image source = Image.FromStream(stream);
|
||||
if (IsPowerOfTwo(source.Width) && IsPowerOfTwo(source.Height))
|
||||
return false; // already power of two
|
||||
|
||||
int newWidth = FlooredPowerOfTwo(source.Width);
|
||||
int newHeight = FlooredPowerOfTwo(source.Height);
|
||||
Resize(ref image, newWidth, newHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static (Guid hashGuid, int width, int height) ExtractImageInfo(byte[] image)
|
||||
{
|
||||
using MemoryStream stream = new(image);
|
||||
using Image img = Image.FromStream(stream);
|
||||
|
||||
int width = img.Width;
|
||||
int height = img.Height;
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
using SHA256 sha256 = SHA256.Create();
|
||||
byte[] hashBytes = sha256.ComputeHash(stream);
|
||||
Guid hashGuid = new(hashBytes.Take(16).ToArray());
|
||||
|
||||
return (hashGuid, width, height);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue