C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("SQPVEMode", "BLOOD KRAKEN", "4.5.5")]
[Description("Advanced PVE/PVP Mode System with JSON Database")]
class SQPVEMode : RustPlugin
{
[PluginReference]
private Plugin ImageLibrary;
#region Configuration
private ConfigData configData;
private class ConfigData
{
[JsonProperty("Database Settings")]
public DatabaseSettings Database = new DatabaseSettings();
[JsonProperty("PVE Settings")]
public PVEOptions PVE = new PVEOptions();
[JsonProperty("Messages")]
public Messages MSG = new Messages();
[JsonProperty("UI Settings")]
public UIOptions UI = new UIOptions();
[JsonProperty("Image Settings")]
public ImageOptions ImageSettings = new ImageOptions();
[JsonProperty("Performance")]
public PerformanceOptions Performance = new PerformanceOptions();
[JsonProperty("Debug")]
public DebugOptions Debug = new DebugOptions();
}
public class DatabaseSettings
{
[JsonProperty("Auto Save Interval (seconds)")]
public float AutoSaveInterval = 60f;
[JsonProperty("Batch Save Size")]
public int BatchSaveSize = 20;
}
public class PVEOptions
{
[JsonProperty("Max Warnings")]
public int MaxWarnings = 2;
[JsonProperty("Protect Buildings")]
public bool ProtectBuildings = true;
[JsonProperty("Protect Players")]
public bool ProtectPlayers = true;
[JsonProperty("Allow Loot Own Corpse")]
public bool AllowLootOwnCorpse = true;
[JsonProperty("Block Build Radius")]
public float BlockBuildRadius = 0f;
[JsonProperty("Auto Convert On Team Join")]
public bool AutoConvertOnTeamJoin = true;
[JsonProperty("Protect From Fire")]
public bool ProtectFromFire = true;
[JsonProperty("Fire Prefabs")]
public List<string> FirePrefabs = new List<string>()
{
"fireball_small",
"fireball_small_arrow",
"fireball_small_molotov",
"fireball_small_shotgun",
"fireball"
};
[JsonProperty("PVE Can Loot Any Corpse")]
public bool PVECanLootAnyCorpse = true;
}
public class Messages
{
[JsonProperty("Welcome")]
public string WelcomeText = "<size=32>Выберите режим игры</size>\n<size=18>Этот выбор определит вашу судьбу до следующего вайпа</size>";
[JsonProperty("Selected PVE")]
public string SelectedPVE = "<color=#81C784>✓ Вы выбрали PVE режим. Вы защищены!</color>";
[JsonProperty("Selected PVP")]
public string SelectedPVP = "<color=#FF7043>⚔ Вы выбрали PVP режим. Удачи!</color>";
[JsonProperty("Attack Blocked")]
public string AttackBlocked = "<color=#FF7043>⚠ Атака запрещена! Предупреждение: {0}/{1}</color>";
[JsonProperty("Forced PVP")]
public string ForcedPVP = "<color=#D32F2F>⛔ Превышен лимит! Вы переведены в PVP.</color>";
[JsonProperty("Team Convert")]
public string TeamConvert = "<color=#D32F2F>⚠ В команде PVP игрок! Автоперевод в PVP.</color>";
[JsonProperty("Cannot Loot")]
public string CannotLoot = "<color=#FF7043>⛔ Нельзя лутать чужие вещи в PVE режиме!</color>";
[JsonProperty("Cannot Loot Sleeping")]
public string CannotLootSleeping = "<color=#FF7043>⛔ Нельзя лутать спящих PVE игроков!</color>";
[JsonProperty("Building Protected")]
public string BuildingProtected = "<color=#81C784>🏠 Строение защищено (PVE)</color>";
[JsonProperty("Building Blocked")]
public string BuildingBlockedRadius = "<color=#FF7043>⛔ Нельзя строиться рядом с PVP базой!</color>";
[JsonProperty("Team Warning")]
public string TeamWarning = "<color=#FF7043>⚠ В команде есть PVP игроки!</color>";
[JsonProperty("Mode PVE")]
public string ModePVE = "<color=#81C784>🛡 PVE</color>";
[JsonProperty("Mode PVP")]
public string ModePVP = "<color=#FF7043>⚔ PVP</color>";
[JsonProperty("Already Chosen")]
public string AlreadyChosen = "<color=#FF7043>⚠ Вы уже выбрали режим! Используйте /mode для проверки.</color>";
[JsonProperty("Cannot Use Explosive")]
public string CannotUseExplosive = "<color=#FF7043>⛔ Нельзя использовать взрывчатку в PVE режиме!</color>";
[JsonProperty("Cannot Use Rocket")]
public string CannotUseRocket = "<color=#FF7043>⛔ Нельзя использовать ракеты в PVE режиме!</color>";
[JsonProperty("PVE Base Nearby")]
public string PVEBaseNearby = "<color=#FF7043>⛔ Рядом PVE база! Нельзя использовать взрывчатку!</color>";
[JsonProperty("Team Mixed Modes")]
public string TeamMixedModes = "<color=#D32F2F>[Команда] Обнаружены смешанные режимы в команде!</color>";
[JsonProperty("Team Converted")]
public string TeamConverted = "<color=#D32F2F>[Команда] {0} игрок(ов) переведено в PVP: {1}</color>";
[JsonProperty("Team PVP Joined")]
public string TeamPVPJoined = "<color=#FF7043>[Команда] {0} переведен в PVP (в команде есть PVP игроки)</color>";
}
public class UIOptions
{
[JsonProperty("Scroll Width")]
public float ScrollWidth = 0.28f;
[JsonProperty("Scroll Spacing")]
public float ScrollSpacing = 0.04f;
[JsonProperty("Max Scroll Height")]
public float MaxScrollHeight = 0.75f;
[JsonProperty("Mode Icon Offset Min")]
public string ModeIconOffsetMin = "-440 16";
[JsonProperty("Mode Icon Offset Max")]
public string ModeIconOffsetMax = "-350 98";
}
public class ImageOptions
{
[JsonProperty("Use ImageLibrary")]
public bool UseImageLibrary = true;
[JsonProperty("PVE Scroll URL")]
public string PVEScrollURL = "https://i.yapx.ru/c3gYD.png ";
[JsonProperty("PVP Scroll URL")]
public string PVPScrollURL = "https://i.yapx.ru/c3gYZ.png ";
[JsonProperty("PVE Icon URL")]
public string PVEIconURL = "https://i.yapx.ru/c3iZi.png ";
[JsonProperty("PVP Icon URL")]
public string PVPIconURL = "https://i.yapx.ru/c3iZj.png ";
[JsonProperty("PVE Scroll Image Name")]
public string PVEScrollImageName = "SQPVEMode_PVE_Scroll";
[JsonProperty("PVP Scroll Image Name")]
public string PVPScrollImageName = "SQPVEMode_PVP_Scroll";
[JsonProperty("PVE Icon Image Name")]
public string PVEIconImageName = "SQPVEMode_PVE_Icon";
[JsonProperty("PVP Icon Image Name")]
public string PVPIconImageName = "SQPVEMode_PVP_Icon";
[JsonProperty("Image Aspect Ratio")]
public float ImageAspectRatio = 0.56f;
}
public class PerformanceOptions
{
[JsonProperty("Cache Player Data")]
public bool CachePlayerData = true;
[JsonProperty("Save On Disconnect")]
public bool SaveOnDisconnect = true;
[JsonProperty("Max Save Queue Size")]
public int MaxSaveQueueSize = 1000;
}
public class DebugOptions
{
[JsonProperty("Enable Debug")]
public bool EnableDebug = false;
[JsonProperty("Log Violations")]
public bool LogViolations = true;
[JsonProperty("Log Database Operations")]
public bool LogDatabase = false;
}
protected override void LoadConfig()
{
base.LoadConfig();
configData = Config.ReadObject<ConfigData>();
if (configData?.PVE?.FirePrefabs != null)
{
var uniquePrefabs = configData.PVE.FirePrefabs.Distinct().ToList();
if (uniquePrefabs.Count != configData.PVE.FirePrefabs.Count)
{
PrintWarning($"[SQPVEMode] Удалено {configData.PVE.FirePrefabs.Count - uniquePrefabs.Count} дубликатов из FirePrefabs");
configData.PVE.FirePrefabs = uniquePrefabs;
}
}
SaveConfig();
}
protected override void LoadDefaultConfig() => configData = new ConfigData();
protected override void SaveConfig() => Config.WriteObject(configData, true);
#endregion
#region Data Models
public class PlayerData
{
public ulong SteamID { get; set; }
public bool IsPVE { get; set; } = true;
public bool HasChosen { get; set; } = false;
public int Warnings { get; set; } = 0;
public long LastUpdate { get; set; } = DateTimeOffset.Now.ToUnixTimeSeconds();
public int Version { get; set; } = 1;
[JsonIgnore]
public bool IsDirty { get; set; } = false;
public void Migrate()
{
if (Version < 1) Version = 1;
}
}
public class ServerData
{
public long LastWipeTime { get; set; } = 0;
public string Version { get; set; } = "4.5.0";
}
#endregion
#region JSON Database
public class JsonDatabase
{
private SQPVEMode plugin;
private string dataPath;
private string playersPath;
private string serverFile;
public JsonDatabase(SQPVEMode plugin)
{
this.plugin = plugin;
dataPath = $"{Interface.Oxide.DataDirectory}/{plugin.Name}";
playersPath = $"{dataPath}/players";
serverFile = $"{dataPath}/server.json";
EnsureDirectory(dataPath);
EnsureDirectory(playersPath);
}
private void EnsureDirectory(string path)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
public PlayerData LoadPlayer(ulong steamId)
{
string path = Path.Combine(playersPath, $"{steamId}.json");
if (!File.Exists(path))
return new PlayerData { SteamID = steamId };
try
{
string json = File.ReadAllText(path);
var playerData = JsonConvert.DeserializeObject<PlayerData>(json);
if (playerData != null)
{
playerData.SteamID = steamId;
playerData.Migrate();
return playerData;
}
}
catch (Exception ex)
{
plugin.PrintError($"Failed to load player {steamId}: {ex.Message}");
}
return new PlayerData { SteamID = steamId };
}
public void SavePlayer(PlayerData playerData)
{
string path = Path.Combine(playersPath, $"{playerData.SteamID}.json");
try
{
playerData.LastUpdate = DateTimeOffset.Now.ToUnixTimeSeconds();
File.WriteAllText(path, JsonConvert.SerializeObject(playerData, Formatting.Indented));
playerData.IsDirty = false;
}
catch (Exception ex)
{
plugin.PrintError($"Failed to save player {playerData.SteamID}: {ex.Message}");
}
}
public void SaveBatch(IEnumerable<PlayerData> batch)
{
foreach (var playerData in batch)
{
if (playerData.IsDirty)
SavePlayer(playerData);
}
}
public void DeletePlayer(ulong steamId)
{
string path = Path.Combine(playersPath, $"{steamId}.json");
if (File.Exists(path))
{
try { File.Delete(path); } catch { }
}
}
public void WipeAll()
{
try
{
if (Directory.Exists(playersPath))
{
foreach (var file in Directory.GetFiles(playersPath, "*.json"))
{
try { File.Delete(file); } catch { }
}
}
}
catch (Exception ex)
{
plugin.PrintError($"Failed to wipe data: {ex.Message}");
}
}
public ServerData LoadServerData()
{
if (!File.Exists(serverFile))
return new ServerData();
try
{
string json = File.ReadAllText(serverFile);
return JsonConvert.DeserializeObject<ServerData>(json) ?? new ServerData();
}
catch
{
return new ServerData();
}
}
public void SaveServerData(ServerData serverData)
{
try
{
File.WriteAllText(serverFile, JsonConvert.SerializeObject(serverData, Formatting.Indented));
}
catch (Exception ex)
{
plugin.PrintError($"Failed to save server data: {ex.Message}");
}
}
}
#endregion
#region Database Manager
public class DatabaseManager
{
private SQPVEMode plugin;
private JsonDatabase database;
private ConcurrentDictionary<ulong, PlayerData> cache;
private ConcurrentQueue<ulong> saveQueue;
private HashSet<ulong> pendingSaves;
private Timer autoSaveTimer;
private bool isShuttingDown = false;
private object lockObject = new object();
public DatabaseManager(SQPVEMode plugin)
{
this.plugin = plugin;
database = new JsonDatabase(plugin);
cache = new ConcurrentDictionary<ulong, PlayerData>();
saveQueue = new ConcurrentQueue<ulong>();
pendingSaves = new HashSet<ulong>();
if (plugin.configData.Database.AutoSaveInterval > 0)
{
autoSaveTimer = plugin.timer.Every(plugin.configData.Database.AutoSaveInterval, ProcessSaveQueue);
}
}
public PlayerData GetPlayer(ulong steamId)
{
if (cache.TryGetValue(steamId, out var cachedData))
return cachedData;
var playerData = database.LoadPlayer(steamId);
cache[steamId] = playerData;
return playerData;
}
public void MarkDirty(ulong steamId)
{
if (!cache.TryGetValue(steamId, out var playerData)) return;
playerData.IsDirty = true;
lock (lockObject)
{
if (!pendingSaves.Contains(steamId) && saveQueue.Count < plugin.configData.Performance.MaxSaveQueueSize)
{
saveQueue.Enqueue(steamId);
pendingSaves.Add(steamId);
}
}
}
public void SavePlayer(ulong steamId, bool immediate = false)
{
if (!cache.TryGetValue(steamId, out var playerData)) return;
playerData.IsDirty = true;
if (immediate)
{
database.SavePlayer(playerData);
lock (lockObject) { pendingSaves.Remove(steamId); }
}
else
{
MarkDirty(steamId);
}
}
public void ProcessSaveQueue()
{
if (isShuttingDown) return;
int batchSize = plugin.configData.Database.BatchSaveSize;
var batch = new List<PlayerData>();
var processed = new List<ulong>();
lock (lockObject)
{
while (batch.Count < batchSize && saveQueue.TryDequeue(out var steamId))
{
if (cache.TryGetValue(steamId, out var playerData) && playerData.IsDirty)
{
batch.Add(playerData);
processed.Add(steamId);
}
pendingSaves.Remove(steamId);
}
}
if (batch.Count > 0)
{
try
{
database.SaveBatch(batch);
if (plugin.configData.Debug.LogDatabase)
plugin.Puts($"Saved {batch.Count} players");
}
catch (Exception ex)
{
plugin.PrintError($"Batch save failed: {ex.Message}");
lock (lockObject)
{
foreach (var steamId in processed)
{
if (cache.TryGetValue(steamId, out var playerData) && playerData.IsDirty && !pendingSaves.Contains(steamId))
{
saveQueue.Enqueue(steamId);
pendingSaves.Add(steamId);
}
}
}
}
}
}
public void SaveAll()
{
var dirtyPlayers = cache.Values.Where(p => p.IsDirty).ToList();
if (dirtyPlayers.Count > 0)
{
try
{
database.SaveBatch(dirtyPlayers);
plugin.Puts($"Saved {dirtyPlayers.Count} players on shutdown");
}
catch (Exception ex)
{
plugin.PrintError($"Failed to save all: {ex.Message}");
}
}
}
public void RemoveFromCache(ulong steamId)
{
if (cache.TryRemove(steamId, out var playerData) && playerData.IsDirty)
{
try
{
database.SavePlayer(playerData);
}
catch (Exception ex)
{
plugin.PrintError($"Failed to save on disconnect: {ex.Message}");
}
}
}
public void WipeAll()
{
cache.Clear();
lock (lockObject)
{
saveQueue.Clear();
pendingSaves.Clear();
}
database.WipeAll();
}
public ServerData LoadServerData() => database.LoadServerData();
public void SaveServerData(ServerData serverData) => database.SaveServerData(serverData);
public void Shutdown()
{
isShuttingDown = true;
autoSaveTimer?.Destroy();
SaveAll();
}
}
#endregion
#region Fields
private DatabaseManager dbManager;
private ServerData serverData;
private ImageManager imageManager;
private const string UI_SELECTION = "SQPVEMode_SelectionUI";
private const string UI_MODE_ICON = "SQPVEMode_ModeIcon";
private Timer imageCheckTimer;
private Timer teamCheckTimer;
private Dictionary<ulong, Timer> disconnectTimers = new Dictionary<ulong, Timer>();
private readonly object disconnectLock = new object();
private Dictionary<ulong, RelationshipManager.PlayerTeam> playerTeamsCache = new Dictionary<ulong, RelationshipManager.PlayerTeam>();
#endregion
#region Image Management
public class ImageManager
{
private SQPVEMode plugin;
private Dictionary<string, string> cachedImages = new Dictionary<string, string>();
private bool isReady = false;
private bool isLoading = false;
private int checkCount = 0;
public bool IsReady => isReady;
public ImageManager(SQPVEMode plugin)
{
this.plugin = plugin;
}
public void LoadImages()
{
if (isLoading) return;
isLoading = true;
cachedImages.Clear();
isReady = false;
checkCount = 0;
if (plugin.ImageLibrary == null)
{
plugin.PrintWarning("[SQPVEMode] ImageLibrary not found! Using fallback colors.");
isReady = false;
isLoading = false;
return;
}
string pveScrollUrl = plugin.configData.ImageSettings.PVEScrollURL?.Trim() ?? "";
string pvpScrollUrl = plugin.configData.ImageSettings.PVPScrollURL?.Trim() ?? "";
string pveIconUrl = plugin.configData.ImageSettings.PVEIconURL?.Trim() ?? "";
string pvpIconUrl = plugin.configData.ImageSettings.PVPIconURL?.Trim() ?? "";
if (string.IsNullOrEmpty(pveScrollUrl) || string.IsNullOrEmpty(pvpScrollUrl) ||
string.IsNullOrEmpty(pveIconUrl) || string.IsNullOrEmpty(pvpIconUrl))
{
plugin.PrintWarning("[SQPVEMode] Some image URLs not configured! Using fallback colors.");
isReady = false;
isLoading = false;
return;
}
plugin.Puts($"[SQPVEMode] Loading images...");
LoadImageWithCallback("pve", pveScrollUrl, plugin.configData.ImageSettings.PVEScrollImageName);
LoadImageWithCallback("pvp", pvpScrollUrl, plugin.configData.ImageSettings.PVPScrollImageName);
LoadImageWithCallback("pve_icon", pveIconUrl, plugin.configData.ImageSettings.PVEIconImageName);
LoadImageWithCallback("pvp_icon", pvpIconUrl, plugin.configData.ImageSettings.PVPIconImageName);
plugin.imageCheckTimer = plugin.timer.Repeat(2f, 40, CheckImagesReady);
}
private void LoadImageWithCallback(string key, string url, string imageName)
{
try
{
Action callback = () =>
{
if (plugin.configData.Debug.EnableDebug)
plugin.Puts($"[SQPVEMode] Callback received for {key}");
CheckAndCacheImage(key, imageName);
};
plugin.ImageLibrary.Call("AddImage", url, imageName, 0UL, callback);
}
catch (Exception ex)
{
plugin.PrintError($"[SQPVEMode] Error loading {key}: {ex.Message}");
}
}
private void CheckAndCacheImage(string key, string imageName)
{
try
{
string imageId = plugin.ImageLibrary.Call<string>("GetImage", imageName, 0UL, false);
if (!string.IsNullOrEmpty(imageId) && imageId != "0" && !imageId.StartsWith("LOADING") && !imageId.StartsWith("NONE"))
{
cachedImages[key] = imageId;
if (plugin.configData.Debug.EnableDebug)
plugin.Puts($"[SQPVEMode] {key} image cached");
}
}
catch (Exception ex)
{
if (plugin.configData.Debug.EnableDebug)
plugin.PrintError($"[SQPVEMode] Error caching {key}: {ex.Message}");
}
}
private void CheckImagesReady()
{
checkCount++;
if (!cachedImages.ContainsKey("pve"))
CheckAndCacheImage("pve", plugin.configData.ImageSettings.PVEScrollImageName);
if (!cachedImages.ContainsKey("pvp"))
CheckAndCacheImage("pvp", plugin.configData.ImageSettings.PVPScrollImageName);
if (!cachedImages.ContainsKey("pve_icon"))
CheckAndCacheImage("pve_icon", plugin.configData.ImageSettings.PVEIconImageName);
if (!cachedImages.ContainsKey("pvp_icon"))
CheckAndCacheImage("pvp_icon", plugin.configData.ImageSettings.PVPIconImageName);
bool pveReady = cachedImages.ContainsKey("pve");
bool pvpReady = cachedImages.ContainsKey("pvp");
bool pveIconReady = cachedImages.ContainsKey("pve_icon");
bool pvpIconReady = cachedImages.ContainsKey("pvp_icon");
if (pveReady && pvpReady && pveIconReady && pvpIconReady)
{
isReady = true;
isLoading = false;
plugin.imageCheckTimer?.Destroy();
plugin.imageCheckTimer = null;
plugin.Puts($"[SQPVEMode] Images loaded successfully!");
foreach (var player in BasePlayer.activePlayerList)
{
var data = plugin.dbManager.GetPlayer(player.userID);
if (!data.HasChosen && player.IsConnected)
{
CuiHelper.DestroyUi(player, UI_SELECTION);
plugin.ShowSelectionUI(player);
}
else if (data.HasChosen && player.IsConnected)
{
plugin.UpdateModeIcon(player);
}
}
}
else if (checkCount >= 40)
{
isReady = false;
isLoading = false;
plugin.imageCheckTimer?.Destroy();
plugin.imageCheckTimer = null;
plugin.PrintWarning($"[SQPVEMode] Image loading timeout. Using fallback colors.");
}
}
public string GetImage(string key)
{
if (cachedImages.TryGetValue(key, out var img) && !string.IsNullOrEmpty(img))
return img;
if (plugin.ImageLibrary != null)
{
string name = key switch
{
"pve" => plugin.configData.ImageSettings.PVEScrollImageName,
"pvp" => plugin.configData.ImageSettings.PVPScrollImageName,
"pve_icon" => plugin.configData.ImageSettings.PVEIconImageName,
"pvp_icon" => plugin.configData.ImageSettings.PVPIconImageName,
_ => null
};
if (name != null)
{
try
{
string imageId = plugin.ImageLibrary.Call<string>("GetImage", name, 0UL, false);
if (!string.IsNullOrEmpty(imageId) && imageId != "0" && !imageId.StartsWith("LOADING") && !imageId.StartsWith("NONE"))
{
cachedImages[key] = imageId;
return imageId;
}
}
catch { }
}
}
return null;
}
public void ForceReload()
{
cachedImages.Clear();
isReady = false;
isLoading = false;
checkCount = 0;
LoadImages();
}
}
#endregion
#region Oxide Hooks
private void Init()
{
imageCheckTimer?.Destroy();
imageCheckTimer = null;
dbManager = new DatabaseManager(this);
imageManager = new ImageManager(this);
serverData = dbManager.LoadServerData();
teamCheckTimer = timer.Every(5f, CheckAllTeamsForMixedModes);
}
private void OnServerInitialized()
{
imageManager.LoadImages();
timer.Once(6f, () =>
{
foreach (var player in BasePlayer.activePlayerList)
OnPlayerConnected(player);
});
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null || player.IsNpc) return;
lock (disconnectLock)
{
if (disconnectTimers.TryGetValue(player.userID, out var existingTimer))
{
existingTimer?.Destroy();
disconnectTimers.Remove(player.userID);
}
}
var playerData = dbManager.GetPlayer(player.userID);
if (!playerData.HasChosen)
{
float delay = imageManager.IsReady ? 0.5f : 10f;
timer.Once(delay, () =>
{
if (player?.IsConnected == true)
{
CuiHelper.DestroyUi(player, UI_SELECTION);
ShowSelectionUI(player);
}
});
}
else
{
timer.Once(1f, () => UpdateModeIcon(player));
}
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player == null) return;
ClearPlayerTeamCache(player.userID);
CuiHelper.DestroyUi(player, UI_MODE_ICON);
if (configData.Performance.SaveOnDisconnect)
dbManager.SavePlayer(player.userID, true);
if (configData.Performance.CachePlayerData)
{
lock (disconnectLock)
{
if (disconnectTimers.TryGetValue(player.userID, out var oldTimer))
{
oldTimer?.Destroy();
}
disconnectTimers[player.userID] = timer.Once(30f, () =>
{
dbManager.RemoveFromCache(player.userID);
lock (disconnectLock)
{
disconnectTimers.Remove(player.userID);
}
});
}
}
}
private void OnNewSave()
{
Puts("New save detected! Resetting all player data...");
playerTeamsCache.Clear();
dbManager.WipeAll();
serverData.LastWipeTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
dbManager.SaveServerData(serverData);
}
private void Unload()
{
imageCheckTimer?.Destroy();
teamCheckTimer?.Destroy();
lock (disconnectLock)
{
foreach (var t in disconnectTimers.Values.ToList())
t?.Destroy();
disconnectTimers.Clear();
}
foreach (var player in BasePlayer.activePlayerList.ToList())
{
CuiHelper.DestroyUi(player, UI_SELECTION);
CuiHelper.DestroyUi(player, UI_MODE_ICON);
}
dbManager?.Shutdown();
}
#endregion
#region UI System
private void ShowSelectionUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
var data = dbManager.GetPlayer(player.userID);
if (data.HasChosen) return;
CuiHelper.DestroyUi(player, UI_SELECTION);
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
CursorEnabled = true,
Image = {
Color = "0.1 0.1 0.1 0.95",
Material = "assets/content/ui/uibackgroundblur-ingamemenu.mat"
},
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, "Overlay", UI_SELECTION);
container.Add(new CuiLabel
{
Text = {
Text = configData.MSG.WelcomeText,
FontSize = 28,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1",
Font = "robotocondensed-bold.ttf"
},
RectTransform = { AnchorMin = "0 0.85", AnchorMax = "1 0.98" }
}, UI_SELECTION);
float width = configData.UI.ScrollWidth;
float spacing = configData.UI.ScrollSpacing;
float startX = (1f - (width * 2 + spacing)) / 2;
CreateImageButton(container, startX, "sqpve_choose pve", "pve");
CreateImageButton(container, startX + width + spacing, "sqpve_choose pvp", "pvp");
CuiHelper.AddUi(player, container);
}
private void CreateImageButton(CuiElementContainer container, float xPos,
string command, string imageKey)
{
string img = imageManager.GetImage(imageKey);
float aspect = configData.ImageSettings.ImageAspectRatio > 0
? configData.ImageSettings.ImageAspectRatio
: 0.56f;
float maxW = configData.UI.ScrollWidth;
float maxH = configData.UI.MaxScrollHeight;
float h = maxW / aspect;
float w = maxW;
if (h > maxH)
{
h = maxH;
w = h * aspect;
}
float yMin = (1f - h) / 2;
float xMin = xPos;
string mainPanelId = $"{UI_SELECTION}_{imageKey}_main";
string imagePanelId = $"{UI_SELECTION}_{imageKey}_img";
string buttonId = $"{UI_SELECTION}_{imageKey}_btn";
container.Add(new CuiPanel
{
RectTransform = {
AnchorMin = $"{xMin} {yMin}",
AnchorMax = $"{xMin + w} {yMin + h}"
},
Image = { Color = "0 0 0 0" }
}, UI_SELECTION, mainPanelId);
if (!string.IsNullOrEmpty(img))
{
container.Add(new CuiElement
{
Parent = mainPanelId,
Name = imagePanelId,
Components =
{
new CuiRawImageComponent {
Png = img,
Color = "1 1 1 1",
FadeIn = 0.5f
},
new CuiRectTransformComponent {
AnchorMin = "0 0",
AnchorMax = "1 1"
}
}
});
}
else
{
container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Image = { Color = imageKey == "pve" ? "0.2 0.6 0.2 0.9" : "0.6 0.2 0.2 0.9" }
}, mainPanelId, imagePanelId);
container.Add(new CuiLabel
{
Text = {
Text = imageKey == "pve" ? "<size=40><b>PVE</b></size>\n<size=20>РЕЖИМ</size>" : "<size=40><b>PVP</b></size>\n<size=20>РЕЖИМ</size>",
FontSize = 32,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1",
Font = "robotocondensed-bold.ttf"
},
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, imagePanelId);
}
container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Button = {
Color = "0 0 0 0",
Command = command
},
Text = { Text = "" }
}, mainPanelId, buttonId);
}
public void UpdateModeIcon(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
var data = dbManager.GetPlayer(player.userID);
if (!data.HasChosen) return;
CuiHelper.DestroyUi(player, UI_MODE_ICON);
string imageKey = data.IsPVE ? "pve_icon" : "pvp_icon";
string img = imageManager.GetImage(imageKey);
var container = new CuiElementContainer();
string offsetMin = configData.UI.ModeIconOffsetMin;
string offsetMax = configData.UI.ModeIconOffsetMax;
if (!string.IsNullOrEmpty(img))
{
container.Add(new CuiElement
{
Name = UI_MODE_ICON,
Parent = "Overlay",
Components =
{
new CuiRawImageComponent
{
Png = img,
Color = "1 1 1 1",
FadeIn = 0.3f
},
new CuiRectTransformComponent
{
AnchorMin = "1 0",
AnchorMax = "1 0",
OffsetMin = offsetMin,
OffsetMax = offsetMax
}
}
});
}
else
{
container.Add(new CuiPanel
{
RectTransform =
{
AnchorMin = "1 0",
AnchorMax = "1 0",
OffsetMin = offsetMin,
OffsetMax = offsetMax
},
Image =
{
Color = data.IsPVE ? "0.2 0.8 0.2 0.8" : "0.8 0.2 0.2 0.8",
FadeIn = 0.3f
}
}, "Overlay", UI_MODE_ICON);
container.Add(new CuiLabel
{
Text =
{
Text = data.IsPVE ? "PVE" : "PVP",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1",
Font = "robotocondensed-bold.ttf"
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "1 1"
}
}, UI_MODE_ICON);
}
CuiHelper.AddUi(player, container);
}
public void DestroyModeIcon(BasePlayer player)
{
if (player != null)
CuiHelper.DestroyUi(player, UI_MODE_ICON);
}
#endregion
#region Commands
[ConsoleCommand("sqpve_choose")]
private void CmdChoose(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!arg.HasArgs(1)) return;
string choice = arg.GetString(0).ToLower();
bool isPVE = choice == "pve";
CuiHelper.DestroyUi(player, UI_SELECTION);
SetMode(player, isPVE);
}
[ChatCommand("mode")]
private void CmdMode(BasePlayer player, string command, string[] args)
{
if (player == null) return;
var playerData = dbManager.GetPlayer(player.userID);
string mode;
if (!playerData.HasChosen)
mode = "<color=#FFD700>❓ НЕ ВЫБРАН</color>";
else if (playerData.IsPVE)
mode = configData.MSG.ModePVE;
else
mode = configData.MSG.ModePVP;
string status = playerData.HasChosen ? "Выбран" : "Не выбран";
SendReply(player, $"{mode} <color=#C9C9C9>({status})</color>");
if (!playerData.HasChosen)
timer.Once(0.5f, () => ShowSelectionUI(player));
}
[ChatCommand("setmode")]
private void CmdSetMode(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin || args.Length < 2)
{
SendReply(player, "Usage: /setmode <name/steamid> <pve/pvp>");
return;
}
BasePlayer target = ulong.TryParse(args[0], out var sid)
? BasePlayer.FindByID(sid)
: BasePlayer.Find(args[0]);
if (target == null)
{
SendReply(player, "Player not found");
return;
}
bool pve = args[1].ToLower() == "pve";
SetMode(target, pve, true);
SendReply(player, $"Set {target.displayName} to {(pve ? "PVE" : "PVP")}");
}
[ChatCommand("warnings")]
private void CmdWarnings(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin)
{
var playerData = dbManager.GetPlayer(player.userID);
SendReply(player, $"Warnings: {playerData.Warnings}/{configData.PVE.MaxWarnings}");
return;
}
if (args.Length == 0)
{
SendReply(player, "Usage: /warnings <name/steamid>");
return;
}
BasePlayer target = ulong.TryParse(args[0], out var sid)
? BasePlayer.FindByID(sid)
: BasePlayer.Find(args[0]);
if (target == null)
{
SendReply(player, "Player not found");
return;
}
var targetData = dbManager.GetPlayer(target.userID);
string mode = targetData.HasChosen ? (targetData.IsPVE ? "PVE" : "PVP") : "NOT CHOSEN";
SendReply(player, $"{target.displayName}: {mode}, Warnings: {targetData.Warnings}/{configData.PVE.MaxWarnings}");
}
[ChatCommand("resetwarn")]
private void CmdResetWarn(BasePlayer player, string command, string[] args)
{
if (!player.IsAdmin) return;
BasePlayer target = args.Length == 0
? player
: (ulong.TryParse(args[0], out var sid) ? BasePlayer.FindByID(sid) : BasePlayer.Find(args[0]));
if (target == null)
{
SendReply(player, "Player not found");
return;
}
var targetData = dbManager.GetPlayer(target.userID);
targetData.Warnings = 0;
dbManager.MarkDirty(target.userID);
dbManager.SavePlayer(target.userID, true);
string msg = target == player ? "Your warnings reset" : $"{target.displayName}'s warnings reset";
SendRe
// ... [Code preview truncated due to length. Full source in attached file] ...