Иконка ресурса

FREE NCP : 1.4.2 1.4.2

Нет прав для скачивания
C#:
#nullable disable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;

using Facepunch;
using Network;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core.Libraries.Covalence;
using Oxide.Game.Rust.Cui;
using UnityEngine;

using static RpcTarget;

namespace Oxide.Plugins;

[Info("NCP", "crash", "1.4.2")]
[Description("Advanced notification system with individual anchoring, collision avoidance, and slide animations")]
public class NCP : RustPlugin
{
    #region Fields & Constants

    private const string Layer = "UI.NCP";
    private const string EditLayer = "UI.NCP.Edit";
    private static NCP _instance;

    private static readonly Dictionary<string, string> _cachedElementNames = new();
    private readonly Dictionary<ulong, NotificationUI> _activeNotifications = new();
    private readonly Dictionary<ulong, int> _editingSessions = new();
    private readonly Dictionary<ulong, string> _persistentPreviewIds = new();
    private readonly Dictionary<ulong, int> _currentEditTab = new();
    private readonly Dictionary<ulong, string> _pendingCustomSounds = new();

    private static readonly Effect _cachedEffect = new Effect();

    private const string PermSee = "ncp.see", PermNotify = "ncp.notify",
        PermPlayer = "ncp.player", PermAll = "ncp.allplayers",
        PermEdit = "ncp.edit";

    private static readonly WaitForSeconds UpdateInterval = CoroutineEx.waitForSeconds(0.5f);

    private static readonly string[] AnchorNames =
        { "TopLeft", "TopRight", "BottomLeft", "BottomRight", "MiddleLeft", "MiddleRight" };

    private static readonly AnchorPosition[] AnchorValues =
    {
        AnchorPosition.TopLeft, AnchorPosition.TopRight, AnchorPosition.BottomLeft,
        AnchorPosition.BottomRight, AnchorPosition.MiddleLeft, AnchorPosition.MiddleRight
    };

    private static readonly string[] SlideDirectionNames =
        { "None", "Left", "Right", "Top", "Bottom", "BottomLeft", "BottomRight", "TopLeft", "TopRight" };

    private enum AnchorPosition
    {
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight,
        MiddleLeft,
        MiddleRight
    }

    private enum SlideDirection
    {
        None,
        Left,
        Right,
        Top,
        Bottom,
        BottomLeft,
        BottomRight,
        TopLeft,
        TopRight
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void AddUI(BasePlayer player, CuiElementContainer container)
        => CommunityEntity.ServerInstance.ClientRPC(
            Player("AddUI", player.net.connection), container.ToJson());

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void DestroyUI(BasePlayer player, string name)
        => CommunityEntity.ServerInstance.ClientRPC(
            Player("DestroyUI", player.net.connection), name);

    #endregion

    #region Configuration

    private Configuration _config;

    private sealed class Configuration
    {
        [JsonProperty("Permission (empty = no permission required)")]
        public string Permission = "";

        [JsonProperty("Notification Types")]
        public Dictionary<int, NotificationType> Types = new Dictionary<int, NotificationType>
        {
            [0] = new NotificationType
            {
                Enabled = true,
                Title = "Information",
                Anchor = AnchorPosition.TopRight,
                MaxVisible = 3,
                IconAsset = "assets/icons/info.png",
                BackgroundColor = new IColor("#1a1a1a", 92),
                EnableGradient = true,
                GradientColor = new IColor("#4A90E2", 20),
                Sprite = "assets/content/ui/ui.background.transparent.linearltr.tga",
                Material = "Assets/Icons/IconMaterial.mat",
                IconBgColor = new IColor("#4A90E2", 80),
                BorderColor = new IColor("#4A90E2", 50),
                TitleColor = new IColor("#FFFFFF", 80),
                TextColor = new IColor("#CCCCCC", 85),
                TitleFontSize = 12,
                MessageFontSize = 8,
                Duration = 5f,
                FadeIn = 0.3f,
                FadeOut = 1.2f,
                Effect = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab",
                XOffset = -15f,
                YOffset = -15f,
                Spacing = 5f,
                TypeSpacing = 10f,
                MainOffsetMin = "0 0",
                MainOffsetMax = "0 0",
                IconOffsetMin = "8 -14",
                IconOffsetMax = "36 14",
                TitleOffsetMin = "42 0",
                TitleOffsetMax = "-10 0",
                MessageOffsetMin = "42 0",
                MessageOffsetMax = "-10 0",
                EnableSlide = true,
                Slide = SlideDirection.Left,
                SlideDistance = 300f
            },
            [1] = new NotificationType
            {
                Enabled = true,
                Title = "Warning",
                Anchor = AnchorPosition.TopRight,
                MaxVisible = 2,
                IconAsset = "assets/icons/warning.png",
                BackgroundColor = new IColor("#1a1a1a", 92),
                EnableGradient = true,
                GradientColor = new IColor("#E2A04A", 20),
                Sprite = "assets/content/ui/ui.background.transparent.linearltr.tga",
                Material = "Assets/Icons/IconMaterial.mat",
                IconBgColor = new IColor("#E2A04A", 80),
                BorderColor = new IColor("#E2A04A", 50),
                TitleColor = new IColor("#FFFFFF", 80),
                TextColor = new IColor("#CCCCCC", 85),
                TitleFontSize = 12,
                MessageFontSize = 8,
                Duration = 7f,
                FadeIn = 0.3f,
                FadeOut = 1.2f,
                Effect = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab",
                XOffset = -15f,
                YOffset = -15f,
                Spacing = 5f,
                TypeSpacing = 10f,
                MainOffsetMin = "0 0",
                MainOffsetMax = "0 0",
                IconOffsetMin = "8 -14",
                IconOffsetMax = "36 14",
                TitleOffsetMin = "42 0",
                TitleOffsetMax = "-10 0",
                MessageOffsetMin = "42 0",
                MessageOffsetMax = "-10 0",
                EnableSlide = true,
                Slide = SlideDirection.Right,
                SlideDistance = 300f
            },
            [2] = new NotificationType
            {
                Enabled = true,
                Title = "Error",
                Anchor = AnchorPosition.BottomLeft,
                MaxVisible = 4,
                IconAsset = "assets/icons/close.png",
                BackgroundColor = new IColor("#1a1a1a", 92),
                EnableGradient = true,
                GradientColor = new IColor("#CE422B", 25),
                Sprite = "assets/content/ui/ui.background.transparent.linearltr.tga",
                Material = "Assets/Icons/IconMaterial.mat",
                IconBgColor = new IColor("#CE422B", 85),
                BorderColor = new IColor("#CE422B", 60),
                TitleColor = new IColor("#FFFFFF", 80),
                TextColor = new IColor("#CCCCCC", 85),
                TitleFontSize = 12,
                MessageFontSize = 8,
                Duration = 8f,
                FadeIn = 0.3f,
                FadeOut = 1.2f,
                Effect = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab",
                XOffset = 15f,
                YOffset = 15f,
                Spacing = 5f,
                TypeSpacing = 10f,
                MainOffsetMin = "0 0",
                MainOffsetMax = "0 0",
                IconOffsetMin = "8 -14",
                IconOffsetMax = "36 14",
                TitleOffsetMin = "42 0",
                TitleOffsetMax = "-10 0",
                MessageOffsetMin = "42 0",
                MessageOffsetMax = "-10 0",
                EnableSlide = true,
                Slide = SlideDirection.Bottom,
                SlideDistance = 300f
            },
            [3] = new NotificationType
            {
                Enabled = true,
                Title = "Success",
                Anchor = AnchorPosition.BottomRight,
                MaxVisible = 5,
                IconAsset = "assets/icons/thumbsup.png",
                BackgroundColor = new IColor("#1a1a1a", 92),
                EnableGradient = true,
                GradientColor = new IColor("#7ED321", 20),
                Sprite = "assets/content/ui/ui.background.transparent.linearltr.tga",
                Material = "Assets/Icons/IconMaterial.mat",
                IconBgColor = new IColor("#7ED321", 80),
                BorderColor = new IColor("#7ED321", 50),
                TitleColor = new IColor("#FFFFFF", 80),
                TextColor = new IColor("#CCCCCC", 85),
                TitleFontSize = 12,
                MessageFontSize = 8,
                Duration = 5f,
                FadeIn = 0.3f,
                FadeOut = 1.2f,
                Effect = "assets/bundled/prefabs/fx/notice/item.select.fx.prefab",
                XOffset = -15f,
                YOffset = 15f,
                Spacing = 5f,
                TypeSpacing = 10f,
                MainOffsetMin = "0 0",
                MainOffsetMax = "0 0",
                IconOffsetMin = "8 -14",
                IconOffsetMax = "36 14",
                TitleOffsetMin = "42 0",
                TitleOffsetMax = "-10 0",
                MessageOffsetMin = "42 0",
                MessageOffsetMax = "-10 0",
                EnableSlide = true,
                Slide = SlideDirection.BottomRight,
                SlideDistance = 300f
            }
        };
    }

    private sealed class NotificationType
    {
        [JsonProperty("Enabled")] public bool Enabled = true;
        [JsonProperty("Title")] public string Title = "Notification";
        [JsonProperty("Anchor Position")]
        [JsonConverter(typeof(StringEnumConverter))] public AnchorPosition Anchor = AnchorPosition.TopRight;
        [JsonProperty("Max Visible")] public int MaxVisible = 3;
        [JsonProperty("Icon Asset")] public string IconAsset = "assets/icons/info.png";
        [JsonProperty("Background Color")] public IColor BackgroundColor = new IColor("#1a1a1a", 95);
        [JsonProperty("Enable Gradient")] public bool EnableGradient = true;
        [JsonProperty("Gradient Color")] public IColor GradientColor = new IColor("#CE422B", 25);
        [JsonProperty("Sprite")] public string Sprite = "assets/content/ui/ui.background.transparent.linearltr.tga";
        [JsonProperty("Material")] public string Material = "Assets/Icons/IconMaterial.mat";
        [JsonProperty("Icon Background Color")] public IColor IconBgColor = new IColor("#CE422B", 85);
        [JsonProperty("Border Color")] public IColor BorderColor = new IColor("#CE422B", 60);
        [JsonProperty("Title Color")] public IColor TitleColor = new IColor("#FFFFFF", 85);
        [JsonProperty("Text Color")] public IColor TextColor = new IColor("#CCCCCC", 90);
        [JsonProperty("Title Font Size")] public int TitleFontSize = 12;
        [JsonProperty("Message Font Size")] public int MessageFontSize = 8;
        [JsonProperty("Duration")] public float Duration = 5f;
        [JsonProperty("Fade In")] public float FadeIn = 0.3f;
        [JsonProperty("Fade Out")] public float FadeOut = 1.2f;
        [JsonProperty("Sound Effect")] public string Effect = "";
        [JsonProperty("X Offset")] public float XOffset = -15f;
        [JsonProperty("Y Offset")] public float YOffset = -15f;
        [JsonProperty("Spacing")] public float Spacing = 5f;
        [JsonProperty("Type Spacing (spacing between different types in same anchor)")] public float TypeSpacing = 10f;
        [JsonProperty("Title Anchor Min")] public string TitleAnchorMin = "0 0.5";
        [JsonProperty("Title Anchor Max")] public string TitleAnchorMax = "1 0.9";
        [JsonProperty("Message Anchor Min")] public string MessageAnchorMin = "0 0.1";
        [JsonProperty("Message Anchor Max")] public string MessageAnchorMax = "1 0.5";
        [JsonProperty("Icon Anchor Min")] public string IconAnchorMin = "0 0.5";
        [JsonProperty("Icon Anchor Max")] public string IconAnchorMax = "0 0.5";
        [JsonProperty("Main Panel Offset Min")] public string MainOffsetMin = "0 0";
        [JsonProperty("Main Panel Offset Max")] public string MainOffsetMax = "0 0";
        [JsonProperty("Container Width")] public float ContainerWidth = 240f;
        [JsonProperty("Container Height")] public float ContainerHeight = 50f;
        [JsonProperty("Container X Offset")] public float ContainerXOffset;
        [JsonProperty("Container Y Offset")] public float ContainerYOffset;
        [JsonProperty("Icon Offset Min")] public string IconOffsetMin = "8 -14";
        [JsonProperty("Icon Offset Max")] public string IconOffsetMax = "36 14";
        [JsonProperty("Title Offset Min")] public string TitleOffsetMin = "42 0";
        [JsonProperty("Title Offset Max")] public string TitleOffsetMax = "-10 0";
        [JsonProperty("Message Offset Min")] public string MessageOffsetMin = "42 0";
        [JsonProperty("Message Offset Max")] public string MessageOffsetMax = "-10 0";
        [JsonProperty("Enable Slide Effect")] public bool EnableSlide = true;
        [JsonProperty("Slide Direction")]
        [JsonConverter(typeof(StringEnumConverter))] public SlideDirection Slide = SlideDirection.Left;
        [JsonProperty("Slide Distance")] public float SlideDistance = 300f;
        [JsonProperty("Slide In Duration")] public float SlideInDuration = 0.3f;
        [JsonProperty("Slide Out Direction")]
        [JsonConverter(typeof(StringEnumConverter))] public SlideDirection SlideOut = SlideDirection.None;
        [JsonProperty("Slide Out Distance")] public float SlideOutDistance = 300f;
        [JsonProperty("Slide Out Duration")] public float SlideOutDuration = 0.3f;

        [JsonIgnore] private string _titleUpper;
        [JsonIgnore] public string TitleUpper => _titleUpper ??= Title.ToUpper();
    }

    private sealed class IColor
    {
        [JsonProperty("HEX")] public string Hex;
        [JsonProperty("Opacity (0 - 100)")] public float Alpha;
        [JsonIgnore] private string _computed;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Invalidate() => _computed = null;

        public string Get()
        {
            if (_computed != null) return _computed;

            if (string.IsNullOrEmpty(Hex)) Hex = "#FFFFFF";
            var str = Hex.Trim('#');
            if (str.Length != 6) throw new Exception(Hex);
            var r = byte.Parse(str[..2], NumberStyles.HexNumber);
            var g = byte.Parse(str.Substring(2, 2), NumberStyles.HexNumber);
            var b = byte.Parse(str.Substring(4, 2), NumberStyles.HexNumber);
            _computed = $"{(double)r / 255} {(double)g / 255} {(double)b / 255} {Alpha / 100}";
            return _computed;
        }

        public IColor(string hex, float alpha)
        {
            Hex = hex;
            Alpha = alpha;
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static NotificationType GetTypeConfig(int type)
        => _instance._config.Types.TryGetValue(type, out var cfg) ? cfg : _instance._config.Types[0];

    protected override void LoadConfig()
    {
        base.LoadConfig();
        try
        {
            _config = Config.ReadObject<Configuration>();
            if (_config == null) throw new Exception();
            SaveConfig();
        }
        catch
        {
            PrintError("Configuration file is corrupt! Using default values.");
            LoadDefaultConfig();
        }
    }

    protected override void LoadDefaultConfig() => _config = new Configuration();
    protected override void SaveConfig() => Config.WriteObject(_config);

    #endregion

    #region Core Hooks

    private void OnServerInitialized()
    {
        _instance = this;
        RegisterPermissions();
        AddCovalenceCommand("ncp.show", nameof(CmdShow));
        AddCovalenceCommand("ncp.player", nameof(CmdPlayer));
        AddCovalenceCommand("ncp.allplayers", nameof(CmdAll));
        AddCovalenceCommand("ncp.edit", nameof(CmdEdit));
    }

    private void Unload()
    {
        var notifications = Pool.Get<List<NotificationUI>>();
        notifications.AddRange(_activeNotifications.Values);

        foreach (var notification in notifications)
            notification?.Destroy();

        Pool.FreeUnmanaged(ref notifications);

        foreach (var player in BasePlayer.activePlayerList)
        {
            DestroyUI(player, Layer);
            DestroyUI(player, EditLayer);
        }

        _instance = null;
    }

    #endregion

    #region Commands

    private void CmdShow(IPlayer player, string command, string[] args)
    {
        if (!CanUse(player, PermNotify)) return;

        if (args.Length < 2)
        {
            player.Reply($"Usage: /{command} <type> <message>");
            return;
        }

        if (!int.TryParse(args[0], out var type))
        {
            player.Reply("Invalid type! Must be a number.");
            return;
        }

        var message = string.Join(" ", args, 1, args.Length - 1);
        if (player.Object is BasePlayer basePlayer) SendNotify(basePlayer, type, message);
    }

    private void CmdPlayer(IPlayer player, string command, string[] args)
    {
        if (!CanUse(player, PermPlayer)) return;

        if (args.Length < 3)
        {
            player.Reply($"Usage: /{command} <player> <type> <message>");
            return;
        }

        if (covalence.Players.FindPlayer(args[0])?.Object is not BasePlayer target)
        {
            player.Reply($"Player '{args[0]}' not found!");
            return;
        }

        if (!int.TryParse(args[1], out var type))
        {
            player.Reply("Invalid type! Must be a number.");
            return;
        }

        var message = string.Join(" ", args, 2, args.Length - 2);
        SendNotify(target, type, message);
        player.Reply($"Notification sent to {target.displayName}");
    }

    private void CmdAll(IPlayer player, string command, string[] args)
    {
        if (!CanUse(player, PermAll)) return;

        if (args.Length < 2)
        {
            player.Reply($"Usage: /{command} <type> <message>");
            return;
        }

        if (!int.TryParse(args[0], out var type))
        {
            player.Reply("Invalid type! Must be a number.");
            return;
        }

        var message = string.Join(" ", args, 1, args.Length - 1);
        SendNotifyAllPlayers(type, message);
        player.Reply($"Notification sent to all online players ({BasePlayer.activePlayerList.Count})");
    }

    private void CmdEdit(IPlayer player, string command, string[] args)
    {
        if (!CanUse(player, PermEdit)) return;

        if (player.Object is not BasePlayer basePlayer) return;

        if (args.Length < 1)
        {
            player.Reply("Usage: /ncp.edit <type> - Available types: 0, 1, 2, 3");
            return;
        }

        if (!int.TryParse(args[0], out var type) || !_config.Types.ContainsKey(type))
        {
            player.Reply("Invalid type! Available types: 0, 1, 2, 3");
            return;
        }

        _editingSessions[basePlayer.userID] = type;
        ShowEditUI(basePlayer, type);
    }

    #endregion

    #region Notification System Core

    private static int _nextNotifId;

    private sealed class NotificationData
    {
        public int Type;
        public string Message;
        public float StartTime;
        public string Id = Interlocked.Increment(ref _nextNotifId).ToString();
        public bool IsEditMode;
        public bool IsSliding;
        public float SlideEndTime;
        public bool IsSlidingOut;
        public float SlideOutEndTime;
        public bool IsFadingOut;
        public float FadeOutEndTime;
    }

    private class NotificationUI : FacepunchBehaviour
    {
        private BasePlayer _player;
        private readonly Dictionary<int, Queue<NotificationData>> _typeQueues = new();
        private readonly Dictionary<int, List<NotificationData>> _typeActive = new();
        private readonly CuiElementContainer _container = new();
        private readonly CuiElementContainer _slideContainer = new();
        private readonly Dictionary<string, string> _renderedPositions = new();
        private readonly HashSet<string> _activeIds = new();
        private readonly List<string> _toRemove = new();
        private readonly HashSet<string> _justFinishedSliding = new();
        private readonly HashSet<int> _slideOutCompletedTypes = new();
        private readonly List<(NotificationData notification, CuiRectTransformComponent position)> _positionedResult = new();
        private readonly Dictionary<AnchorPosition, List<(int type, NotificationData notification)>> _anchorGroups = new();
        private readonly List<List<(int, NotificationData)>> _anchorGroupPool = new();
        private bool _layerCreated;
        private Coroutine _updateCoroutine;
        private Coroutine _slideCoroutine;

        private void Awake()
        {
            _player = GetComponent<BasePlayer>();
            _instance._activeNotifications[_player.userID] = this;
            _updateCoroutine = StartCoroutine(UpdateNotifications());

            foreach (var typeId in _instance._config.Types.Keys)
            {
                _typeQueues[typeId] = new Queue<NotificationData>();
                _typeActive[typeId] = new List<NotificationData>();
            }
        }

        private void OnDestroy()
        {
            if (_updateCoroutine is not null) StopCoroutine(_updateCoroutine);
            if (_slideCoroutine is not null) StopCoroutine(_slideCoroutine);
            DestroyUI(_player, Layer);
            _renderedPositions.Clear();
            _layerCreated = false;
            _instance?._activeNotifications.Remove(_player.userID);
        }

        public void AddNotification(int type, string message)
        {
            var config = GetTypeConfig(type);

            var data = new NotificationData
            {
                Type = type,
                Message = message,
                StartTime = Time.time
            };

            if (config.EnableSlide && config.Slide != SlideDirection.None)
            {
                data.IsSliding = true;
                data.SlideEndTime = Time.time + config.SlideInDuration;
            }

            if (!_typeQueues.TryGetValue(type, out var queue))
            {
                queue = new Queue<NotificationData>();
                _typeQueues[type] = queue;
                _typeActive[type] = new List<NotificationData>();
            }

            queue.Enqueue(data);
            ProcessQueue(type);
            PlayEffect(type);

            if (data.IsSliding && _slideCoroutine == null)
                _slideCoroutine = StartCoroutine(SlideUpdateLoop());
        }

        private void ProcessQueue(int type)
        {
            var config = GetTypeConfig(type);
            var maxVisible = config.MaxVisible;

            if (!_typeQueues.TryGetValue(type, out var queue) ||
                !_typeActive.TryGetValue(type, out var active))
                return;

            while (queue.Count > 0 && active.Count < maxVisible)
            {
                var notification = queue.Dequeue();
                active.Add(notification);
            }

            UpdateUI();
        }

        private IEnumerator UpdateNotifications()
        {
            while (true)
            {
                yield return UpdateInterval;

                var hasChanges = false;

                foreach (var kvp in _typeActive)
                {
                    var type = kvp.Key;
                    var active = kvp.Value;

                    for (var i = active.Count - 1; i >= 0; i--)
                    {
                        var notif = active[i];
                        if (notif.IsEditMode) continue;

                        if (notif.IsFadingOut)
                        {
                            if (Time.time >= notif.FadeOutEndTime)
                            {
                                active.RemoveAt(i);
                                hasChanges = true;
                            }

                            continue;
                        }

                        if (notif.IsSlidingOut) continue;

                        var typeConfig = GetTypeConfig(notif.Type);

                        if (!(Time.time - notif.StartTime >= typeConfig.Duration)) continue;

                        if (typeConfig.EnableSlide && typeConfig.SlideOut != SlideDirection.None)
                        {
                            notif.IsSlidingOut = true;
                            notif.SlideOutEndTime = Time.time + typeConfig.SlideOutDuration;

                            _slideCoroutine ??= StartCoroutine(SlideUpdateLoop());

                            continue;
                        }

                        if (typeConfig.FadeOut > 0f && !typeConfig.EnableSlide)
                        {
                            InvalidateRendered(notif.Id);
                            notif.IsFadingOut = true;
                            notif.FadeOutEndTime = Time.time + typeConfig.FadeOut;
                            continue;
                        }

                        active.RemoveAt(i);
                        hasChanges = true;
                    }

                    if (hasChanges) ProcessQueue(type);
                }

                if (!hasChanges) continue;

                var anyActive = false;
                foreach (var list in _typeActive.Values)
                {
                    if (list.Count <= 0) continue;
                    anyActive = true;
                    break;
                }

                if (anyActive) continue;
                var anyQueued = false;
                foreach (var queue in _typeQueues.Values)
                {
                    if (queue.Count <= 0) continue;
                    anyQueued = true;
                    break;
                }

                if (anyQueued) continue;
                Destroy();
                yield break;
            }
        }

        #region Slide Animation

        private bool HasSlidingNotifications()
        {
            foreach (var kvp in _typeActive)
            {
                var active = kvp.Value;
                foreach (var t in active)
                {
                    if (t.IsSliding || t.IsSlidingOut) return true;
                }
            }

            return false;
        }

        private void UpdateSlidingState()
        {
            foreach (var kvp in _typeActive)
            {
                var active = kvp.Value;
                for (var i = active.Count - 1; i >= 0; i--)
                {
                    var notif = active[i];

                    if (notif.IsSliding && Time.time >= notif.SlideEndTime)
                    {
                        notif.IsSliding = false;
                        _justFinishedSliding.Add(notif.Id);
                    }

                    if (!notif.IsSlidingOut || !(Time.time >= notif.SlideOutEndTime)) continue;
                    DestroyNotificationElements(notif.Id);
                    _renderedPositions.Remove(notif.Id);
                    active.RemoveAt(i);
                    _slideOutCompletedTypes.Add(kvp.Key);
                }
            }

            foreach (var type in _slideOutCompletedTypes)
                ProcessQueue(type);
            _slideOutCompletedTypes.Clear();
        }

        private IEnumerator SlideUpdateLoop()
        {
            while (HasSlidingNotifications())
            {
                UpdateSlidingState();
                RebuildSlidingPanels();
                yield return null;
            }

            UpdateUI();
            _slideCoroutine = null;
        }

        private void RebuildSlidingPanels()
        {
            var positionedNotifications = CalculateNotificationPositions();

            _slideContainer.Clear();

            foreach (var positioned in positionedNotifications)
            {
                var notif = positioned.notification;
                if (!notif.IsSliding && !notif.IsSlidingOut) continue;

                var position = positioned.position;
                var cfg = GetTypeConfig(notif.Type);

                if (notif.IsSliding)
                {
                    var slideDuration = cfg.SlideInDuration;
                    if (slideDuration <= 0f) slideDuration = 0.3f;

                    var slideProgress =
                        Mathf.Clamp01(1f - (notif.SlideEndTime - Time.time) / slideDuration);
                    var smoothProgress = Mathf.SmoothStep(0f, 1f, slideProgress);
                    var remainingOffset = cfg.SlideDistance * (1f - smoothProgress);

                    ApplySlideOffset(position, cfg.Slide, remainingOffset);
                }
                else
                {
                    var slideDuration = cfg.SlideOutDuration;
                    if (slideDuration <= 0f) slideDuration = 0.3f;

                    var slideProgress =
                        Mathf.Clamp01(1f - (notif.SlideOutEndTime - Time.time) / slideDuration);
                    var smoothProgress = Mathf.SmoothStep(0f, 1f, slideProgress);
                    var currentOffset = cfg.SlideOutDistance * smoothProgress;

                    ApplySlideOffset(position, cfg.SlideOut, currentOffset);
                }

                var panelId = $"{Layer}.{notif.Id}";

                DestroyUI(_player, $"{panelId}.Shadow");
                DestroyUI(_player, panelId);

                CreateNotificationPanel(_slideContainer, notif, position, 0f);
            }

            AddUI(_player, _slideContainer);
        }

        private static void ApplySlideOffset(CuiRectTransformComponent position, SlideDirection direction,
            float remainingOffset)
        {
            if (!TryParseOffsetPair(position.OffsetMin, out var minX, out var minY)) return;
            if (!TryParseOffsetPair(position.OffsetMax, out var maxX, out var maxY)) return;

            switch (direction)
            {
                case SlideDirection.Left:
                    minX -= remainingOffset;
                    maxX -= remainingOffset;
                    break;
                case SlideDirection.Right:
                    minX += remainingOffset;
                    maxX += remainingOffset;
                    break;
                case SlideDirection.Top:
                    minY += remainingOffset;
                    maxY += remainingOffset;
                    break;
                case SlideDirection.Bottom:
                    minY -= remainingOffset;
                    maxY -= remainingOffset;
                    break;
                case SlideDirection.TopLeft:
                    minX -= remainingOffset;
                    maxX -= remainingOffset;
                    minY += remainingOffset;
                    maxY += remainingOffset;
                    break;
                case SlideDirection.TopRight:
                    minX += remainingOffset;
                    maxX += remainingOffset;
                    minY += remainingOffset;
                    maxY += remainingOffset;
                    break;
                case SlideDirection.BottomLeft:
                    minX -= remainingOffset;
                    maxX -= remainingOffset;
                    minY -= remainingOffset;
                    maxY -= remainingOffset;
                    break;
                case SlideDirection.BottomRight:
                    minX += remainingOffset;
                    maxX += remainingOffset;
                    minY -= remainingOffset;
                    maxY -= remainingOffset;
                    break;
            }

            position.OffsetMin = FormatPair(minX, minY);
            position.OffsetMax = FormatPair(maxX, maxY);
        }

        #endregion

        private void EnsureLayer()
        {
            if (_layerCreated) return;
            _container.Clear();
            CreateMainPanel(_container);
            AddUI(_player, _container);
            _layerCreated = true;
        }

        private void DestroyLayer()
        {
            if (!_layerCreated) return;
            DestroyUI(_player, Layer);
            _layerCreated = false;
            _renderedPositions.Clear();
            _justFinishedSliding.Clear();
        }

        private void DestroyNotificationElements(string notifId)
        {
            var panelId = $"{Layer}.{notifId}";
            DestroyUI(_player, $"{panelId}.Shadow");
            DestroyUI(_player, panelId);
        }

        private void InvalidateRendered(string notifId)
        {
            if (_renderedPositions.Remove(notifId))
                DestroyNotificationElements(notifId);
        }

        private void UpdateUI()
        {
            var positionedNotifications = CalculateNotificationPositions();

            if (positionedNotifications.Count == 0)
            {
                DestroyLayer();
                return;
            }

            EnsureLayer();
            _container.Clear();
            _activeIds.Clear();

            foreach (var positioned in positionedNotifications)
            {
                var notif = positioned.notification;
                var position = positioned.position;
                _activeIds.Add(notif.Id);

                if (notif.IsFadingOut)
                    continue;

                var basePosKey = $"{position.OffsetMin}|{position.OffsetMax}";
                var alreadyRendered = _renderedPositions.TryGetValue(notif.Id, out var oldPosKey);

                if (alreadyRendered && (notif.IsSliding || notif.IsSlidingOut))
                {
                    _renderedPositions[notif.Id] = basePosKey;
                    continue;
                }

                var positionChanged = alreadyRendered &&
                                      !string.Equals(oldPosKey, basePosKey, StringComparison.Ordinal);
                var justFinishedSlide = _justFinishedSliding.Contains(notif.Id);

                switch (alreadyRendered)
                {
                    case true when !positionChanged && !justFinishedSlide:
                        continue;
                    case true:
                        DestroyNotificationElements(notif.Id);
                        break;
                }

                float effectiveFadeIn;
                if (notif.IsSliding)
                {
                    var cfg = GetTypeConfig(notif.Type);
                    var slideDuration = cfg.SlideInDuration;
                    if (slideDuration <= 0f) slideDuration = 0.3f;

                    var slideProgress =
                        Mathf.Clamp01(1f - (notif.SlideEndTime - Time.time) / slideDuration);
                    var smoothProgress = Mathf.SmoothStep(0f, 1f, slideProgress);
                    var remainingOffset = cfg.SlideDistance * (1f - smoothProgress);

                    ApplySlideOffset(position, cfg.Slide, remainingOffset);
                    effectiveFadeIn = 0f;
                }
                else if (alreadyRendered)
                {
                    effectiveFadeIn = 0f;
                }
                else
                {
                    var cfg = GetTypeConfig(notif.Type);
                    effectiveFadeIn = (cfg.EnableSlide && cfg.Slide != SlideDirection.None) ? 0f : cfg.FadeIn;
                }

                CreateNotificationPanel(_container, notif, position, effectiveFadeIn);
                _renderedPositions[notif.Id] = basePosKey;
            }

            _toRemove.Clear();
            foreach (var kvp in _renderedPositions)
            {
                if (!_activeIds.Contains(kvp.Key))
                    _toRemove.Add(kvp.Key);
            }

            foreach (var id in _toRemove)
            {
                DestroyNotificationElements(id);
                _renderedPositions.Remove(id);
            }

            _justFinishedSliding.Clear();

            if (_container.Count > 0)
                AddUI(_player, _container);
        }

        private List<(NotificationData notification, CuiRectTransformComponent position)>
            CalculateNotificationPositions()
        {
            var result = new List<(NotificationData, CuiRectTransformComponent)>();

            var anchorGroups =
                new Dictionary<AnchorPosition, List<(int type, NotificationData notification)>>();

            foreach (var kvp in _typeActive)
            {
                var type = kvp.Key;
                var notifications = kvp.Value;

                if (notifications.Count == 0) continue;

                var config = GetTypeConfig(type);

                if (!anchorGroups.TryGetValue(config.Anchor, out var group))
                {
                    group = new List<(int, NotificationData)>();
                    anchorGroups[config.Anchor] = group;
                }

                foreach (var notification in notifications)
                    group.Add((type, notification));
            }

            foreach (var anchorGroup in anchorGroups)
            {
                var notifications = anchorGroup.Value;

                notifications.Sort((a, b) => a.type.CompareTo(b.type));

                var cumulativeTypeOffset = 0f;
                var groupStart = 0;

                while (groupStart < notifications.Count)
                {
                    var type = notifications[groupStart].type;
                    var groupEnd = groupStart;

                    while (groupEnd < notifications.Count && notifications[groupEnd].type == type)
                        groupEnd++;

                    var config = GetTypeConfig(type);
                    var groupSize = groupEnd - groupStart;

                    for (var j = 0; j < groupSize; j++)
                    {
                        var notification = notifications[groupStart + j].notification;
                        var position = CalculateIndividualPosition(type, j, cumulativeTypeOffset);
                        result.Add((notification, position));
                    }

                    if (groupSize > 0)
                        cumulativeTypeOffset += groupSize * (config.ContainerHeight + config.Spacing) +
                                                config.TypeSpacing;

                    groupStart = groupEnd;
                }
            }

            return result;
        }

        private static CuiRectTransformComponent CalculateIndividualPosition(int type, int indexInType,
            float cumulativeTypeOffset)
        {
            var config = GetTypeConfig(type);

            var isBottom = config.Anchor is AnchorPosition.BottomLeft or AnchorPosition.BottomRight;
            var isRight = config.Anchor is AnchorPosition.TopRight or AnchorPosition.BottomRight
                or AnchorPosition.MiddleRight;
            var isMiddle = config.Anchor is AnchorPosition.MiddleLeft or AnchorPosition.MiddleRight;

            var width = config.ContainerWidth;
            var height = config.ContainerHeight;
            var containerXOffset = config.ContainerXOffset;
            var containerYOffset = config.ContainerYOffset;

            var xOffset = config.XOffset + containerXOffset;

            float yOffset;
            if (isMiddle || isBottom)
                yOffset = config.YOffset + containerYOffset + cumulativeTypeOffset +
                          indexInType * (height + config.Spacing);
            else
                yOffset = config.YOffset + containerYOffset - cumulativeTypeOffset -
                          indexInType * (height + config.Spacing);

            string anchorMinX, anchorMaxX, anchorMinY, anchorMaxY;

            if (isMiddle)
            {
                anchorMinX = isRight ? "1" : "0";
                anchorMaxX = isRight ? "1" : "0";
                anchorMinY = "0.5";
                anchorMaxY = "0.5";
            }
            else
            {
                anchorMinX = isRight ? "1" : "0";
                anchorMaxX = isRight ? "1" : "0";
                anchorMinY = isBottom ? "0" : "1";
                anchorMaxY = isBottom ? "0" : "1";
            }

            string offsetMinX, offsetMaxX, offsetMinY, offsetMaxY;

            if (isRight)
            {
                offsetMinX = $"{xOffset - width}";
                offsetMaxX = $"{xOffset}";
            }
            else
            {
                offsetMinX = $"{-xOffset}";
                offsetMaxX = $"{-xOffset + width}";
            }

            if (isMiddle)
            {
                offsetMinY = $"{yOffset - height / 2}";
                offsetMaxY = $"{yOffset + height / 2}";
            }
            else if (isBottom)
            {
                offsetMinY = $"{yOffset}";
                offsetMaxY = $"{yOffset + height}";
            }
            else
            {
                offsetMinY = $"{yOffset - height}";
                offsetMaxY = $"{yOffset}";
            }

            return new CuiRectTransformComponent
            {
                AnchorMin = $"{anchorMinX} {anchorMinY}",
                AnchorMax = $"{anchorMaxX} {anchorMaxY}",
                OffsetMin = $"{offsetMinX} {offsetMinY}",
                OffsetMax = $"{offsetMaxX} {offsetMaxY}"
            };
        }

        public void AddPreviewNotification(NotificationData data)
        {
            if (!_typeActive.TryGetValue(data.Type, out var active))
            {
                active = new List<NotificationData>();
                _typeActive[data.Type] = active;
            }

            active.Add(data);
            UpdateUI();
        }

        public void UpdatePreviewNotification(string previewId, int type, string message)
        {
            foreach (var activeList in _typeActive.Values)
            {
                NotificationData preview = null;
                foreach (var t in activeList)
                {
                    if (!string.Equals(t.Id, previewId, StringComparison.Ordinal)) continue;
                    preview = t;
                    break;
                }

                if (preview is null) continue;
                InvalidateRendered(previewId);
                preview.Type = type;
                preview.Message = message;
                preview.StartTime = Time.time;
                UpdateUI();
                return;
            }
        }

        public void RemovePreviewNotification(string previewId)
        {
            foreach (var activeList in _typeActive.Values)
            {
                for (var i = 0; i < activeList.Count; i++)
                {
                    if (!string.Equals(activeList[i].Id, previewId, StringComparison.Ordinal)) continue;
                    activeList.RemoveAt(i);
                    UpdateUI();
                    return;
                }
            }
        }

        private static void CreateMainPanel(CuiElementContainer container)
        {
            container.Add(new CuiElement
            {
                Name = Layer,
                Parent = "Hud",
                Components =
                {
                    new CuiImageComponent { Color = "0 0 0 0" },
                    new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1" }
                }
            });
        }

        private static void CreateNotificationPanel(CuiElementContainer container, NotificationData data,
            CuiRectTransformComponent position, float fadeIn)
        {
            var config = GetTypeConfig(data.Type);
            var useSlide = config.EnableSlide && config.Slide != SlideDirection.None;
            var fadeOut = useSlide ? 0f : config.FadeOut;

            var panelId = $"{Layer}.{data.Id}";

            CreateShadowElement(container, panelId, position, config, fadeIn, fadeOut);

            container.Add(new CuiElement
            {
                Name = panelId,
                Parent = Layer,
                Components =
                    { new CuiImageComponent { Color = config.BackgroundColor.Get(), FadeIn = fadeIn }, position },
                FadeOut = fadeOut
            });

            CreateBorderElements(container, panelId, config, fadeIn, fadeOut);
            if (config.EnableGradient) CreateGradientElement(container, panelId, config, fadeIn, fadeOut);

            CreateIconElements(container, panelId, config, fadeIn, fadeOut);

            CreateTextElements(container, panelId, config, data, fadeIn, fadeOut);
        }

        private static void CreateShadowElement(CuiElementContainer container, string panelId,
            CuiRectTransformComponent position, NotificationType config, float fadeIn, float fadeOut)
        {
            if (!TryParseOffsetPair(position.OffsetMin, out var sMinX, out var sMinY)) return;
            if (!TryParseOffsetPair(position.OffsetMax, out var sMaxX, out var sMaxY)) return;

            container.Add(new CuiElement
            {
                Name = $"{panelId}.Shadow",
                Parent = Layer,
                Components =
                {
                    new CuiImageComponent { Color = "0 0 0 0.4", FadeIn = fadeIn },
                    new CuiRectTransformComponent
                    {
                        AnchorMin = position.AnchorMin,
                        AnchorMax = position.AnchorMax,
                        OffsetMin = FormatPair(sMinX + 2f, sMinY - 2f),
                        OffsetMax = FormatPair(sMaxX + 2f, sMaxY - 2f)
                    }
                },
                FadeOut = fadeOut
            });
        }

        private static void CreateBorderElements(CuiElementContainer container, string panelId,
            NotificationType config, float fadeIn, float fadeOut)
        {
            container.Add(new CuiElement
            {
                Name = $"{panelId}.HighlightBorder",
                Parent = panelId,
                Components =
                {
                    new CuiImageComponent { Color = config.BorderColor.Get(), FadeIn = fadeIn },
                    new CuiRectTransformComponent
                        { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -2", OffsetMax = "0 0" }
                },
                FadeOut = fadeOut
            });

            container.Add(new CuiElement
            {
                Name = $"{panelId}.AccentBorder",
                Parent = panelId,
                Components =
                {
                    new CuiImageComponent { Color = config.IconBgColor.Get(), FadeIn = fadeIn },
                    new CuiRectTransformComponent
                        { AnchorMin = "0 0", AnchorMax = "0 1", OffsetMin = "0 0", OffsetMax = "3 0" }
                },
                FadeOut = fadeOut
            });
        }

        private static void CreateGradientElement(CuiElementContainer container, string panelId,
            NotificationType config, float fadeIn, float fadeOut)
        {
            container.Add(new CuiElement
            {
                Name = $"{panelId}.Gradient",
                Parent = panelId,
                Components =
                {
                    new CuiImageComponent
                    {
                        Color = config.GradientColor.Get(),
                        Sprite = config.Sprite,
                        Material = config.Material,
                
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху