C#:
using System;
using System.Collections.Generic;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("PulseTickets", "Sergioklv", "2.0.0")]
public class PulseTickets : RustPlugin
{
private const int MaxTickets = 60000;
private const string TicketDataFile = "PulseTickets/Tickets";
private const string PlaytimeDataFile = "PulseTickets/Playtime";
private const string DiscordDataFile = "PulseTickets/Discords";
private Dictionary<int, TicketInfo> ticketDatabase = new Dictionary<int, TicketInfo>();
private Dictionary<string, int> playtimeMinutes = new Dictionary<string, int>();
private Dictionary<string, string> discordBySteam = new Dictionary<string, string>();
private Dictionary<ulong, Timer> playerTimers = new Dictionary<ulong, Timer>();
private readonly object ticketLock = new object();
private class TicketInfo
{
public string steamid;
public string name;
public string discord;
}
#region Oxide hooks
void Init()
{
ticketDatabase = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<int, TicketInfo>>(TicketDataFile) ?? new Dictionary<int, TicketInfo>();
playtimeMinutes = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>(PlaytimeDataFile) ?? new Dictionary<string, int>();
discordBySteam = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(DiscordDataFile) ?? new Dictionary<string, string>();
Puts("PulseTickets: данные загружены. Найдено билетов: " + ticketDatabase.Count);
}
void OnServerInitialized()
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null) continue;
EnsurePlaytimeEntry(player.userID);
StartPlayerTimer(player);
ShowTicketUI(player);
}
}
void OnPlayerConnected(BasePlayer player)
{
if (player == null) return;
EnsurePlaytimeEntry(player.userID);
StartPlayerTimer(player);
ShowTicketUI(player);
}
void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
StopPlayerTimer(player.userID);
SavePlaytimeFor(player.userID);
}
void Unload()
{
foreach (var t in playerTimers.Values)
t.Destroy();
playerTimers.Clear();
SaveAllData();
}
#endregion
#region Timers & playtime
private void EnsurePlaytimeEntry(ulong userID)
{
string sid = userID.ToString();
if (!playtimeMinutes.ContainsKey(sid))
playtimeMinutes[sid] = 0;
}
private void StartPlayerTimer(BasePlayer player)
{
if (player == null) return;
ulong id = player.userID;
if (playerTimers.ContainsKey(id)) return;
Timer t = timer.Repeat(60f, 0, () =>
{
try
{
AddMinuteAndCheckTicket(player.userID, player);
}
catch (Exception ex)
{
Puts("PulseTickets: ошибка в таймере: " + ex);
}
});
playerTimers[id] = t;
}
private void StopPlayerTimer(ulong userID)
{
if (playerTimers.TryGetValue(userID, out Timer t))
{
t.Destroy();
playerTimers.Remove(userID);
}
}
private void ShowTicketUI(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
string sid = player.UserIDString;
int count = 0;
if (!string.IsNullOrEmpty(sid))
{
foreach (var kv in ticketDatabase)
{
if (kv.Value != null && kv.Value.steamid == sid)
count++;
}
}
int minutesLeft = 60;
if (playtimeMinutes.ContainsKey(sid))
minutesLeft = Math.Max(0, 60 - playtimeMinutes[sid]);
var ui = new CuiElementContainer();
var label = new CuiLabel
{
Text =
{
Text =
$"<color=#4DFFFF>У вас всего</color> <color=#FFFF55>{count}</color> <color=#4DFFFF>билетов <color=#FFFF55>/mytickets</color></color>\n" +
$"<color=#FF8080>Следующий билет через:</color> <color=#FFFF55>{minutesLeft} мин</color>",
FontSize = 14,
Color = "1 1 1 1"
},
RectTransform =
{
AnchorMin = "0.832 0.94",
AnchorMax = "1.0 1.0"
}
};
CuiHelper.DestroyUi(player, "PulseTicketsUI");
ui.Add(label, "Hud", "PulseTicketsUI");
CuiHelper.AddUi(player, ui);
}
private void DestroyTicketUI(BasePlayer player)
{
if (player == null) return;
CuiHelper.DestroyUi(player, "PulseTicketsUI");
}
private void AddMinuteAndCheckTicket(ulong userID, BasePlayer playerRef)
{
string sid = userID.ToString();
if (!playtimeMinutes.ContainsKey(sid))
playtimeMinutes[sid] = 0;
playtimeMinutes[sid]++;
Interface.Oxide.DataFileSystem.WriteObject(PlaytimeDataFile, playtimeMinutes);
BasePlayer p = playerRef;
if (p == null || !p.IsConnected)
p = BasePlayer.FindByID(userID);
if (p != null && p.IsConnected)
ShowTicketUI(p);
if (playtimeMinutes[sid] >= 60)
{
playtimeMinutes[sid] -= 60;
Interface.Oxide.DataFileSystem.WriteObject(PlaytimeDataFile, playtimeMinutes);
BasePlayer bp = playerRef;
if (bp == null || !bp.IsConnected)
{
bp = BasePlayer.FindByID(userID);
}
if (bp != null && bp.IsConnected)
{
GiveTicketSafe(bp);
}
else
{
string name = bp != null ? bp.displayName : "Unknown";
GiveTicketOffline(userID.ToString(), name);
}
}
}
#endregion
#region Ticket assignment (thread-safe)
private void GiveTicketSafe(BasePlayer player)
{
if (player == null) return;
int assigned = -1;
lock (ticketLock)
{
assigned = GetNextAvailableTicketLocked();
if (assigned == -1)
{
Puts("PulseTickets: билеты закончились (1-" + MaxTickets + ").");
}
else
{
var info = new TicketInfo
{
steamid = player.UserIDString,
name = player.displayName,
discord = GetSavedDiscord(player.UserIDString)
};
ticketDatabase[assigned] = info;
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
}
}
if (assigned != -1)
{
string discord = GetSavedDiscord(player.UserIDString);
if (string.IsNullOrEmpty(discord))
{
player.ChatMessage($"<color=#00ff00>Вы получили билет № {assigned}.</color>\n" +
$"<color=#ffff00>Не забудьте использовать команду /discord ВашDiscord, чтобы мы могли связаться с вами в случае выигрыша.</color>");
}
else
{
player.ChatMessage($"<color=#00ff00>Вы получили билет № {assigned}.</color>\n" +
$"<color=#ffff00>Мы свяжемся с вами через Discord: {discord}</color>");
}
Puts($"PulseTickets: выдан билет #{assigned} игроку {player.displayName} ({player.UserIDString}).");
ShowTicketUI(player);
}
else
{
if (player != null && player.IsConnected)
player.ChatMessage("<color=#ff4444>Нет доступных билетов (1–30000).</color>");
}
}
private void GiveTicketOffline(string steamid, string name)
{
int assigned = -1;
lock (ticketLock)
{
assigned = GetNextAvailableTicketLocked();
if (assigned == -1)
{
Puts("PulseTickets: билеты закончились (1-" + MaxTickets + ").");
}
else
{
var info = new TicketInfo
{
steamid = steamid,
name = name ?? "Unknown",
discord = GetSavedDiscord(steamid)
};
ticketDatabase[assigned] = info;
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
Puts($"PulseTickets: выдан оффлайн-билет #{assigned} для steamid {steamid} ({name}).");
}
}
}
private int GetNextAvailableTicketLocked()
{
for (int i = 1; i <= MaxTickets; i++)
{
if (!ticketDatabase.ContainsKey(i))
return i;
}
return -1;
}
#endregion
#region Discord command & helpers
[ChatCommand("discord")]
private void CmdDiscord(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (args == null || args.Length == 0)
{
player.ChatMessage("Использование: /discord ВашDiscord");
return;
}
string discord = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(discord))
{
player.ChatMessage("Ошибка: пустое имя Discord. Использование: /discord ВашDiscord");
return;
}
string sid = player.UserIDString;
if (discordBySteam.ContainsKey(sid) && !string.IsNullOrEmpty(discordBySteam[sid]))
{
player.ChatMessage($"Ваш Discord уже зарегистрирован: <color=#00ffff>{discordBySteam[sid]}</color>");
return;
}
discordBySteam[sid] = discord;
Interface.Oxide.DataFileSystem.WriteObject(DiscordDataFile, discordBySteam);
bool updatedAny = false;
lock (ticketLock)
{
foreach (var kv in new Dictionary<int, TicketInfo>(ticketDatabase))
{
if (kv.Value != null && kv.Value.steamid == sid)
{
kv.Value.discord = discord;
ticketDatabase[kv.Key] = kv.Value;
updatedAny = true;
}
}
if (updatedAny)
{
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
}
}
player.ChatMessage($"Ваш Discord <color=#00ffff>{discord}</color> успешно сохранён.");
if (updatedAny)
player.ChatMessage("Ваш Discord также добавлен к уже выданным билетам.");
}
private string GetSavedDiscord(string steamid)
{
if (string.IsNullOrEmpty(steamid)) return null;
if (discordBySteam.TryGetValue(steamid, out string d))
return string.IsNullOrEmpty(d) ? null : d;
return null;
}
private string GetSavedDiscord(ulong steamid)
{
return GetSavedDiscord(steamid.ToString());
}
#endregion
#region Persistence helpers
private void SavePlaytimeFor(ulong userID)
{
string sid = userID.ToString();
if (playtimeMinutes.ContainsKey(sid))
{
Interface.Oxide.DataFileSystem.WriteObject(PlaytimeDataFile, playtimeMinutes);
}
}
private void SaveAllData()
{
try
{
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
Interface.Oxide.DataFileSystem.WriteObject(PlaytimeDataFile, playtimeMinutes);
Interface.Oxide.DataFileSystem.WriteObject(DiscordDataFile, discordBySteam);
Puts("PulseTickets: все данные сохранены.");
}
catch (Exception ex)
{
Puts("PulseTickets: ошибка при сохранении данных: " + ex);
}
}
#endregion
#region Utility admin commands (optional)
[ConsoleCommand("validatetickets")]
private void CmdValidateTicketsConsole(ConsoleSystem.Arg arg)
{
// Si lo ejecuta un jugador desde F1, comprobamos admin
BasePlayer player = arg.Player();
if (player != null && !player.IsAdmin)
{
arg.ReplyWith("Команда доступна только администраторам.");
return;
}
Dictionary<int, List<TicketInfo>> grouped = new Dictionary<int, List<TicketInfo>>();
foreach (var kv in ticketDatabase)
{
if (!grouped.ContainsKey(kv.Key))
grouped[kv.Key] = new List<TicketInfo>();
grouped[kv.Key].Add(kv.Value);
}
int fixedCount = 0;
foreach (var group in grouped)
{
int ticketNumber = group.Key;
List<TicketInfo> owners = group.Value;
if (owners.Count > 1)
{
Puts($"VALIDATE: Ticket #{ticketNumber} имеет {owners.Count} владельцев. Исправление...");
TicketInfo keeper = owners[0];
ticketDatabase[ticketNumber] = keeper;
for (int i = 1; i < owners.Count; i++)
{
TicketInfo dup = owners[i];
int newTicket = -1;
lock (ticketLock)
{
newTicket = GetNextAvailableTicketLocked();
if (newTicket != -1)
{
ticketDatabase[newTicket] = new TicketInfo
{
steamid = dup.steamid,
name = dup.name,
discord = dup.discord
};
}
}
if (newTicket != -1)
{
Puts($"VALIDATE: {dup.name} ({dup.steamid}) переназначен на билет #{newTicket}");
}
else
{
Puts("VALIDATE: Билеты закончились — невозможно переназначить!");
}
fixedCount++;
}
}
}
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
arg.ReplyWith($"Проверка завершена. Исправлено дубликатов: {fixedCount}.");
Puts($"VALIDATE: Завершено. Исправлено дубликатов: {fixedCount}.");
}
[ChatCommand("validatetickets")]
private void CmdValidateTickets(BasePlayer player, string cmd, string[] args)
{
if (player == null) return;
if (!player.IsAdmin)
{
player.ChatMessage("Команда доступна только администраторам.");
return;
}
Dictionary<int, List<TicketInfo>> grouped = new Dictionary<int, List<TicketInfo>>();
foreach (var kv in ticketDatabase)
{
if (!grouped.ContainsKey(kv.Key))
grouped[kv.Key] = new List<TicketInfo>();
grouped[kv.Key].Add(kv.Value);
}
int fixedCount = 0;
foreach (var group in grouped)
{
int ticketNumber = group.Key;
List<TicketInfo> owners = group.Value;
if (owners.Count > 1)
{
Puts($"VALIDATE: Ticket #{ticketNumber} имеет {owners.Count} владельцев. Исправление...");
TicketInfo keeper = owners[0];
ticketDatabase[ticketNumber] = keeper;
for (int i = 1; i < owners.Count; i++)
{
TicketInfo dup = owners[i];
int newTicket;
lock (ticketLock)
{
newTicket = GetNextAvailableTicketLocked();
if (newTicket != -1)
{
ticketDatabase[newTicket] = new TicketInfo
{
steamid = dup.steamid,
name = dup.name,
discord = dup.discord
};
}
}
if (newTicket != -1)
{
Puts($"VALIDATE: {dup.name} ({dup.steamid}) переназначен на билет #{newTicket}");
}
else
{
Puts("VALIDATE: Билеты закончились — невозможно переназначить!");
}
fixedCount++;
}
}
}
Interface.Oxide.DataFileSystem.WriteObject(TicketDataFile, ticketDatabase);
player.ChatMessage($"Проверка завершена. Исправлено дубликатов: {fixedCount}.");
Puts($"VALIDATE: Завершено. Исправлено дубликатов: {fixedCount}.");
}
[ChatCommand("ticketinfo")]
private void CmdTicketInfo(BasePlayer player, string cmd, string[] args)
{
if (player == null) return;
if (!player.IsAdmin)
{
player.ChatMessage("Только админам.");
return;
}
if (args.Length == 0 || !int.TryParse(args[0], out int tnum))
{
player.ChatMessage("Использование: /ticketinfo <номер>");
return;
}
if (ticketDatabase.TryGetValue(tnum, out TicketInfo info))
{
player.ChatMessage($"Билет #{tnum}: SteamID {info.steamid}, Имя: {info.name}, Discord: {info.discord ?? "не задан"}");
}
else
{
player.ChatMessage($"Билет #{tnum} не найден (не выдан).");
}
}
[ChatCommand("mytickets")]
private void CmdMyTickets(BasePlayer player, string cmd, string[] args)
{
if (player == null) return;
string sid = player.UserIDString;
List<int> my = new List<int>();
foreach (var kv in ticketDatabase)
{
if (kv.Value != null && kv.Value.steamid == sid)
my.Add(kv.Key);
}
if (my.Count == 0)
player.ChatMessage("У вас ещё нет билетов.");
else
player.ChatMessage("Ваши билеты: " + string.Join(", ", my));
}
#endregion
}
}