C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using CompanionServer.Handlers;
using ConVar;
using Epic.OnlineServices.AntiCheatCommon;
using Facepunch;
using Facepunch.Math;
using HarmonyLib;
using JetBrains.Annotations;
using Network;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins {
[Info("Starguard", "starguard.gg", "1.0.8")]
public class StarGuard : RustPlugin {
[PluginReference]
private Plugin BetterChat;
private static StarGuard _instance;
private static string _mapUrl;
private static readonly StringBuilder Builder = new StringBuilder();
#region starguard.mapurl
// This patch fixes the auto-upload map image feature, which requires the Companion Server (Rust+) to be enabled.
[HarmonyPatch(typeof(CompanionServer.Server), nameof(CompanionServer.Server.Initialize)), AutoPatch]
private static class MissingCompanionServerPatch {
[HarmonyPrefix, UsedImplicitly]
private static void Prefix() {
if (App.port >= 0) return;
// initialization should allow upload
if (ConVar.Server.autoUploadMapImages && Map.ImageData == null) Map.PopulateCache();
}
}
[HarmonyPatch, AutoPatch]
private static class AfterUploadPatch {
[HarmonyTargetMethod]
public static MethodBase TargetMethod() {
return AccessTools.Method(((System.Runtime.CompilerServices.AsyncStateMachineAttribute)AccessTools.Method(typeof(MapUploader), "UploadMapImage").GetCustomAttribute(typeof(System.Runtime.CompilerServices.AsyncStateMachineAttribute), false)).StateMachineType, "MoveNext");
}
[HarmonyTranspiler, UsedImplicitly]
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) {
var instructionList = new List<CodeInstruction>(instructions);
int retIndex = -1;
for (int i = instructionList.Count - 1; i >= 0; i--) {
if (instructionList[i].opcode != OpCodes.Ret) continue;
retIndex = i;
break;
}
if (retIndex == -1) return instructionList;
instructionList.Insert(retIndex, new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AfterUploadPatch), nameof(AfterUploadAttempt))));
return instructionList;
}
private static void AfterUploadAttempt() {
if (!MapUploader.IsImageUploaded || string.IsNullOrEmpty(MapUploader.ImageUrl)) {
Debug.LogWarning("[Starguard] Map image is missing after upload - some features will be unavailable");
return;
}
_mapUrl = MapUploader.ImageUrl;
}
}
[ConsoleCommand("starguard.mapurl")]
private void ConsoleCommandMapUrl(ConsoleSystem.Arg arg) {
if (!arg.IsRcon) return;
arg.ReplyWith(string.IsNullOrEmpty(_mapUrl) ? "null" : _mapUrl);
}
#endregion
#region starguard.chat
public class ProxyPlayer : IPlayer {
public void Ban(string reason, TimeSpan duration = new TimeSpan()) {
}
public void Heal(float amount) {
}
public void Hurt(float amount) {
}
public void Kick(string reason) {
}
public void Kill() {
}
public void Rename(string name) {
}
public void Teleport(float x, float y, float z) {
}
public void Teleport(GenericPosition pos) {
}
public void Unban() {
}
public void Position(out float x, out float y, out float z) {
x = 0.0f;
y = 0.0f;
z = 0.0f;
}
public GenericPosition Position() {
return new GenericPosition(0.0f, 0.0f, 0.0f);
}
public void Message(string message, string prefix, params object[] args) {
}
public void Message(string message) {
}
public void Reply(string message, string prefix, params object[] args) {
}
public void Reply(string message) {
}
public void Command(string command, params object[] args) {
}
public bool HasPermission(string perm) {
return _instance.permission.UserHasPermission(Id, perm);
}
public void GrantPermission(string perm) {
}
public void RevokePermission(string perm) {
_instance.permission.RevokeUserPermission(Id, perm);
}
public bool BelongsToGroup(string group) {
return _instance.permission.UserHasGroup(Id, group);
}
public void AddToGroup(string group) {
_instance.permission.AddUserGroup(Id, group);
}
public void RemoveFromGroup(string group) {
_instance.permission.RemoveUserGroup(Id, group);
}
public object Object => null;
public string Id {
get;
}
public CommandType LastCommand {
get;
set;
}
public string Name {
get;
set;
}
public string Address => "";
public int Ping => 0;
public CultureInfo Language {
get;
}
= CultureInfo.InvariantCulture;
public bool IsConnected => false;
public bool IsSleeping => true;
public bool IsServer => false;
public bool IsAdmin => false;
public bool IsBanned => false;
public TimeSpan BanTimeRemaining => TimeSpan.Zero;
public float Health {
get;
set;
}
= 100;
public float MaxHealth {
get;
set;
}
= 100;
public ProxyPlayer(string steamId, string name) {
Id = steamId;
Name = name;
}
private static readonly Dictionary<string, ProxyPlayer> Players = new Dictionary<string, ProxyPlayer>();
public static ProxyPlayer Get(string id, string name) {
if (Players.TryGetValue(id, out var player)) {
player.Name = name;
return player;
}
player = new ProxyPlayer(id, name);
Players.TryAdd(id, player);
return player;
}
}
[ConsoleCommand("starguard.chat")]
private void ConsoleCommandChat(ConsoleSystem.Arg arg) {
if (!arg.IsRcon || !arg.HasArgs(3)) return;
var userId = arg.GetString(0);
if (!ulong.TryParse(userId, out var userIdUl)) return;
var userName = arg.GetString(1);
var message = arg.GetString(2);
string output;
if (BetterChat?.IsLoaded ?? false) {
var player = ProxyPlayer.Get(userId, userName);
output = BetterChat.Call<string>("API_GetFormattedMessage", player, message, false);
}
else {
var result = Interface.CallHook("SG_GetPlayerMessage", userName, userIdUl, message) as string;
output = string.IsNullOrEmpty(result) ? $"{userName}: {message}" : result;
}
ConsoleNetwork.BroadcastToAllClients("chat.add", 0, userIdUl, output);
Chat.Record(new Chat.ChatEntry {
Username = userName, Message = message, UserId = userId, Channel = Chat.ChatChannel.Global, Color = "#ffffff", Time = Epoch.Current
}
);
}
#endregion
#region global.playerlist
private const string TARGET_COMMAND = "global.playerlist";
private static Action<ConsoleSystem.Arg> _originalMethod = null;
private static bool TryGetCommand(out ConsoleSystem.Command command) {
command = null;
foreach (var cmd in ConsoleSystem.Index.All) {
if (cmd.FullName != TARGET_COMMAND) continue;
command = cmd;
return true;
}
return false;
}
private static void GetPlayerList(ConsoleSystem.Arg arg) {
arg.ReplyWith(JsonConvert.SerializeObject(BasePlayer.activePlayerList.Select(player => new {
SteamID = player.UserIDString, OwnerSteamID = player.OwnerID.ToString(), DisplayName = player.displayName, Ping = Network.Net.sv.GetAveragePing(player.net.connection), Address = player.net.connection.ipaddress, EntityId = player.net.ID.Value, ConnectedSeconds = (int)player.net.connection.GetSecondsConnected(), ViolationLevel = player.ViolationLevel, Health = player.Health(), NetworkId = player.net.ID.Value, S_TeamId = player.currentTeam.ToString(), S_Pos = player.transform.position
}
), Formatting.Indented));
}
#endregion
#region EAC Integration
// Hook into EAC kicks
[HarmonyPatch(typeof(EACServer), "OnClientActionRequired"), AutoPatch]
public static class EACLogPatch {
[HarmonyTranspiler, UsedImplicitly]
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) {
// Look for BroadcastToAllClients call
var matcher = new CodeMatcher(instructions);
matcher.MatchStartForward(CodeMatch.Calls(AccessTools.Method(typeof(ConsoleNetwork), "BroadcastToAllClients")));
if (!matcher.IsValid) {
Debug.LogWarning("[Starguard] Failed to patch EACServer - detailed EAC ban details will not be available!");
return instructions;
}
matcher.Advance(1);
matcher.Insert(new CodeInstruction(OpCodes.Ldarg_0), new CodeInstruction(OpCodes.Ldloc_2), CodeInstruction.Call(typeof(EACLogPatch), nameof(LogBan)));
return matcher.InstructionEnumeration();
}
public static void LogBan(ref OnClientActionRequiredCallbackInfo data, Connection connection) {
try {
var reason = data.ActionReasonDetailsString.ToString();
var str = $"[SG-EAC]{connection.userid.ToString()}|TEMP={(data.ActionReasonCode == AntiCheatCommonClientActionReason.TemporaryBanned ? "1" : "0")}|REASON={reason}";
RCon.Broadcast(RCon.LogType.Generic, str);
}
catch (Exception e) {
Debug.LogWarning($"[Starguard] Failed to broadcast EAC ban details: {e.Message}\n{e.StackTrace}");
}
}
}
#endregion
#region ToxVoice Integration
private void OnPlayerVoiceText(string steamId, string text, string position, string recipients) {
if (string.IsNullOrEmpty(steamId) || string.IsNullOrEmpty(text)) return;
Builder.Clear();
Builder.Append("[TOX] ");
Builder.Append(steamId);
Builder.Append(": ");
Builder.Append(text);
RCon.Broadcast(RCon.LogType.Chat, Builder.ToString());
}
#endregion
private void Loaded() {
_instance = this;
if (!TryGetCommand(out var command)) {
Puts("[Starguard] Error locating global.playerlist command - some features may be unavailable.");
return;
}
_originalMethod = command.Call;
command.Call = GetPlayerList;
Puts("[Starguard] Successfully Loaded");
}
private void Unload() {
if (_originalMethod != null && TryGetCommand(out var command)) {
command.Call = _originalMethod;
}
Puts("[Starguard] Unloaded");
}
private void OnServerInitialized() {
if (string.IsNullOrEmpty(MapUploader.ImageUrl)) return;
_mapUrl = MapUploader.ImageUrl;
}
}
}