C#:
// Reference: ZString
// Reference: UniTask
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using ConVar;
using Cysharp.Text;
using Cysharp.Threading.Tasks;
using JetBrains.Annotations;
using Network;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Rust;
using Steamworks;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Networking;
using Color = System.Drawing.Color;
using Graphics = System.Drawing.Graphics;
using Pool = Facepunch.Pool;
using Star = ProtoBuf.PatternFirework.Star;
namespace Oxide.Plugins;
// Original ship built by Hougan.
// Planks replaced by DezLifer, Bombardir, Phrog, Nimant
// The Ship of Theseus problem: is this still his code?
[Info("RustApp", "RustApp.io", "3.1.1")]
public class RustApp : RustPlugin {
#region Variables
// References to other plugin with API
[PluginReference]
private Plugin NoEscape, RaidZone, RaidBlock, MultiFighting, TGPP, ExtRaidBlock;
private static MetaInfo _MetaInfo = MetaInfo.Read();
private static CheckInfo _CheckInfo = CheckInfo.Read();
private static bool _TempWipeMarker = false;
private static RustApp _RustApp;
private static Configuration _Settings;
private static RustAppEngine _RustAppEngine;
private static JsonSerializer _jsonSerializer = JsonSerializer.CreateDefault(null);
private static (string name, string value)[] _ApiHeaders = Array.Empty<(string, string)>();
private static readonly object _false = false;
// avoid boxing for hooks
private static readonly object _true = true;
// avoid boxing for hooks
private string _UiCommandName;
private string _UiSearchCommand;
#endregion
#region Web API
private static class Api {
public static bool ErrorContains(string error, string text) {
return error.Contains(text, StringComparison.OrdinalIgnoreCase);
}
}
private static class CourtApi {
#region Shared
public class PluginServerDto : Pool.IPooled {
public string name;
public string hostname;
public string level;
public string level_url;
public string level_image_url;
public int world_size;
public string description;
public string branch;
public string avatar_big;
public string avatar_url;
public string banner_url;
public int online;
public int slots;
public int reserved;
public string version;
public string protocol;
public string performance;
public int port;
public bool connected;
public void FillSnapshot() {
// Static — cache all fields that do not change during plugin uptime
name = _cachedName ??= ConVar.Server.hostname;
hostname = _cachedHostname ??= ConVar.Server.hostname;
level = _cachedLevel ??= (SteamServer.MapName ?? ConVar.Server.level);
level_url = _cachedLevelUrl ??= ConVar.Server.levelurl;
level_image_url = _cachedLevelImageUrl ??= MapUploader.ImageUrl;
world_size = _cachedWorldSize ??= ConVar.Server.worldsize;
description = _cachedDescription ??= (ConVar.Server.description + " " + ConVar.Server.motd);
branch = _cachedBranch ??= Facepunch.BuildInfo.Current.Scm.Branch;
avatar_big = _cachedAvatarBig ??= ConVar.Server.logoimage;
avatar_url = _cachedAvatarUrl ??= ConVar.Server.logoimage;
banner_url = _cachedBannerUrl ??= ConVar.Server.headerimage;
version = _cachedVersion ??= _RustApp.Version.ToString();
protocol = _cachedProtocol ??= Protocol.printable.ToString();
port = _cachedPort ??= ConVar.Server.port;
// Dynamic — fresh value each cycle.
online = BasePlayer.activePlayerList.Count + ServerMgr.Instance.connectionQueue.queue.Count + ServerMgr.Instance.connectionQueue.joining.Count;
slots = ConVar.Server.maxplayers;
reserved = ServerMgr.Instance.connectionQueue.ReservedCount;
performance = _RustApp.TotalHookTime.ToString();
connected = _RustAppEngine?.AuthWorker.IsAuthed ?? false;
}
private static string _cachedName;
private static string _cachedHostname;
private static string _cachedLevel;
private static string _cachedLevelUrl;
private static string _cachedLevelImageUrl;
private static int? _cachedWorldSize;
private static string _cachedDescription;
private static string _cachedBranch;
private static string _cachedAvatarBig;
private static string _cachedAvatarUrl;
private static string _cachedBannerUrl;
private static string _cachedVersion;
private static string _cachedProtocol;
private static int? _cachedPort;
public static void ResetCache() {
_cachedName = null;
_cachedHostname = null;
_cachedLevel = null;
_cachedLevelUrl = null;
_cachedLevelImageUrl = null;
_cachedWorldSize = null;
_cachedDescription = null;
_cachedBranch = null;
_cachedAvatarBig = null;
_cachedAvatarUrl = null;
_cachedBannerUrl = null;
_cachedVersion = null;
_cachedProtocol = null;
_cachedPort = null;
}
public virtual void LeavePool() {
}
public virtual void EnterPool() {
name = null;
hostname = null;
level = null;
level_url = null;
level_image_url = null;
world_size = 0;
description = null;
branch = null;
avatar_big = null;
avatar_url = null;
banner_url = null;
online = 0;
slots = 0;
reserved = 0;
version = null;
protocol = null;
performance = null;
port = 0;
connected = false;
}
}
#endregion
private const string BaseUrl = "https://court.rustapp.io";
private const string PairUriPrefix = BaseUrl + "/plugin/pair?code=";
private static readonly Uri UriServerInfo = new(BaseUrl + "/plugin/");
private static readonly Uri UriState = new(BaseUrl + "/plugin/state");
private static readonly Uri UriChat = new(BaseUrl + "/plugin/chat");
private static readonly Uri UriReports = new(BaseUrl + "/plugin/reports");
private static readonly Uri UriContact = new(BaseUrl + "/plugin/contact");
private static readonly Uri UriWipe = new(BaseUrl + "/plugin/wipe");
private static readonly Uri UriSleepingBag = new(BaseUrl + "/plugin/sleeping-bag");
private static readonly Uri UriBan = new(BaseUrl + "/plugin/ban");
private static readonly Uri UriUnban = new(BaseUrl + "/plugin/unban");
private static readonly Uri UriAlerts = new(BaseUrl + "/plugin/alerts");
private static readonly Uri UriCustomAlert = new(BaseUrl + "/plugin/custom-alert");
private static readonly Uri UriSignage = new(BaseUrl + "/plugin/signage");
private static readonly Uri UriKills = new(BaseUrl + "/plugin/kills");
private static readonly Uri UriPlayerMuteGetActive = new(BaseUrl + "/plugin/player-mute/get-active");
private static readonly Uri UriPlayerMuteCreate = new(BaseUrl + "/plugin/player-mute/mute-player");
private static readonly Uri UriPlayerMuteDelete = new(BaseUrl + "/plugin/player-mute/unmute-player");
#region GetServerInfo
public static StableRequest<object> GetServerInfo() {
return new StableRequest<object>(UriServerInfo, RequestMethod.GET, null);
}
#endregion
#region SendPairDetails
public class PluginPairPayload : PluginServerDto {
}
public class PluginPairResponse {
[CanBeNull]
public int ttl;
[CanBeNull]
public string token;
}
public static StableRequest<PluginPairResponse> SendPairDetails(string code) {
PluginPairPayload payload = new();
payload.FillSnapshot();
return new StableRequest<PluginPairResponse>(ZString.Concat(PairUriPrefix, code), RequestMethod.POST, payload);
}
#endregion
#region StateUpdate
public class PluginStatePlayerMetaDto {
public readonly Dictionary<string, string> tags = new();
public readonly Dictionary<string, string> fields = new();
}
public static Dictionary<ulong, PluginStatePlayerDto> players = new();
public class PluginStatePlayerDto {
public static PluginStatePlayerDto FromConnection(Connection connection, string status) {
var userid = connection.userid;
if (!players.TryGetValue(userid, out var payload)) {
players[userid] = payload = new PluginStatePlayerDto {
steam_id = connection.player is BasePlayer basePlayer ? basePlayer.UserIDString : GetSteamIdString(userid), steam_name = connection.username.Replace("<blank>", "blank"), ip = IPAddressWithoutPort(connection.ipaddress), no_license = DetectNoLicense(connection)
}
;
}
payload.ping = Network.Net.sv.GetAveragePing(connection);
payload.seconds_connected = (int)connection.GetSecondsConnected();
payload.language = _RustApp.lang.GetLanguage(payload.steam_id);
payload.status = status;
payload.team ??= Pool.Get<List<ulong>>();
payload.team.Clear();
var newTeam = RelationshipManager.ServerInstance.FindPlayersTeam(userid);
if (newTeam != null) {
var members = newTeam.members;
for (int i = 0; i < members.Count; i++) {
ulong member = members[i];
if (member != userid) {
payload.team.Add(member);
}
}
}
try {
payload.meta = CollectPlayerMeta(payload.steam_id, payload.meta);
}
catch (Exception ex) {
Debug(ex.ToString());
}
return payload;
}
public static PluginStatePlayerDto FromPlayer(BasePlayer player) {
PluginStatePlayerDto payload = FromConnection(player.Connection, "active");
payload.position = player.transform.position;
payload.rotation = player.eyes.rotation;
payload.coords = PositionToGridString(player.transform.position);
payload.can_build = DetectBuildingAuth(player);
payload.is_raiding = DetectIsRaidBlock(player);
payload.is_alive = player.IsAlive();
return payload;
}
public string steam_id;
public string steam_name;
public string ip;
public int ping = 0;
public int seconds_connected;
public string language;
[JsonConverter(typeof(Vector3Converter))]
public Vector3 position;
[JsonConverter(typeof(QuaternionConverter))]
public Quaternion rotation;
[CanBeNull]
public string coords;
public bool can_build = false;
public bool is_raiding = false;
public bool no_license = false;
public bool is_alive = false;
public string status;
public PluginStatePlayerMetaDto meta = new();
[JsonProperty(ItemConverterType = typeof(UlongConverter))]
public List<ulong> team;
public void FreePooledFields() {
if (team != null) Pool.FreeUnmanaged(ref team);
}
}
public class PluginStateUpdatePayload : PluginServerDto {
public List<PluginStatePlayerDto> players;
public Dictionary<string, string> disconnected;
public Dictionary<string, string> team_changes;
public override void EnterPool() {
base.EnterPool();
if (players != null) Pool.FreeUnmanaged(ref players);
if (disconnected != null) Pool.FreeUnmanaged(ref disconnected);
if (team_changes != null) Pool.FreeUnmanaged(ref team_changes);
}
}
public static StableRequest<object> SendStateUpdate(PluginStateUpdatePayload data) {
return new StableRequest<object>(UriState, RequestMethod.PUT, data);
}
#endregion
#region SendChatMessages
public class PluginChatMessageDto : Pool.IPooled {
public string steam_id;
public string target_steam_id;
public bool is_team;
public bool is_clan;
public string text;
public static PluginChatMessageDto Create(string steamId, string text, bool isTeam, bool isClan, [CanBeNull] string targetSteamId = null) {
PluginChatMessageDto dto = Pool.Get<PluginChatMessageDto>();
dto.steam_id = steamId;
dto.target_steam_id = targetSteamId;
dto.is_team = isTeam;
dto.is_clan = isClan;
dto.text = text;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
steam_id = null;
target_steam_id = null;
is_team = false;
text = null;
}
}
public class PluginChatMessagePayload : Pool.IPooled {
public List<PluginChatMessageDto>? messages;
public void LeavePool() => messages = Pool.Get<List<PluginChatMessageDto>>();
public void EnterPool() {
if (messages != null) Pool.Free(ref messages, freeElements : true);
}
}
public static StableRequest<object> SendChatMessages(PluginChatMessagePayload payload) {
return new StableRequest<object>(UriChat, RequestMethod.POST, payload);
}
#endregion
#region SendReports
public class PluginReportDto : Pool.IPooled {
public string initiator_steam_id;
public string target_steam_id;
public List<string> sub_targets_steam_ids;
public string reason;
public string message;
public static PluginReportDto Create(string initiatorSteamId, string targetSteamId, string reason, string message = null) {
PluginReportDto? dto = Pool.Get<PluginReportDto>();
dto.initiator_steam_id = initiatorSteamId;
dto.target_steam_id = targetSteamId;
dto.reason = reason;
dto.message = message;
return dto;
}
public void LeavePool() => sub_targets_steam_ids = Pool.Get<List<string>>();
public void EnterPool() {
initiator_steam_id = null;
target_steam_id = null;
reason = null;
message = null;
if (sub_targets_steam_ids != null) Pool.FreeUnmanaged(ref sub_targets_steam_ids);
}
}
public class PluginReportBatchPayload : Pool.IPooled {
public List<PluginReportDto>? reports;
public void LeavePool() => reports = Pool.Get<List<PluginReportDto>>();
public void EnterPool() {
if (reports != null) Pool.Free(ref reports, freeElements : true);
}
}
public static StableRequest<object> SendReports(PluginReportBatchPayload payload) {
return new StableRequest<object>(UriReports, RequestMethod.POST, payload);
}
#endregion
#region SendContact
public static StableRequest<object> SendContact(string steamId, string contact) {
return new StableRequest<object>(UriContact, RequestMethod.POST, new {
steam_id = steamId, message = contact
}
);
}
#endregion
#region SendWipe
public static StableRequest<object> SendWipe() {
return new StableRequest<object>(UriWipe, RequestMethod.POST, null);
}
#endregion
#region Sleeping bag
public class PluginSleepingBagDto : Pool.IPooled {
public string initiator_steam_id;
public string target_steam_id;
[JsonConverter(typeof(Vector3Converter))]
public Vector3 position;
public bool are_friends;
public static PluginSleepingBagDto Create(string initiatorSteamId, string targetSteamId, Vector3 position, bool areFriends) {
PluginSleepingBagDto? dto = Pool.Get<PluginSleepingBagDto>();
dto.initiator_steam_id = initiatorSteamId;
dto.target_steam_id = targetSteamId;
dto.position = position;
dto.are_friends = areFriends;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
initiator_steam_id = null;
target_steam_id = null;
position = default;
are_friends = false;
}
}
public class PluginSleepingBagBatchDto : Pool.IPooled {
public List<PluginSleepingBagDto> sleeping_bags;
public void LeavePool() => sleeping_bags = Pool.Get<List<PluginSleepingBagDto>>();
public void EnterPool() {
if (sleeping_bags != null) Pool.Free(ref sleeping_bags, freeElements : true);
}
}
public static StableRequest<object> SleepingBagCreate(PluginSleepingBagBatchDto payload) {
return new StableRequest<object>(UriSleepingBag, RequestMethod.POST, payload);
}
#endregion
#region BanCreate
public class PluginBanCreatePayload : Pool.IPooled {
public string target_steam_id;
public string reason;
public bool global;
public bool ban_ip;
public string? duration;
public string? comment;
public static PluginBanCreatePayload Create(string targetSteamId, string reason, string? duration, bool global, bool banIp, string comment) {
PluginBanCreatePayload? dto = Pool.Get<PluginBanCreatePayload>();
dto.target_steam_id = targetSteamId;
dto.reason = reason;
dto.duration = duration;
dto.global = global;
dto.ban_ip = banIp;
dto.comment = comment;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
target_steam_id = null;
reason = null;
duration = null;
global = false;
ban_ip = false;
comment = null;
}
}
public static StableRequest<object> BanCreate(PluginBanCreatePayload payload) {
return new StableRequest<object>(UriBan, RequestMethod.POST, payload);
}
#endregion
#region BanDelete
public static StableRequest<object> BanDelete(string steamId) {
return new StableRequest<object>(UriUnban, RequestMethod.POST, new {
target_steam_id = steamId
}
);
}
#endregion
#region PlayerAlert
public static class PluginPlayerAlertType {
public static readonly string join_with_ip_ban = "join_with_ip_ban";
public static readonly string custom_api = "custom_api";
}
public class PluginPlayerAlertDto : Pool.IPooled {
public string type;
public object meta;
public static PluginPlayerAlertDto Create(string type, object meta) {
PluginPlayerAlertDto? dto = Pool.Get<PluginPlayerAlertDto>();
dto.type = type;
dto.meta = meta;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
type = null;
switch (meta) {
case PluginPlayerAlertJoinWithIpBanMeta join : PluginPlayerAlertJoinWithIpBanMeta? j = join;
Pool.Free(ref j);
break;
// Add new IPooled meta types here!!
}
meta = null;
}
}
public class PluginPlayerAlertJoinWithIpBanMeta : Pool.IPooled {
public string steam_id;
public string ip;
public int ban_id;
public static PluginPlayerAlertJoinWithIpBanMeta Create(string steamId, string ip, int banId) {
PluginPlayerAlertJoinWithIpBanMeta? dto = Pool.Get<PluginPlayerAlertJoinWithIpBanMeta>();
dto.steam_id = steamId;
dto.ip = ip;
dto.ban_id = banId;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
steam_id = null;
ip = null;
ban_id = 0;
}
}
public class PluginPlayerAlertDugUpStashMeta {
public string steam_id;
[JsonConverter(typeof(UlongConverter))]
public ulong owner_steam_id;
[JsonConverter(typeof(Vector3Converter))]
public Vector3 position;
public string square;
}
public class PluginPlayerAlertsBatchPayload : Pool.IPooled {
public List<PluginPlayerAlertDto> alerts;
public void LeavePool() => alerts = Pool.Get<List<PluginPlayerAlertDto>>();
public void EnterPool() {
if (alerts != null) Pool.Free(ref alerts, freeElements : true);
}
}
public static StableRequest<object> CreatePlayerAlerts(PluginPlayerAlertsBatchPayload payload) {
return new StableRequest<object>(UriAlerts, RequestMethod.POST, payload);
}
#endregion
#region PlayerAlertCustom
public class PluginPlayerAlertCustomDto : Pool.IPooled {
public string msg;
public object data;
public string custom_icon;
public bool hide_in_table;
public string category;
public List<string> custom_links;
public static PluginPlayerAlertCustomDto Create(string msg, object? data, string? customIcon, string category, List<string>? customLinks) {
PluginPlayerAlertCustomDto? dto = Pool.Get<PluginPlayerAlertCustomDto>();
dto.msg = msg;
dto.data = data;
dto.custom_icon = customIcon;
dto.hide_in_table = false;
dto.category = category;
dto.custom_links = customLinks;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
msg = null;
data = null;
custom_icon = null;
hide_in_table = false;
category = null;
custom_links = null;
}
}
public class PluginPlayerAlertCustomAlertMeta {
public string name = "";
public string custom_icon = null;
public List<string> custom_links = null;
}
public static StableRequest<object> CreatePlayerAlertsCustom(PluginPlayerAlertCustomDto payload) {
return new StableRequest<object>(UriCustomAlert, RequestMethod.POST, payload);
}
#endregion
#region SendSignage
public class PluginSignageCreateDto : Pool.IPooled {
public string steam_id;
[JsonConverter(typeof(UlongConverter))]
public ulong net_id;
public byte[] base64_image;
public string type;
[JsonConverter(typeof(Vector3Converter))]
public Vector3 position;
public string square;
public static PluginSignageCreateDto Create(string steamId, ulong netId, byte[] base64Image, string type, Vector3 position, string square) {
PluginSignageCreateDto? dto = Pool.Get<PluginSignageCreateDto>();
dto.steam_id = steamId;
dto.net_id = netId;
dto.base64_image = base64Image;
dto.type = type;
dto.position = position;
dto.square = square;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
steam_id = null;
net_id = 0;
base64_image = null;
type = null;
position = default;
square = null;
}
}
public static StableRequest<object> SendSignage(PluginSignageCreateDto payload) {
return new StableRequest<object>(UriSignage, RequestMethod.POST, payload);
}
#endregion
#region SendSignageDestroyed
public class SignageDestroyedDto : Pool.IPooled {
[JsonProperty(ItemConverterType = typeof(UlongConverter))]
public List<ulong> net_ids;
public void LeavePool() => net_ids = Pool.Get<List<ulong>>();
public void EnterPool() {
if (net_ids != null) Pool.FreeUnmanaged(ref net_ids);
}
}
public static StableRequest<object> SendSignageDestroyed(SignageDestroyedDto payload) {
return new StableRequest<object>(UriSignage, RequestMethod.DELETE, payload);
}
#endregion
#region SendKills
public class PluginKillsDto : Pool.IPooled {
public List<PluginKillEntryDto> kills;
public void LeavePool() => kills = Pool.Get<List<PluginKillEntryDto>>();
public void EnterPool() {
if (kills != null) Pool.Free(ref kills, freeElements : true);
}
}
public class PluginKillEntryDto : Pool.IPooled {
public string initiator_steam_id;
public string target_steam_id;
public string game_time;
public float distance;
public string weapon;
public bool is_headshot;
public List<CombatLogEventDto> hit_history;
public static PluginKillEntryDto Create(string initiatorSteamId, string targetSteamId, string gameTime, float distance, string weapon, bool isHeadshot, List<CombatLogEventDto> hitHistory) {
PluginKillEntryDto? dto = Pool.Get<PluginKillEntryDto>();
dto.initiator_steam_id = initiatorSteamId;
dto.target_steam_id = targetSteamId;
dto.game_time = gameTime;
dto.distance = distance;
dto.weapon = weapon;
dto.is_headshot = isHeadshot;
dto.hit_history = hitHistory;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
initiator_steam_id = null;
target_steam_id = null;
game_time = null;
distance = 0f;
weapon = null;
is_headshot = false;
if (hit_history != null) Pool.Free(ref hit_history, freeElements : true);
}
}
public class CombatLogEventDto : Pool.IPooled {
public float time;
public string attacker_steam_id;
public string target_steam_id;
public string attacker;
public string target;
public string weapon;
public string ammo;
public string bone;
public float distance;
public float hp_old;
public float hp_new;
public string info;
public int proj_hits;
public float pi;
public float proj_travel;
public float pm;
public int desync;
public bool ad;
public static CombatLogEventDto Create(float time, CombatLog.Event ev, Dictionary<ulong, string>? idCache = null) {
CombatLogEventDto? dto = Pool.Get<CombatLogEventDto>();
dto.Init(time, ev, idCache);
return dto;
}
private void Init(float time, CombatLog.Event ev, Dictionary<ulong, string>? idCache) {
if (ev.attacker is "player" or "you") attacker_steam_id = ResolveSteamId(ev.attacker_id, idCache);
if (ev.target is "player" or "you") target_steam_id = ResolveSteamId(ev.target_id, idCache);
this.time = time - ev.time;
attacker = ev.attacker;
target = ev.target;
weapon = NormalizeName(ev.weapon);
ammo = ev.ammo;
bone = ev.bone;
distance = (float)Math.Round(ev.distance, 2);
hp_old = (float)Math.Round(ev.health_old, 2);
hp_new = (float)Math.Round(ev.health_new, 2);
info = ev.info;
proj_hits = ev.proj_hits;
proj_travel = ev.proj_travel;
desync = ev.desync;
pi = ev.proj_integrity;
pm = ev.proj_mismatch;
ad = ev.attacker_dead;
}
public void LeavePool() {
}
public void EnterPool() {
time = 0f;
attacker_steam_id = null;
target_steam_id = null;
attacker = null;
target = null;
weapon = null;
ammo = null;
bone = null;
distance = 0f;
hp_old = 0f;
hp_new = 0f;
info = null;
proj_hits = 0;
pi = 0f;
proj_travel = 0f;
pm = 0f;
desync = 0;
ad = false;
}
private static string ResolveSteamId(ulong netId, Dictionary<ulong, string>? idCache) {
if (idCache != null && idCache.TryGetValue(netId, out string? cached)) return cached;
string? sid = BaseNetworkable.serverEntities.TryGetEntity<BasePlayer>(new NetworkableId(netId), out BasePlayer? player) ? player.UserIDString : "";
if (idCache != null) idCache[netId] = sid;
return sid;
}
private static readonly Dictionary<string, string> _normalizedWeaponNameCache = new();
public static void ClearCache() => _normalizedWeaponNameCache.Clear();
private static string NormalizeName(string fullName) {
const string Suffix = ".entity.prefab";
if (string.IsNullOrEmpty(fullName) || fullName == "N/A" || !fullName.EndsWith(Suffix, StringComparison.Ordinal)) return fullName;
if (_normalizedWeaponNameCache.TryGetValue(fullName, out string? normalized)) return normalized;
int lastSlash = fullName.LastIndexOf('/');
int start = lastSlash + 1;
int len = fullName.Length - start - Suffix.Length;
normalized = fullName.Substring(start, len);
_normalizedWeaponNameCache[fullName] = normalized;
return normalized;
}
}
public static StableRequest<object> SendKills(PluginKillsDto payload) {
return new StableRequest<object>(UriKills, RequestMethod.POST, payload);
}
#endregion
#region PlayerMuteGetActive
public class PlayerMuteDto {
public string target_steam_id;
public string reason;
public long left_time_ms;
public long received_at = DateTimeOffset.Now.ToUnixTimeMilliseconds();
private long GetExpiredAt() {
return received_at + left_time_ms;
}
public string GetLeftTime() {
long leftMs = LeftSeconds();
if (leftMs <= 0) return RustApp._RustApp.lang.GetMessage("Time.Seconds", RustApp._RustApp, null).Replace("%COUNT%", "0");
TimeSpan time = TimeSpan.FromMilliseconds(leftMs);
List<string> parts = new();
if (time.Days > 0) parts.Add(RustApp._RustApp.lang.GetMessage("Time.Days", RustApp._RustApp, null).Replace("%COUNT%", time.Days.ToString()));
if (time.Hours > 0) parts.Add(RustApp._RustApp.lang.GetMessage("Time.Hours", RustApp._RustApp, null).Replace("%COUNT%", time.Hours.ToString()));
if (time.Minutes > 0) parts.Add(RustApp._RustApp.lang.GetMessage("Time.Minutes", RustApp._RustApp, null).Replace("%COUNT%", time.Minutes.ToString()));
if (time.Seconds > 0) parts.Add(RustApp._RustApp.lang.GetMessage("Time.Seconds", RustApp._RustApp, null).Replace("%COUNT%", time.Seconds.ToString()));
return string.Join(", ", parts);
}
public string GetUnmuteDate() {
DateTimeOffset date = DateTimeOffset.FromUnixTimeMilliseconds(GetExpiredAt());
return $"{date.DateTime.ToShortDateString()} {date.DateTime.ToShortTimeString()}";
}
public long LeftSeconds() {
return GetExpiredAt() - DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
}
public class PlayerMuteDtoIn {
public List<PlayerMuteDto> data;
}
public static StableRequest<PlayerMuteDtoIn> PlayerMuteGetActive() {
return new StableRequest<PlayerMuteDtoIn>(UriPlayerMuteGetActive, RequestMethod.GET, null);
}
#endregion
#region PlayerMuteCreate
public class PlayerMuteCreateDto : Pool.IPooled {
public string target_steam_id;
public string reason;
public string duration;
public bool broadcast;
public string? comment;
public string? references_message;
public static PlayerMuteCreateDto Create(string targetSteamId, string reason, string duration, bool broadcast = false, string? comment = null, string? referencesMessage = null) {
PlayerMuteCreateDto? dto = Pool.Get<PlayerMuteCreateDto>();
dto.target_steam_id = targetSteamId;
dto.reason = reason;
dto.duration = duration;
dto.broadcast = broadcast;
dto.comment = comment;
dto.references_message = referencesMessage;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
target_steam_id = null;
reason = null;
duration = null;
broadcast = false;
comment = null;
references_message = null;
}
}
public static StableRequest<object> PlayerMuteCreate(PlayerMuteCreateDto data) {
return new StableRequest<object>(UriPlayerMuteCreate, RequestMethod.POST, data);
}
#endregion
#region PlayerMuteDelete
public class PlayerMuteDeleteDto : Pool.IPooled {
public string target_steam_id;
public static PlayerMuteDeleteDto Create(string targetSteamId) {
PlayerMuteDeleteDto? dto = Pool.Get<PlayerMuteDeleteDto>();
dto.target_steam_id = targetSteamId;
return dto;
}
public void LeavePool() {
}
public void EnterPool() => target_steam_id = null;
}
public static StableRequest<object> PlayerMuteDelete(PlayerMuteDeleteDto data) {
return new StableRequest<object>(UriPlayerMuteDelete, RequestMethod.POST, data);
}
#endregion
}
private static class QueueApi {
#region Shared
public class QueueTaskResponse {
public string id;
public QueueTaskRequestConfigDto request;
}
public class QueueTaskRequestConfigDto {
public string name;
public JObject data;
}
#endregion
private const string BaseUrl = "https://queue.rustapp.io";
private static readonly Uri UriQueueRoot = new(BaseUrl + "/");
#region GetQueueTasks
public static StableRequest<List<QueueTaskResponse>> GetQueueTasks() {
return new StableRequest<List<QueueTaskResponse>>(UriQueueRoot, RequestMethod.GET, null);
}
#endregion
#region ProcessQueueTasks
public class QueueTaskResponsePayload {
public Dictionary<string, object> data;
}
public static StableRequest<object> ProcessQueueTasks(QueueTaskResponsePayload payload) {
return new StableRequest<object>(UriQueueRoot, RequestMethod.PUT, payload);
}
#endregion
}
private static class BanApi {
private const string BaseUrl = "https://ban.rustapp.io";
private static readonly Uri UriBanGetBatchV2 = new(BaseUrl + "/plugin/v2");
#region BanGetBatch
public class BanGetBatchEntryPayloadDto : Pool.IPooled {
public string steam_id;
public string ip;
public static BanGetBatchEntryPayloadDto Create(string steamId, string ip) {
BanGetBatchEntryPayloadDto? dto = Pool.Get<BanGetBatchEntryPayloadDto>();
dto.steam_id = steamId;
dto.ip = ip;
return dto;
}
public void LeavePool() {
}
public void EnterPool() {
steam_id = null;
ip = null;
}
}
public class BanGetBatchPayload : Pool.IPooled {
public List<BanGetBatchEntryPayloadDto> players;
public void LeavePool() => players = Pool.Get<List<BanGetBatchEntryPayloadDto>>();
public void EnterPool() {
if (players != null) Pool.FreeUnmanaged(ref players);
}
}
public class BanGetBatchEntryResponseDto {
public string steam_id;
public string ip;
public List<BanDto> bans;
}
public class BanDto {
public int id;
public string steam_id;
public string ban_ip;
public string reason;
public long expired_at;
public bool ban_ip_active;
public bool computed_is_active;
public int sync_project_id = 0;
public bool sync_should_kick = false;
}
public class BanGetBatchResponse {
public List<BanGetBatchEntryResponseDto> entries;
}
public static StableRequest<BanGetBatchResponse> BanGetBatch(BanGetBatchPayload payload) {
return new StableRequest<BanGetBatchResponse>(UriBanGetBatchV2, RequestMethod.POST, payload);
}
#endregion
}
#endregion
#region Configuration
public class MetaInfo {
public static MetaInfo Read() {
if (!Interface.Oxide.DataFileSystem.ExistsDatafile("RustApp_Configuration")) {
_MetaInfo = null;
return null;
}
var obj = Interface.Oxide.DataFileSystem.ReadObject<MetaInfo>("RustApp_Configuration");
_MetaInfo = obj;
return obj;
}
public static void write(MetaInfo courtMeta) {
Interface.Oxide.DataFileSystem.WriteObject("RustApp_Configuration", courtMeta);
MetaInfo.Read();
}
[JsonProperty("It is access for RustApp Court, never edit or share it")]
public string Value;
}
public class CheckInfo {
public static CheckInfo Read() {
if (!Interface.Oxide.DataFileSystem.ExistsDatafile("RustApp_CheckMeta")) {
return new CheckInfo();
}
return Interface.Oxide.DataFileSystem.ReadObject<CheckInfo>("RustApp_CheckMeta");
}
public static void write(CheckInfo courtMeta) {
const double ThirtyDaysSeconds = 30 * 24 * 60 * 60;
double now = _RustApp.CurrentTime();
List<string>? toRemove = Pool.Get<List<string>>();
try {
foreach (KeyValuePair<string, double> kv in courtMeta.LastChecks) {
if (now - kv.Value >= ThirtyDaysSeconds) toRemove.Add(kv.Key);
}
for (int i = 0; i < toRemove.Count; i++) {
courtMeta.LastChecks.Remove(toRemove[i]);
}
}
finally {
Pool.FreeUnmanaged(ref toRemove);
}
Interface.Oxide.DataFileSystem.WriteObject("RustApp_CheckMeta", courtMeta);
}
[JsonProperty("List of recent checks to show green-check on player")]
public Dictionary<string, double> LastChecks = new Dictionary<string, double>();
}
private class Configuration {
[JsonProperty("[UI] Chat commands")]
public List<string> report_ui_commands = new List<string>();
[JsonProperty("[UI] Report reasons")]
public List<string> report_ui_reasons = new List<string>();
[JsonProperty("[UI] Cooldown between reports (seconds)")]
public int report_ui_cooldown = 300;
[JsonProperty("[UI] Auto-parse reports from F7 (ingame reports)")]
public bool report_ui_auto_parse = true;
[JsonProperty("[UI • Starter Plan] Show 'recently checked' checkbox (amount of days)")]
public int report_ui_show_check_in = 7;
[JsonProperty("[Chat] SteamID for message avatar (default account contains RustApp logo)")]
public string chat_default_avatar_steamid = "76561198134964268";
[JsonProperty("[Check] Command to send contact")]
public string check_contact_command = "contact";
[JsonProperty("[Components • Custom actions] Allow console command execution")]
public bool components_custom_actions_enabled = true;
[JsonProperty("[Components • Signages] Collect signages")]
public bool components_signages_enabled = true;
[JsonProperty("[Components • Kills] Collect kills")]
public bool components_kills_enabled = true;
[JsonProperty("[Components • Mutes] Support mutes system")]
public bool components_mutes_enabled = true;
public static Configuration Generate() {
return new Configuration {
report_ui_commands = new List<string> {
"report", "reports"
}
, report_ui_reasons = new List<string> {
"Cheat", "Macros", "Abuse"
}
, report_ui_cooldown = 300, report_ui_auto_parse = true, report_ui_show_check_in = 7, chat_default_avatar_steamid = "76561198134964268", components_custom_actions_enabled = true, components_kills_enabled = true, components_signages_enabled = true, components_mutes_enabled = true,
}
;
}
}
#endregion
#region Workers
private class RustAppEngine : RustAppWorker {
public GameObject ChildObjectToWorkers;
public AuthWorker? AuthWorker;
public BanWorker? BanWorker;
public StateWorker? StateWorker;
public CheckWorker? CheckWorker;
public QueueWorker? QueueWorker;
public ChatWorker? ChatWorker;
public ReportWorker? ReportWorker;
public PlayerAlertsWorker? PlayerAlertsWorker;
public SignageWorker? SignageWorker;
public KillsWorker? KillsWorker;
public PlayerMuteWorker? PlayerMuteWorker;
public SleepingBagWorker? SleepingBagWorker;
private new void Awake() {
base.Awake();
SetupAuthWorker();
}
public bool IsPairingNow() {
return this.gameObject.GetComponent<PairWorker>() != null;
}
private void SetupAuthWorker() {
SetupHeaders();
AuthWorker = gameObject.AddComponent<AuthWorker>();
AuthWorker.OnAuthSuccess += () => {
Trace("Authed success, components enabled");
CreateSubWorkers();
}
;
AuthWorker.OnAuthFailed += () => {
Trace("Auth failed, components disabled");
DestroySubWorkers();
}
;
AuthWorker.CycleAuthUpdate();
}
public void SetupHeaders() {
_ApiHeaders = new[] {
("x-plugin-auth", _MetaInfo?.Value ?? ""), ("x-plugin-version", _RustApp.Version.ToString()), ("x-plugin-port", ConVar.Server.port.ToString()), ("Content-Type", "application/json")
}
;
}
private void CreateSubWorkers() {
ChildObjectToWorkers = gameObject.CreateChild();
StateWorker = ChildObjectToWorkers.AddComponent<StateWorker>();
CheckWorker = ChildObjectToWorkers.AddComponent<CheckWorker>();
QueueWorker = ChildObjectToWorkers.AddComponent<QueueWorker>();
ChatWorker = ChildObjectToWorkers.AddComponent<ChatWorker>();
BanWorker = ChildObjectToWorkers.AddComponent<BanWorker>();
ReportWorker = ChildObjectToWorkers.AddComponent<ReportWorker>();
PlayerAlertsWorker = ChildObjectToWorkers.AddComponent<PlayerAlertsWorker>();
SignageWorker = ChildObjectToWorkers.AddComponent<SignageWorker>();
KillsWorker = ChildObjectToWorkers.AddComponent<KillsWorker>();
PlayerMuteWorker = ChildObjectToWorkers.AddComponent<PlayerMuteWorker>();
SleepingBagWorker = ChildObjectToWorkers.AddComponent<SleepingBagWorker>();
}
private void DestroySubWorkers() {
if (ChildObjectToWorkers == null) {
return;
}
Destroy(ChildObjectToWorkers);
}
}
private class AuthWorker : RustAppWorker {
public bool? IsAuthed;
public Action? OnAuthSuccess;
public Action
// ... [Code preview truncated due to length. Full source in attached file] ...