C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using ConVar;
using Network;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;
using WebSocketSharp;
namespace Oxide.Plugins {
/*
* This Version Changes:
* - Some bug fixes
* - Now works with Carbon
* - New webhook config
* - Fix ADR support.*/
[Info("Auto Demo Record", "Pho3niX90", "1.2.93")]
[Description("Automatic recording based on conditions.")]
internal class AutoDemoRecord : RustPlugin {
const string PermissionRecord = "autodemorecord.record";
private List<ARReport> _reports = new List<ARReport>();
List<PendingUploads> _pendingUploads = new List<PendingUploads>();
private Dictionary<ulong, Timer> _timers = new Dictionary<ulong, Timer>();
private new Dictionary<ulong, BlacklistEntry> _blacklistPlayers = new Dictionary<ulong, BlacklistEntry>();
private ARConfig config;
int lastSavedCount = 0;
int lastSavedCount_blacklist = 0;
int _discordSize = 6000000;
const string DISCORD_INTRO_URL = "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
[PluginReference]
Plugin ServerArmour;
bool HasPermission(BasePlayer player, string permissionName = PermissionRecord) {
return permission.UserHasPermission(player.UserIDString, permissionName);
}
private void Loaded() {
if (!permission.PermissionExists(PermissionRecord, this)) permission.RegisterPermission(PermissionRecord, this);
lang.RegisterMessages(new Dictionary<string, string> {
["Recording Started"]
= "Recording started for player {0}, eta is {1} mins", ["Recording Ended"] = "Recording finished for player {0}, player was recorded for {1} mins", ["Your Recording Started"] = "Your recording has started, and will auto stop in {0} mins", ["Your Recording Ended"] = "Your recording has ended."
}
, this);
LoadData();
foreach (var player in BasePlayer.activePlayerList) {
CheckUploads(player);
}
}
private void OnPlayerConnected(BasePlayer player) {
CheckUploads(player);
if (_blacklistPlayers.ContainsKey(player.userID)) {
_recordOnInterval(player.userID, _blacklistPlayers[player.userID]);
}
else if (config.AR_Fyhack_Seconds > 0 && player.lastViolationType.Equals(AntiHackType.FlyHack)) {
float secondsAgo = (UnityEngine.Time.realtimeSinceStartup - player.lastViolationTime);
if (secondsAgo <= config.AR_Fyhack_Seconds) {
API_StartRecordingWithWebhook(player, $"Flyhacked {secondsAgo:0.000} seconds ago", null);
}
}
}
private void OnPlayerDisconnected(BasePlayer player) {
StopRecording(player);
if (_timers.ContainsKey(player.userID) && !_timers[player.userID].Destroyed) {
_timers[player.userID].Destroy();
}
}
void OnPlayerCommand(BasePlayer player, string command, string[] args) {
if (!command.ToLower().Equals("report") || args.Length < 2) return;
var target = BasePlayer.Find(args[0]);
if (target == null) return;
List<string> reason = args.Skip(1).ToList();
var report = new ARReport(player.UserIDString, player.displayName, target.UserIDString, target.displayName, "Report", string.Join(" ", reason), "Report");
_reports.Add(report);
ProcessF7(target.userID, report);
}
private void Unload() {
SaveData();
foreach (var player in BasePlayer.activePlayerList) {
if (player.Connection.IsRecording) StopRecording(player);
}
_pendingUploads.Clear();
foreach (Timer timer in _timers.Values) {
timer.Destroy();
}
_timers.Clear();
_reports.Clear();
}
string GetMsg(string key) => lang.GetMessage(key, this);
[ChatCommand("testrec")]
private void cmdTestRec(BasePlayer player, string command, string[] args) {
if (player.IsAdmin || player.UserIDString.Equals("76561198007433923")) {
var report = new ARReport(player.UserIDString, player.displayName, player.UserIDString, player.displayName, "Test Subejct", "Test description/message", "Test type");
_reports.Add(report);
ProcessF7(player.userID, report);
}
}
[ChatCommand("record")]
private void cmdRecord(BasePlayer player, string command, string[] args) {
if (!HasPermission(player)) {
SendReply(player, "You do not have rights to use this command.");
return;
}
SendReply(player, string.Format(GetMsg("Your Recording Started"), config.AR_Report_Length_Self));
API_StartRecording4(player, $"Player {player.displayName} initiated self recording.", config.AR_Report_Length_Self, config.AR_Discord_Self_Webhook);
}
[ChatCommand("record.stop")]
private void cmdRecordStop(BasePlayer player, string command, string[] args) {
if (!HasPermission(player)) {
SendReply(player, "You do not have rights to use this command.");
return;
}
SendReply(player, string.Format(GetMsg("Your Recording Ended"), config.AR_Report_Length_Self));
StopRecording(player, config.AR_Discord_Webhook);
}
[ConsoleCommand("autodemorecord.record")]
void cmdRecordConsole(ConsoleSystem.Arg arg) {
ulong playerId;
int minutes;
if (ulong.TryParse(arg.Args[0], out playerId) && int.TryParse(arg.Args[1], out minutes)) {
BasePlayer player = BasePlayer.FindByID(playerId);
API_StartRecording4(player, $"Recording started of {player.displayName}.", minutes, config.AR_Discord_Webhook);
}
else {
arg.ReplyWith("Syntax: autodemorecord.record steam64id minutes");
}
}
void _recordOnInterval(ulong playerId, BlacklistEntry entry) {
BasePlayer player = BasePlayer.FindByID(playerId);
if (player != null && player.IsConnected) API_StartRecordingWithLength(player, $"Recording started of {player.displayName}.", entry.recordingMinutes);
}
[ConsoleCommand("autodemorecord.blacklist")]
void cmdRecordBlacklist(ConsoleSystem.Arg arg) {
string action = "";
ulong playerId = 0;
int minutes = 0;
int interval = 0;
action = arg.Args.Length > 0 ? arg.Args[0].ToLower() : "";
switch (action) {
case "add" : if (arg.Args.Length == 4 && ulong.TryParse(arg.Args[1], out playerId) && int.TryParse(arg.Args[2], out minutes) && int.TryParse(arg.Args[3], out interval)) {
if (minutes == 0 && interval == 0) {
goto default;
}
if (_blacklistPlayers.ContainsKey(playerId)) {
arg.ReplyWith("Player already blacklisted, remove first.");
}
else {
var entry = new BlacklistEntry(minutes, interval);
if (_blacklistPlayers.TryAdd(playerId, entry)) {
_recordOnInterval(playerId, entry);
SaveData();
}
}
}
else {
goto default;
}
break;
case "remove" : if (arg.Args.Length > 0) {
if (_blacklistPlayers.ContainsKey(playerId)) {
if (_blacklistPlayers.Remove(playerId)) {
arg.ReplyWith("Player removed from blacklist");
}
}
else {
arg.ReplyWith("Player not blacklisted");
}
}
else {
goto default;
}
break;
default : arg.ReplyWith("Syntax: autodemorecord.blacklist add [steam64id] [recordingMinutes] [intervalMinutes]\nautodemorecord.blacklist remove [steam64id]");
break;
}
}
private void API_Report(string reporterId, string reporterName, string targetId, string targetName, string reason) => ProcessF7(ulong.Parse(targetId), new ARReport(reporterId, reporterName, targetId, targetName, reason));
void OnPlayerReported(BasePlayer reporter, string targetName, string targetId, string subject, string message, string type) {
ulong targetIdLong = 0;
if (ulong.TryParse(targetId, out targetIdLong)) {
var report = new ARReport(reporter.UserIDString, reporter.displayName, targetId, targetName, subject, message, type);
_reports.Add(report);
ProcessF7(targetIdLong, report);
}
}
void ProcessF7(ulong targetId, ARReport report = null) {
BasePlayer accused = BasePlayer.FindByID(targetId);
if (accused == null) return;
if (accused.IsConnected) {
// record player only if he has reaced the amount in the config.And only when there is no recording active.if (CheckReports(accused) >= config.AR_Report)
{
if (!accused.Connection.IsRecording) {
StartRecording(accused, report);
}
else {
//TODO extend recording period.}
}
else if (config.AR_SendAllReports) {
NotifyDiscord(accused, null, true);
}
}
}
private void API_StartRecording(BasePlayer player, string reason) => StartRecording(player, null, reason);
/*
private void API_StartRecording(BasePlayer player, string reason, int length) =>
StartRecording(player, null, reason, null, length);
private void API_StartRecording(BasePlayer player, string reason, string webhook) =>
StartRecording(player, null, reason, webhook);
*/
private void API_StartRecording(BasePlayer player, string reason, int length, string webhook) => StartRecording(player, null, reason, webhook, length);
// broken method overloading fixes
private void API_StartRecordingWithLength(BasePlayer player, string reason, int length) => StartRecording(player, null, reason, null, length);
private void API_StartRecordingWithWebhook(BasePlayer player, string reason, string webhook) => StartRecording(player, null, reason, webhook);
private void API_StartRecording4(BasePlayer player, string reason, int length, string webhook) => StartRecording(player, null, reason, webhook, length);
void StartRecording(BasePlayer player, ARReport report = null, string reason = null, string webhook = null, int length = 0) {
Puts($"API_StartRecording called with reason: {reason}");
if (player == null) {
Puts("Plugin sent a null player object.Cannot record.");
return;
}
if (player.Connection.IsRecording) {
Puts($"Already recording for {player.displayName}");
return;
}
if (length == 0) {
Puts($"Recording length passed is 0, so defaulting to {config.AR_Report_Length}");
length = config.AR_Report_Length;
}
var msg = string.Format(GetMsg("Recording Started"), player.UserIDString, length);
if (report != null) {
AddReport(player.userID, report);
}
if (!reason.IsNullOrEmpty()) {
AddReason(player.userID, reason);
}
else {
AddReason(player.userID, "N/A");
}
if (!webhook.IsNullOrEmpty() && webhook.Equals(config.AR_Discord_Self_Webhook)) {
IsSelfReport(player.userID);
}
if (config.AR_Discord_Notify_RecordStart || config.AR_SendAllReports) NotifyDiscord(player, msg, true, webhook);
if (config.AR_Upload_CombatLog) GenCombatLog(player, string.Format("{0} {1:yyyy-MM-dd-hhmmss}.txt", player.UserIDString, DateTime.Now));
player.StartDemoRecording();
int recordingTime = length * 60;
if (length > 0) {
_timers[player.userID] = timer.Repeat(10, (recordingTime / 10) + 1, () => MonitorSplit(player, _timers[player.userID], webhook, length));
}
}
void StopRecording(BasePlayer player, string wh = "") {
if (player.Connection.IsRecording) {
PendingUploads pup = GetUploads(player.userID);
if (pup != null && pup.isSelfRecord) {
SendReply(player, GetMsg("Your Recording Ended"));
}
if (config.AR_Upload || !config.AR_SaveRecording) AddFile(player.Connection);
if (!config.AR_SaveRecording) {
if (!wh.IsNullOrEmpty() || !GetWebhook(pup.isSelfRecord).IsNullOrEmpty()) {
typeof(Connection).GetField("recordHeader", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(player.Connection, null);
}
else {
PrintWarning("Webhooks not setup.Won't upload.");
}
}
player.StopDemoRecording();
// upload
if (pup != null) {
UploadFiles(pup, player, wh);
if (config.AR_Clear_Counter) {
_reports.RemoveAll(x => x.targetId == player.UserIDString);
}
}
_timers.FirstOrDefault(x => x.Key == player.userID).Value?.Destroy();
CheckUploads(player);
}
if (_blacklistPlayers.ContainsKey(player.userID)) {
var entry = _blacklistPlayers[player.userID];
Puts($"{player.displayName} is blacklisted, will start another recording again in {entry.intervalMinutes}m for {entry.recordingMinutes}m");
try {
timer.Once(entry.intervalMinutes * 60, () => _recordOnInterval(player.userID, entry));
}
catch (Exception e) {
if (player && player.IsConnected) {
_recordOnInterval(player.userID, entry);
}
else {
Puts($"Blacklisted Player {player.displayName} went offline.");
}
}
}
}
void SplitRecording(BasePlayer player, string wh) {
if (player == null || !player.Connection.IsRecording) return;
AddFile(player.Connection);
if (!config.AR_SaveRecording) {
typeof(Connection).GetField("recordHeader", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(player.Connection, null);
}
player?.StopDemoRecording();
UploadFiles(GetUploads(player.userID), player, wh);
player?.StartDemoRecording();
}
void MonitorSplit(BasePlayer player, Timer timer, string wh, int length = 0) {
Connection conn = player.Connection;
int recordLength = length > 0 ? length * 60 : config.AR_Report_Length * 60;
if (conn != null && conn.IsRecording) {
if ((recordLength <= Math.Round(GetTotalSecondsRecorded(conn))) || timer.Destroyed || !player.IsConnected) {
StopRecording(player, wh);
timer.Destroy();
_timers.Remove(player.userID);
}
else if (config.AR_Discord_Split && conn.RecordFilesize >= _discordSize) {
//Puts($"conn.RecordFilesize {conn.RecordFilesize}");
SplitRecording(player, wh);
}
}
else {
timer.Destroy();
}
}
// Temporary reflection until fields are exposed.byte[] GetDemoFileBytes(Connection conn)
{
if (conn == null) {
PrintWarning($"connection is null");
return null;
}
if (!conn.IsRecording) {
PrintWarning($"no recording active");
return null;
}
// Temporary reflection until fields are exposed.var recordStream = typeof(Connection).GetField("recordStream", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(conn);
if (recordStream == null) {
PrintWarning($"record stream empty");
return null;
}
// Temporary reflection until fields are exposed.var recordHeader = typeof(Connection).GetField("recordHeader", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(conn);
if (recordHeader != null) {
// Manually converting data to a byte array since System.IO is unavailable to us.var demoRecordHeader = recordHeader as Demo.Header;
demoRecordHeader.length = (long)conn.RecordTimeElapsed.TotalMilliseconds;
var protoBytes = demoRecordHeader.ToProtoBytes();
List<byte> bytes = new List<byte>() {
0x10
}
;
var encoding = new UTF8Encoding(false, true);
bytes.AddRange(encoding.GetBytes("RUST DEMO FORMAT"));
bytes.AddRange(GetIntBytes(protoBytes.Length));
bytes.AddRange(protoBytes);
bytes.AddRange(encoding.GetBytes(new char[] {
'\0'
}
));
bytes.AddRange((byte[])recordStream.GetType().GetMethod("ToArray", BindingFlags.Instance | BindingFlags.Public).Invoke(recordStream, Array.Empty<object>()));
//UploadFile(fileName, bytes.ToArray());
return bytes.ToArray();
}
else {
PrintWarning($"record header empty");
return null;
}
}
byte[] GetIntBytes(int value) {
var buffer = new byte[4];
buffer[0] = (byte)value;
buffer[1] = (byte)(value >> 8);
buffer[2] = (byte)(value >> 16);
buffer[3] = (byte)(value >> 24);
return buffer;
}
int CheckReports(BasePlayer player) => config.AR_Report_Seconds > 0 ? this._reports.Count(x => secondsAgo(x.created) <= config.AR_Report_Seconds && x.targetId == player.UserIDString) : this._reports.Count(x => x.targetId == player.UserIDString);
int secondsAgo(DateTime timeFrom) {
DateTime now = DateTime.UtcNow;
return (int)Math.Round((now - timeFrom).TotalSeconds);
}
DiscordMessageConfig GenerateFieldContent(BasePlayer player, ARReport report, string msgRec, string avatar = "") {
List<FieldConfig> content = new List<FieldConfig>();
if (report != null) {
string targetUserLink = $"{report.reporterName}\n{report.reporterId}\n[Steam](https://steamcommunity.com/profiles/{player.UserIDString}) - [SA](https://io.serverarmour.com/profile/{player.UserIDString}) - [BM](https://www.battlemetrics.com/rcon/players?filter%5Bsearch%5D={player.UserIDString})";
content.Add(new FieldConfig() {
Title = "Reporter", Value = targetUserLink, Inline = true, Enabled = true, Order = 3
}
);
content.Add(new FieldConfig() {
Title = "Report Subject", Value = $"{report.type}: {report.subject}", Inline = false, Enabled = true, Order = 4
}
);
content.Add(new FieldConfig() {
Title = "Report Message", Value = report.message, Inline = true, Enabled = true, Order = 5
}
);
}
else {
content.Add(new FieldConfig() {
Title = "Reason", Value = string.Empty, Inline = false, Enabled = true, Order = 6
}
);
}
string userLink = $"[{player?.displayName}\n{player.UserIDString}](https://io.serverarmour.com/profile/{player.UserIDString})";
content.Add(new FieldConfig() {
Title = "Player", Value = userLink, Inline = true, Enabled = true, Order = 2
}
);
content.Add(new FieldConfig() {
Title = covalence.Server.Name, Value = $"[steam://connect/{covalence.Server.Address.MapToIPv4()}:{covalence.Server.Port}](steam://connect/{covalence.Server.Address}:{covalence.Server.Port})", Inline = false, Enabled = true, Order = 7
}
);
var msg = new DiscordMessageConfig {
Content = string.Empty, Embed = new EmbedConfig {
Title = config.AR_Discord_WebhookTitle, Description = msgRec, Color = config.AR_Discord_Color, Image = string.Empty, Thumbnail = avatar, // "https://assets.umod.org/images/icons/user/5e6f8629518b5.jpg",
Fields = content, Footer = new FooterConfig {
IconUrl = "https://assets.umod.org/images/icons/user/5e6f8629518b5.jpg", Text = $"{Title} V{Version} by {Author}", Enabled = true
}
, Enabled = true
}
}
;
return msg;
}
void NotifyDiscord(BasePlayer player, string msgRec, bool isStart = false, string webhook = null) {
string webHook = !webhook.IsNullOrEmpty() && !webhook.Equals(DISCORD_INTRO_URL) ? webhook : config.AR_Discord_Webhook;
if (!webHook.IsNullOrEmpty() && !config.Equals(DISCORD_INTRO_URL) && (config.AR_Discord_Notify_RecordStart || config.AR_Discord_Notify_RecordStop)) {
PendingUploads pup = GetUploads(player.userID);
if (pup == null) return;
DiscordMessageConfig msg = null;
if ((isStart && config.AR_Discord_Notify_RecordStartMsg) || (!isStart && config.AR_Discord_Notify_RecordStopMsg) || config.AR_SendAllReports) {
msg = GenerateFieldContent(player, pup.report, msgRec);
}
else {
msg = GenerateFieldContent(player, null, msgRec);
}
try {
webrequest.Enqueue($"https://steamcommunity.com/profiles/{player.userID}?xml=1", string.Empty, (code, result) => {
if (code >= 200 && code <= 204) {
msg.Embed.Thumbnail = new Regex(@"(?<=<avatarMedium>[\w\W]+)https://.+\.jpg(?=[\w\W]+<\/avatarMedium>)", RegexOptions.Compiled).Match(result).Value;
DiscordMessage message = ParseMessage(msg);
SendDiscordMessage(webHook, message);
}
else {
throw new Exception();
}
}
, this);
}
catch (Exception e) {
if (!webhook.IsNullOrEmpty()) {
DiscordMessage message = ParseMessage(msg);
SendDiscordMessage(webHook, message);
}
else {
PrintWarning("Webhooks not setup.Won't upload.");
}
}
}
}
#region Configuration
string GetWebhook(bool isSelfStart) => isSelfStart && !config.AR_Discord_Self_Webhook.Replace(DISCORD_INTRO_URL, "").IsNullOrEmpty() ? config.AR_Discord_Self_Webhook : config.AR_Discord_Webhook.Replace(DISCORD_INTRO_URL, "");
private class ARConfig {
// Config default vars
public int AR_Report = 2;
public int AR_Report_Length = 5;
public int AR_Report_Length_Self = 10;
public bool AR_Clear_Counter = false;
public string AR_Discord_Webhook = DISCORD_INTRO_URL;
public string AR_Discord_Self_Webhook = DISCORD_INTRO_URL;
public string AR_Discord_Color = "#de8732";
public bool AR_Discord_Notify_RecordStart = false;
public bool AR_Discord_Notify_RecordStop = false;
public int AR_Report_Seconds = 0;
public bool AR_Discord_Notify_RecordStartMsg = true;
public bool AR_Discord_Notify_RecordStopMsg = false;
public bool AR_Save_Reports = true;
public bool AR_Upload = true;
public bool AR_Upload_CombatLog = true;
public bool AR_Discord_Split = true;
public bool AR_SaveRecording = false;
public bool AR_SendAllReports = false;
public bool AR_Discord_SingleUpload = false;
public int AR_Fyhack_Seconds = 600;
public string AR_Discord_WebhookTitle = "Auto Demo Recorder";
// Plugin reference
private AutoDemoRecord plugin;
public ARConfig(AutoDemoRecord plugin) {
this.plugin = plugin;
/**
* Load all saved config values
* */
GetConfig(ref AR_Report, "Auto record after X reports");
GetConfig(ref AR_Report_Length, "Auto record for X minutes");
GetConfig(ref AR_Report_Length_Self, "Auto record for X minutes (Self record)");
GetConfig(ref AR_Clear_Counter, "Clear report counter after recording?");
GetConfig(ref AR_Discord_WebhookTitle, "Discord Webhook Title");
GetConfig(ref AR_Discord_Webhook, "Discord Webhook");
GetConfig(ref AR_Discord_Self_Webhook, "Discord Webhook - Self Record");
GetConfig(ref AR_Discord_Color, "Discord MSG Color");
GetConfig(ref AR_Discord_Notify_RecordStart, "Discord: Notify if recording is started");
GetConfig(ref AR_Discord_Notify_RecordStartMsg, "Discord: Include report with start message?");
GetConfig(ref AR_Discord_Notify_RecordStop, "Discord: Notify if recording is stopped");
GetConfig(ref AR_Discord_Notify_RecordStopMsg, "Discord: Include report with end message?");
GetConfig(ref AR_Discord_SingleUpload, "Discord: Single upload msg?");
GetConfig(ref AR_Report_Seconds, "Only record when reports within X seconds");
GetConfig(ref AR_Save_Reports, "Save/Load reports to datafile on reload");
GetConfig(ref AR_Upload, "Upload DEMO file to discord webhook?");
GetConfig(ref AR_SaveRecording, "Save recording to server?");
GetConfig(ref AR_Upload_CombatLog, "Upload Combat Log to discord webhook?");
GetConfig(ref AR_Discord_Split, "Split DEMO files for non nitro discord (8mb chunks)?");
GetConfig(ref AR_SendAllReports, "Send all reports/F7 to discord?");
GetConfig(ref AR_Fyhack_Seconds, "Record if FlyHacked in last X seconds");
plugin.SaveConfig();
}
private void GetConfig<T>(ref T variable, params string[] path) {
if (path.Length == 0) return;
if (plugin.Config.Get(path) == null) {
SetConfig(ref variable, path);
plugin.PrintWarning($"Added new field to config: {string.Join("/", path)}");
}
variable = (T)Convert.ChangeType(plugin.Config.Get(path), typeof(T));
}
private void SetConfig<T>(ref T variable, params string[] path) => plugin.Config.Set(path.Concat(new object[] {
variable
}
).ToArray());
}
protected override void LoadConfig() {
base.LoadConfig();
config = new ARConfig(this);
}
void LoadData() {
try {
_reports = Interface.Oxide.DataFileSystem.ReadObject<List<ARReport>>(this.Name);
lastSavedCount = _reports.Count();
}
catch (Exception e) {
Puts(e.Message);
}
try {
_blacklistPlayers = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, BlacklistEntry>>($"{this.Name}_blacklist");
lastSavedCount_blacklist = _blacklistPlayers.Count();
}
catch (Exception e) {
Puts(e.Message);
}
}
void SaveData() {
int records = _reports.Count();
int recordsDiff = records - lastSavedCount;
lastSavedCount = records;
if (config.AR_Save_Reports && recordsDiff != 0) {
try {
Interface.Oxide.DataFileSystem.WriteObject(this.Name, _reports, true);
}
catch (Exception e) {
Puts(e.Message);
}
}
records = _blacklistPlayers.Count();
recordsDiff = records - lastSavedCount_blacklist;
lastSavedCount_blacklist = records;
if (recordsDiff == 0) return;
try {
Interface.Oxide.DataFileSystem.WriteObject($"{this.Name}_blacklist", _blacklistPlayers, true);
}
catch (Exception e) {
Puts(e.Message);
}
}
protected override void LoadDefaultConfig() => PrintWarning("Generating new configuration file.");
#endregion
#region Classes
public class PendingUploads {
public ulong userID;
public ARReport report;
public string reason;
public List<PFile> files;
public bool shouldUpload;
public bool isSelfRecord;
public PendingUploads(BasePlayer player) {
this.userID = player.userID;
this.files = new List<PFile>();
this.reason = null;
this.report = null;
this.shouldUpload = false;
}
public PendingUploads AddReport(ARReport report) {
this.report = report;
return this;
}
public PendingUploads AddReason(string reason) {
this.reason = reason;
return this;
}
public PendingUploads AddFile(PFile file) {
this.files.Add(file);
return this;
}
public double GetTotalSeconds() {
return this.files.Sum(x => x.totalSeconds);
}
}
public class PFile {
public string fileName;
public byte[] fileBytes;
public double totalSeconds;
public PFile(string filename, byte[] bytes) {
this.fileName = filename;
this.fileBytes = bytes;
this.totalSeconds = 0;
}
public PFile(string filename, byte[] bytes, double totalSeconds) {
this.fileName = filename;
this.fileBytes = bytes;
this.totalSeconds = totalSeconds;
}
}
public class ARReport {
public string reporterName;
public string reporterId;
public string targetName;
public string targetId;
public string subject;
public string message;
public string type;
public DateTime created;
public ARReport() {
}
public ARReport(string reporterId, string reporterName, string targetId, string targetName, string subject, string message, string type) {
this.reporterId = reporterId;
this.reporterName = reporterName;
this.targetId = targetId;
this.targetName = targetName;
this.subject = subject;
this.message = message;
this.type = type;
this.created = DateTime.UtcNow;
}
public ARReport(string targetId, string targetName) {
this.targetName = targetName;
this.targetId = targetId;
}
public ARReport(string reporterId, string reporterName, string targetId, string targetName, string reason) {
this.reporterId = reporterId;
this.reporterName = reporterName;
this.targetName = targetName;
this.targetId = targetId;
this.message = reason;
}
}
public class BlacklistEntry {
public BlacklistEntry() {
}
public BlacklistEntry(int rMin, int iMin) {
this.recordingMinutes = rMin;
this.intervalMinutes = iMin;
}
public int recordingMinutes;
public int intervalMinutes;
}
#endregion
#region Combat
private void GenCombatLog(BasePlayer target, string filename, int logSize = 30) {
var cLog = CombatLog.Get(target.userID);
TextTable textTable = new TextTable();
textTable.AddColumns(new[] {
"time", "attacker", "id", "target", "id", "weapon", "ammo", "area", "distance", "old_hp", "new_hp"
}
);
int num = cLog.Count - logSize;
int delay = ConVar.Server.combatlogdelay;
timer.Once(delay, () => {
foreach (CombatLog.Event evt in cLog) {
string time = (UnityEngine.Time.realtimeSinceStartup - evt.time).ToString("0s");
bool isPlayer = evt.attacker == "you";
string attackerName = isPlayer ? target.displayName : UintFind(evt.attacker_id);
string attacker_id = isPlayer ? target.UserIDString : evt.attacker_id.ToString();
isPlayer = evt.target == "you";
string targetName = isPlayer ? target.displayName : UintFind(evt.target_id);
string target_id = isPlayer ? target.UserIDString : evt.target_id.ToString();
textTable.AddRow(new string[] {
time, attackerName, attacker_id, targetName, target_id, evt.weapon, evt.ammo, HitAreaUtil.Format(evt.area).ToLower(), evt.distance.ToString("0.0m"), evt.health_old.ToString("0.0"), evt.health_new.ToString("0.0")
}
);
}
if (config.AR_Upload_CombatLog && !filename.IsNullOrEmpty()) {
if (config.AR_SaveRecording) {
SaveCombatLog(filename, textTable);
}
try {
AddFile(target.userID, filename, Encoding.ASCII.GetBytes(textTable.ToString()));
//AddFile(target.userID, filename, textTable.ToString().Select(x =>Convert.ToByte(x)).ToArray());
}
catch (OverflowException e) {
Puts($"Cannot upload combatlog due to an error: size={textTable.ToString().Length}, msg={e.Message}");
}
}
}
);
}
private void SaveCombatLog(string filename, TextTable data) => LogToFile($"{filename}_combatlog", data.ToString(), this, false);
private string UintFind(ulong id) {
BasePlayer player = null;
try {
player = BasePlayer.activePlayerList.First(x => x.net.ID.Equals(id));
}
catch (Exception e) {
}
return player != null ? player.displayName : id.ToString();
}
#endregion
#region Data Handling
PendingUploads CheckUploads(BasePlayer player) {
_pendingUploads.RemoveAll(x => x.userID == player.userID);
var pup = new PendingUploads(player);
_pendingUploads.Add(pup);
return pup;
}
public double GetTotalSecondsRecorded(Connection conn) {
return _pendingUploads.Find(x => x.userID == conn.userid).GetTotalSeconds() + conn.RecordTimeElapsed.TotalSeconds;
}
PendingUploads GetUploads(ulong userid) {
return _pendingUploads.Find(x => x.userID == userid);
}
private void AddFile(ulong userid, string filename, byte[] bytes) {
_pendingUploads.Find(x => x.userID == userid).AddFile(new PFile(filename, bytes));
}
private void AddFile(Connection conn) {
var fileBytes = GetDemoFileBytes(conn);
if (fileBytes != null) {
_pendingUploads.Find(x => x.userID == conn.userid) ?.AddFile(new PFile($"{conn.userid} {conn.RecordFilename.Split('/').Last()}", fileBytes, conn.RecordTimeElapsed.TotalSeconds));
}
}
private void AddReport(ulong userid, ARReport report) {
_pendingUploads.Find(x => x.userID == userid).AddReport(report);
}
private void IsSelfReport(ulong userid) {
_pendingUploads.Find(x => x.userID == userid).isSelfRecord = true;
}
private void AddReason(ulong userid, string reason) {
_pendingUploads.Find(x => x.userID == userid).AddReason(reason).isSelfRecord = false;
}
#endregion
#region Discord Embed
#region Send Embed Methods
/// <summary>
/// Headers when sending an embeded message
/// </summary>
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>() {
{
"Content-Type", "application/json"
}
}
;
/// <summary>
/// Sends the DiscordMessage to the specified webhook url
/// </summary>
/// <param name="url">Webhook url</param>
/// <param name="message">Message being sent</param>
private void SendDiscordMessage(string url, DiscordMessage message) {
webrequest.Enqueue(url, message.ToJson(), SendDiscordMessageCallback, this, RequestMethod.POST, _headers);
}
/// <summary>
/// Callback when sending the embed if any errors occured
/// </summary>
/// <param name="code">HTTP response code</param>
/// <param name="message">Response message</param>
private void SendDiscordMessageCallback(int code, string message) {
if (code != 204) {
if (code == 413) {
if (!this.config.AR_Discord_Split) {
this.config.AR_Discord_Split = true;
PrintWarning($"Upload failed due too split not enabled for a non boosted server.Reverting settting");
SaveConfig();
}
else {
PrintWarning($"{_discordSize} bytes too large for your server, decreasing by 50K");
_discordSize -= 50000;
}
}
PrintError(message);
}
}
/// <summary>
/// Sends the DiscordMessage to the specified webhook url with attachments
/// </summary>
/// <param name="url">Webhook url</param>
/// <param name="message">Message being sent</param>
/// <param name="files">Attachments to be added to the DiscordMessage</param>
private void SendDiscordAttachmentMessage(string url, DiscordMessage message, List<Attachment> files) {
List<IMultipartFormSection> formData = new List<IMultipartFormSection> {
new MultipartFormDataSection("payload_json", message.ToJson())
}
;
for (int i = 0; i < files.Count; i++) {
Attachment attachment = files[i];
formData.Add(new MultipartFormFileSection($"file{i + 1}", attachment.Data, attachment.Filename, attachment.ContentType));
}
InvokeHandler.Instance.StartCoroutine(SendDiscordAttachmentMessageHandler(url, formData));
}
private IEnumerator SendDiscordAttachmentMessageHandler(string url, List<IMultipartFormSection> data) {
UnityWebRequest www = UnityWebRequest.Post(url, data);
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError) {
PrintError($"{www.error} {www.downloadHandler.text}");
}
}
#endregion
#region Helper Methods
private const string OwnerIcon = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/47/47db946f27bc76d930ac82f1656f7a10707bb67d_full.jpg";
private void AddPluginInfoFooter(Embed embed) {
embed.AddFooter($"{Title} V{Version} by {Author}", OwnerIcon);
}
private string GetPositionField(Vector3 pos) {
return $"{pos.x:0.00} {pos.y:0.00} {pos.z:0.00}";
}
#endregion
#region Embed Classes
private class DiscordMessage {
/// <summary>
/// The name of the user sending the message changing this will change the webhook bots name
/// </summary>
[JsonProperty("username")]
private string Username {
get;
set;
}
/// <summary>
/// The avatar url of the user sending the message changing this will change the webhook bots avatar
/// </summary>
[JsonProperty("avatar_url")]
private string AvatarUrl {
get;
set;
}
/// <summary>
/// String only content to be sent
/// </summary>
[JsonProperty("content")]
private string Content {
get;
set;
}
/// <summary>
/// Embeds to be sent
/// </summary>
[JsonProperty("embeds")]
private List<Embed> Embeds {
get;
}
public DiscordMessage(string username = null, string avatarUrl = null) {
Username = username;
AvatarUrl = avatarUrl;
Embeds = new List<Embed>();
}
public DiscordMessage(string content, string username = null, string avatarUrl = null) {
Content = content;
Username = username;
AvatarUrl = avatarUrl;
Embeds = new List<Embed>();
}
public DiscordMessage(Embed embed, string username = null, string avatarUrl = null) {
Embeds = new List<Embed> {
embed
}
;
Username = username;
AvatarUrl = avatarUrl;
}
/// <summary>
/// Adds a new embed to the list of embed to send
/// </summary>
/// <param name="embed">Embed to add</param>
/// <returns>This</returns>
/// <exception cref="IndexOutOfRangeException">Thrown if more than 10 embeds are added in a send as that is the discord limit</exception>
public DiscordMessage AddEmbed(Embed embed) {
if (Embeds.Count >= 10) {
throw new IndexOutOfRangeException("Only 10 embed are allowed per message");
}
Embeds.Add(embed);
return this;
}
/// <summary>
/// A
// ... [Code preview truncated due to length. Full source in attached file] ...