C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("RSurvivalStore", "RustInnovate", "2.9.7")]
[Description(
"Клиент RSurvivalStore с пользовательским интерфейсом на основе изображений, ScrollView и WipeBlock."
)]
public class RSurvivalStore : RustPlugin
{
private Configuration _config;
private Dictionary<string, string> _imageCache = new Dictionary<string, string>();
private Dictionary<string, string> _slotStates = new Dictionary<string, string>();
private Dictionary<string, JArray> _playerItems = new Dictionary<string, JArray>();
private Dictionary<ulong, string> _activeAddonParent = new Dictionary<ulong, string>();
private bool _isRegistered = false;
private string _hudIconPngId = "";
private string _blueprintIconPngId = "";
#region Configuration
private class Configuration
{
[JsonProperty("Настройка магазина SurvivalShop")]
public ShopConfig Settings { get; set; } = new ShopConfig();
[JsonProperty("Настройка блокировки товаров после вайпа")]
public List<WipeBlockItem> WipeBlocks { get; set; } = new List<WipeBlockItem>();
[JsonProperty("Настройки интерфейса")]
public UISettingsConfig UI { get; set; } = new UISettingsConfig();
}
private class ShopConfig
{
[JsonProperty("SiteID")]
public string SiteID { get; set; } = "";
[JsonProperty("SiteKey")]
public string SiteKey { get; set; } = "";
[JsonProperty("Ссылка на магазин (для уведомлений)")]
public string ShopURL { get; set; } = "https://myshop.survivalshop.org";
[JsonProperty("Запретить открывать корзину магазина в зоне чужого шкафа?")]
public bool BlockInEnemyTC { get; set; } = false;
[JsonProperty("Запретить открывать корзину магазина во время рейдблока?")]
public bool BlockInRaid { get; set; } = true;
[JsonProperty("Запретить открывать корзину магазина во время комбатблока?")]
public bool BlockInCombat { get; set; } = true;
}
private class WipeBlockItem
{
[JsonProperty("Имя предмета (опционально)")]
public string Name { get; set; } = "";
[JsonProperty("ID товара (ОБЯЗАТЕЛЬНО)")]
public string ItemId { get; set; } = "";
[JsonProperty("Время блокировки (в часах) (ОБЯЗАТЕЛЬНО)")]
public float BlockHours { get; set; } = 24f;
}
private class UISettingsConfig
{
[JsonProperty("1.Общий фон меню в автономном режиме (цвет)")]
public string MainBgColor { get; set; } = "0 0 0 0.5";
#region Panels (Header & Content)
[JsonProperty("2.Смещение панели при встраивании в меню (RServerMenu)")]
public float EmbedOffsetX { get; set; } = 103.0f;
[JsonProperty("3.Смещение панели в автономном режиме (/store)")]
public float StandaloneOffsetX { get; set; } = 0.0f;
[JsonProperty("4.Верхняя панель (Заголовок)")]
public HeaderPanelConfig HeaderPanel { get; set; } = new HeaderPanelConfig();
[JsonProperty("5.Кнопка 'Забрать всё' в шапке")]
public TakeAllButtonConfig TakeAllButton { get; set; } = new TakeAllButtonConfig();
[JsonProperty("6.Нижняя панель (Контент)")]
public ContentPanelConfig ContentPanel { get; set; } = new ContentPanelConfig();
[JsonProperty("7.Сетка корзины (Карточки товаров)")]
public CartGridConfig CartGrid { get; set; } = new CartGridConfig();
[JsonProperty("8.Иконка корзины на экране (HUD)")]
public CartHudConfig CartHud { get; set; } = new CartHudConfig();
#endregion
}
private class HeaderPanelConfig
{
[JsonProperty("Высота")]
public float Height { get; set; } = 50.0f;
[JsonProperty("Ширина")]
public float Width { get; set; } = 699.0f;
[JsonProperty("Вверх/вниз")]
public float OffsetY { get; set; } = 226.5f;
[JsonProperty("Цвет фона")]
public string BgColor { get; set; } = "0.45 0.46 0.46 0.78";
[JsonProperty("Цвет текста заголовка")]
public string TitleColor { get; set; } = "0.25 0.69 1 1";
[JsonProperty("Размер шрифта заголовка")]
public int TitleSize { get; set; } = 20;
[JsonProperty("Отступ заголовка слева")]
public float TitlePaddingLeft { get; set; } = 15.0f;
[JsonProperty("Отступ заголовка справа")]
public float TitlePaddingRight { get; set; } = 170.0f;
}
private class TakeAllButtonConfig
{
[JsonProperty("Высота")]
public float Height { get; set; } = 30.0f;
[JsonProperty("Ширина")]
public float Width { get; set; } = 140.0f;
[JsonProperty("Вверх/вниз")]
public float OffsetY { get; set; } = 0.0f;
[JsonProperty("Влево/вправо")]
public float OffsetX { get; set; } = -10.0f;
[JsonProperty("Цвет фона")]
public string BgColor { get; set; } = "0.76 0.43 0.20 0.95";
[JsonProperty("Цвет текста")]
public string TextColor { get; set; } = "1 1 1 1";
[JsonProperty("Размер шрифта")]
public int FontSize { get; set; } = 13;
}
private class ContentPanelConfig
{
[JsonProperty("Высота")]
public float Height { get; set; } = 444.0f;
[JsonProperty("Ширина")]
public float Width { get; set; } = 699.0f;
[JsonProperty("Вверх/вниз")]
public float OffsetY { get; set; } = -30.5f;
[JsonProperty("Цвет фона")]
public string BgColor { get; set; } = "0.45 0.46 0.46 0.78";
}
private class CartGridConfig
{
[JsonProperty("Сетка - Внутренний отступ")]
public float Padding { get; set; } = 10.0f;
[JsonProperty("Количество колонок")]
public int Columns { get; set; } = 5;
[JsonProperty("Карточка - Ширина")]
public float CardWidth { get; set; } = 126.0f;
[JsonProperty("Карточка - Высота")]
public float CardHeight { get; set; } = 135.0f;
[JsonProperty("Карточка - Отступ по горизонтали (X)")]
public float GapX { get; set; } = 8.0f;
[JsonProperty("Карточка - Отступ по вертикали (Y)")]
public float GapY { get; set; } = 8.0f;
[JsonProperty("Карточка (Обычная) - Цвет фона")]
public string CardBgColor { get; set; } = "0.18 0.20 0.20 0.90";
[JsonProperty("Карточка (Чертеж) - URL изображения чертежа")]
public string BlueprintIconUrl { get; set; } =
"https://pic.survivalhost.org/images/2026/08/25/blueprint.png";
[JsonProperty("Карточка (Успешно взято) - Цвет фона")]
public string SuccessBgColor { get; set; } = "0.15 0.35 0.15 0.92";
[JsonProperty("Карточка (Заблокировано) - Цвет фона")]
public string BlockedBgColor { get; set; } = "0.35 0.15 0.15 0.92";
[JsonProperty("Карточка - Цвет текста названия")]
public string TitleColor { get; set; } = "1 1 1 1";
[JsonProperty("Карточка - Размер шрифта названия")]
public int TitleFontSize { get; set; } = 11;
[JsonProperty("Карточка - Цвет текста количества")]
public string CountColor { get; set; } = "0.63 0.89 0.18 1.0";
[JsonProperty("Карточка - Размер шрифта количества")]
public int CountFontSize { get; set; } = 12;
[JsonProperty("Иконка предмета - Размер")]
public float ImageSize { get; set; } = 80.0f;
[JsonProperty("Иконка предмета - Смещение по Y внутри карточки")]
public float ImageOffsetY { get; set; } = 0.0f;
}
private class CartHudConfig
{
[JsonProperty("Включить иконку HUD")]
public bool Enabled { get; set; } = true;
[JsonProperty("URL изображения")]
public string IconUrl { get; set; } =
"https://pic.survivalhost.org/images/2026/08/25/store.png";
[JsonProperty("Высота")]
public float Height { get; set; } = 40.0f;
[JsonProperty("Ширина")]
public float Width { get; set; } = 40.0f;
[JsonProperty("Вверх/вниз")]
public float OffsetY { get; set; } = 340.0f;
[JsonProperty("Влево/вправо")]
public float OffsetX { get; set; } = -615.0f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null)
throw new Exception();
}
catch
{
Config.WriteObject(_config, false, $"{Config.Filename}.Error.txt");
PrintError(
"The configuration file contains an error and has been replaced with a default config."
);
LoadDefaultConfig();
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
_config = new Configuration();
_config.WipeBlocks.Add(
new WipeBlockItem
{
Name = "",
ItemId = "",
BlockHours = 24f,
}
);
}
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Hooks & Setup
private void Unload()
{
foreach (var player in BasePlayer.activePlayerList)
{
DestroyPlayerUi(player);
DestroyHUD(player);
}
}
private void OnServerInitialized(bool serverInitialized = false)
{
Puts($"Загрузка плагина RSurvivalStore v{Version}");
Puts("==================================================");
Puts(" Plugin by RustInnovate ");
Puts("--------------------------------------------------");
Puts(" VK: vk.com/rustinnovate ");
Puts(" Discord: discord.gg/e244z6aGs7 ");
Puts(" Telegram: t.me/RobinPlay ");
Puts("==================================================");
// CHANGE: Загрузка иконки HUD и изображения чертежа по URL
DownloadHudIcon();
DownloadBlueprintIcon();
if (
string.IsNullOrEmpty(_config.Settings.SiteID)
|| string.IsNullOrEmpty(_config.Settings.SiteKey)
)
{
PrintWarning(
"[RUS] Плагин не настроен.Пожалуйста, используйте 'rsurvivalstore.setup <SiteID> <SiteKey>' в RCON.\n"
+ "[EN] Plugin is not configured.Please use 'rsurvivalstore.setup <SiteID> <SiteKey>' в RCON."
);
return;
}
RegisterServer();
timer.Every(60f, RegisterServer); // Send heartbeat every 60 seconds
foreach (var player in BasePlayer.activePlayerList)
{
DrawHUD(player);
}
}
private void OnPlayerSleepEnded(BasePlayer player)
{
DrawHUD(player);
}
private void OnPluginLoaded(Plugin plugin)
{
if (plugin.Name == "RServerMenu")
{
foreach (var player in BasePlayer.activePlayerList)
DestroyHUD(player);
}
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin.Name == "RServerMenu")
{
timer.Once(
0.1f,
() =>
{
foreach (var player in BasePlayer.activePlayerList)
DrawHUD(player);
}
);
}
}
// CHANGE: Метод загрузки и кэширования иконки корзины HUD по URL
private void DownloadHudIcon()
{
var hudCfg = _config?.UI?.CartHud;
if (hudCfg == null || !hudCfg.Enabled || string.IsNullOrEmpty(hudCfg.IconUrl))
return;
string cacheDir = Path.Combine(
Interface.Oxide.DataDirectory,
"RSystem",
"RSurvivalStore",
"ImageCache"
);
if (!Directory.Exists(cacheDir))
Directory.CreateDirectory(cacheDir);
string fileName = "hud_cart_" + hudCfg.IconUrl.GetHashCode().ToString() + ".png";
string localPath = Path.Combine(cacheDir, fileName);
if (File.Exists(localPath))
{
try
{
byte[] data = File.ReadAllBytes(localPath);
uint id = FileStorage.server.Store(
data,
FileStorage.Type.png,
CommunityEntity.ServerInstance.net.ID
);
_hudIconPngId = id.ToString();
return;
}
catch (Exception ex)
{
PrintError(
$"[RSurvivalStore] Ошибка загрузки закэшированной иконки HUD: {ex.Message}"
);
}
}
ServerMgr.Instance.StartCoroutine(
DownloadImageCoroutine(
hudCfg.IconUrl,
localPath,
"hud_cart",
() =>
{
if (_imageCache.TryGetValue("hud_cart", out string pngId))
{
_hudIconPngId = pngId;
foreach (var p in BasePlayer.activePlayerList)
{
DrawHUD(p);
}
}
}
)
);
}
// CHANGE: Метод загрузки и кэширования изображения чертежа по URL
private void DownloadBlueprintIcon()
{
var gridCfg = _config?.UI?.CartGrid;
if (gridCfg == null || string.IsNullOrEmpty(gridCfg.BlueprintIconUrl))
return;
string cacheDir = Path.Combine(
Interface.Oxide.DataDirectory,
"RSystem",
"RSurvivalStore",
"ImageCache"
);
if (!Directory.Exists(cacheDir))
Directory.CreateDirectory(cacheDir);
string fileName = "bp_" + gridCfg.BlueprintIconUrl.GetHashCode().ToString() + ".png";
string localPath = Path.Combine(cacheDir, fileName);
if (File.Exists(localPath))
{
try
{
byte[] data = File.ReadAllBytes(localPath);
uint id = FileStorage.server.Store(
data,
FileStorage.Type.png,
CommunityEntity.ServerInstance.net.ID
);
_blueprintIconPngId = id.ToString();
return;
}
catch (Exception ex)
{
PrintError(
$"[RSurvivalStore] Ошибка загрузки закэшированной иконки чертежа: {ex.Message}"
);
}
}
ServerMgr.Instance.StartCoroutine(
DownloadImageCoroutine(
gridCfg.BlueprintIconUrl,
localPath,
"blueprint_icon",
() =>
{
if (_imageCache.TryGetValue("blueprint_icon", out string pngId))
{
_blueprintIconPngId = pngId;
}
}
)
);
}
[ConsoleCommand("rsurvivalstore.setup")]
private void CmdSetup(ConsoleSystem.Arg arg)
{
if (arg.Connection != null)
return;
if (!arg.HasArgs(2))
{
Puts("Usage: rsurvivalstore.setup <SiteID> <SiteKey>");
return;
}
_config.Settings.SiteID = arg.GetString(0);
_config.Settings.SiteKey = arg.GetString(1);
SaveConfig();
Puts("RSurvivalStore setup complete! Registering server...");
RegisterServer();
}
#endregion
#region API Interaction
private void SendApiRequest(
string endpoint,
Dictionary<string, object> data,
Action<int, string> callback
)
{
if (
string.IsNullOrEmpty(_config.Settings.SiteID)
|| string.IsNullOrEmpty(_config.Settings.SiteKey)
)
return;
string dataJson = JsonConvert.SerializeObject(data, Formatting.None);
string salt = "salt";
string signature;
using (var sha256 = new SHA256Managed())
{
byte[] bytes = sha256.ComputeHash(
Encoding.UTF8.GetBytes(dataJson + _config.Settings.SiteKey + salt)
);
StringBuilder hex = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
hex.AppendFormat("{0:x2}", b);
signature = $"{hex}:{_config.Settings.SiteID}:{salt}";
}
data["__sign"] = signature;
string finalJson = JsonConvert.SerializeObject(data, Formatting.None);
webrequest.Enqueue(
$"https://api.survivalshop.org/{endpoint}",
finalJson,
(code, response) =>
{
callback?.Invoke(code, response);
},
this,
RequestMethod.POST,
new Dictionary<string, string> { { "Content-Type", "application/json" } }
);
}
private void RegisterServer()
{
var data = new Dictionary<string, object>
{
["siteId"] = _config.Settings.SiteID,
["serverData"] = new Dictionary<string, object>
{
["game"] = "RUST",
["name"] = ConVar.Server.hostname,
["port"] = ConVar.Server.queryport,
["online"] = BasePlayer.activePlayerList.Count,
["maxplayers"] = ConVar.Server.maxplayers,
["pluginName"] = Name,
["pluginVersion"] = Version.ToString(),
},
};
SendApiRequest(
"servers.register2",
data,
(code, response) =>
{
if (code == 200)
{
if (!_isRegistered)
{
Puts("Successfully registered on SurvivalShop.");
_isRegistered = true;
}
}
else
{
if (_isRegistered)
Puts($"Lost connection to SurvivalShop or failed to heartbeat: {code}");
_isRegistered = false;
}
}
);
}
#endregion
#region Wipe Block Logic
private bool IsItemBlocked(string itemId, out float remainingSeconds)
{
remainingSeconds = 0f;
foreach (var block in _config.WipeBlocks)
{
if (block.ItemId == itemId)
{
float blockSeconds = block.BlockHours * 3600f;
float secondsSinceWipe = (float)
(DateTime.Now - SaveRestore.SaveCreatedTime).TotalSeconds;
if (secondsSinceWipe < blockSeconds)
{
remainingSeconds = blockSeconds - secondsSinceWipe;
return true;
}
break; // Item found, but time elapsed
}
}
return false;
}
private string FormatTime(float seconds)
{
TimeSpan t = TimeSpan.FromSeconds(seconds);
return string.Format(
"{0:D2}ч {1:D2}м {2:D2}с",
t.Hours + (t.Days * 24),
t.Minutes,
t.Seconds
);
}
#endregion
#region UI & Commands
private const string UIName = "RSurvivalStoreUI";
private const string LayerBg = "RSurvivalStore.Bg";
private const string LayerStatic = "RSurvivalStore.Static";
private const string LayerHeader = "RSurvivalStore.Header";
private const string LayerContent = "RSurvivalStore.Content";
private const string HUDName = "RSurvivalStoreHUD";
private void DestroyPlayerUi(BasePlayer player)
{
if (player == null)
return;
_activeAddonParent.Remove(player.userID);
CuiHelper.DestroyUi(player, UIName);
CuiHelper.DestroyUi(player, LayerBg);
CuiHelper.DestroyUi(player, LayerStatic);
CuiHelper.DestroyUi(player, LayerHeader);
CuiHelper.DestroyUi(player, LayerContent);
CuiHelper.DestroyUi(player, UIName + "_TakeAll");
CuiHelper.DestroyUi(player, UIName + "_Content");
CuiHelper.DestroyUi(player, UIName + "_Empty");
}
private void DrawHUD(BasePlayer player)
{
if (player == null || !player.IsConnected)
return;
var rmenu = plugins.Find("RServerMenu");
if (rmenu != null && rmenu.IsLoaded)
return;
var cartCfg = _config?.UI?.CartHud;
if (cartCfg == null || !cartCfg.Enabled)
return;
CuiHelper.DestroyUi(player, HUDName);
CuiElementContainer container = new CuiElementContainer();
float halfW = cartCfg.Width / 2f;
float halfH = cartCfg.Height / 2f;
float xMin = cartCfg.OffsetX - halfW;
float xMax = cartCfg.OffsetX + halfW;
float yMin = cartCfg.OffsetY - halfH;
float yMax = cartCfg.OffsetY + halfH;
// CHANGE: Если изображение по ссылке загружено - используем CuiRawImageComponent, иначе fallback кнопку
if (!string.IsNullOrEmpty(_hudIconPngId))
{
container.Add(
new CuiElement
{
Parent = "Hud",
Name = HUDName,
Components =
{
new CuiRawImageComponent { Png = _hudIconPngId },
new CuiRectTransformComponent
{
AnchorMin = "0.5 0.5",
AnchorMax = "0.5 0.5",
OffsetMin =
$"{xMin.ToString(System.Globalization.CultureInfo.InvariantCulture)} {yMin.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
OffsetMax =
$"{xMax.ToString(System.Globalization.CultureInfo.InvariantCulture)} {yMax.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
},
},
}
);
}
else
{
container.Add(
new CuiPanel
{
Image = { Color = "0.76 0.43 0.20 0.95" },
RectTransform =
{
AnchorMin = "0.5 0.5",
AnchorMax = "0.5 0.5",
OffsetMin =
$"{xMin.ToString(System.Globalization.CultureInfo.InvariantCulture)} {yMin.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
OffsetMax =
$"{xMax.ToString(System.Globalization.CultureInfo.InvariantCulture)} {yMax.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
},
},
"Hud",
HUDName
);
container.Add(
new CuiLabel
{
Text =
{
Text = "🛒",
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1",
},
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
},
HUDName
);
}
container.Add(
new CuiButton
{
Button = { Command = "rsurvivalstore.open", Color = "0 0 0 0" },
Text = { Text = "" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
},
HUDName
);
CuiHelper.AddUi(player, container);
}
private void DestroyHUD(BasePlayer player)
{
if (player == null)
return;
CuiHelper.DestroyUi(player, HUDName);
}
[ChatCommand("store")]
private void CmdShopChat(BasePlayer player, string command, string[] args)
{
if (_config.Settings.BlockInEnemyTC && player.IsBuildingBlocked())
{
ShowGameTip(player, Lang("BlockedInTC", player.UserIDString), 2);
return;
}
if (_config.Settings.BlockInRaid && IsPlayerRaidBlocked(player))
{
ShowGameTip(player, Lang("BlockedInRaid", player.UserIDString), 2);
return;
}
if (_config.Settings.BlockInCombat && IsPlayerCombatBlocked(player))
{
ShowGameTip(player, Lang("BlockedInCombat", player.UserIDString), 2);
return;
}
var rmenu = plugins.Find("RServerMenu");
if (rmenu != null && rmenu.IsLoaded)
{
rmenu.Call("OpenMenuCategory", player, "cart");
return;
}
_activeAddonParent.Remove(player.userID);
OpenShopUI(player);
}
[HookMethod("DrawEmbedUI_Fixed")]
public void DrawEmbedUI_Fixed(BasePlayer player, string parentPanel = null)
{
OpenAddonUI(player, parentPanel);
}
[HookMethod("DrawEmbedUI")]
public void DrawEmbedUI(BasePlayer player, string parentPanel = null)
{
OpenAddonUI(player, parentPanel);
}
[HookMethod("OpenAddonUI")]
public void OpenAddonUI(BasePlayer player, string parentName = null)
{
if (player == null || !player.IsConnected)
return;
if (_config.Settings.BlockInEnemyTC && player.IsBuildingBlocked())
{
ShowGameTip(player, Lang("BlockedInTC", player.UserIDString), 2);
return;
}
if (_config.Settings.BlockInRaid && IsPlayerRaidBlocked(player))
{
ShowGameTip(player, Lang("BlockedInRaid", player.UserIDString), 2);
return;
}
if (_config.Settings.BlockInCombat && IsPlayerCombatBlocked(player))
{
ShowGameTip(player, Lang("BlockedInCombat", player.UserIDString), 2);
return;
}
_activeAddonParent[player.userID] = !string.IsNullOrEmpty(parentName)
? parentName
: "Overlay";
OpenShopUI(player);
}
// CHANGE: Универсальный хук закрытия для RServerMenu
[HookMethod("CloseEmbedUI")]
public void CloseEmbedUI(BasePlayer player)
{
if (player == null)
return;
DestroyPlayerUi(player);
}
[HookMethod("CloseAddonUI")]
public void CloseAddonUI(BasePlayer player)
{
if (player == null)
return;
DestroyPlayerUi(player);
}
private bool IsPlayerRaidBlocked(BasePlayer player)
{
var raidBlock = plugins.Find("RaidBlock");
if (raidBlock != null && raidBlock.IsLoaded)
{
object result = raidBlock.Call("IsBlocked", player);
if (result is bool && (bool)result)
return true;
result = raidBlock.Call("IsRaidBlocked", player);
if (result is bool && (bool)result)
return true;
}
var noEscape = plugins.Find("NoEscape");
if (noEscape != null && noEscape.IsLoaded)
{
object result = noEscape.Call("IsRaidBlocked", player);
if (result is bool && (bool)result)
return true;
}
return false;
}
private bool IsPlayerCombatBlocked(BasePlayer player)
{
var noEscape = plugins.Find("NoEscape");
if (noEscape != null && noEscape.IsLoaded)
{
object result = noEscape.Call("IsCombatBlocked", player);
if (result is bool && (bool)result)
return true;
}
var combatBlock = plugins.Find("CombatBlock");
if (combatBlock != null && combatBlock.IsLoaded)
{
object result = combatBlock.Call("IsCombatBlocked", player);
if (result is bool && (bool)result)
return true;
result = combatBlock.Call("IsCombatBlock", player);
if (result is bool && (bool)result)
return true;
}
return false;
}
[ConsoleCommand("rsurvivalstore.open")]
private void CmdOpenUI(ConsoleSystem.Arg arg)
{
if (arg.Player() != null)
{
CmdShopChat(arg.Player(), "", null);
}
}
private void ShowGameTip(BasePlayer player, string message, int type = 2)
{
if (player?.IsConnected != true)
return;
player.SendConsoleCommand("gametip.showtoast", type, message);
}
[ConsoleCommand("rsurvivalstore.close")]
private void CmdCloseUI(ConsoleSystem.Arg arg)
{
if (arg.Player() != null)
{
DestroyPlayerUi(arg.Player());
}
}
private void OpenShopUI(BasePlayer player)
{
if (player == null || !player.IsConnected)
return;
if (!_isRegistered)
{
Player.Message(player, Lang("NotConfigured", player.UserIDString));
return;
}
bool isEmbedded = _activeAddonParent.ContainsKey(player.userID);
// CHANGE: Мгновенная отрисовка каркаса и кэшированных предметов (устраняет мерцание и задержку при переключении категорий)
JArray cachedItems = null;
if (_playerItems.TryGetValue(player.UserIDString, out cachedItems))
{
DrawUI(player, cachedItems, false);
}
else
{
DrawUI(player, null, false);
}
var data = new Dictionary<string, object>
{
["siteId"] = _config.Settings.SiteID,
["clientSid"] = player.UserIDString,
["criteria"] = new Dictionary<string, object> { ["_start"] = 0, ["_limit"] = 100 },
};
SendApiRequest(
"client.getInventory",
data,
(code, response) =>
{
if (player == null || !player.IsConnected)
return;
// CHANGE: Если меню было закрыто или переключена вкладка во время ответа API - прерываем отрисовку
if (isEmbedded && !_activeAddonParent.ContainsKey(player.userID))
return;
if (code != 200 || string.IsNullOrEmpty(response))
{
DrawUI(player, null, true);
return;
}
try
{
JObject json = JObject.Parse(response);
string errorMsg = json["error_msg"]?.ToString();
if (errorMsg == null && json["error_code"] != null)
errorMsg = "Unknown Error";
if (!string.IsNullOrEmpty(errorMsg))
{
if (
errorMsg.IndexOf(
"Mesh not exist",
StringComparison.OrdinalIgnoreCase
) >= 0
|| errorMsg.IndexOf(
"Клиент не найден",
StringComparison.OrdinalIgnoreCase
) >= 0
)
{
DrawUI(player, null, true);
return;
}
Player.Message(player, Lang("ApiError", player.UserIDString));
PrintError(
$"[RSurvivalStore] API Error for {player.UserIDString}: {errorMsg}"
);
return;
}
JArray items = (JArray)json["response"]?["result"];
CacheItemImages(
player,
items,
() =>
{
if (player == null || !player.IsConnected)
return;
if (isEmbedded && !_activeAddonParent.ContainsKey(player.userID))
return;
// CHANGE: Если список предметов идентичен уже отрисованному кэшу, пропускаем перерисовку для исключения мерцания
if (cachedItems != null && JToken.DeepEquals(cachedItems, items))
return;
DrawUI(player, items, false);
}
);
}
catch (Exception ex)
{
PrintError($"Failed to parse shop data: {ex.Message}");
}
}
);
}
private void CacheItemImages(BasePlayer player, JArray items, Action onComplete)
{
if (items == null || items.Count == 0)
{
onComplete?.Invoke();
return;
}
string cacheDir = Path.Combine(
Interface.Oxide.DataDirectory,
"RSystem",
"RSurvivalStore",
"ImageCache"
);
if (!Directory.Exists(cacheDir))
Directory.CreateDirectory(cacheDir);
Dictionary<string, string> pendingDownloads = new Dictionary<string, string>();
foreach (JObject item in items)
{
string logoLink = (string)item["logoLink"];
if (string.IsNullOrEmpty(logoLink))
continue;
if (_imageCache.ContainsKey(logoLink))
continue;
string imageUrl = logoLink;
if (logoLink.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
imageUrl = "https://" + logoLink.Substring(7);
else if (!logoLink.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
imageUrl = "https://survivalshop.org" + logoLink;
string fileName = logoLink.GetHashCode().ToString() + ".png";
string localPath = Path.Combine(cacheDir, fileName);
if (File.Exists(localPath))
{
try
{
byte[] data = File.ReadAllBytes(localPath);
uint id = FileStorage.server.Store(
data,
FileStorage.Type.png,
CommunityEntity.ServerInstance.net.ID
);
_imageCache[logoLink] = id.ToString();
}
catch (Exception ex)
{
PrintError($"Failed to load cached image {fileName}: {ex.Message}");
if (!pendingDownloads.ContainsKey(logoLink))
pendingDownloads.Add(logoLink, imageUrl);
}
}
else
{
if (!pendingDownloads.ContainsKey(logoLink))
pendingDownloads.Add(logoLink, imageUrl);
}
}
if (pendingDownloads.Count == 0)
{
onComplete?.Invoke();
return;
}
int total = pendingDownloads.Count;
int completed = 0;
Action itemCompleted = () =>
{
completed++;
if (completed >= total)
onComplete?.Invoke();
};
foreach (var kvp in pendingDownloads)
{
string logoLink = kvp.Key;
string imageUrl = kvp.Value;
string fileName = logoLink.GetHashCode().ToString() + ".png";
string localPath = Path.Combine(cacheDir, fileName);
ServerMgr.Instance.StartCoroutine(
DownloadImageCoroutine(imageUrl, localPath, logoLink, itemCompleted)
);
}
}
private System.Collections.IEnumerator DownloadImageCoroutine(
string url,
string localPath,
string logoLink,
Action onComplete
)
{
using (var www = UnityEngine.Networking.UnityWebRequest.Get(url))
{
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError)
{
PrintError($"Failed to download image {url}: {www.error}");
}
else
{
byte[] bytes = www.downloadHandler.data;
if (bytes != null && bytes.Length > 0)
{
try
{
System.IO.File.WriteAllBytes(localPath, bytes);
uint id = FileStorage.server.Store(
bytes,
FileStorage.Type.png,
CommunityEntity.ServerInstance.net.ID
);
_imageCache[logoLink] = id.ToString();
}
catch (Exception ex)
{
PrintError($"Failed to save downloaded image {logoLink}: {ex.Message}");
}
}
}
onComplete?.Invoke();
}
}
private bool CheckIsBlueprint(JObject item)
{
if (item == null)
return false;
string title = (string)item["title"] ?? "";
if (
title.IndexOf("чертеж", StringComparison.OrdinalIgnoreCase) >= 0
|| title.IndexOf("чертёж", StringComparison.OrdinalIgnoreCase) >= 0
|| title.IndexOf("blueprint", StringComparison.OrdinalIgnoreCase) >= 0
|| title.IndexOf("[ч]", StringComparison.OrdinalIgnoreCase) >= 0
|| title.IndexOf("[b]", StringComparison.OrdinalIgnoreCase) >= 0
|| title.IndexOf("[bp]", StringComparison.OrdinalIgnoreCase) >= 0
)
{
return true;
}
if (item["is_blueprint"] != null && item["is_blueprint"].Value<bool>())
return true;
if (item["isBlueprint"] != null && item["isBlueprint"].Value<bool>())
return true;
JArray equips = (JArray)item["content"]?["equips"];
if (equips != null)
{
foreach (JToken t in equips)
{
if (t is JObject equip)
{
if (equip["is_blueprint"] != null && equip["is_blueprint"].Value<bool>())
return true;
if (equip["isBlueprint"] != null && equip["isBlueprint"].Value<bool>())
return true;
if (equip["info"] is JObject info)
{
if (info["is_blueprint"] != null && info["is_blueprint"].Value<bool>())
return true;
if (info["isBlueprint"] != null && info["isBlueprint"].Value<bool>())
return true;
}
}
}
}
return false;
}
private void DrawSingleItemSlot(BasePlayer player, string targetSlotId, bool remove)
{
if (!_playerItems.TryGetValue(player.UserIDString, out JArray items))
return;
int index = -1;
for (int i = 0; i < items.Count; i++)
{
if ((string)items[i]["_id"] == targetSlotId)
{
index = i;
break;
}
}
if (index == -1)
return;
string contentName = UIName + "_Content";
string itemPanel = $"{UIName}_Item_{index}";
CuiHelper.DestroyUi(player, itemPanel);
if (remove)
{
// CHANGE: Проверка, остались ли еще предметы в корзине; если нет - отрисовать надпись "Ваша корзина пуста."
int validRemaining = 0;
foreach (JToken t in items)
{
if (t is JObject obj && (int)(obj["count"] ?? 0) > 0)
validRemaining++;
}
if (validRemaining == 0)
{
CuiHelper.DestroyUi(player, UIName + "_TakeAll");
CuiHelper.DestroyUi(player, contentName);
CuiHelper.DestroyUi(player, UIName + "_Empty");
var emptyContainer = new CuiElementContainer();
string emptyPanel = UIName + "_Empty";
emptyContainer.Add(
new CuiPanel
{
Image = { Color = "0 0 0 0" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
},
LayerContent,
emptyPanel
);
emptyContainer.Add(
new CuiLabel
{
Text =
{
Text = Lang("Empty", player.UserIDString),
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = "0.8 0.8 0.8 1",
Font = "robotocondensed-bold.ttf",
},
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
},
emptyPanel
);
CuiHelper.AddUi(player, emptyContainer);
}
return;
}
var gridCfg = _config.UI.CartGrid;
int columns = gridCfg.Columns > 0 ? gridCfg.Columns : 5;
float totalGridWidth = (columns * gridCfg.CardWidth) + ((columns - 1) * gridCfg.GapX);
float startX = -(totalGridWidth / 2f) + (gridCfg.CardWidth / 2f);
float startY = -(gridCfg.CardHeight / 2f) - gridCfg.Padding;
JObject item = (JObject)items[index];
string slotId = (string)item["_id"];
string itemId = (string)item["meta"]?["_item"] ?? slotId;
string title = (string)item["title"];
int count = (int)item["count"];
string logoLink = (string)item["logoLink"];
bool isBlueprint = CheckIsBlueprint(item);
bool hasState = _slotStates.TryGetValue(slotId, out string slotState);
bool isNoSpace = hasState && slotState.StartsWith("nospace");
bool isBlocked = IsItemBlocked(itemId, out float remaining);
string cardBg = gridCfg.CardBgColor;
if (hasState && slotState == "success")
cardBg = gridCfg.SuccessBgColor;
else if (isBlocked)
cardBg = gridCfg.BlockedBgColor;
int row = index / columns;
int col = index % columns;
float currentX = startX + (col * (gridCfg.CardWidth + gridCfg.GapX));
float currentY = startY - (row * (gridCfg.CardHeight + gridCfg.GapY));
float halfW = gridCfg.CardWidth / 2f;
float halfH = gridCfg.CardHeight / 2f;
float minX = currentX - halfW;
float maxX = currentX + halfW;
float minY = currentY - halfH;
float maxY = currentY + halfH;
CuiElementContainer container = new CuiElementContainer();
// Card Background (Pure Vector Panel)
container.Add(
new CuiPanel
{
Image = { Color = cardBg },
RectTrans
// ... [Code preview truncated due to length. Full source in attached file] ...