C#:
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Notif", "fufeldev", "1.0.0")]
public class Notif : RustPlugin
{
[PluginReference] private Plugin SClan;
private const string UiRoot = "Notif_UI";
private const string UiLayer = "Notif_UI.Layer";
private const float HoldTime = 2.5f;
private const float KillNoticeCooldown = 60f;
private const int KillstreakPoints = 3;
private int _nextId;
private string _heliPng;
private string _killPng;
private string _bradleyPng;
private readonly List<Notice> _notices = new List<Notice>();
private readonly Dictionary<ulong, int> _killStreaks = new Dictionary<ulong, int>();
private readonly Dictionary<ulong, float> _nextKillNoticeTime = new Dictionary<ulong, float>();
private readonly Dictionary<string, string> _imageStorageIds = new Dictionary<string, string>();
private static readonly float[] SlotY = { 90f, 43.3333f, -4f };
private static readonly Dictionary<string, string> ImageFiles = new Dictionary<string, string>
{
{ "Hudaima_Event_Helicopter_CardIcon_1", "Hudaima_Event_Helicopter_CardIcon_1.png" },
{ "Hudaima_Event_Killstreak_CardIcon_1", "Hudaima_Event_Killstreak_CardIcon_1.png" },
{ "Hudaima_Event_Bradley_CardIcon_1", "Hudaima_Event_Bradley_CardIcon_1.png" }
};
private class Notice
{
public int Id;
public string Kind;
public string Text;
public float Y;
public bool Created;
}
private void OnServerInitialized()
{
LoadImageAssets();
}
private void LoadImageAssets()
{
var imageDir = Path.Combine(Interface.Oxide.DataDirectory, "Notif", "Images");
Directory.CreateDirectory(imageDir);
_imageStorageIds.Clear();
foreach (var kv in ImageFiles)
{
var path = Path.Combine(imageDir, kv.Value);
if (!File.Exists(path))
{
PrintWarning($"[UIAssets] Missing PNG file: {path}");
continue;
}
try
{
var bytes = File.ReadAllBytes(path);
_imageStorageIds[kv.Key] = FileStorage.server.Store(bytes, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID).ToString();
}
catch (Exception ex)
{
PrintWarning($"[UIAssets] Failed to load {kv.Key}: {ex.Message}");
}
}
_heliPng = Img("Hudaima_Event_Helicopter_CardIcon_1");
_killPng = Img("Hudaima_Event_Killstreak_CardIcon_1");
_bradleyPng = Img("Hudaima_Event_Bradley_CardIcon_1");
}
private string Img(string key)
{
return _imageStorageIds.TryGetValue(key, out var stored) ? stored : string.Empty;
}
private void OnPlayerDeath(BasePlayer player, HitInfo info)
{
if (player == null || player.IsNpc)
{
return;
}
_killStreaks[player.userID] = 0;
_nextKillNoticeTime.Remove(player.userID);
var killer = info?.InitiatorPlayer;
if (killer == null || killer == player || killer.IsNpc || !killer.IsConnected)
{
return;
}
int streak = _killStreaks.ContainsKey(killer.userID) ? _killStreaks[killer.userID] + 1 : 1;
_killStreaks[killer.userID] = streak;
if (streak < 3)
{
return;
}
float nextTime;
if (_nextKillNoticeTime.TryGetValue(killer.userID, out nextTime) && Time.realtimeSinceStartup < nextTime)
{
return;
}
_nextKillNoticeTime[killer.userID] = Time.realtimeSinceStartup + KillNoticeCooldown;
AddNotice("kill", killer.displayName);
SClan?.Call("AddPointsToPlayer", killer.userID, KillstreakPoints);
}
private void OnEntitySpawned(BaseNetworkable entity)
{
if (entity is PatrolHelicopter)
{
AddNotice("heli", null);
return;
}
if (entity is BradleyAPC)
{
AddNotice("bradley", null);
}
}
[ConsoleCommand("notif.testall")]
private void TestAllCommand(ConsoleSystem.Arg arg)
{
AddNotice("heli", null);
AddNotice("kill", arg.Player()?.displayName ?? "PLAYER");
AddNotice("bradley", null);
}
private void AddNotice(string kind, string text)
{
for (int i = _notices.Count - 1; i >= 0; i--)
{
if (_notices[i].Kind != kind)
{
continue;
}
DestroyNotice(_notices[i]);
_notices.RemoveAt(i);
}
var notice = new Notice
{
Id = ++_nextId,
Kind = kind,
Text = text
};
_notices.Add(notice);
SortNotices();
var container = ApplyLayout() ?? new CuiElementContainer();
AddNoticeUi(container, notice);
Broadcast(container);
notice.Created = true;
timer.Once(HoldTime, () => RemoveNotice(notice));
}
private void SortNotices()
{
_notices.Sort((a, b) => Priority(a.Kind).CompareTo(Priority(b.Kind)));
}
private int Priority(string kind)
{
if (kind == "heli")
{
return 0;
}
if (kind == "kill")
{
return 1;
}
return 2;
}
private CuiElementContainer ApplyLayout()
{
CuiElementContainer updates = null;
for (int i = 0; i < _notices.Count; i++)
{
var notice = _notices[i];
float target = SlotY[Mathf.Min(i, SlotY.Length - 1)];
float y = target - OriginalY(notice.Kind);
if (notice.Created && Math.Abs(notice.Y - y) > 0.001f)
{
notice.Y = y;
if (updates == null)
{
updates = new CuiElementContainer();
}
AddNoticePositionUpdate(updates, notice);
continue;
}
notice.Y = y;
}
return updates;
}
private float OriginalY(string kind)
{
if (kind == "heli")
{
return 90f;
}
if (kind == "kill")
{
return 43.3333f;
}
return -4f;
}
private void Broadcast(CuiElementContainer container)
{
Broadcast(CuiHelper.ToJson(container));
}
private void Broadcast(string json)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected || player.IsNpc)
{
continue;
}
CuiHelper.AddUi(player, json);
}
}
private void AddNoticeUi(CuiElementContainer container, Notice notice)
{
string root = RootName(notice);
string layer = LayerName(notice);
container.Add(new CuiElement
{
Name = root,
Parent = "Hud",
Components =
{
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "0 0", OffsetMax = "0 0" }
}
});
container.Add(new CuiElement
{
Name = layer,
Parent = root,
Components =
{
CreateLayerRect(notice.Y)
}
});
if (notice.Kind == "heli")
{
AddHeli(container, layer);
}
else if (notice.Kind == "kill")
{
AddKill(container, layer, notice.Text);
}
else
{
AddBradley(container, layer);
}
}
private CuiRectTransformComponent CreateLayerRect(float y)
{
return new CuiRectTransformComponent
{
AnchorMin = "0 0",
AnchorMax = "1 1",
OffsetMin = $"0 {Format(y)}",
OffsetMax = $"0 {Format(y)}"
};
}
private void AddHeli(CuiElementContainer container, string parent)
{
container.Add(new CuiElement
{
Name = "Hudaima_Event_Helicopter_CardIcon_1." + parent,
Parent = parent,
Components =
{
new CuiRawImageComponent { Color = "1 1 1 1", Png = _heliPng },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-170.0000 90.0000", OffsetMax = "-22.0000 138.6667" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = "HELICOPTER", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "0.925 0.925 0.925 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-127.6667 113.3333", OffsetMax = "-32.3333 129.3333" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = "INCOMING", FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "1.000 0.278 0.278 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-113.0000 101.3333", OffsetMax = "-52.3333 114.6667" }
}
});
}
private void AddKill(CuiElementContainer container, string parent, string name)
{
container.Add(new CuiElement
{
Name = "Hudaima_Event_Killstreak_CardIcon_1." + parent,
Parent = parent,
Components =
{
new CuiRawImageComponent { Color = "1 1 1 1", Png = _killPng },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-170.0000 43.3333", OffsetMax = "-22.0000 92.0000" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = "KILLSTREAK", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "0.925 0.925 0.925 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-127.0000 66.0000", OffsetMax = "-31.6667 82.0000" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = string.IsNullOrEmpty(name) ? "PLAYER" : name.ToUpperInvariant(), FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "0.239 1.000 0.361 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-109.0000 54.0000", OffsetMax = "-48.3333 67.3333" }
}
});
}
private void AddBradley(CuiElementContainer container, string parent)
{
container.Add(new CuiElement
{
Name = "Hudaima_Event_Bradley_CardIcon_1." + parent,
Parent = parent,
Components =
{
new CuiRawImageComponent { Color = "1 1 1 1", Png = _bradleyPng },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-170.0000 -4.0000", OffsetMax = "-22.0000 44.6667" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = "BRADLEY", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "0.925 0.925 0.925 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-115.3333 18.6667", OffsetMax = "-46.0000 34.6667" }
}
});
container.Add(new CuiElement
{
Parent = parent,
Components =
{
new CuiTextComponent { Text = "SPAWNED", FontSize = 9, Align = TextAnchor.MiddleCenter, Color = "1.000 0.278 0.278 1", Font = "robotocondensed-bold.ttf" },
new CuiRectTransformComponent { AnchorMin = "1.0000 0.5000", AnchorMax = "1.0000 0.5000", OffsetMin = "-107.6667 6.6667", OffsetMax = "-53.6667 20.0000" }
}
});
}
private void AddNoticePositionUpdate(CuiElementContainer container, Notice notice)
{
container.Add(new CuiElement
{
Name = LayerName(notice),
Update = true,
Components =
{
CreateLayerRect(notice.Y)
}
});
}
private void RemoveNotice(Notice notice)
{
if (!_notices.Remove(notice))
{
return;
}
DestroyNotice(notice);
var updates = ApplyLayout();
if (updates != null)
{
Broadcast(updates);
}
}
private void DestroyNotice(Notice notice)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected || player.IsNpc)
{
continue;
}
CuiHelper.DestroyUi(player, RootName(notice));
}
}
private string RootName(Notice notice)
{
return UiRoot + "." + notice.Id;
}
private string LayerName(Notice notice)
{
return UiLayer + "." + notice.Id;
}
private static string Format(float value)
{
return value.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture);
}
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || !player.IsConnected || player.IsNpc)
{
continue;
}
for (int i = 0; i <= _nextId; i++)
{
CuiHelper.DestroyUi(player, UiRoot + "." + i);
}
}
_notices.Clear();
_killStreaks.Clear();
_nextKillNoticeTime.Clear();
}
}
}