Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimGifPosters v1.1.0
ValheimGifPosters.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn.Configs; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; using UnityEngine.Video; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("XareN")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+b5ed5861f89cf5e1d37b4194c6261c1d1326b0a9")] [assembly: AssemblyProduct("Valheim GIF Posters")] [assembly: AssemblyTitle("ValheimGifPosters")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimGifPosters { internal static class GifCache { internal sealed class Entry { internal readonly string Url; internal GifDecoder? Decoder; internal long EstimatedBytes; internal GifFrame? CurrentFrame; internal Task<GifFrame?>? PendingFrame; internal Texture2D? Texture; internal int RefCount; internal bool Failed; internal bool Loading = true; internal bool Started; internal float FailedAt; internal string Status = "Downloading image..."; internal float LastUsed; internal float FrameTime; internal Entry(string url) { Url = url; } } private sealed class DecodedStart { internal readonly GifDecoder Decoder; internal readonly GifFrame Frame; internal DecodedStart(GifDecoder decoder, GifFrame frame) { Decoder = decoder; Frame = frame; } } private const ulong MaxDownloadBytes = 16777216uL; private const long MaxCachedBytes = 100663296L; private const int MaxEntries = 8; private static readonly Dictionary<string, Entry> Entries = new Dictionary<string, Entry>(StringComparer.Ordinal); internal static Entry Acquire(string url) { if (!Entries.TryGetValue(url, out Entry value)) { value = new Entry(url); Entries.Add(url, value); } value.RefCount++; value.LastUsed = Time.realtimeSinceStartup; if (!value.Started) { value.Started = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(Download(value)); } Prune(); return value; } internal static void Release(Entry entry) { if (entry.RefCount > 0) { entry.RefCount--; } entry.LastUsed = Time.realtimeSinceStartup; if (entry.RefCount == 0 && entry.Failed) { Evict(entry); } Prune(); } internal static void Tick(float dt) { foreach (Entry value in Entries.Values) { if (value.RefCount == 0 || value.CurrentFrame == null || (Object)(object)value.Texture == (Object)null || value.PendingFrame == null || value.Failed) { continue; } float num = (float)value.CurrentFrame.DelayMs / 1000f; value.FrameTime = Math.Min(num + 0.25f, value.FrameTime + Math.Min(dt, 0.25f)); if (!value.PendingFrame.IsCompleted || value.FrameTime < num) { continue; } if (value.PendingFrame.IsFaulted || value.PendingFrame.IsCanceled) { Plugin.Instance.LogWarning("GIF frame decode failed: " + value.PendingFrame.Exception?.GetBaseException().Message); value.PendingFrame = null; Fail(value, "Could not decode the next GIF frame."); continue; } GifFrame result = value.PendingFrame.Result; if (result == null) { value.PendingFrame = null; continue; } value.FrameTime = Math.Max(0f, value.FrameTime - num); try { value.Texture.LoadRawTextureData(result.Rgba); value.Texture.Apply(false, false); } catch (Exception ex) { Plugin.Instance.LogWarning("GIF frame upload failed: " + ex.Message); value.PendingFrame = null; Fail(value, "Could not display the next GIF frame."); continue; } value.CurrentFrame = result; value.PendingFrame = DecodeNext(value.Decoder); } } private static IEnumerator Download(Entry entry) { UnityWebRequest request = UnityWebRequest.Get(GifUrl.ForDownload(entry.Url)); try { request.timeout = 60; UnityWebRequestAsyncOperation operation = request.SendWebRequest(); while (!((AsyncOperation)operation).isDone) { ulong result; bool flag = ulong.TryParse(request.GetResponseHeader("Content-Length"), out result) && result > 16777216; if (entry.RefCount == 0 || request.downloadedBytes > 16777216 || flag) { request.Abort(); Fail(entry, "Download stopped or the file is too large."); yield break; } yield return null; } if ((int)request.result != 1 || request.responseCode != 200) { Fail(entry, "Could not download the image."); yield break; } byte[] data = request.downloadHandler.data; ImageKind imageKind = ImageFile.Detect(data); if ((ulong)data.Length <= 16777216uL) { switch (imageKind) { case ImageKind.Unknown: break; default: LoadStaticImage(entry, data, imageKind); yield break; case ImageKind.Gif: { entry.Status = "Decoding GIF..."; Task<DecodedStart> task = Task.Run(delegate { GifDecoder gifDecoder = new GifDecoder(data); return new DecodedStart(gifDecoder, gifDecoder.ReadNextFrame()); }); while (!task.IsCompleted) { yield return null; } if (task.IsFaulted || task.IsCanceled) { Plugin.Instance.LogWarning("GIF decode failed: " + task.Exception?.GetBaseException().Message); Fail(entry, "Could not decode the GIF."); yield break; } if (entry.RefCount == 0) { Entries.Remove(entry.Url); yield break; } try { DecodedStart result2 = task.Result; if (!MakeRoom(result2.Decoder.EstimatedBytes)) { Fail(entry, "Too many GIF animations near the player."); yield break; } Texture2D val = new Texture2D(result2.Decoder.Width, result2.Decoder.Height, (TextureFormat)4, false); ((Texture)val).wrapMode = (TextureWrapMode)1; ((Texture)val).filterMode = (FilterMode)1; val.LoadRawTextureData(result2.Frame.Rgba); val.Apply(false, false); entry.Decoder = result2.Decoder; entry.EstimatedBytes = result2.Decoder.EstimatedBytes; entry.CurrentFrame = result2.Frame; entry.Texture = val; entry.PendingFrame = DecodeNext(result2.Decoder); entry.Status = ""; entry.Loading = false; Prune(); yield break; } catch (Exception ex) { Plugin.Instance.LogWarning("GIF texture failed: " + ex.Message); Fail(entry, "Could not display the GIF."); yield break; } } } } Fail(entry, "URL must point directly to a GIF, PNG, or JPEG (max 16 MiB)."); } finally { ((IDisposable)request)?.Dispose(); } } private static void LoadStaticImage(Entry entry, byte[] data, ImageKind kind) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown if (entry.RefCount == 0) { Entries.Remove(entry.Url); return; } if (!ImageFile.TryGetStaticSize(data, kind, out var width, out var height)) { Fail(entry, "Image dimensions are invalid or exceed 4096 x 4096."); return; } long num = (long)width * (long)height * 4; if (!MakeRoom(num)) { Fail(entry, "Too many images near the player."); return; } entry.Status = "Decoding image..."; Texture2D val = null; try { val = new Texture2D(2, 2, (TextureFormat)4, false); if (!ImageConversion.LoadImage(val, data, true) || ((Texture)val).width != width || ((Texture)val).height != height) { throw new InvalidOperationException("Image data or dimensions are invalid."); } ((Texture)val).wrapMode = (TextureWrapMode)1; ((Texture)val).filterMode = (FilterMode)1; entry.Texture = val; entry.EstimatedBytes = num; entry.Status = ""; entry.Loading = false; Prune(); } catch (Exception ex) { if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } Plugin.Instance.LogWarning("Image decode failed: " + ex.Message); Fail(entry, "Could not display the image."); } } private static Task<GifFrame?> DecodeNext(GifDecoder decoder) { return Task.Run(delegate { GifFrame gifFrame = decoder.ReadNextFrame(); if (gifFrame != null) { return gifFrame; } if (decoder.FramesRead == 1) { return (GifFrame)null; } decoder.Rewind(); return decoder.ReadNextFrame() ?? throw new InvalidOperationException("GIF lost its first frame after rewind."); }); } private static void Fail(Entry entry, string message) { entry.Status = message; entry.Loading = false; entry.Failed = true; entry.FailedAt = Time.realtimeSinceStartup; if (entry.RefCount == 0) { Evict(entry); } } private static void Prune() { long num = Entries.Values.Sum((Entry e) => e.EstimatedBytes); Entry[] array = (from e in Entries.Values where e.RefCount == 0 && !e.Loading orderby e.LastUsed select e).ToArray(); foreach (Entry entry in array) { if (Entries.Count > 8 || num > 100663296) { num -= entry.EstimatedBytes; Evict(entry); continue; } break; } } private static bool MakeRoom(long incoming) { long num = Entries.Values.Sum((Entry e) => e.EstimatedBytes); Entry[] array = (from e in Entries.Values where e.RefCount == 0 && !e.Loading orderby e.LastUsed select e).ToArray(); foreach (Entry entry in array) { if (num + incoming <= 100663296) { break; } num -= entry.EstimatedBytes; Evict(entry); } return num + incoming <= 100663296; } private static void Evict(Entry entry) { if (Entries.TryGetValue(entry.Url, out Entry value) && value == entry) { Entries.Remove(entry.Url); } if ((Object)(object)entry.Texture != (Object)null) { Object.Destroy((Object)(object)entry.Texture); } entry.Texture = null; } internal static void Clear() { foreach (Entry value in Entries.Values) { if ((Object)(object)value.Texture != (Object)null) { Object.Destroy((Object)(object)value.Texture); } } Entries.Clear(); } } internal sealed class GifDecoder { private readonly struct Rgb { internal readonly byte R; internal readonly byte G; internal readonly byte B; internal Rgb(byte r, byte g, byte b) { R = r; G = g; B = b; } } private sealed class Reader { private readonly byte[] _bytes; private int _position; internal int Position { get { return _position; } set { _position = value; } } internal Reader(byte[] bytes) { _bytes = bytes; } internal int ReadByte() { if (_position >= _bytes.Length) { throw new InvalidDataException("Truncated GIF file."); } return _bytes[_position++]; } internal int ReadU16() { return ReadByte() | (ReadByte() << 8); } internal string ReadAscii(int count) { if (_position + count > _bytes.Length) { throw new InvalidDataException("Truncated GIF header."); } string result = Encoding.ASCII.GetString(_bytes, _position, count); _position += count; return result; } internal void SkipSubBlocks() { while (true) { int num = ReadByte(); if (num == 0) { return; } if (_position + num > _bytes.Length) { break; } _position += num; } throw new InvalidDataException("Truncated GIF extension."); } internal byte[] ReadSubBlocks() { using MemoryStream memoryStream = new MemoryStream(); while (true) { int num = ReadByte(); if (num == 0) { return memoryStream.ToArray(); } if (_position + num > _bytes.Length) { break; } memoryStream.Write(_bytes, _position, num); _position += num; } throw new InvalidDataException("Truncated GIF image data."); } } private const int MaxSide = 512; private readonly Reader _reader; private readonly Rgb[]? _globalPalette; private readonly byte[] _canvas; private readonly int _firstBlockOffset; private int _previousDisposal; private int _previousX; private int _previousY; private int _previousW; private int _previousH; private byte[]? _previousSnapshot; private int _disposal; private int _delay = 100; private int _transparent = -1; private bool _ended; private int _framesRead; internal int Width { get; } internal int Height { get; } internal long EstimatedBytes { get; } internal int FramesRead => _framesRead; internal GifDecoder(byte[] bytes) { _reader = new Reader(bytes); string text = _reader.ReadAscii(6); if (text != "GIF87a" && text != "GIF89a") { throw new InvalidDataException("Not a GIF file."); } Width = _reader.ReadU16(); Height = _reader.ReadU16(); if (Width < 1 || Height < 1 || Width > 512 || Height > 512) { throw new InvalidDataException("GIF dimensions exceed 512 x 512."); } int num = _reader.ReadByte(); _reader.ReadByte(); _reader.ReadByte(); _globalPalette = (((num & 0x80) != 0) ? ReadPalette(_reader, 1 << (num & 7) + 1) : null); _canvas = new byte[checked(Width * Height * 4)]; _firstBlockOffset = _reader.Position; EstimatedBytes = bytes.Length + 5L * (long)_canvas.Length + Width * Height; } internal GifFrame? ReadNextFrame() { if (_ended) { return null; } while (true) { switch (_reader.ReadByte()) { case 59: _ended = true; if (_framesRead == 0) { throw new InvalidDataException("GIF contains no frames."); } return null; case 33: if (_reader.ReadByte() == 249) { if (_reader.ReadByte() != 4) { throw new InvalidDataException("Malformed GIF graphic control extension."); } int num7 = _reader.ReadByte(); _disposal = (num7 >> 2) & 7; _delay = Math.Max(50, _reader.ReadU16() * 10); int num8 = _reader.ReadByte(); _transparent = (((num7 & 1) != 0) ? num8 : (-1)); if (_reader.ReadByte() != 0) { throw new InvalidDataException("Malformed GIF graphic control terminator."); } } else { _reader.SkipSubBlocks(); } break; default: throw new InvalidDataException("Unexpected GIF block."); case 44: { ApplyDisposal(_canvas, Width, _previousDisposal, _previousX, _previousY, _previousW, _previousH, _previousSnapshot); int num = _reader.ReadU16(); int num2 = _reader.ReadU16(); int num3 = _reader.ReadU16(); int num4 = _reader.ReadU16(); if (num3 < 1 || num4 < 1 || num + num3 > Width || num2 + num4 > Height) { throw new InvalidDataException("GIF frame is outside the canvas."); } int num5 = _reader.ReadByte(); Rgb[] array = (((num5 & 0x80) != 0) ? ReadPalette(_reader, 1 << (num5 & 7) + 1) : _globalPalette); if (array == null) { throw new InvalidDataException("GIF has no color table."); } int minCodeSize = _reader.ReadByte(); byte[] indices = DecodeLzw(_reader.ReadSubBlocks(), minCodeSize, checked(num3 * num4)); byte[] previousSnapshot = ((_disposal == 3) ? ((byte[])_canvas.Clone()) : null); int source = 0; if ((num5 & 0x40) != 0) { foreach (int item in InterlacedRows(num4)) { DrawRow(_canvas, Width, num, num2 + item, num3, indices, ref source, array, _transparent); } } else { for (int i = 0; i < num4; i++) { DrawRow(_canvas, Width, num, num2 + i, num3, indices, ref source, array, _transparent); } } byte[] array2 = new byte[_canvas.Length]; int num6 = Width * 4; for (int j = 0; j < Height; j++) { Buffer.BlockCopy(_canvas, j * num6, array2, (Height - 1 - j) * num6, num6); } int delay = _delay; _previousDisposal = _disposal; _previousX = num; _previousY = num2; _previousW = num3; _previousH = num4; _previousSnapshot = previousSnapshot; _disposal = 0; _delay = 100; _transparent = -1; _framesRead++; return new GifFrame(array2, delay); } } } } internal void Rewind() { _reader.Position = _firstBlockOffset; Array.Clear(_canvas, 0, _canvas.Length); _previousDisposal = (_previousX = (_previousY = (_previousW = (_previousH = 0)))); _previousSnapshot = null; _disposal = 0; _delay = 100; _transparent = -1; _ended = false; _framesRead = 0; } private static void DrawRow(byte[] canvas, int canvasWidth, int x, int y, int width, byte[] indices, ref int source, Rgb[] palette, int transparent) { for (int i = 0; i < width; i++) { int num = indices[source++]; if (num != transparent) { if (num >= palette.Length) { throw new InvalidDataException("GIF color index exceeds the palette."); } int num2 = (y * canvasWidth + x + i) * 4; canvas[num2] = palette[num].R; canvas[num2 + 1] = palette[num].G; canvas[num2 + 2] = palette[num].B; canvas[num2 + 3] = byte.MaxValue; } } } private static IEnumerable<int> InterlacedRows(int height) { (int, int)[] array = new(int, int)[4] { (0, 8), (4, 8), (2, 4), (1, 2) }; for (int i = 0; i < array.Length; i++) { (int, int) pass = array[i]; var (row, _) = pass; for (; row < height; row += pass.Item2) { yield return row; } } } private static void ApplyDisposal(byte[] canvas, int width, int disposal, int x, int y, int w, int h, byte[]? snapshot) { if (disposal == 3 && snapshot != null) { Buffer.BlockCopy(snapshot, 0, canvas, 0, canvas.Length); } else if (disposal == 2) { for (int i = y; i < y + h; i++) { Array.Clear(canvas, (i * width + x) * 4, w * 4); } } } private static Rgb[] ReadPalette(Reader reader, int count) { Rgb[] array = new Rgb[count]; for (int i = 0; i < count; i++) { array[i] = new Rgb((byte)reader.ReadByte(), (byte)reader.ReadByte(), (byte)reader.ReadByte()); } return array; } private static byte[] DecodeLzw(byte[] compressed, int minCodeSize, int expected) { if (minCodeSize < 2 || minCodeSize > 8) { throw new InvalidDataException("Unsupported GIF LZW code size."); } int num = 1 << minCodeSize; int num2 = num + 1; int num3 = num2 + 1; int num4 = minCodeSize + 1; int num5 = -1; int num6 = 0; short[] array = new short[4096]; byte[] array2 = new byte[4096]; byte[] array3 = new byte[4096]; byte[] array4 = new byte[expected]; int num7 = 0; int num8 = 0; bool flag = false; while (num7 + num4 <= compressed.Length * 8) { int num9 = 0; for (int i = 0; i < num4; i++) { num9 |= ((compressed[num7 + i >> 3] >> ((num7 + i) & 7)) & 1) << i; } num7 += num4; if (num9 == num) { num3 = num2 + 1; num4 = minCodeSize + 1; num5 = -1; continue; } if (num9 == num2) { flag = true; break; } if (num9 > num3 || (num9 == num3 && num5 < 0)) { throw new InvalidDataException("Invalid GIF LZW dictionary code."); } int num10 = num9; int num11 = 0; if (num9 == num3) { array3[num11++] = (byte)num6; num9 = num5; } while (num9 >= num) { if (num9 >= num3 || num11 >= array3.Length) { throw new InvalidDataException("Invalid GIF LZW prefix chain."); } array3[num11++] = array2[num9]; num9 = array[num9]; } num6 = num9; array3[num11++] = (byte)num6; while (num11 > 0) { if (num8 >= expected) { throw new InvalidDataException("GIF frame contains too many pixels."); } array4[num8++] = array3[--num11]; } if (num5 >= 0 && num3 < 4096) { array[num3] = (short)num5; array2[num3] = (byte)num6; num3++; if (num3 == 1 << num4 && num4 < 12) { num4++; } } num5 = num10; } if (!flag || num8 != expected) { throw new InvalidDataException("GIF frame has incomplete pixel data."); } return array4; } } internal sealed class GifFrame { internal readonly byte[] Rgba; internal readonly int DelayMs; internal GifFrame(byte[] rgba, int delayMs) { Rgba = rgba; DelayMs = delayMs; } } public sealed class GifPoster : MonoBehaviour, TextReceiver { private const float ViewDistance = 28f; private const string ScaleKey = "szopski_gif_poster_scale100"; private const string LegacySizeKey = "szopski_gif_poster_size"; private const string SetScaleRpc = "XareNGifPosterSetScale"; private const string VolumeKey = "xaren_gif_poster_volume"; private const string SetVolumeRpc = "XareNGifPosterSetVolume"; private static readonly FieldInfo? ViewableField = AccessTools.Field(typeof(Sign), "m_isViewable"); private Sign? _sign; private ZNetView? _view; private RawImage? _image; private RawImage[]? _placeholder; private GifCache.Entry? _entry; private PosterVideo? _video; private string _url = ""; private float _nextCheck; private Vector3 _originalScale; private Collider[] _colliders = Array.Empty<Collider>(); private int _scalePercent = 100; private bool _editingVolume; private int _volumePercent = 50; internal string Status => _video?.Status ?? _entry?.Status ?? ""; internal string ScaleLabel => PosterScale.Format(_scalePercent) + "×"; internal string VolumeLabel => _volumePercent + "%"; private void Start() { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) _sign = ((Component)this).GetComponent<Sign>(); _view = ((Component)this).GetComponent<ZNetView>(); _originalScale = ((Component)this).transform.localScale; _colliders = ((Component)this).GetComponentsInChildren<Collider>(true); ZNetView? view = _view; if (view != null) { view.Register<int>("XareNGifPosterSetScale", (Action<long, int>)OnSetScale); } ZNetView? view2 = _view; if (view2 != null) { view2.Register<int>("XareNGifPosterSetVolume", (Action<long, int>)OnSetVolume); } if ((Object)(object)_view != (Object)null && _view.IsValid()) { IgnorePlayerCollisions(); } } private void InitializeDisplay() { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Expected O, but got Unknown //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_sign == (Object)null) && !((Object)(object)_sign.m_textWidget == (Object)null) && (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsDedicated())) { Renderer[] componentsInChildren = ((Component)this).GetComponentsInChildren<Renderer>(true); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].enabled = false; } RectTransform rectTransform = ((TMP_Text)_sign.m_textWidget).rectTransform; _placeholder = (RawImage[]?)(object)new RawImage[5] { CreatePlaceholderFill(rectTransform), CreateOutlineEdge(rectTransform, "Bottom", new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0f, 4f)), CreateOutlineEdge(rectTransform, "Top", new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0f, 4f)), CreateOutlineEdge(rectTransform, "Left", new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(4f, 0f)), CreateOutlineEdge(rectTransform, "Right", new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(4f, 0f)) }; GameObject val = new GameObject("Image surface", new Type[3] { typeof(RectTransform), typeof(RawImage), typeof(AspectRatioFitter) }); RectTransform component = val.GetComponent<RectTransform>(); ((Transform)component).SetParent((Transform)(object)rectTransform, false); component.anchorMin = Vector2.zero; component.anchorMax = Vector2.one; component.sizeDelta = Vector2.zero; component.anchoredPosition = Vector2.zero; val.GetComponent<AspectRatioFitter>().aspectMode = (AspectMode)3; _image = val.GetComponent<RawImage>(); ((Graphic)_image).raycastTarget = false; ((Behaviour)_image).enabled = false; ((Behaviour)_sign.m_textWidget).enabled = false; } } private void Update() { //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_view == (Object)null || !_view.IsValid()) { return; } if ((Object)(object)_image == (Object)null && ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsDedicated())) { InitializeDisplay(); } if (Time.time < _nextCheck) { return; } _nextCheck = Time.time + 0.5f; IgnorePlayerCollisions(); int num = ReadScale(_view.GetZDO()); _volumePercent = ReadVolume(_view.GetZDO()); if (num != _scalePercent) { _scalePercent = num; ((Component)this).transform.localScale = _originalScale * ((float)num / 100f); } if ((Object)(object)_image == (Object)null) { return; } string text = _view.GetZDO().GetString(ZDOVars.s_text, ""); if (!GifUrl.TryValidate(text, Plugin.AllowHttp.Value, out string _)) { text = ""; } bool flag = (Object)(object)Player.m_localPlayer != (Object)null && Vector3.Distance(((Component)Player.m_localPlayer).transform.position, ((Component)this).transform.position) <= 28f; int num2; if ((Object)(object)_sign != (Object)null) { object obj = ViewableField?.GetValue(_sign); num2 = ((obj is bool && (bool)obj) ? 1 : 0); } else { num2 = 0; } bool flag2 = (byte)num2 != 0; if (text != _url || !flag || !flag2) { Release(); _url = text; } GifCache.Entry? entry = _entry; if (entry != null && entry.Failed && Time.realtimeSinceStartup - _entry.FailedAt >= 30f) { Release(); } if (flag && flag2 && _entry == null && _video == null && _url.Length > 0) { if (GifUrl.IsVideo(_url)) { _video = new PosterVideo(((Component)this).transform, _url, _volumePercent); } else { _entry = GifCache.Acquire(_url); } } _video?.Tick(_volumePercent); Texture val = (Texture)(((object)_video?.Texture) ?? ((object)_entry?.Texture)); if ((Object)(object)val != (Object)null) { _image.texture = val; ((Component)_image).GetComponent<AspectRatioFitter>().aspectRatio = (float)val.width / (float)val.height; ((Behaviour)_image).enabled = true; SetPlaceholderVisible(visible: false); } else { _image.texture = null; ((Behaviour)_image).enabled = false; SetPlaceholderVisible(visible: true); } } private void OnDestroy() { Release(); } private void OnDisable() { Release(); } private void IgnorePlayerCollisions() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) foreach (Player allPlayer in Player.GetAllPlayers()) { if ((Object)(object)allPlayer == (Object)null || Vector3.Distance(((Component)allPlayer).transform.position, ((Component)this).transform.position) > 28f) { continue; } CapsuleCollider collider = ((Character)allPlayer).GetCollider(); if ((Object)(object)collider == (Object)null || !((Collider)collider).enabled || !((Component)collider).gameObject.activeInHierarchy) { continue; } Collider[] colliders = _colliders; foreach (Collider val in colliders) { if (!((Object)(object)val == (Object)null) && !val.isTrigger && val.enabled && ((Component)val).gameObject.activeInHierarchy && !Physics.GetIgnoreCollision(val, (Collider)(object)collider)) { Physics.IgnoreCollision(val, (Collider)(object)collider); } } } } internal bool RequestScaleInput() { if ((Object)(object)_view == (Object)null || !_view.IsValid() || (Object)(object)TextInput.instance == (Object)null) { return false; } _editingVolume = false; TextInput.instance.RequestText((TextReceiver)(object)this, "Poster scale (0.25–10×)", 12); return true; } internal bool RequestVolumeInput() { if ((Object)(object)_view == (Object)null || !_view.IsValid() || (Object)(object)TextInput.instance == (Object)null) { return false; } _editingVolume = true; TextInput.instance.RequestText((TextReceiver)(object)this, "Poster volume (0–100%; 0 = mute)", 3); return true; } public string GetText() { if (!_editingVolume) { return PosterScale.Format(((Object)(object)_view != (Object)null && _view.IsValid()) ? ReadScale(_view.GetZDO()) : _scalePercent); } return (((Object)(object)_view != (Object)null && _view.IsValid()) ? ReadVolume(_view.GetZDO()) : _volumePercent).ToString(); } public void SetText(string text) { //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) int percent; if (_editingVolume) { if (!PosterVolume.TryParse(text, out var value)) { MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, "Enter a whole number from 0 to 100 (0 = mute).", 0, (Sprite)null, false, false); } } else if ((Object)(object)_view != (Object)null && _view.IsValid() && PrivateArea.CheckAccess(((Component)this).transform.position, 0f, true, false)) { _view.InvokeRPC("XareNGifPosterSetVolume", new object[1] { value }); } } else if (!PosterScale.TryParse(text, out percent)) { if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, "Enter a scale from 0.25 to 10 (up to 2 decimal places).", 0, (Sprite)null, false, false); } } else if (!((Object)(object)_view == (Object)null) && _view.IsValid() && PrivateArea.CheckAccess(((Component)this).transform.position, 0f, true, false)) { _view.InvokeRPC("XareNGifPosterSetScale", new object[1] { percent }); } } private void OnSetScale(long sender, int percent) { if (PosterScale.IsValid(percent) && !((Object)(object)_view == (Object)null) && _view.IsValid() && _view.IsOwner()) { _view.GetZDO().Set("szopski_gif_poster_scale100", percent); } } private void OnSetVolume(long sender, int percent) { if (PosterVolume.IsValid(percent) && !((Object)(object)_view == (Object)null) && _view.IsValid() && _view.IsOwner()) { _view.GetZDO().Set("xaren_gif_poster_volume", percent); } } private static int ReadVolume(ZDO zdo) { int num = zdo.GetInt("xaren_gif_poster_volume", 50); if (!PosterVolume.IsValid(num)) { return 50; } return num; } private static int ReadScale(ZDO zdo) { int num = zdo.GetInt("szopski_gif_poster_scale100", 0); if (num == 0) { return PosterScale.FromLegacySize(zdo.GetInt("szopski_gif_poster_size", 0)); } if (!PosterScale.IsValid(num)) { return 100; } return num; } private static RawImage CreateOutlineEdge(RectTransform parent, string name, Vector2 anchorMin, Vector2 anchorMax, Vector2 size) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Poster outline " + name, new Type[2] { typeof(RectTransform), typeof(RawImage) }); RectTransform component = val.GetComponent<RectTransform>(); ((Transform)component).SetParent((Transform)(object)parent, false); component.anchorMin = anchorMin; component.anchorMax = anchorMax; component.sizeDelta = size; component.anchoredPosition = Vector2.zero; RawImage component2 = val.GetComponent<RawImage>(); ((Graphic)component2).color = new Color(0.65f, 0.85f, 1f, 0.7f); ((Graphic)component2).raycastTarget = false; return component2; } private static RawImage CreatePlaceholderFill(RectTransform parent) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Image poster placeholder", new Type[2] { typeof(RectTransform), typeof(RawImage) }); RectTransform component = val.GetComponent<RectTransform>(); ((Transform)component).SetParent((Transform)(object)parent, false); component.anchorMin = Vector2.zero; component.anchorMax = Vector2.one; component.sizeDelta = Vector2.zero; RawImage component2 = val.GetComponent<RawImage>(); ((Graphic)component2).color = new Color(0.25f, 0.45f, 0.55f, 0.2f); ((Graphic)component2).raycastTarget = false; return component2; } private void SetPlaceholderVisible(bool visible) { if (_placeholder != null) { RawImage[] placeholder = _placeholder; for (int i = 0; i < placeholder.Length; i++) { ((Behaviour)placeholder[i]).enabled = visible; } } } private void Release() { _video?.Dispose(); _video = null; if (_entry != null) { GifCache.Release(_entry); } _entry = null; if ((Object)(object)_image != (Object)null) { _image.texture = null; ((Behaviour)_image).enabled = false; } SetPlaceholderVisible(visible: true); } } internal static class GifUrl { internal const int MaxLength = 1024; internal static bool IsVideo(string value) { if (!Uri.TryCreate(value, UriKind.Absolute, out Uri result)) { return false; } if (result.Fragment.Equals("#video", StringComparison.OrdinalIgnoreCase)) { return true; } string text = Path.GetExtension(result.AbsolutePath).ToLowerInvariant(); if (text != null) { int length = text.Length; if (length != 4) { if (length == 5) { char c = text[1]; if (c != 'm') { if (c == 'w' && text == ".webm") { goto IL_0153; } } else if (text == ".mpeg") { goto IL_0153; } } } else { switch (text[2]) { case 'p': break; case '4': goto IL_00dd; case 'o': goto IL_00ec; case 'v': goto IL_00fb; case 'm': goto IL_010a; case 's': goto IL_0119; case 'g': goto IL_0128; default: goto IL_0157; } if (text == ".mp4" || text == ".mpg") { goto IL_0153; } } } goto IL_0157; IL_00fb: if (text == ".avi") { goto IL_0153; } goto IL_0157; IL_0128: if (text == ".ogv") { goto IL_0153; } goto IL_0157; IL_0157: return false; IL_0153: return true; IL_010a: if (text == ".wmv") { goto IL_0153; } goto IL_0157; IL_00ec: if (text == ".mov") { goto IL_0153; } goto IL_0157; IL_00dd: if (text == ".m4v") { goto IL_0153; } goto IL_0157; IL_0119: if (text == ".asf") { goto IL_0153; } goto IL_0157; } internal static string VideoDownloadUrl(string value) { return new Uri(value).GetLeftPart(UriPartial.Query); } internal static string ForDownload(string value) { if (!Uri.TryCreate(value, UriKind.Absolute, out Uri result) || !result.Host.Equals("media.discordapp.net", StringComparison.OrdinalIgnoreCase)) { return value; } int num = value.IndexOf('?'); if (num < 0) { return value; } string[] array = value.Substring(num + 1).Split('&'); bool flag = false; for (int i = 0; i < array.Length; i++) { if (array[i].Equals("format=webp", StringComparison.OrdinalIgnoreCase) || array[i].Equals("format=avif", StringComparison.OrdinalIgnoreCase)) { array[i] = "format=png"; flag = true; } } if (!flag) { return value; } return value.Substring(0, num + 1) + string.Join("&", array); } internal static bool TryValidate(string? value, bool allowHttp, out string error) { error = ""; if (string.IsNullOrWhiteSpace(value)) { return true; } if (value.Length > 1024 || value != value.Trim() || !Uri.TryCreate(value, UriKind.Absolute, out Uri result)) { error = "Invalid image or video URL."; return false; } if (result.Scheme != Uri.UriSchemeHttps && (!allowHttp || !(result.Scheme == Uri.UriSchemeHttp))) { error = (allowHttp ? "URL must start with HTTP or HTTPS." : "URL must start with HTTPS."); return false; } if (string.IsNullOrEmpty(result.Host) || !string.IsNullOrEmpty(result.UserInfo) || (result.Fragment.Length > 0 && !result.Fragment.Equals("#video", StringComparison.OrdinalIgnoreCase))) { error = "URL cannot contain credentials. Only the optional #video fragment is allowed."; return false; } bool flag; switch (Path.GetExtension(result.AbsolutePath).ToLowerInvariant()) { case ".mkv": case ".flv": case ".mts": case ".vob": case ".3gp": case ".ts": case ".m2ts": flag = true; break; default: flag = false; break; } if (flag) { error = "This video format is not supported. Use MP4 (H.264/AAC) or WebM (VP8/Vorbis)."; return false; } return true; } } internal enum ImageKind { Unknown, Gif, Png, Jpeg } internal static class ImageFile { internal const int MaxSide = 4096; internal static ImageKind Detect(byte[] data) { if (data.Length >= 6 && data[0] == 71 && data[1] == 73 && data[2] == 70 && data[3] == 56 && (data[4] == 55 || data[4] == 57) && data[5] == 97) { return ImageKind.Gif; } if (data.Length >= 24 && data[0] == 137 && data[1] == 80 && data[2] == 78 && data[3] == 71 && data[4] == 13 && data[5] == 10 && data[6] == 26 && data[7] == 10 && data[8] == 0 && data[9] == 0 && data[10] == 0 && data[11] == 13 && data[12] == 73 && data[13] == 72 && data[14] == 68 && data[15] == 82) { return ImageKind.Png; } if (data.Length >= 4 && data[0] == byte.MaxValue && data[1] == 216 && data[2] == byte.MaxValue) { return ImageKind.Jpeg; } return ImageKind.Unknown; } internal static bool TryGetStaticSize(byte[] data, ImageKind kind, out int width, out int height) { width = (height = 0); switch (kind) { case ImageKind.Png: { if (data.Length < 24) { return false; } uint num3 = ReadU32(data, 16); uint num4 = ReadU32(data, 20); if (num3 > 4096 || num4 > 4096) { return false; } width = (int)num3; height = (int)num4; break; } case ImageKind.Jpeg: { int i = 2; while (i + 3 < data.Length) { if (data[i++] != byte.MaxValue) { return false; } for (; i < data.Length && data[i] == byte.MaxValue; i++) { } if (i >= data.Length) { return false; } int num = data[i++]; switch (num) { case 217: case 218: return false; case 1: case 208: case 209: case 210: case 211: case 212: case 213: case 214: case 215: continue; } if (i + 2 > data.Length) { return false; } int num2 = (data[i] << 8) | data[i + 1]; if (num2 < 2 || num2 > data.Length - i) { return false; } if (num >= 192 && num <= 207 && num != 196 && num != 200 && num != 204) { if (num2 < 7) { return false; } height = (data[i + 3] << 8) | data[i + 4]; width = (data[i + 5] << 8) | data[i + 6]; break; } i += num2; } break; } } if (width > 0 && height > 0 && width <= 4096) { return height <= 4096; } return false; } private static uint ReadU32(byte[] data, int offset) { return (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); } } [BepInPlugin("xaren.valheim.gifposters", "Valheim GIF Posters", "1.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "xaren.valheim.gifposters"; public const string Name = "Valheim GIF Posters"; public const string Version = "1.1.0"; public const string PrefabName = "piece_szopski_gif_poster"; private Harmony? _harmony; internal static Plugin Instance { get; private set; } internal static ConfigEntry<bool> AllowHttp { get; private set; } private void Awake() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown Instance = this; AllowHttp = ((BaseUnityPlugin)this).Config.Bind<bool>("Links", "AllowHttp", false, "Allow plain HTTP image/video URLs. HTTPS is enabled by default. All clients fetch the URL directly."); PrefabManager.OnVanillaPrefabsAvailable += RegisterPoster; _harmony = new Harmony("xaren.valheim.gifposters"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Valheim GIF Posters 1.1.0 loaded"); } private void RegisterPoster() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown PrefabManager.OnVanillaPrefabsAvailable -= RegisterPoster; PieceConfig val = new PieceConfig { Name = "Media poster", Description = "A frameless poster for images and looping videos with spatial audio and adjustable volume", PieceTable = PieceTables.Hammer, Category = PieceCategories.Furniture }; val.AddRequirement("Wood", 2, true); CustomPiece val2 = new CustomPiece("piece_szopski_gif_poster", "sign", val); Sign component = val2.PiecePrefab.GetComponent<Sign>(); if ((Object)(object)component == (Object)null) { ((BaseUnityPlugin)this).Logger.LogError((object)"The vanilla sign prefab has no Sign component; poster was not registered."); return; } component.m_characterLimit = 1024; val2.PiecePrefab.AddComponent<GifPoster>(); if (!PieceManager.Instance.AddPiece(val2)) { ((BaseUnityPlugin)this).Logger.LogError((object)"Could not register the GIF poster piece."); } } private void Update() { GifCache.Tick(Time.unscaledDeltaTime); } private void OnDestroy() { PrefabManager.OnVanillaPrefabsAvailable -= RegisterPoster; Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } GifCache.Clear(); PosterVideo.Clear(); VideoCache.Clear(); } internal void LogError(string message) { ((BaseUnityPlugin)this).Logger.LogError((object)message); } internal void LogWarning(string message) { ((BaseUnityPlugin)this).Logger.LogWarning((object)message); } internal void LogInfo(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)message); } } internal static class PosterScale { internal const int DefaultPercent = 100; internal const int MinPercent = 25; internal const int MaxPercent = 1000; internal static bool IsValid(int percent) { if (percent >= 25) { return percent <= 1000; } return false; } internal static bool TryParse(string? text, out int percent) { percent = 100; if (string.IsNullOrWhiteSpace(text)) { return false; } if (!decimal.TryParse(text.Trim().Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) || result < 0.25m || result > 10m || result * 100m != decimal.Truncate(result * 100m)) { return false; } percent = decimal.ToInt32(result * 100m); return true; } internal static int FromLegacySize(int size) { return size switch { 1 => 200, 2 => 400, _ => 100, }; } internal static string Format(int percent) { return ((decimal)percent / 100m).ToString("0.##", CultureInfo.CurrentCulture); } } internal sealed class PosterVideo : IDisposable { private static int ActiveCount; private static readonly HashSet<PosterVideo> Instances = new HashSet<PosterVideo>(); private readonly GameObject _host; private VideoCache.Entry? _file; private VideoPlayer? _player; private AudioSource? _audio; private bool _disposed; private bool _failed; private string? _playbackError; private float _prepareAt; private int _volume; internal string Status { get; private set; } = "Waiting for video..."; internal Texture? Texture { get { if (_failed || !((Object)(object)_player != (Object)null) || !_player.isPrepared) { return null; } return _player.texture; } } internal PosterVideo(Transform parent, string url, int volume) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Expected O, but got Unknown //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Expected O, but got Unknown _host = new GameObject("Poster video"); _host.transform.SetParent(parent, false); Instances.Add(this); _volume = volume; if (ActiveCount >= 4) { _failed = true; Status = "Too many videos nearby (maximum 4). Move away and return to retry."; return; } _player = _host.AddComponent<VideoPlayer>(); ActiveCount++; _audio = _host.AddComponent<AudioSource>(); _audio.playOnAwake = false; _audio.spatialBlend = 1f; _audio.rolloffMode = (AudioRolloffMode)1; _audio.minDistance = 2f; _audio.maxDistance = 28f; _audio.dopplerLevel = 0f; _audio.bypassReverbZones = true; _audio.reverbZoneMix = 0f; _audio.bypassListenerEffects = true; _audio.bypassEffects = true; _audio.volume = (float)volume / 100f; _player.playOnAwake = false; _player.isLooping = true; _player.skipOnDrop = false; _player.playbackSpeed = 1f; _player.source = (VideoSource)1; _player.renderMode = (VideoRenderMode)4; _player.audioOutputMode = (VideoAudioOutputMode)1; _player.controlledAudioTrackCount = 1; _player.SetTargetAudioSource((ushort)0, _audio); _player.EnableAudioTrack((ushort)0, true); _player.prepareCompleted += new EventHandler(OnPrepared); _player.errorReceived += new ErrorEventHandler(OnError); _file = VideoCache.Acquire(url); if (_file == null) { Fail("Too many video files nearby. Move away and return to retry."); } } internal void Tick(int volume) { _volume = volume; if ((Object)(object)_audio != (Object)null) { _audio.volume = (float)volume / 100f; } if (!_failed && !_disposed && !((Object)(object)_player == (Object)null) && _file != null) { if (_playbackError != null) { string playbackError = _playbackError; _playbackError = null; Fail(playbackError); } else if (!_file.File.IsCompleted) { Status = _file.Status; } else if (_file.File.IsFaulted || _file.File.IsCanceled) { Exception ex = _file.File.Exception?.GetBaseException(); string message = ((ex is InvalidDataException) ? ex.Message : "Could not download the video. Check the direct URL and your connection."); Fail(message); } else if (_prepareAt == 0f) { _prepareAt = Time.realtimeSinceStartup + 0.001f; Status = "Preparing video..."; _player.url = new Uri(_file.File.Result).AbsoluteUri; _player.Prepare(); } else if (!_player.isPrepared && Time.realtimeSinceStartup - _prepareAt > 30f) { _playbackError = "Video decoder timed out."; } } } private void OnPrepared(VideoPlayer player) { if (_disposed || _failed) { return; } if (player.width == 0 || player.height == 0 || Math.Max(player.width, player.height) > 1920 || Math.Min(player.width, player.height) > 1080) { _playbackError = "Video dimensions must fit within 1920 × 1080 (landscape) or 1080 × 1920 (portrait)."; return; } if ((Object)(object)_audio != (Object)null) { _audio.volume = (float)_volume / 100f; } Status = ""; player.Play(); } private void OnError(VideoPlayer player, string message) { if (!_disposed) { _playbackError = "Could not play the video. Try an MP4 with H.264 video and AAC audio."; } } private void Fail(string message) { _failed = true; Status = message; StopPlayer(); if (_file != null) { VideoCache.Release(_file); } _file = null; Plugin.Instance.LogWarning(message); } public void Dispose() { if (!_disposed) { _disposed = true; Instances.Remove(this); StopPlayer(); if (_file != null) { VideoCache.Release(_file); } _file = null; _host.SetActive(false); Object.Destroy((Object)(object)_host); } } private void StopPlayer() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown if ((Object)(object)_player != (Object)null) { _player.prepareCompleted -= new EventHandler(OnPrepared); _player.errorReceived -= new ErrorEventHandler(OnError); _player.Stop(); Object.Destroy((Object)(object)_player); _player = null; ActiveCount--; } AudioSource? audio = _audio; if (audio != null) { audio.Stop(); } } internal static void Clear() { PosterVideo[] array = Instances.ToArray(); for (int i = 0; i < array.Length; i++) { array[i].Dispose(); } } } internal static class PosterVolume { internal const int DefaultPercent = 50; internal static bool IsValid(int value) { if (value >= 0) { return value <= 100; } return false; } internal static bool TryParse(string? text, out int value) { if (int.TryParse(text?.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out value)) { return IsValid(value); } return false; } } [HarmonyPatch(typeof(Sign), "Interact")] internal static class PosterInteractPatch { private static bool Prefix(Sign __instance, bool hold, ref bool __result) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) GifPoster component = ((Component)__instance).GetComponent<GifPoster>(); if ((Object)(object)component == (Object)null) { return true; } __result = false; if (hold || !PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, true, false)) { return false; } if (ZInput.GetKey((KeyCode)304, false) || ZInput.GetKey((KeyCode)303, false)) { __result = component.RequestVolumeInput(); return false; } if (ZInput.GetKey((KeyCode)308, false) || ZInput.GetKey((KeyCode)307, false)) { __result = component.RequestScaleInput(); return false; } if ((Object)(object)TextInput.instance == (Object)null) { return false; } TextInput.instance.RequestText((TextReceiver)(object)__instance, "Paste a direct image or video URL", 1024); __result = true; return false; } } [HarmonyPatch(typeof(Sign), "SetText")] internal static class PosterSetTextPatch { private static bool Prefix(Sign __instance, string text) { if ((Object)(object)((Component)__instance).GetComponent<GifPoster>() == (Object)null) { return true; } if (GifUrl.TryValidate(text, Plugin.AllowHttp.Value, out string error)) { return true; } if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, error, 0, (Sprite)null, false, false); } return false; } } [HarmonyPatch(typeof(Sign), "GetHoverText")] internal static class PosterHoverPatch { private static bool Prefix(Sign __instance, ref string __result) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) GifPoster component = ((Component)__instance).GetComponent<GifPoster>(); if ((Object)(object)component == (Object)null) { return true; } if (!PrivateArea.CheckAccess(((Component)__instance).transform.position, 0f, false, false)) { __result = ""; return false; } __result = Localization.instance.Localize("Media poster (" + component.ScaleLabel + ", volume " + component.VolumeLabel + ")\n[<color=yellow><b>$KEY_Use</b></color>] Paste image or video URL\n[<color=yellow><b>Alt + $KEY_Use</b></color>] Set poster scale\n[<color=yellow><b>Shift + $KEY_Use</b></color>] Set volume"); if (!string.IsNullOrEmpty(component.Status)) { __result = __result + "\n" + component.Status; } return false; } } internal static class VideoCache { internal sealed class Entry { internal readonly string Key; internal readonly CancellationTokenSource Cancellation = new CancellationTokenSource(); internal Task<string> File; internal volatile string Status = "Waiting for video..."; internal int References; internal float LastUsed; internal Entry(string key) { Key = key; } } private const long DiskBudget = 536870912L; private static readonly string DirectoryPath = Path.Combine(Path.GetTempPath(), "ValheimGifPosters", Guid.NewGuid().ToString("N")); private static readonly Dictionary<string, Entry> Entries = new Dictionary<string, Entry>(); private static bool _closed; internal static Entry? Acquire(string url) { if (_closed) { return null; } string key = url; if (!Entries.TryGetValue(key, out Entry entry)) { Prune(makeSlot: true); if (Entries.Count >= 8) { return null; } entry = new Entry(key); Entries.Add(key, entry); bool allowHttp = Plugin.AllowHttp.Value; entry.File = Task.Run(() => VideoFiles.PrepareAsync(url, DirectoryPath, allowHttp, delegate(string status) { entry.Status = status; }, entry.Cancellation.Token)); entry.File.ContinueWith(delegate(Task<string> task) { _ = task.Exception; }, TaskContinuationOptions.OnlyOnFaulted); } entry.References++; entry.LastUsed = Time.realtimeSinceStartup; return entry; } internal static void Release(Entry entry) { entry.References = Math.Max(0, entry.References - 1); entry.LastUsed = Time.realtimeSinceStartup; if (entry.References == 0 && (!entry.File.IsCompleted || entry.File.IsFaulted || entry.File.IsCanceled)) { Evict(entry); } Prune(makeSlot: false); } private static void Prune(bool makeSlot) { long num = Entries.Values.Where((Entry e) => e.File.Status == TaskStatus.RanToCompletion).Sum((Entry e) => (!File.Exists(e.File.Result)) ? 0 : new FileInfo(e.File.Result).Length); Entry[] array = (from e in Entries.Values where e.References == 0 orderby e.LastUsed select e).ToArray(); foreach (Entry entry in array) { if (num > 536870912 || (makeSlot && Entries.Count >= 8)) { if (entry.File.Status == TaskStatus.RanToCompletion && File.Exists(entry.File.Result)) { num -= new FileInfo(entry.File.Result).Length; } Evict(entry); continue; } break; } } private static Task Evict(Entry entry) { if (!Entries.TryGetValue(entry.Key, out Entry value) || entry != value) { return Task.CompletedTask; } Entries.Remove(entry.Key); entry.Cancellation.Cancel(); return entry.File.ContinueWith(delegate(Task<string> task) { if (task.Status == TaskStatus.RanToCompletion) { VideoFiles.Delete(task.Result); } entry.Cancellation.Dispose(); }); } internal static void Clear() { _closed = true; Task.WhenAll(Entries.Values.ToArray().Select(Evict).ToArray()).ContinueWith(delegate(Task task) { _ = task.Exception; try { Directory.Delete(DirectoryPath, recursive: false); } catch (IOException) { } catch (UnauthorizedAccessException) { } }); } } internal static class VideoFiles { internal const long MaxBytes = 268435456L; private static readonly SemaphoreSlim Workers = new SemaphoreSlim(2); internal static async Task<string> PrepareAsync(string url, string directory, bool allowHttp, Action<string> status, CancellationToken token) { await Workers.WaitAsync(token).ConfigureAwait(continueOnCapturedContext: false); string input = Path.Combine(directory, Guid.NewGuid().ToString("N") + (GifUrl.IsVideo(url) ? Path.GetExtension(new Uri(url).AbsolutePath) : ".video")); bool success = false; try { Directory.CreateDirectory(directory); status("Downloading video..."); await DownloadAsync(GifUrl.VideoDownloadUrl(url), input, allowHttp, token).ConfigureAwait(continueOnCapturedContext: false); success = true; return input; } finally { if (!success) { Delete(input); } Workers.Release(); } } internal static async Task DownloadAsync(string url, string path, bool allowHttp, CancellationToken token) { using HttpClientHandler handler = new HttpClientHandler { AllowAutoRedirect = false }; using HttpClient client = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(3.0) }; for (int redirects = 0; redirects <= 5; redirects++) { if (!GifUrl.TryValidate(url, allowHttp, out string _)) { throw new InvalidDataException("Video redirect requires an allowed HTTP(S) URL."); } using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(continueOnCapturedContext: false); int statusCode = (int)response.StatusCode; if (((uint)(statusCode - 301) <= 2u || (uint)(statusCode - 307) <= 1u) ? true : false) { if (response.Headers.Location == null) { throw new InvalidDataException("Video redirect has no destination."); } url = new Uri(new Uri(url), response.Headers.Location).AbsoluteUri; continue; } response.EnsureSuccessStatusCode(); if (response.Content.Headers.ContentLength > 268435456) { throw new InvalidDataException("Video exceeds the 256 MiB download limit."); } string text = response.Content.Headers.ContentType?.MediaType; if (text == "text/html" || text == "application/json") { throw new InvalidDataException("Use a direct video file URL, not a web page."); } using Stream source = await response.Content.ReadAsStreamAsync().ConfigureAwait(continueOnCapturedContext: false); using FileStream destination = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 65536, useAsync: true); byte[] buffer = new byte[65536]; long total = 0L; using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { token }); timeout.CancelAfter(TimeSpan.FromMinutes(3.0)); int num; while ((num = await source.ReadAsync(buffer, 0, buffer.Length, timeout.Token).ConfigureAwait(continueOnCapturedContext: false)) > 0) { total += num; if (total > 268435456) { throw new InvalidDataException("Video exceeds the 256 MiB download limit."); } await destination.WriteAsync(buffer, 0, num, timeout.Token).ConfigureAwait(continueOnCapturedContext: false); } if (total == 0L) { throw new InvalidDataException("The video file is empty."); } return; } throw new InvalidDataException("Too many video redirects."); } internal static void Delete(string path) { try { File.Delete(path); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } }