C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
// SkinDrop.cs — SKINDROP с обратным отсчётом до следующего розыгрыша
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using UnityEngine.Networking;
using System.IO;
namespace Oxide.Plugins {
[Info("SkinDrop", "Sempai + DropRust", "1.1.0")]
class SkinDrop : RustPlugin {
// --------------------------- Ссылки и данные ---------------------------
[PluginReference]
Plugin TPEconomic, TPMenuSystem;
private List<Drop> DB = new List<Drop>();
private Dictionary<ulong, string> UrlTrade = new Dictionary<ulong, string>();
private Dictionary<ulong, GiveSkin> GivsDrop = new Dictionary<ulong, GiveSkin>();
// Таймеры обратного отсчёта только для тех игроков, у кого открыт SKINDROP
private readonly Dictionary<ulong, Timer> _countdownTimers = new Dictionary<ulong, Timer>();
// Время следующего автодропа (UTC)
private DateTime _nextDrawAtUtc = DateTime.MinValue;
private Timer _autoDrawTimer;
private Timer _warningTimer;
// --------------------------- Модели ---------------------------
public class Drop {
[JsonProperty("Ник игрока")]
public string DisplayName;
[JsonProperty("Название предмета")]
public string ShortName;
[JsonProperty("Название скина как в стим")]
public string SkinName;
[JsonProperty("SkinID предмета")]
public ulong SkinID;
[JsonProperty("Ссылка на картинку скина")]
public string Url;
[JsonProperty("Цена предмета")]
public float Price;
[JsonProperty("Время и дата")]
public string TimeDate;
[JsonProperty("Покупка")]
public bool Purchased;
}
public class GiveSkin {
public string DisplayName;
public string ShortName;
public string SkinName;
public string SkinID;
public string Url;
public string Price;
}
public class PrizeSkin {
[JsonProperty("ShortName")]
public string ShortName;
[JsonProperty("SkinID")]
public ulong SkinID;
[JsonProperty("SkinName")]
public string SkinName;
[JsonProperty("Price")]
public float Price;
[JsonProperty("Quantity")]
public int Quantity = 1;
}
// --------------------------- Конфиг ---------------------------
public Configuration config;
public class Configuration {
[JsonProperty("Описание плагина")]
public string Info = "<b>SKINDROP — скин для активных игроков!</b>\n<size=11>Пока вы играете, система периодически случайно выбирает победителей и показывает их здесь.Обязательно укажите трейд-ссылку ниже, чтобы мы могли отправить приз.\n\n<b>Выдача призов:</b> все скины за день отправляются <color=#DC143C>в конце дня</color> всем победителям.</size>";
[JsonProperty("Пермишен для выдачи скинов")]
public string Perm = "skindrop.use";
[JsonProperty("Через сколько дестроить уведомление")]
public float TimeAlert = 5f;
[JsonProperty("Webhook")]
public string WebhookNotify;
[JsonProperty("Webhook 2")]
public string WebhookDropNotify;
[JsonProperty("Webhook покупок в магазине")]
public string WebhookShopNotify;
[JsonProperty("Цвет сообщения в Discord (Можно найти на сайте - https://old.message.style/dashboard в разделе JSON)")]
public int Color;
[JsonProperty("Заголовок сообщения")]
public string AuthorName;
[JsonProperty("Ссылка на иконку для аватарки сообщения")]
public string IconURL;
[JsonProperty("Интервал автодропа (минут)")]
public int AutoDrawIntervalMinutes = 60;
[JsonProperty("Время предварительного уведомления (минут)")]
public int WarningMinutes = 15;
// Интеграция со Steam-ботом
[JsonProperty("SteamBotEnabled")]
public bool SteamBotEnabled = false;
[JsonProperty("SteamBotUrl")]
public string SteamBotUrl = "http://127.0.0.1:3000/api/send";
[JsonProperty("SteamBotToken")]
public string SteamBotToken = "";
// transport: http | file (очередь в data)
[JsonProperty("SteamBotTransport")]
public string SteamBotTransport = "http";
// относительный путь к файлу очереди (от oxide/data), без расширения
[JsonProperty("SteamBotQueueFile")]
public string SteamBotQueueFile = "SkinDrop/bot-queue";
[JsonProperty("Список призовых скинов для автодропа")]
public List<PrizeSkin> PrizeSkins = new List<PrizeSkin> {
new PrizeSkin {
ShortName = "rifle.ak", SkinID = 1234567890, SkinName = "AK-47 | Example Skin", Price = 100.0f, Quantity = 3
}
}
;
// Категории магазина (вкладки)
[JsonProperty("Категории магазина")]
public List<ShopCategory> Categories = new List<ShopCategory>();
}
// --------------------------- Магазин: модели ---------------------------
public class ShopSkinItem {
[JsonProperty("ShortName")]
public string ShortName;
[JsonProperty("SkinID")]
public ulong SkinID;
[JsonProperty("SkinName")]
public string SkinName;
[JsonProperty("Price")]
public float Price;
// стоимость в монетах
[JsonProperty("Quantity")]
public int Quantity = -1;
// остаток (−1 = бесконечно)
[JsonProperty("GiveCount")]
public int GiveCount = 1;
// сколько выдавать за покупку
}
public class ShopCategory {
[JsonProperty("Название вкладки")]
public string Name;
[JsonProperty("Товары")]
public List<ShopSkinItem> Items = new List<ShopSkinItem>();
}
protected override void LoadConfig() {
base.LoadConfig();
try {
config = Config.ReadObject<Configuration>();
if (config == null) LoadDefaultConfig();
}
catch {
LoadDefaultConfig();
}
SaveConfig();
// >>> ЕДИНСТВЕННОЕ ДОБАВЛЕНИЕ: сразу подтягиваем интервал из конфига в момент загрузки
// чтобы обратный отсчёт в UI сходу показывал нужное значение (например, 30 минут)._nextDrawAtUtc = DateTime.UtcNow.AddMinutes(Mathf.Max(1, config.AutoDrawIntervalMinutes));
// Автозаполнение примерами магазина при пустом списке
EnsureDefaultShopItems();
SaveConfig();
}
private void EnsureDefaultShopItems() {
if (config.Categories == null || config.Categories.Count == 0) config.Categories = new List<ShopCategory>();
if (!config.Categories.Any(c => c.Items != null && c.Items.Count > 0)) {
config.Categories = new List<ShopCategory> {
new ShopCategory {
Name = "Rust", Items = new List<ShopSkinItem> {
new ShopSkinItem {
ShortName = "rifle.ak", SkinID = 0UL, SkinName = "RUST | AK-47", Price = 100, Quantity = 9
}
, new ShopSkinItem {
ShortName = "rifle.lr300", SkinID = 0UL, SkinName = "RUST | LR-300", Price = 120, Quantity = 9
}
, new ShopSkinItem {
ShortName = "smg.2", SkinID = 0UL, SkinName = "RUST | Custom SMG", Price = 80, Quantity = 9
}
, new ShopSkinItem {
ShortName = "smg.thompson", SkinID = 0UL, SkinName = "RUST | Thompson", Price = 90, Quantity = 9
}
, new ShopSkinItem {
ShortName = "pistol.m92", SkinID = 0UL, SkinName = "RUST | M92", Price = 60, Quantity = 9
}
, new ShopSkinItem {
ShortName = "pistol.python", SkinID = 0UL, SkinName = "RUST | Python", Price = 70, Quantity = 9
}
, new ShopSkinItem {
ShortName = "shotgun.pump", SkinID = 0UL, SkinName = "RUST | Pump Shotgun", Price = 75, Quantity = 9
}
, new ShopSkinItem {
ShortName = "shotgun.doublebarrel", SkinID = 0UL, SkinName = "RUST | Double Barrel", Price = 65, Quantity = 9
}
, new ShopSkinItem {
ShortName = "rifle.bolt", SkinID = 0UL, SkinName = "RUST | Bolt Action", Price = 140, Quantity = 9
}
}
}
, new ShopCategory {
Name = "CS", Items = new List<ShopSkinItem> {
new ShopSkinItem {
ShortName = "hoodie", SkinID = 0UL, SkinName = "CS | Hoodie", Price = 50, Quantity = 9
}
, new ShopSkinItem {
ShortName = "pants", SkinID = 0UL, SkinName = "CS | Pants", Price = 45, Quantity = 9
}
, new ShopSkinItem {
ShortName = "tshirt.long", SkinID = 0UL, SkinName = "CS | Longsleeve T-Shirt", Price = 30, Quantity = 9
}
, new ShopSkinItem {
ShortName = "mask.balaclava", SkinID = 0UL, SkinName = "CS | Balaclava", Price = 25, Quantity = 9
}
, new ShopSkinItem {
ShortName = "metal.plate.helmet", SkinID = 0UL, SkinName = "CS | Metal Helmet", Price = 90, Quantity = 9
}
, new ShopSkinItem {
ShortName = "metal.plate.torso", SkinID = 0UL, SkinName = "CS | Metal Chestplate", Price = 110, Quantity = 9
}
, new ShopSkinItem {
ShortName = "roadsign.jacket", SkinID = 0UL, SkinName = "CS | Roadsign Jacket", Price = 85, Quantity = 9
}
, new ShopSkinItem {
ShortName = "roadsign.kilt", SkinID = 0UL, SkinName = "CS | Roadsign Kilt", Price = 80, Quantity = 9
}
, new ShopSkinItem {
ShortName = "gloves.tactical", SkinID = 0UL, SkinName = "CS | Tactical Gloves", Price = 40, Quantity = 9
}
}
}
, new ShopCategory {
Name = "Dota", Items = new List<ShopSkinItem> {
new ShopSkinItem {
ShortName = "hatchet", SkinID = 0UL, SkinName = "DOTA | Hatchet", Price = 20, Quantity = 9
}
, new ShopSkinItem {
ShortName = "pickaxe", SkinID = 0UL, SkinName = "DOTA | Pickaxe", Price = 20, Quantity = 9
}
, new ShopSkinItem {
ShortName = "knife.bone", SkinID = 0UL, SkinName = "DOTA | Bone Knife", Price = 15, Quantity = 9
}
, new ShopSkinItem {
ShortName = "mace", SkinID = 0UL, SkinName = "DOTA | Mace", Price = 25, Quantity = 9
}
, new ShopSkinItem {
ShortName = "spear.wooden", SkinID = 0UL, SkinName = "DOTA | Wooden Spear", Price = 10, Quantity = 9
}
, new ShopSkinItem {
ShortName = "spear.stone", SkinID = 0UL, SkinName = "DOTA | Stone Spear", Price = 12, Quantity = 9
}
, new ShopSkinItem {
ShortName = "salvaged.cleaver", SkinID = 0UL, SkinName = "DOTA | Cleaver", Price = 22, Quantity = 9
}
, new ShopSkinItem {
ShortName = "sickle", SkinID = 0UL, SkinName = "DOTA | Sickle", Price = 18, Quantity = 9
}
, new ShopSkinItem {
ShortName = "rock", SkinID = 0UL, SkinName = "DOTA | Rock", Price = 5, Quantity = 9
}
}
}
}
;
}
}
protected override void LoadDefaultConfig() {
config = new Configuration();
EnsureDefaultShopItems();
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config, true);
// --------------------------- Жизненный цикл ---------------------------
void OnServerInitialized() {
DB = Interface.Oxide.DataFileSystem.ReadObject<List<Drop>>($"{Name}/DropList");
permission.RegisterPermission(config.Perm, this);
// Инициализация ImageLibrary больше не требуется — все изображения из локальной папки
// Локальные картинки из TPSystem/TPSkinDrop/images/
if (_shopImageUI == null) {
_shopImageUI = new ImageUI();
_shopImageUI.DownloadAllImages();
}
// Предзагрузка не требуется — изображения берутся из локальной папки
StartAutoDraw();
}
void Unload() {
SaveDataBase();
// Останавливаем все тики обратного отсчёта
foreach (var t in _countdownTimers.Values) t?.Destroy();
_countdownTimers.Clear();
_autoDrawTimer?.Destroy();
_warningTimer?.Destroy();
}
void OnPlayerDisconnected(BasePlayer player, string reason) {
StopCountdown(player);
}
// --------------------------- Автодроп и предупреждения ---------------------------
private void StartAutoDraw() {
_autoDrawTimer?.Destroy();
_warningTimer?.Destroy();
var interval = Mathf.Max(1, config.AutoDrawIntervalMinutes);
// Первый запуск через interval минут от текущего момента
_nextDrawAtUtc = DateTime.UtcNow.AddMinutes(interval);
_autoDrawTimer = timer.Every(interval * 60f, () => {
AutoDrawRoutine();
// после розыгрыша назначаем следующее время
_nextDrawAtUtc = DateTime.UtcNow.AddMinutes(interval);
}
);
if (config.WarningMinutes > 0 && config.WarningMinutes < interval) {
_warningTimer = timer.Every((interval - config.WarningMinutes) * 60f, SendWarningMessage);
}
}
private void SendWarningMessage() {
var availableSkins = config.PrizeSkins?.Where(s => s.Quantity > 0).ToList();
if (availableSkins == null || availableSkins.Count == 0) return;
var skin = availableSkins.GetRandom();
foreach (var player in BasePlayer.activePlayerList) {
player.ChatMessage("<color=#DC143C><size=16><b>SKIN DROP</b></size></color>");
player.ChatMessage($"<color=#FFD700>Через {config.WarningMinutes} мин.состоится розыгрыш скина!</color>");
player.ChatMessage($"Приз: <color=#00FF00>{skin.SkinName}</color> — <color=#FFD700>${skin.Price}</color>");
player.ChatMessage("<color=#87CEFA>Не забудьте ввести свою трейд-ссылку в меню:</color> <color=#00FFFF>/menu -> SKINDROP</color>");
}
}
private void AutoDrawRoutine() {
var availableSkins = config.PrizeSkins?.Where(s => s.Quantity > 0).ToList();
var players = BasePlayer.activePlayerList.ToList();
if (availableSkins == null || availableSkins.Count == 0 || players.Count == 0) return;
var skin = availableSkins.GetRandom();
var player = players.GetRandom();
skin.Quantity--;
var now = DateTime.Now;
DB.Add(new Drop {
DisplayName = player.displayName, ShortName = skin.ShortName, SkinName = skin.SkinName, SkinID = skin.SkinID, Price = skin.Price, TimeDate = $"{now.ToShortTimeString()} {now:dd.MM.yy}"
}
);
SaveDataBase();
AlertUI(player, player.displayName, skin.ShortName, skin.SkinID);
TrySendToSteamBot(player, skin.ShortName, skin.SkinID, 1);
var fields = Drop_Player(player, skin.SkinName, $"https://steamcommunity.com/sharedfiles/filedetails/?id={skin.SkinID}", skin.Price.ToString());
SendDiscord(config.WebhookDropNotify, fields, null, new Thumbnail($"https://steamcommunity.com/sharedfiles/filedetails/?id={skin.SkinID}"), config.Color);
// Обновим надпись у тех, у кого открыт UI (на следующем тике обновится сам)
}
private void SaveDataBase() => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/DropList", DB);
// --------------------------- Команды ---------------------------
[ConsoleCommand("dropskin")]
void ConsoleTrade(ConsoleSystem.Arg args) {
var player = args.Player();
if (player == null) return;
if (!UrlTrade.ContainsKey(player.userID)) UrlTrade[player.userID] = "";
if (!GivsDrop.ContainsKey(player.userID)) GivsDrop[player.userID] = new GiveSkin();
if (!args.HasArgs(1)) {
SkinDropUI(player);
return;
}
var sub = args.Args[0];
if (sub == "give") {
GiveDropUI(player);
}
else if (sub == "back") {
SkinDropUI(player);
}
else if (sub == "close") {
StopCountdown(player);
}
else if (sub == "tradeurl") {
var trade = string.Join("", args.Args.Skip(1));
if (string.IsNullOrEmpty(trade)) return;
if (!trade.StartsWith("http://") && !trade.StartsWith("https://")) {
SendReply(player, "Ошибка! Введите корректную ссылку, начинающуюся с http:// или https://");
return;
}
UrlTrade[player.userID] = trade;
SendReply(player, "Трейд-ссылка успешно сохранена!");
}
else if (sub == "sendds") {
if (UrlTrade[player.userID] == "") return;
var fields = DT_PlayerSendTrade(player, UrlTrade[player.userID]);
SendDiscord(config.WebhookNotify, fields, new Authors(player.displayName, "", "", ""), null, config.Color);
UrlTrade.Remove(player.userID);
SendReply(player, "Вы успешно отправили свою трейд-ссылку!");
SkinDropUI(player);
}
else if (sub == "name") {
if (!args.HasArgs(2)) return;
var target = FindBasePlayer(args.Args[1]);
if (target == null) {
SendReply(player, $"Игрок {args.Args[1]} не найден");
return;
}
GivsDrop[player.userID].DisplayName = target.displayName;
}
else if (sub == "shortname") {
GivsDrop[player.userID].ShortName = string.Join("", args.Args.Skip(1));
}
else if (sub == "skinname") {
GivsDrop[player.userID].SkinName = string.Join("%20", args.Args.Skip(1));
}
else if (sub == "skinid") {
GivsDrop[player.userID].SkinID = string.Join("", args.Args.Skip(1));
}
else if (sub == "url") {
GivsDrop[player.userID].Url = string.Join("", args.Args.Skip(1));
}
else if (sub == "price") {
GivsDrop[player.userID].Price = string.Join("", args.Args.Skip(1));
}
else if (sub == "send") {
if (!permission.UserHasPermission(player.UserIDString, config.Perm)) return;
var date = DateTime.Now;
if (GivsDrop[player.userID].DisplayName == null || GivsDrop[player.userID].ShortName == null || GivsDrop[player.userID].SkinName == null || GivsDrop[player.userID].SkinID == null || GivsDrop[player.userID].Url == null || GivsDrop[player.userID].Price == null) {
SendReply(player, "Вы не указали все данные!");
return;
}
var target = FindBasePlayer(GivsDrop[player.userID].DisplayName);
DB.Add(new Drop {
DisplayName = target.displayName, ShortName = GivsDrop[player.userID].ShortName, SkinName = GivsDrop[player.userID].SkinName, SkinID = ulong.Parse(GivsDrop[player.userID].SkinID), Url = GivsDrop[player.userID].Url, Price = float.Parse(GivsDrop[player.userID].Price), TimeDate = $"{date.ToShortTimeString()} {date:dd.MM.yy}"
}
);
foreach (var check in BasePlayer.activePlayerList) AlertUI(check, GivsDrop[player.userID].DisplayName, GivsDrop[player.userID].ShortName, ulong.Parse(GivsDrop[player.userID].SkinID));
var fields = Drop_Player(target, GivsDrop[player.userID].SkinName, $"https://steamcommunity.com/sharedfiles/filedetails/?id={GivsDrop[player.userID].SkinID}", GivsDrop[player.userID].Price);
SendDiscord(config.WebhookDropNotify, fields, null, new Thumbnail($"https://steamcommunity.com/sharedfiles/filedetails/?id={GivsDrop[player.userID].SkinID}"), config.Color);
// Если такой скин есть в конфиге — уменьшим остаток
var skinInConfig = config.PrizeSkins.FirstOrDefault(s => s.ShortName == GivsDrop[player.userID].ShortName && s.SkinID == ulong.Parse(GivsDrop[player.userID].SkinID));
if (skinInConfig != null && skinInConfig.Quantity > 0) {
skinInConfig.Quantity--;
SaveConfig();
}
GivsDrop.Remove(player.userID);
GiveDropUI(player);
}
}
// --------------------------- UI ---------------------------
private void SkinDropUI(BasePlayer player) {
// Закрываем и запускаем отсчёт с нуля
StopCountdown(player);
CuiHelper.DestroyUi(player, "skindrop.Main");
var container = new CuiElementContainer();
container.Add(new CuiElement {
Name = "skindrop.Main", Parent = ".Mains", Components = {
new CuiRawImageComponent {
Png = _shopImageUI?.GetImage("skindrop")
}
, new CuiRectTransformComponent {
AnchorMin = "-0.315 -0.27", AnchorMax = "1.3 1.275"
}
}
}
);
// Кнопка "Закрыть меню" (крестик)
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.802 0.806", AnchorMax = "0.815 0.829"
}
, // обновлено
Button = {
Close = "Menu_UI", Color = "0 0 0 0"
}
, Text = {
Text = ""
}
}
, "skindrop.Main");
// ДОБАВЛЕНО: Невидимые кнопки категорий на главном экране (RUST / CS / DOTA)
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.243 0.737", AnchorMax = "0.261 0.802"
}
, Button = {
Color = "0 0 0 0", Command = "skindrop.tab 0; skindrop.shop page 0"
}
, Text = {
Text = ""
}
}
, "skindrop.Main");
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.269 0.737", AnchorMax = "0.287 0.802"
}
, Button = {
Color = "0 0 0 0", Command = "skindrop.tab 1; skindrop.shop page 0"
}
, Text = {
Text = ""
}
}
, "skindrop.Main");
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.294 0.737", AnchorMax = "0.312 0.801"
}
, Button = {
Color = "0 0 0 0", Command = "skindrop.tab 2; skindrop.shop page 0"
}
, Text = {
Text = ""
}
}
, "skindrop.Main");
// Кнопка "Выдать" только для модераторов с правом
if (permission.UserHasPermission(player.UserIDString, config.Perm)) {
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.76 0.804", AnchorMax = "0.8 0.832"
}
, Button = {
Color = "0 0 0 0", Command = "dropskin give"
}
, Text = {
Text = "Выдать", Color = "1 1 1 0.6", FontSize = 10, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.Main");
}
// Описание слева
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.27 0.4", AnchorMax = "0.5 0.69"
}
, Button = {
Color = "0 0 0 0"
}
, Text = {
Text = config.Info, Color = "1 1 1 0.4", FontSize = 14, Align = TextAnchor.UpperCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.Main");
// ----- Блок обратного отсчёта -----
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "0.330 0.246", AnchorMax = "0.419 0.300"
}
, // обновлено
Image = {
Color = "0 0 0 0"
}
}
, "skindrop.Main", "skindrop.CountdownBox");
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0 0.7", AnchorMax = "1 1"
}
, Text = {
Text = "До следующего розыгрыша:", Color = "1 1 1 0.6", FontSize = 12, Align = TextAnchor.LowerCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.CountdownBox");
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0 0", AnchorMax = "1 0.75"
}
, Text = {
Text = "—:—:—", Color = "1 1 1 0.95", FontSize = 24, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-bold.ttf"
}
}
, "skindrop.CountdownBox", "skindrop.CountdownText");
// ДОБАВЛЕНО: Баланс игрока (жёлтая цифра, только число)
float __amountBalance = 0f;
try {
var __bal = TPEconomic?.Call("API_GET_BALANCE", (ulong)player.userID);
if (__bal is float) __amountBalance = (float)__bal;
else if (__bal is double) __amountBalance = (float)(double)__bal;
else if (__bal is int) __amountBalance = (int)__bal;
}
catch {
__amountBalance = 0f;
}
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.715 0.731", AnchorMax = "0.748 0.796"
}
, // новые якоря
Text = {
Text = ((int)__amountBalance).ToString(), Color = "1 1 0 0.95", Align = TextAnchor.MiddleCenter, Font = "robotocondensed-bold.ttf", FontSize = 16
}
}
, "skindrop.Main");
// Поле ввода трейд-ссылки
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "0.218 0.445", AnchorMax = "0.467 0.497"
}
, // обновлено
Image = {
Color = "0 0 0 0"
}
}
, "skindrop.Main", "FieldTrade");
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.04 0", AnchorMax = "1 1"
}
, Text = {
Text = "", Color = "1 1 1 0.05", FontSize = 10, Align = TextAnchor.MiddleLeft, Font = "robotocondensed-regular.ttf"
}
}
, "FieldTrade", "TradeText");
container.Add(new CuiElement {
Parent = "FieldTrade", Components = {
new CuiInputFieldComponent {
Command = "dropskin tradeurl ", Text = "", Color = "1 1 1 0.3", Align = TextAnchor.MiddleLeft, FontSize = 12, Font = "robotocondensed-regular.ttf", NeedsKeyboard = true
}
, new CuiRectTransformComponent {
AnchorMin = "0.04 0", AnchorMax = "1 1"
}
}
}
);
// Кнопка "КУПИТЬ СКИНЫ" — переходит в магазин
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.523 0.734", AnchorMax = "0.594 0.775"
}
, // обновлено
Button = {
Color = "0 0 0 0", Command = "skindrop.shop page 0"
}
, Text = {
Text = "КУПИТЬ СКИНЫ", Color = "1 1 1 0.8", FontSize = 10, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.Main");
// Кнопка "Отправить" трейд-ссылку
container.Add(new CuiButton {
RectTransform = {
AnchorMin = "0.477 0.445", AnchorMax = "0.508 0.499"
}
, // обновлено
Button = {
Color = "0 0 0 0", Command = "dropskin sendds"
}
, Text = {
Text = "Отправить", Color = "1 1 1 0.8", FontSize = 10, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.Main");
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.260 0.25", AnchorMax = "0.5 0.28"
}
, Text = {
Text = "Пример: http://steamcommunity.com/my/tradeoffers/privacy", Color = "1 1 1 0.4", FontSize = 10, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-regular.ttf"
}
}
, "skindrop.Main");
// Правый список победителей
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "0.531 0.240", AnchorMax = "0.780 0.659"
}
, // обновлено
Image = {
Color = "0 0 0 0"
}
}
, "skindrop.Main", "DropTop");
int page = Math.Max(0, DB.Count - 8);
float width = 1f, height = 0.117f, startxBox = 0f, startyBox = 0.999f - height, xmin = startxBox, ymin = startyBox;
var last = DB.Skip(page).Take(8).ToList();
last.Reverse();
// показываем последние сверху вниз
foreach (var check in last) {
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = $"{xmin} {ymin}", AnchorMax = $"{xmin + width} {ymin + height}", OffsetMin = "2 2", OffsetMax = "-2 -2"
}
, Image = {
Color = "0 0 0 0"
}
}
, "DropTop", "Top");
xmin += width;
if (xmin + width >= 1) {
xmin = startxBox;
ymin -= height + 0.01f;
}
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "0.02 0", AnchorMax = "0.15 1"
}
, Image = {
Color = "0 0 0 0"
}
}
, "Top", "Image");
container.Add(new CuiElement {
Parent = "Image", Components = {
new CuiImageComponent {
ItemId = ItemManager.FindItemDefinition(check.ShortName).itemid, SkinId = check.SkinID
}
, new CuiRectTransformComponent {
AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "4 4", OffsetMax = "-4 -4"
}
}
}
);
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.19 0", AnchorMax = "0.7 1"
}
, Text = {
Text = $"<b><color=#FFFFFF99>{check.DisplayName}</color></b>\n<size=10>{check.TimeDate}</size>", Color = "1 1 1 0.4", FontSize = 11, Align = TextAnchor.MiddleLeft, Font = "robotocondensed-regular.ttf"
}
}
, "Top");
// Цена и метка «Куплен» при необходимости
string rightText = check.Purchased ? $"Куплен\n<size=10>{check.Price} ₽</size>" : $"{check.Price} ₽";
container.Add(new CuiLabel {
RectTransform = {
AnchorMin = "0.825 0.21", AnchorMax = "0.97 0.79"
}
, Text = {
Text = rightText, Color = "1 1 1 0.8", FontSize = 10, Align = TextAnchor.MiddleCenter, Font = "robotocondensed-regular.ttf"
}
}
, "Top");
}
CuiHelper.AddUi(player, container);
// Стартуем обратный отсчёт
StartCountdown(player);
}
// --------------------------- Вкладка магазина ---------------------------
private const string ShopRoot = "skindrop.Shop";
// Локальный загрузчик картинок, совместимый с TPBaraxolka
private static ImageUI _shopImageUI;
// Состояние активной категории по игрокам
private readonly Dictionary<ulong, int> _currentCategory = new Dictionary<ulong, int>();
private class ImageUI {
private const string PathRel = "TPSystem/TPSkinDrop/images/";
private readonly Dictionary<string, ImageData> _images = new Dictionary<string, ImageData>(StringComparer.OrdinalIgnoreCase);
private class ImageData {
public string Id;
public bool Loaded;
}
public string GetImage(string name) {
if (string.IsNullOrWhiteSpace(name)) return null;
ImageData img;
if (_images.TryGetValue(name, out img) && img.Loaded) return img.Id;
// Попытка ленивой загрузки по ключу
if (TryLoadAndStore(name)) return _images[name].Id;
return null;
}
public void DownloadAllImages() {
try {
var folder = System.IO.Path.Combine(Interface.Oxide.DataDirectory, PathRel);
if (!Directory.Exists(folder)) return;
foreach (var file in Directory.GetFiles(folder, "*.png", SearchOption.TopDirectoryOnly)) {
var key = System.IO.Path.GetFileNameWithoutExtension(file);
TryLoadAndStore(key);
}
}
catch {
}
}
private bool TryLoadAndStore(string key) {
try {
var filePath = System.IO.Path.Combine(Interface.Oxide.DataDirectory, PathRel, key + ".png");
if (!System.IO.File.Exists(filePath)) return false;
var bytes = System.IO.File.ReadAllBytes(filePath);
var id = FileStorage.server.Store(bytes, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID).ToString();
_images[key] = new ImageData {
Id = id, Loaded = true
}
;
return true;
}
catch {
return false;
}
}
}
[ChatCommand("skins")]
private void CmdOpenShop(BasePlayer player, string cmd, string[] args) {
_currentCategory[player.userID] = 0;
RenderShopUI(player, 0);
}
[ConsoleCommand("skindrop.shop")]
private void CmdShop(ConsoleSystem.Arg args) {
var player = args.Player();
if (player == null) return;
if (!args.HasArgs(2)) {
RenderShopUI(player, 0);
return;
}
if (args.Args[0] != "page") {
RenderShopUI(player, 0);
return;
}
int page;
if (!int.TryParse(args.Args[1], out page)) page = 0;
RenderShopUI(player, Math.Max(0, page));
}
[ConsoleCommand("skindrop.buy")]
private void CmdBuy(ConsoleSystem.Arg args) {
var player = args.Player();
if (player == null) return;
if (!args.HasArgs(1)) return;
int index;
if (!int.TryParse(args.Args[0], out index)) return;
var list = GetActiveItems(player);
if (list == null) return;
var item = list.ElementAtOrDefault(index);
if (item == null) return;
// Проверка экономики
if (TPEconomic == null) {
SendReply(player, "Экономика не подключена (нет TPEconomic).Покупка невозможна.");
return;
}
float balance = 0f;
try {
var bal = TPEconomic.Call("API_GET_BALANCE", (ulong)player.userID);
if (bal is float) balance = (float)bal;
else if (bal is double) balance = (float)(double)bal;
else if (bal is int) balance = (int)bal;
}
catch {
balance = 0f;
}
var price = item.Price;
// Проверка остатка (если ограничен)
if (item.Quantity == 0) {
SendReply(player, "Товар распродан.");
return;
}
if (balance < price) {
SendReply(player, "Недостаточно средств.");
return;
}
// Предмет НЕ выдаём сразу — оформление обмена ботом в течение 15 минут
if (string.IsNullOrEmpty(item.ShortName)) {
SendReply(player, "Неверно настроен товар.");
return;
}
// Уведомление о покупке, списать баланс и обновить UI баланса
var nextRemaining = item.Quantity > 0 ? Math.Max(0, item.Quantity - 1) : item.Quantity;
AlertUIBuy(player, player.displayName, item.ShortName, item.SkinID, nextRemaining);
TPEconomic.Call("API_PUT_BALANCE_MINUS", (ulong)player.userID, (float)price);
TPMenuSystem?.Call("UpdateUIBalance", player);
// Красивые оповещения в чат для игрока
player.ChatMessage("<color=#DC143C><size=16><b>SKIN DROP — Покупка оформлена</b></size></color>");
player.ChatMessage($"Вы приобрели <color=#FFD700>{item.SkinName}</color>.В течение <color=#00FF00>15 минут</color> бот отправит вам обмен в Steam.");
var hasTrade = UrlTrade.ContainsKey(player.userID) && !string.IsNullOrEmpty(UrlTrade[player.userID]);
if (!hasTrade) player.ChatMessage("<color=#87CEFA>Важно:</color> укажите свою трейд-ссылку в меню <color=#00FFFF>/menu → SKINDROP</color>, иначе бот не сможет отправить скин.");
else player.ChatMessage("<color=#87CEFA>Проверьте входящие предложения обмена в Steam.</color>");
// Запись в историю как "покупка", чтобы показалось в правом списке на главной
var now = DateTime.Now;
DB.Add(new Drop {
DisplayName = player.displayName, ShortName = item.ShortName, SkinName = item.SkinName, SkinID = item.SkinID, Url = null, Price = item.Price, TimeDate = $"{now.ToShortTimeString()} {now:dd.MM.yy}", Purchased = true
}
);
SaveDataBase();
// Отправка задачи боту на обмен
TrySendToSteamBot(player, item.ShortName, item.SkinID, Math.Max(1, item.GiveCount));
// Discord-уведомление о покупке (детальное логирование)
int catIdx = 0;
_currentCategory.TryGetValue(player.userID, out catIdx);
var catNames = GetCategoryNames();
string categoryName = (catIdx >= 0 && catIdx < catNames.Count) ? catNames[catIdx] : "Unknown";
var buyFields = Shop_BuyPlayer(player, item, balance, balance - price, categoryName, nextRemaining);
var author = new Authors(config.AuthorName ?? "SkinDrop Shop", "", config.IconURL ?? "", "");
// Превью: мастерская по SkinID
SendDiscord(config.WebhookShopNotify, buyFields, author, new Thumbnail($"https://steamcommunity.com/sharedfiles/filedetails/?id={item.SkinID}"), config.Color);
// Уменьшаем остаток, если ограничен
if (item.Quantity > 0) {
item.Quantity = Math.Max(0, item.Quantity - 1);
SaveConfig();
}
RenderShopUI(player, 0);
}
private void RenderShopUI(BasePlayer player, int page) {
CuiHelper.DestroyUi(player, ShopRoot);
CuiHelper.DestroyUi(player, "skindrop.Main");
if (!HasAnyItems()) {
SendReply(player, "Магазин пуст.");
return;
}
var container = new CuiElementContainer();
// Фон, как в TPBaraxolka (BACKGROUND_BUY)
container.Add(new CuiElement {
Name = ShopRoot, Parent = ".Mains", Components = {
new CuiRawImageComponent {
Png = _shopImageUI?.GetImage("BACKGROUND_BUY")
}
, new CuiRectTransformComponent {
AnchorMin = "-0.315 -0.27", AnchorMax = "1.3 1.275"
}
}
}
);
// Внутренняя область.R как в TPBaraxolka (контентная рамка)
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "0.25 0.2", AnchorMax = "0.754 0.64"
}
, Image = {
Color = "1 1 1 0"
}
}
, ShopRoot, ShopRoot + ".R");
// Область с товарами (внутри.R)
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = "-0.01 0.015", AnchorMax = "1.005 1"
}
, Image = {
Color = "1 1 1 0"
}
}
, ShopRoot + ".R", ShopRoot + ".Bottle");
// Стрелка следующая страница (как в TPBaraxolka)
var items = GetActiveItems(player).Skip(page * 12).Take(12).ToList();
// Баланс игрока для окраски кнопок как в TPBaraxolka
float amountBalance = 0f;
if (TPEconomic != null) {
try {
var bal = TPEconomic.Call("API_GET_BALANCE", (ulong)player.userID);
if (bal is float) amountBalance = (float)bal;
else if (bal is double) amountBalance = (float)(double)bal;
else if (bal is int) amountBalance = (int)bal;
}
catch {
amountBalance = 0f;
}
}
if (GetActiveItems(player).Count > (page + 1) * 12) {
container.Add(new CuiButton {
Button = {
Command = $"skindrop.shop page {page + 1}", Color = "0 0 0 0"
}
, Text = {
Text = "▶", FontSize = 60, Align = TextAnchor.MiddleRight, Color = "0.929 0.882 0.847 0.7"
}
, RectTransform = {
AnchorMin = "0.89 -0.15", AnchorMax = "0.99 0"
}
}
, ShopRoot + ".Bottle");
}
// Сетка 3x4 как в TPBaraxolka
float x = 0f;
float y = -0.01f;
int i = 0;
for (int idx = 0; idx < items.Count; idx++) {
if ((i % 3) == 0 && i != 0) {
x = 0f;
y += 0.259f;
}
var globalIndex = page * 12 + idx;
var panelName = ShopRoot + $".Shop.{i}";
var shopItem = items[idx];
container.Add(new CuiPanel {
RectTransform = {
AnchorMin = $"{0 + x} {0.77f - y}", AnchorMax = $"{0.32f + x} {1 - y}"
}
, Image = {
Color = "1 1 1 0"
}
// ... [Code preview truncated due to length. Full source in attached file] ...