C#:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Configuration;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("RustLauncher", "Arturka", "1.4.4")]
[Description("Интеграция Rust-сервера с HTTP-сервером SABR RUST Launcher и системой банов")]
public class RustLauncher : RustPlugin
{
private const string LauncherSourceName = "SabrRustLauncher";
private const string UnknownLauncherName = "NoNameLauncher";
private const string ErrorCheckHwid = "ErrorCheckHwid";
private const string ErrorCheckLauncher = "ErrorCheckLauncher";
#region Configuration
private Configuration _config;
private class ConnectionSettings
{
[JsonProperty("Адрес сервера лаунчера")]
public string LauncherServerUrl = "http://localhost:3000";
[JsonProperty("Интервал проверки подключения (секунды)")]
public int ConnectionCheckIntervalSeconds = 900;
[JsonProperty("Задержка перед первой проверкой связи (секунды)")]
public float ConnectionCheckStartupDelaySeconds = 0f;
[JsonProperty("Порт HTTP API плагина на сервере Rust")]
public int PluginHttpApiPort = 8082;
[JsonProperty("API Ключ сервера")]
public string ServerApiKey = "ENTER_API_KEY_SERVER";
}
private class LoggingSettings
{
[JsonProperty("Логировать вход игрока и источник лаунчера в консоль")]
public bool LogPlayerJoinAndLauncherSource = false;
[JsonProperty("Задержка перед проверкой источника лаунчера (секунды)")]
public float LauncherSourceCheckDelaySeconds = 2f;
}
private class SecuritySettings
{
[JsonProperty("Разрешить вход только с своего лаунчера")]
public bool AllowJoinOnlyFromLauncher = false;
[JsonProperty("Пермишен с которым не будет кикать игрока если тот без лаунчера")]
public string PermissionBypassLauncherKick = "rustlauncher.noneedlauncher";
[JsonProperty("Пермишен с которым игроку нужно иметь лаунчер не зависимо от того, нужен ли он всем")]
public string PermissionRequireLauncher = "rustlauncher.needlauncher";
[JsonProperty("Задержка проверки безопасности перед киком (секунды)")]
public float SecurityKickCheckDelaySeconds = 2f;
}
private class BonusSettings
{
[JsonProperty("Включить бонус игроку за вход с своего лаунчера")]
public bool EnableLauncherJoinBonus = false;
[JsonProperty("Выберите, что игроку будет выдано (1 - Group, 2 - Permissions, 3 - Group and Permissions)")]
public int BonusGrantMode = 1;
[JsonProperty("Группы которые будут выданы")]
public List<string> BonusGroups = new List<string>();
[JsonProperty("Пермишены которые будут выданы")]
public List<string> BonusPermissions = new List<string>();
[JsonProperty("Задержка перед проверкой бонуса (секунды)")]
public float BonusCheckDelaySeconds = 2f;
}
private string ConfigFilePath
{
get { return Path.Combine(Interface.Oxide.ConfigDirectory, Name + ".json"); }
}
private class SerializableConfiguration
{
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public Dictionary<string, object> ToDictionary()
{
return JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
}
}
private static class JsonHelper
{
public static object Deserialize(string json)
{
return ToObject(JToken.Parse(json));
}
private static object ToObject(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return token.Children<JProperty>()
.ToDictionary(prop => prop.Name, prop => ToObject(prop.Value));
case JTokenType.Array:
return token.Select(ToObject).ToList();
default:
return ((JValue)token).Value;
}
}
}
private class Configuration : SerializableConfiguration
{
[JsonProperty("Настройка подключения")]
public ConnectionSettings ConnectionSettings = new ConnectionSettings();
[JsonProperty("Настройка логирования")]
public LoggingSettings LoggingSettings = new LoggingSettings();
[JsonProperty("Настройка безопасности")]
public SecuritySettings SecuritySettings = new SecuritySettings();
[JsonProperty("Настройка бонусов")]
public BonusSettings BonusSettings = new BonusSettings();
}
private string _serverUrl = "http://localhost:3000";
private int _checkInterval = 900;
private int _pluginApiPort = 8082;
private string _serverApiKey = "ENTER_API_KEY_SERVER";
private string _serverCategory = "";
private bool _apiKeyValidated = false;
private bool _launcherUnreachableLogged = false;
private readonly HashSet<ulong> _onlineSteamIds = new HashSet<ulong>();
private bool HasConfiguredApiKey =>
!string.IsNullOrEmpty(_serverApiKey)
&& !string.Equals(_serverApiKey, "ENTER_API_KEY_SERVER", StringComparison.Ordinal);
private Configuration GetDefaultConfiguration()
{
return new Configuration();
}
private Configuration GetConfigStructureTemplate()
{
return GetDefaultConfiguration();
}
private void ApplyConfiguration()
{
if (_config == null)
_config = GetDefaultConfiguration();
if (_config.ConnectionSettings == null)
_config.ConnectionSettings = new ConnectionSettings();
if (_config.LoggingSettings == null)
_config.LoggingSettings = new LoggingSettings();
if (_config.SecuritySettings == null)
_config.SecuritySettings = new SecuritySettings();
if (_config.BonusSettings == null)
_config.BonusSettings = new BonusSettings();
if (_config.BonusSettings.BonusGroups == null)
_config.BonusSettings.BonusGroups = new List<string>();
if (_config.BonusSettings.BonusPermissions == null)
_config.BonusSettings.BonusPermissions = new List<string>();
_serverUrl = (_config.ConnectionSettings.LauncherServerUrl ?? "http://localhost:3000").TrimEnd('/');
_checkInterval = _config.ConnectionSettings.ConnectionCheckIntervalSeconds;
_pluginApiPort = _config.ConnectionSettings.PluginHttpApiPort;
_serverApiKey = (_config.ConnectionSettings.ServerApiKey ?? "ENTER_API_KEY_SERVER").Trim();
RegisterConfiguredPermissions();
}
private void RegisterConfiguredPermissions()
{
if (_config?.SecuritySettings == null)
return;
string bypassPerm = (_config.SecuritySettings.PermissionBypassLauncherKick ?? string.Empty).Trim();
if (!string.IsNullOrEmpty(bypassPerm))
permission.RegisterPermission(bypassPerm, this);
string requirePerm = (_config.SecuritySettings.PermissionRequireLauncher ?? string.Empty).Trim();
if (!string.IsNullOrEmpty(requirePerm))
permission.RegisterPermission(requirePerm, this);
}
private Dictionary<string, object> ReadConfigFileDictionary()
{
try
{
return JsonHelper.Deserialize(File.ReadAllText(ConfigFilePath)) as Dictionary<string, object>;
}
catch
{
return null;
}
}
private bool IsConfigOutdated()
{
Dictionary<string, object> fileDict = ReadConfigFileDictionary();
if (fileDict == null)
return false;
return IsConfigMissingKeys(GetConfigStructureTemplate().ToDictionary(), fileDict);
}
private static bool IsConfigMissingKeys(Dictionary<string, object> defaults, Dictionary<string, object> current)
{
foreach (KeyValuePair<string, object> pair in defaults)
{
object currentValue;
if (!current.TryGetValue(pair.Key, out currentValue))
return true;
Dictionary<string, object> defaultNested = pair.Value as Dictionary<string, object>;
if (defaultNested == null)
continue;
Dictionary<string, object> currentNested = AsStringObjectDictionary(currentValue);
if (currentNested == null || IsConfigMissingKeys(defaultNested, currentNested))
return true;
}
return false;
}
private static Dictionary<string, object> AsStringObjectDictionary(object value)
{
Dictionary<string, object> dict = value as Dictionary<string, object>;
if (dict != null)
return dict;
IDictionary genericDict = value as IDictionary;
if (genericDict != null)
{
Dictionary<string, object> result = new Dictionary<string, object>();
foreach (DictionaryEntry entry in genericDict)
result[entry.Key.ToString()] = entry.Value;
return result;
}
return null;
}
private void LogConfigCreated()
{
Puts("Создан новый конфигурационный файл: " + ConfigFilePath);
}
private void LogConfigUpdated()
{
Puts("Конфигурация устарела — добавлены новые настройки, файл обновлён: " + ConfigFilePath);
}
private void TryMigrateLegacyConfigFile()
{
string legacyPath = Path.Combine(Interface.Oxide.ConfigDirectory, "OkeyRustLauncher.json");
if (!File.Exists(ConfigFilePath) && File.Exists(legacyPath))
{
File.Copy(legacyPath, ConfigFilePath);
Puts("Конфиг перенесён из OkeyRustLauncher.json в " + Name + ".json");
}
}
protected override void LoadDefaultConfig()
{
_config = GetDefaultConfiguration();
}
protected override void LoadConfig()
{
TryMigrateLegacyConfigFile();
bool isNewConfig = !File.Exists(ConfigFilePath);
base.LoadConfig();
try
{
if (isNewConfig)
{
_config = GetDefaultConfiguration();
LogConfigCreated();
SaveConfig();
ApplyConfiguration();
return;
}
_config = Config.ReadObject<Configuration>();
if (_config == null)
throw new JsonException("Конфиг пустой.");
if (IsConfigOutdated())
{
LogConfigUpdated();
SaveConfig();
}
ApplyConfiguration();
}
catch (Exception ex)
{
PrintError("Ошибка чтения конфига (" + ConfigFilePath + "): " + ex.Message);
LoadDefaultConfig();
LogConfigCreated();
SaveConfig();
ApplyConfiguration();
}
}
protected override void SaveConfig()
{
if (_config == null)
_config = GetDefaultConfiguration();
Config.WriteObject(_config, true);
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["KickOnlyLauncher"] = "На сервер можно войти только с SABR RUST Launcher. Перезайдите с него в игру!",
["BonusLauncherGranted"] = "Вы зашли с SABR RUST Launcher, вам был выдан бонус!",
["BanKick"] = "Доступ ограничен! Причина: {0}",
["BanDefaultReason"] = "Заблокирован",
["LbanNoPermission"] = "<color=#FF2D2D>Нет доступа (rustlauncher.lban)</color>",
["UnlbanNoPermission"] = "<color=#FF2D2D>Нет доступа (rustlauncher.unlban)</color>",
["LbanUsage"] = "<color=#FF2D2D>Использование: /lban <SteamID/Ник/HWID/IP> <причина></color>",
["UnlbanUsage"] = "<color=#FF2D2D>Использование: /unlban <SteamID/Ник/HWID/IP></color>",
["BanSuccess"] = "<color=#4ADE80>Игрок забанен.</color>",
["UnbanSuccess"] = "<color=#4ADE80>Бан снят.</color>",
["BanError"] = "<color=#FF2D2D>Ошибка: {0}</color>",
["BanErrUnknown"] = "Неизвестная ошибка",
["BanErrTargetRequired"] = "Укажите цель бана",
["BanErrPlayerNotFound"] = "Игрок не найден: {0}",
["BanErrAlreadyBanned"] = "Уже в бане: {0}",
["BanErrResolveFailed"] = "Не удалось определить SteamID, IP или HWID",
["BanErrNotFound"] = "Бан не найден: {0}",
["BanHelpTitle"] = "<color=#FF2D2D>SABR RUST Launcher — баны</color>",
["BanHelpLban"] = "<color=#E8E8EC>/lban</color> <SteamID/Ник/HWID/IP> <причина> — забанить (SteamID, IP с сервера, HWID с лаунчера)",
["BanHelpUnlban"] = "<color=#E8E8EC>/unlban</color> <SteamID/Ник/HWID/IP> — снять бан",
["BanHelpLbanPerm"] = "Право: <color=#FFC107>rustlauncher.lban</color>",
["BanHelpUnlbanPerm"] = "Право: <color=#FFC107>rustlauncher.unlban</color>",
["BanHelpIgnorePerm"] = "Обход банов: <color=#FFC107>rustlauncher.ignoreban</color>",
["BanHelpKick"] = "При входе: кик с текстом «Доступ ограничен! Причина: …»"
}, this, "ru");
lang.RegisterMessages(new Dictionary<string, string>
{
["KickOnlyLauncher"] = "You can only join this server using SABR RUST Launcher. Please launch the game from the launcher!",
["BonusLauncherGranted"] = "You joined from SABR RUST Launcher — your bonus has been granted!",
["BanKick"] = "Access denied! Reason: {0}",
["BanDefaultReason"] = "Banned",
["LbanNoPermission"] = "<color=#FF2D2D>No permission (rustlauncher.lban)</color>",
["UnlbanNoPermission"] = "<color=#FF2D2D>No permission (rustlauncher.unlban)</color>",
["LbanUsage"] = "<color=#FF2D2D>Usage: /lban <SteamID/Nick/HWID/IP> <reason></color>",
["UnlbanUsage"] = "<color=#FF2D2D>Usage: /unlban <SteamID/Nick/HWID/IP></color>",
["BanSuccess"] = "<color=#4ADE80>Player banned.</color>",
["UnbanSuccess"] = "<color=#4ADE80>Ban removed.</color>",
["BanError"] = "<color=#FF2D2D>Error: {0}</color>",
["BanErrUnknown"] = "Unknown error",
["BanErrTargetRequired"] = "Ban target is required",
["BanErrPlayerNotFound"] = "Player not found: {0}",
["BanErrAlreadyBanned"] = "Already banned: {0}",
["BanErrResolveFailed"] = "Could not resolve SteamID, IP or HWID",
["BanErrNotFound"] = "Ban not found: {0}",
["BanHelpTitle"] = "<color=#FF2D2D>SABR RUST Launcher — bans</color>",
["BanHelpLban"] = "<color=#E8E8EC>/lban</color> <SteamID/Nick/HWID/IP> <reason> — ban (SteamID, game IP, launcher HWID)",
["BanHelpUnlban"] = "<color=#E8E8EC>/unlban</color> <SteamID/Nick/HWID/IP> — remove ban",
["BanHelpLbanPerm"] = "Permission: <color=#FFC107>rustlauncher.lban</color>",
["BanHelpUnlbanPerm"] = "Permission: <color=#FFC107>rustlauncher.unlban</color>",
["BanHelpIgnorePerm"] = "Bypass bans: <color=#FFC107>rustlauncher.ignoreban</color>",
["BanHelpKick"] = "On join: kick with «Access denied! Reason: …»"
}, this, "en");
}
private string GetMsg(string key, string userId = null, params object[] args)
{
string message;
if (!string.IsNullOrEmpty(userId) && IsRussianRustLanguage(lang.GetLanguage(userId)))
message = lang.GetMessage(key, this, userId);
else
message = lang.GetMessage(key, this, null);
if (args != null && args.Length > 0)
return string.Format(message, args);
return message;
}
private void SendBanChat(BasePlayer player, string key, params object[] args)
{
if (player == null)
return;
player.ChatMessage(GetMsg(key, player.UserIDString, args));
}
private void SendBanHelp(BasePlayer player)
{
if (player == null)
return;
SendBanChat(player, "BanHelpTitle");
SendBanChat(player, "BanHelpLban");
SendBanChat(player, "BanHelpUnlban");
SendBanChat(player, "BanHelpLbanPerm");
SendBanChat(player, "BanHelpUnlbanPerm");
SendBanChat(player, "BanHelpIgnorePerm");
SendBanChat(player, "BanHelpKick");
}
private string ResolveBanResultMessage(BasePlayer player, Dictionary<string, object> result)
{
if (result == null)
return GetMsg("BanErrUnknown", player?.UserIDString);
object keyObj;
if (result.TryGetValue("messageKey", out keyObj) && keyObj != null)
{
object[] msgArgs = null;
object argsObj;
if (result.TryGetValue("messageArgs", out argsObj) && argsObj is object[])
msgArgs = (object[])argsObj;
return GetMsg(keyObj.ToString(), player?.UserIDString, msgArgs ?? new object[0]);
}
object msgObj;
if (result.TryGetValue("message", out msgObj) && msgObj != null)
return msgObj.ToString();
return GetMsg("BanErrUnknown", player?.UserIDString);
}
private Dictionary<string, object> ApiFailKey(string key, params object[] args)
{
var dict = new Dictionary<string, object>
{
["status"] = "error",
["success"] = false,
["messageKey"] = key,
["message"] = GetMsg(key, null, args)
};
if (args != null && args.Length > 0)
dict["messageArgs"] = args;
return dict;
}
private static bool IsRussianRustLanguage(string code)
{
if (string.IsNullOrEmpty(code))
return false;
code = code.Trim();
if (code.Equals("ru", StringComparison.OrdinalIgnoreCase))
return true;
return code.StartsWith("ru-", StringComparison.OrdinalIgnoreCase);
}
private string GetKickOnlyLauncherMessage(BasePlayer player)
{
if (IsRussianRustLanguage(lang.GetLanguage(player.UserIDString)))
return lang.GetMessage("KickOnlyLauncher", this, player.UserIDString);
return lang.GetMessage("KickOnlyLauncher", this, null);
}
private string GetBonusChatMessage(BasePlayer player)
{
if (IsRussianRustLanguage(lang.GetLanguage(player.UserIDString)))
return lang.GetMessage("BonusLauncherGranted", this, player.UserIDString);
return lang.GetMessage("BonusLauncherGranted", this, null);
}
private static List<string> NormalizeBonusList(IList<string> list)
{
if (list == null || list.Count == 0)
return new List<string>();
return list
.Select(v => (v ?? string.Empty).Trim())
.Where(v => !string.IsNullOrEmpty(v))
.Distinct()
.ToList();
}
/// <summary>
/// Обход кика без лаунчера (rustlauncher.noneedlauncher) — при любом режиме проверки.
/// </summary>
private bool CanBypassLauncherKick(BasePlayer player)
{
if (player == null || _config?.SecuritySettings == null)
return false;
string perm = (_config.SecuritySettings.PermissionBypassLauncherKick ?? string.Empty).Trim();
if (string.IsNullOrEmpty(perm))
return false;
return permission.UserHasPermission(player.UserIDString, perm);
}
/// <summary>
/// Пермишенный режим: кик только у игроков с PermissionRequireLauncher, когда глобальный кик выключен.
/// </summary>
private bool IsPermissionBasedLauncherRequiredActive()
{
if (_config?.SecuritySettings == null)
return false;
if (_config.SecuritySettings.AllowJoinOnlyFromLauncher)
return false;
string perm = (_config.SecuritySettings.PermissionRequireLauncher ?? string.Empty).Trim();
return !string.IsNullOrEmpty(perm);
}
private bool IsLauncherSecurityCheckActive()
{
if (_config?.SecuritySettings == null)
return false;
return _config.SecuritySettings.AllowJoinOnlyFromLauncher || IsPermissionBasedLauncherRequiredActive();
}
/// <summary>
/// Кик без лаунчера: глобальный режим или пермишен needlauncher (если глобальный выключен).
/// </summary>
private bool ShouldKickPlayerWithoutLauncher(BasePlayer player, bool fromOk)
{
if (player == null || fromOk || !_isConnected || _config?.SecuritySettings == null)
return false;
if (CanBypassLauncherKick(player))
return false;
SecuritySettings sec = _config.SecuritySettings;
if (sec.AllowJoinOnlyFromLauncher)
return true;
string requirePerm = (sec.PermissionRequireLauncher ?? string.Empty).Trim();
if (string.IsNullOrEmpty(requirePerm))
return false;
return permission.UserHasPermission(player.UserIDString, requirePerm);
}
private void ApplyOrStripLauncherBonus(BasePlayer player, bool fromOk)
{
if (!_isConnected)
return;
BonusSettings b = _config != null ? _config.BonusSettings : null;
if (b == null)
return;
string steamId = player.UserIDString;
int mode = Mathf.Clamp(b.BonusGrantMode, 1, 3);
List<string> groups = NormalizeBonusList(b.BonusGroups);
List<string> perms = NormalizeBonusList(b.BonusPermissions);
bool give = fromOk && b.EnableLauncherJoinBonus;
if (!give)
{
StripLauncherBonus(steamId, mode, groups, perms);
return;
}
bool canGrant = (mode == 1 && groups.Count > 0) ||
(mode == 2 && perms.Count > 0) ||
(mode == 3 && (groups.Count > 0 || perms.Count > 0));
if (!canGrant)
return;
GrantLauncherBonus(steamId, mode, groups, perms);
player.ChatMessage("<color=#4ADE80>" + GetBonusChatMessage(player) + "</color>");
}
private void GrantLauncherBonus(string steamId, int mode, List<string> groups, List<string> perms)
{
try
{
if (mode == 1 || mode == 3)
{
foreach (string g in groups)
{
if (!permission.UserHasGroup(steamId, g))
permission.AddUserGroup(steamId, g);
}
}
if (mode == 2 || mode == 3)
{
foreach (string p in perms)
{
if (!permission.UserHasPermission(steamId, p))
permission.GrantUserPermission(steamId, p, this);
}
}
}
catch (Exception ex)
{
PrintError("[Bonus] Grant error for " + steamId + ": " + ex.Message);
}
}
private void StripLauncherBonus(string steamId, int mode, List<string> groups, List<string> perms)
{
try
{
if (mode == 1 || mode == 3)
{
foreach (string g in groups)
{
if (permission.UserHasGroup(steamId, g))
permission.RemoveUserGroup(steamId, g);
}
}
if (mode == 2 || mode == 3)
{
foreach (string p in perms)
{
if (permission.UserHasPermission(steamId, p))
permission.RevokeUserPermission(steamId, p);
}
}
}
catch (Exception ex)
{
PrintError("[Bonus] Strip error for " + steamId + ": " + ex.Message);
}
}
#endregion
#region Variables
private Timer _checkTimer;
private bool _isConnected;
private HttpListener _httpListener;
private Thread _httpListenerThread;
private bool _isRunning;
private string _httpListenerPrefix = string.Empty;
#endregion
#region Ban System
private const string PermLban = "rustlauncher.lban";
private const string PermUnlban = "rustlauncher.unlban";
private const string PermIgnoreBan = "rustlauncher.ignoreban";
private const string BanDataRoot = "ASystem";
private const string BanDataPlugin = "RustLauncher";
private static readonly DateTime BanEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private string BanStorageScope => GetBanStorageScope();
private string BanListPath => $"{BanDataRoot}/{BanDataPlugin}/{BanStorageScope}/BanList";
private string BanIdPath => $"{BanDataRoot}/{BanDataPlugin}/{BanStorageScope}/BanId";
private string LegacyBanListPath => $"{BanDataRoot}/{BanDataPlugin}/BanList";
private string LegacyBanIdPath => $"{BanDataRoot}/{BanDataPlugin}/BanId";
private DynamicConfigFile _banIdFile;
private int _banIdCounter;
private StoredBanData _storedBans;
private readonly Dictionary<int, LauncherBanData> _banCache = new Dictionary<int, LauncherBanData>();
private class LauncherBanData
{
public int id;
public string steamid = "";
public string ip = "";
public string name = "";
public string hwid = "";
public string source = "";
public string serverScope = "";
public double date;
public string reason = "";
}
private string GetBanStorageScope()
{
// Отдельный список банов на каждый API-ключ (проект / партнёр не пересекаются).
if (!string.IsNullOrEmpty(_serverApiKey)
&& !string.Equals(_serverApiKey, "ENTER_API_KEY_SERVER", StringComparison.Ordinal))
return GetApiKeyScopeKey(_serverApiKey);
return "unconfigured";
}
private static string GetApiKeyScopeKey(string apiKey)
{
if (string.IsNullOrEmpty(apiKey))
return "unconfigured";
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(apiKey.Trim()));
StringBuilder sb = new StringBuilder(16);
for (int i = 0; i < 8; i++)
sb.Append(hash[i].ToString("x2"));
return sb.ToString();
}
}
private class StoredBanData
{
public HashSet<string> Banlist = new HashSet<string>();
}
private class PlayerProfileDto
{
public bool ok;
public bool registered;
public string steamId = "";
public string steamName = "";
public string hwid = "";
public string lastIp = "";
public string ip = "";
}
private class LauncherPlayerData
{
public string Nick = "";
public string SteamID64 = "";
public string IP = "";
public string HWID = ""; // пусто до первого успешного входа с лаунчера
public string Launcher = ErrorCheckLauncher; // "true" | "false" | ErrorCheckLauncher
public string PlayerStatus = "offline"; // online | offline
public double UpdatedAt;
}
private class StoredPlayersData
{
public Dictionary<string, LauncherPlayerData> Players = new Dictionary<string, LauncherPlayerData>();
}
private string PlayersDataPath => $"{BanDataRoot}/{BanDataPlugin}/{BanStorageScope}/Players";
private StoredPlayersData _storedPlayers = new StoredPlayersData();
private void LoadPlayersData()
{
EnsureBanDirectories();
try
{
_storedPlayers = Interface.Oxide.DataFileSystem.ReadObject<StoredPlayersData>(PlayersDataPath) ?? new StoredPlayersData();
}
catch
{
_storedPlayers = new StoredPlayersData();
}
if (_storedPlayers.Players == null)
_storedPlayers.Players = new Dictionary<string, LauncherPlayerData>();
}
private void SavePlayersData()
{
if (_storedPlayers == null)
_storedPlayers = new StoredPlayersData();
if (_storedPlayers.Players == null)
_storedPlayers.Players = new Dictionary<string, LauncherPlayerData>();
EnsureBanDirectories();
Interface.Oxide.DataFileSystem.WriteObject(PlayersDataPath, _storedPlayers);
}
private static string NormalizeGameIp(string value)
{
var ip = (value ?? string.Empty).Trim();
if (ip.Contains(":"))
ip = ip.Split(':')[0];
return ip;
}
private LauncherPlayerData UpsertPlayerData(
string steamId,
string nick,
string ip,
string hwid = null,
string launcher = null,
string status = null)
{
if (!_apiKeyValidated)
return null;
if (string.IsNullOrEmpty(steamId))
return null;
LauncherPlayerData rec;
bool isNew = false;
if (!_storedPlayers.Players.TryGetValue(steamId, out rec) || rec == null)
{
rec = new LauncherPlayerData { SteamID64 = steamId };
_storedPlayers.Players[steamId] = rec;
isNew = true;
}
if (!string.IsNullOrEmpty(nick))
rec.Nick = nick;
if (!string.IsNullOrEmpty(ip))
rec.IP = NormalizeGameIp(ip);
if (!string.IsNullOrEmpty(hwid))
rec.HWID = hwid;
if (!string.IsNullOrEmpty(launcher))
rec.Launcher = launcher;
if (!string.IsNullOrEmpty(status))
rec.PlayerStatus = status;
rec.SteamID64 = steamId;
rec.UpdatedAt = BanLogTime();
SavePlayersData();
if (isNew)
Puts($"Игрок {rec.Nick} ({rec.SteamID64}) добавлен в data плагина");
return rec;
}
private bool TryGetPlayerRecord(string steamId, out LauncherPlayerData record)
{
record = null;
if (string.IsNullOrEmpty(steamId) || _storedPlayers?.Players == null)
return false;
return _storedPlayers.Players.TryGetValue(steamId, out record) && record != null;
}
private void MarkPlayerOffline(string steamId)
{
if (!_apiKeyValidated)
return;
LauncherPlayerData rec;
if (!TryGetPlayerRecord(steamId, out rec))
return;
rec.PlayerStatus = "offline";
rec.UpdatedAt = BanLogTime();
SavePlayersData();
}
private void InitBanSystem()
{
permission.RegisterPermission(PermLban, this);
permission.RegisterPermission(PermUnlban, this);
permission.RegisterPermission(PermIgnoreBan, this);
LoadBanData();
LoadPlayersData();
}
private static double BanLogTime()
{
return DateTime.UtcNow.Subtract(BanEpoch).TotalSeconds;
}
private void EnsureBanDirectories()
{
Directory.CreateDirectory(Path.Combine(Interface.Oxide.DataDirectory, BanDataRoot, BanDataPlugin, BanStorageScope));
}
private void LoadBanId()
{
try
{
_banIdFile = Interface.Oxide.DataFileSystem.GetDatafile(BanIdPath);
_banIdCounter = Convert.ToInt32(_banIdFile["id"] ?? 0);
}
catch
{
_banIdCounter = 0;
_banIdFile = Interface.Oxide.DataFileSystem.GetDatafile(BanIdPath);
_banIdFile["id"] = _banIdCounter;
SaveBanId();
}
}
private void SaveBanId()
{
EnsureBanDirectories();
_banIdFile["id"] = _banIdCounter;
Interface.Oxide.DataFileSystem.SaveDatafile(BanIdPath);
}
private int GetNewBanId()
{
_banIdCounter++;
SaveBanId();
return _banIdCounter;
}
private void LoadBanData()
{
EnsureBanDirectories();
LoadBanId();
_banCache.Clear();
try
{
_storedBans = Interface.Oxide.DataFileSystem.ReadObject<StoredBanData>(BanListPath) ?? new StoredBanData();
}
catch
{
_storedBans = new StoredBanData();
}
if (_storedBans.Banlist == null)
_storedBans.Banlist = new HashSet<string>();
foreach (string entry in _storedBans.Banlist)
{
try
{
LauncherBanData bd = JsonConvert.DeserializeObject<LauncherBanData>(entry);
if (bd != null && bd.id > 0 && !_banCache.ContainsKey(bd.id))
_banCache[bd.id] = bd;
}
catch { }
}
if (_banCache.Count == 0)
TryMigrateLegacyBanData();
Puts($"[Ban] Loaded {_banCache.Count} ban record(s) for scope '{BanStorageScope}'.");
}
private void TryMigrateLegacyBanData()
{
if (BanListPath == LegacyBanListPath)
return;
try
{
StoredBanData legacy = Interface.Oxide.DataFileSystem.ReadObject<StoredBanData>(LegacyBanListPath);
if (legacy?.Banlist == null || legacy.Banlist.Count == 0)
return;
foreach (string entry in legacy.Banlist)
{
try
{
LauncherBanData bd = JsonConvert.DeserializeObject<LauncherBanData>(entry);
if (bd == null || bd.id <= 0 || _banCache.ContainsKey(bd.id))
continue;
bd.serverScope = BanStorageScope;
_banCache[bd.id] = bd;
_storedBans.Banlist.Add(JsonConvert.SerializeObject(bd));
if (bd.id > _banIdCounter)
_banIdCounter = bd.id;
}
catch { }
}
if (_banCache.Count > 0)
{
SaveBanId();
SaveBanData();
Puts($"[Ban] Migrated {_banCache.Count} record(s) from legacy list to scope '{BanStorageScope}'.");
}
}
catch { }
}
private void SaveBanData()
{
if (_storedBans == null)
_storedBans = new StoredBanData();
if (_storedBans.Banlist == null)
_storedBans.Banlist = new HashSet<string>();
EnsureBanDirectories();
Interface.Oxide.DataFileSystem.WriteObject(BanListPath, _storedBans);
}
private bool CanIgnoreBan(BasePlayer player)
{
return player != null && permission.UserHasPermission(player.UserIDString, PermIgnoreBan);
}
private static string NormalizeBanKey(string value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant();
}
private static bool LooksLikeSteamId(string value)
{
if (string.IsNullOrEmpty(value)) return false;
ulong id;
return ulong.TryParse(value, out id) && value.Length >= 15;
}
private static bool LooksLikeIp(string value)
{
if (string.IsNullOrEmpty(value)) return false;
string[] parts = value.Split('.');
if (parts.Length != 4) return false;
foreach (string p in parts)
{
int n;
if (!int.TryParse(p, out n) || n < 0 || n > 255) return false;
}
return true;
}
private void FetchPlayerProfile(string steamId, Action<PlayerProfileDto> callback)
{
if (string.IsNullOrEmpty(steamId) || !_isConnected)
{
callback(new PlayerProfileDto { ok = false, steamId = steamId ?? "" });
return;
}
string url = _serverUrl + "/api/rust-plugin/player-profile?steam_id=" + Uri.EscapeDataString(steamId);
webrequest.Enqueue(url, null, (code, response) =>
{
PlayerProfileDto dto = new PlayerProfileDto { ok = false, steamId = steamId };
if (code == 200 && !string.IsNullOrEmpty(response))
{
try
{
dto = JsonConvert.DeserializeObject<PlayerProfileDto>(response) ?? dto;
}
catch { }
}
callback(dto);
}, this);
}
private class LauncherCheckResult
{
public bool Ok;
public bool FromLauncher;
public bool SessionOpen;
public bool InGame;
public string Hwid = "";
public string Error = "";
}
private const int LauncherVerifyMaxAttempts = 5;
private const float LauncherVerifyRetrySeconds = 2f;
private void RequestLauncherAndProfile(string steamId, Action<LauncherCheckResult> callback)
{
if (string.IsNullOrEmpty(steamId) || !_isConnected)
{
callback(new LauncherCheckResult
{
Ok = false,
Error = "Launcher server unavailable"
});
return;
}
string q = Uri.EscapeDataString(steamId);
string url = _serverUrl + "/api/rust-plugin/player-from-launcher?steam_id=" + q;
webrequest.Enqueue(url, null, (code, response) =>
{
if (code != 200 || string.IsNullOrEmpty(response))
{
callback(new LauncherCheckResult
{
Ok = false,
Error = "ErrorCheckLauncher"
});
return;
}
bool fromOk;
bool sessionOpen;
bool inGame;
try
{
JObject jr = JObject.Parse(response);
JToken fromToken = jr["fromOkeyRustLauncher"] ?? jr["fromSabrRustLauncher"];
if (fromToken == null)
{
callback(new LauncherCheckResult { Ok = false, Error = "ErrorCheckLauncher" });
return;
}
fromOk = fromToken.Value<bool>();
sessionOpen = jr["sessionOpen"] != null && jr["sessionOpen"].Value<bool>();
inGame = jr["inGame"] != null && jr["inGame"].Value<bool>();
}
catch
{
callback(new LauncherCheckResult { Ok = false, Error = "ErrorCheckLauncher" });
return;
}
FetchPlayerProfile(steamId, profile =>
{
callback(new LauncherCheckResult
{
Ok = true,
FromLauncher = fromOk,
SessionOpen = sessionOpen,
InGame = inGame,
Hwid = profile?.hwid ?? string.Empty
});
});
}, this);
}
private bool TryFindBanMatch(string steamId, string ip, string hwid, string name, out LauncherBanData match)
{
match = null;
string sSteam = NormalizeBanKey(steamId);
string sIp = NormalizeBanKey(ip);
string sHwid = NormalizeBanKey(hwid);
string sName = NormalizeBanKey(name);
foreach (LauncherBanData bd in _banCache.Values)
{
if (bd == null) continue;
if (!string.IsNullOrEmpty(sSteam) && NormalizeBanKey(bd.steamid) == sSteam)
{
match = bd;
return true;
}
if (!string.IsNullOrEmpty(sIp) && !string.IsNullOrEmpty(bd.ip) && NormalizeBanKey(bd.ip) == sIp)
{
match = bd;
return true;
}
if (!string.IsNullOrEmpty(sHwid) && !string.IsNullOrEmpty(bd.hwid) && NormalizeBanKey(bd.hwid) == sHwid)
{
match = bd;
return true;
}
if (!string.IsNullOrEmpty(sName) && !string.IsNullOrEmpty(bd.name) && NormalizeBanKey(bd.name) == sName)
{
match = bd;
return true;
}
}
return false;
}
private Dictionary<string, object> ApiCheckBan(string steamId, string ip, string hwid, string name)
{
if (!_apiKeyValidated)
{
return new Dictionary<string, object>
{
["status"] = "error",
["success"] = false,
["message"] = "API key not validated"
};
}
LauncherBanData match;
if (TryFindBanMatch(steamId, ip, hwid, name, out match))
{
return new Dictionary<string, object>
{
["status"] = "success",
["success"] = true,
["banned"] = true,
["reason"] = match.reason ?? "Banned",
["banId"] = match.id
};
}
return new Dictionary<string, object>
{
["status"] = "success",
["success"] = true,
["banned"] = false
};
}
private bool EnsureBanSystemReady(BasePlayer player = null)
{
if (_apiKeyValidated)
return true;
if (player != null)
SendBanChat(player, "BanError", "API ключ сервера не подтверждён — баны недоступны.");
return false;
}
private Dictionary<string, object> ApiAddBan(string sourceName, string target, string reason)
{
if (!_apiKeyValidated)
return ApiFail("API key not validated");
if (string.IsNullOrWhiteSpace(target))
return ApiFailKey("BanErrTargetRequired");
if (string.IsNullOrWhiteSpace(reason))
reason = GetMsg("BanDefaultReason");
return ExecuteBan(sourceName, target.Trim(), reason.Trim(), null, null, null);
}
private Dictionary<string, object> ApiRemoveBan(string sourceName, string target)
{
if (!_apiKeyValidated)
return ApiFail("API key not validated");
if (string.IsNullOrWhiteSpace(target))
return ApiFailKey("BanErrTargetRequired");
return ExecuteUnban(sourceName, target.Trim());
}
private static Dictionary<string, object> ApiFail(string message)
{
return new Dictionary<string, object>
{
["status"] = "error",
["success"] = false,
["message"] = message
};
}
private Dictionary<string, object> ExecuteBan(string sourceName, string target, string reason, string presetSteamId, string presetIp, string presetHwid)
{
string steamId = presetSteamId ?? "";
string ip = presetIp ?? "";
string hwid = presetHwid ?? "";
string name = "";
if (LooksLikeSteamId(target))
steamId = target;
else if (LooksLikeIp(target))
ip = target;
else if (target.Length >= 16 && !target.Contains(" "))
hwid = target;
else
{
BasePlayer online = BasePlayer.Find(target) ?? BasePlayer.FindSleeping(target);
if (online != null)
{
steamId = online.UserIDString;
name = online.displayName ?? "";
ip = online.net?.connection?.ipaddress ?? ip;
}
else
{
LauncherBanData byName;
if (TryFindBanMatch("", "", "", target, out byName))
{
steamId = byName.steamid;
ip = byName.ip;
hwid = byName.hwid;
name = byName.name;
}
else
{
return ApiFailKey("BanErrPlayerNotFound", target);
}
}
}
if (!string.IsNullOrEmpty(steamId))
{
BasePlayer bp = BasePlayer.Find(steamId) ?? BasePlayer.
// ... [Code preview truncated due to length. Full source in attached file] ...