C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
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.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;
using Physics = UnityEngine.Physics;
namespace Oxide.Plugins {
/*
* This Version Changes:
* - Add more detail to breached layer names.*/
[Info("ESP Detection", "Pho3niX90", "1.1.2")]
[Description("Records a demo of suspected ESPers")]
internal class EspDetection : RustPlugin {
bool debug = false;
[PluginReference]
Plugin AutoDemoRecord;
const string PermissionIgnore = "espdetection.ignore";
private Configuration config;
private int[] LAYERS_CAST = {
0, 4, 8, 21, 17
}
;
private int[] LAYERS_BLACKLIST = {
17
}
;
private readonly int BUFFER_TIME = 3;
private readonly string[] IGNORE_FIRST_SEEN = {
"wall.frame.shopfront", "wall.frame.shopfront.metal", "autoturret", "flameturret"
}
;
Dictionary<ulong, List<Violation>> _allViolations = new Dictionary<ulong, List<Violation>>();
List<ViolationSound> _allSounds = new List<ViolationSound>();
Dictionary<ulong, ViolationMeta> _maxViolation = new Dictionary<ulong, ViolationMeta>();
#region Helpers
bool HasPermission(BasePlayer player, string permissionName = PermissionIgnore) {
return permission.UserHasPermission(player.UserIDString, permissionName);
}
void Record(BasePlayer player, string msg) => AutoDemoRecord?.Call("API_StartRecording4", player, msg, config.ADR.recordMinutes, config.ADR.webhook ?? config.Discord.webhook);
double GetPercentage(int val) {
LogDebug($"{val} violations against {Samples()} samples");
return Math.Round(((double)val / (double)Samples()) * 100d);
}
int Samples() => config.General.checkInterval * config.Violations.intervalSamples;
bool CheckViolations(ulong playerId, int cnt) {
BasePlayer player = BasePlayer.FindByID(playerId);
//clear old data
try {
_allViolations[playerId] = _allViolations[playerId].Take(config.Violations.intervalSamples).ToList();
// ?.RemoveAll(x => x.dateTime < DateTime.Now.AddSeconds(-(Samples())));
_allSounds?.RemoveAll(x => x.dateTime < DateTime.Now.AddSeconds(-(Samples() + BUFFER_TIME)));
}
catch (Exception e) {
Puts(e.Message);
}
if (player != null && HasPermission(player)) {
return false;
}
double perc = Math.Min(GetPercentage(cnt), 99);
LogDebug(perc.ToString());
if (_maxViolation.ContainsKey(playerId)) {
if (_maxViolation[playerId].maxViolation < perc) {
_maxViolation[playerId].dateTime = DateTime.Now;
_maxViolation[playerId].maxViolation = perc;
return false;
}
}
else {
_maxViolation.Add(playerId, new ViolationMeta() {
dateTime = DateTime.Now, maxViolation = perc
}
);
}
var trigger = false;
if (_maxViolation[playerId].dateTime < DateTime.Now.AddSeconds(-BUFFER_TIME)) {
LogDebug($"violationCurrentMax {_maxViolation[playerId].maxViolation}, maxViolationsConfig {config.Violations.maxViolationsPerc}");
trigger = _maxViolation[playerId].maxViolation >= config.Violations.maxViolationsPerc;
if (trigger) timer.Once(0.1f, () => {
_maxViolation[playerId].maxViolation = 0d;
_allViolations.Clear();
}
);
}
LogDebug($"Triggered: {trigger}, {perc}");
return trigger;
}
#endregion
#region Commands
[ChatCommand("testesp")]
private void StashTestPlaceCommand(BasePlayer player, string command, string[] args) {
if (!player.IsAdmin) return;
List<Violation> violations = new List<Violation>();
violations.Add(new Violation {
dateTime = DateTime.Now, distance = 50, rotation = 67, victim = 7656L, victimBlip = new Blip {
x = 47, y = 67
}
, victimName = "Random Player"
}
);
DiscordSend(player, violations);
}
[ConsoleCommand("testesp")]
private void StashTestPlaceCommandConsole(ConsoleSystem.Arg cmdArgs) {
if (!cmdArgs.IsAdmin && !cmdArgs.IsConnectionAdmin) return;
List<Violation> violations = new List<Violation>();
violations.Add(new Violation {
dateTime = DateTime.Now, distance = 50, rotation = 67, victim = 7656L, victimBlip = new Blip {
x = 47, y = 67
}
, victimName = "Pho3"
}
);
DiscordSend(new BasePlayer() {
userID = 76561199078488569L, UserIDString = "76561199078488569"
}
, violations);
}
[ChatCommand("testlayer")]
private void TestLayerCommand(BasePlayer player, string command, string[] args) {
if (!player.IsAdmin) return;
RaycastHit[] hitInfos = Physics.RaycastAll(player.eyes.HeadRay(), 100);
var hitsSorted = (from p in hitInfos orderby Vector3.Distance(p.transform.position, player.transform.position) //where LAYERS_CAST.Contains(p.transform.gameObject.layer)
select p).ToArray();
var firstEnt = hitsSorted[0].GetEntity();
BaseEntity blacklistedEntity;
for (int i = 0; i < hitsSorted.Length; i++) {
var hitInfo = hitsSorted[i];
// if (!LAYERS_BLACKLIST.Contains(hitInfo.transform.gameObject.layer)) continue;
if (debug) SendReply(player, i + $": Layer ({hitInfo.transform.gameObject.layer}): " + LayerMask.LayerToName(hitInfo.transform.gameObject.layer) + "\n Shortname: " + hitInfo.GetEntity()?.ShortPrefabName + "\n Dist: " + Vector3.Distance(hitInfo.transform.position, player.transform.position));
}
}
ViolationTestResult TestEsp(BasePlayer player, int distance) {
RaycastHit[] hitInfos = Physics.RaycastAll(player.eyes.HeadRay(), distance);
var hitsSorted = (from p in hitInfos orderby Vector3.Distance(p.transform.position, player.transform.position) where LAYERS_CAST.Contains(p.transform.gameObject.layer) select p).ToArray();
if (hitsSorted == null || !hitsSorted.Any()) return null;
var firstEnt = hitsSorted[0].GetEntity();
BaseEntity blacklistedEntity;
try {
blacklistedEntity = hitsSorted.FirstOrDefault(x => LAYERS_BLACKLIST.Contains(x.transform.gameObject.layer)).GetEntity();
}
catch (Exception e) {
return null;
}
/***
* TODO use breached layers and draw them to the radar image.*/
List<ViolationBreachedLayer> entitiesBreached = new List<ViolationBreachedLayer>();
try {
entitiesBreached = hitsSorted.Select(x => new ViolationBreachedLayer() {
id = x.transform.gameObject.layer, layerType = LayerMask.LayerToName(x.transform.gameObject.layer), objectName = x.transform.gameObject.name, position = x.transform.position
}
).ToList();
}
catch (Exception e) {
}
if (HasSight(player, blacklistedEntity.transform.position)) {
return null;
}
/***
* We ignore common first seen prefabs, that cause false positives.*/
if (IGNORE_FIRST_SEEN.Contains(firstEnt.ShortPrefabName)) {
return null;
}
for (int i = 0; i < hitsSorted.Length; i++) {
var hitInfo = hitsSorted[i];
if (LAYERS_BLACKLIST.Contains(hitInfo.transform.gameObject.layer)) {
if (debug) {
SendReply(player, i + $": Layer ({hitInfo.transform.gameObject.layer}): " + LayerMask.LayerToName(hitInfo.transform.gameObject.layer) + "\n Shortname: " + hitInfo.GetEntity()?.ShortPrefabName + "\n Dist: " + Vector3.Distance(hitInfo.transform.position, player.transform.position) );
}
else {
break;
}
}
}
return new ViolationTestResult() {
baseEntity = blacklistedEntity, breachedLayers = entitiesBreached
}
;
}
void SaveViolation(ulong userId, Violation violation) {
// save violation
if (_allViolations.ContainsKey(userId)) {
_allViolations[userId].Add(violation);
}
else {
_allViolations.Add(userId, new List<Violation>() {
violation
}
);
}
}
/// <summary>
/// Checks if the entity can see
/// </summary>
/// <param name="ent"></param>
/// <param name="pos"></param>
/// <returns></returns>
bool HasSight(BaseEntity ent, Vector3 pos) => ent.IsVisible(pos) && ent.IsVisibleAndCanSee(pos);
#endregion
#region Hooks
void OnPlayerVoice(BasePlayer player, byte[] data) {
if (MadeNoiseIn(player.userID, 1)) return;
LogDebug("No sound in 1 sec");
_allSounds.Add(new ViolationSound() {
playerId = player.userID, dateTime = DateTime.Now, type = SoundType.Voice
}
);
}
void OnServerInitialized() {
LoadConfig();
if (!permission.PermissionExists(PermissionIgnore, this)) permission.RegisterPermission(PermissionIgnore, this);
timer.Once(config.General.checkInterval, RefreshData);
}
void OnServerSave() {
ClearVoices();
}
#endregion
#region Main Logic
void RefreshData() {
try {
Stopwatch perfTotal = new Stopwatch();
Dictionary<ulong, ulong> dataList = new Dictionary<ulong, ulong>();
var allPlayers = BasePlayer.activePlayerList.Where(x => x.IdleTime < config.General.ignoreIdleTime ).ToList();
if (allPlayers != null && allPlayers.Count > 0) for (int i = 0, n = allPlayers.Count; i < n; i++) {
var currentPlayer = allPlayers[i];
if (currentPlayer == null) continue;
DoChecks(currentPlayer);
/***
* If we only want to look at players with weapons active, then we ignore this player if they have one active.*/
if (config.General.trackWeaponsOnly && !currentPlayer.IsHostileItem(currentPlayer.GetActiveItem())) continue;
var isOutside = currentPlayer.IsOutside();
var isAiming = currentPlayer.IsAiming;
var insideDistance = config.General.checkDistanceInside;
var outsideDistance = config.General.checkDistanceOutside;
var activeItem = currentPlayer.GetActiveItem();
var attachments = activeItem.contents?.itemList.Select(x => x.info.shortname).Distinct().ToArray();
// Lets check if there is specific settings for this weapon.//LogDebug($"Before Weapon");
var weaponConfig = config.Weapons.FirstOrDefault(x => x.shortname == activeItem.info.shortname);
if (weaponConfig != null) {
// Lets check if it should be tracked, if not, we ignore this player.if (!weaponConfig.track)
{
continue;
}
// set the weapon specific distances.insideDistance = weaponConfig.checkDistanceInside;
outsideDistance = weaponConfig.checkDistanceOutside;
}
var addMax = 0;
// Lets check if there is specific settings for these attachments, if the player is aiming.//LogDebug($"Before Attachment");
if (isAiming) {
var ignorePlayer = false;
foreach (var attachment in attachments) {
var attachmentConfig = config.Attachments.FirstOrDefault(x => x.shortname == attachment);
if (attachmentConfig != null) {
// Lets check if it should be tracked, if not, we ignore this player.if (!attachmentConfig.track)
{
ignorePlayer = true;
break;
}
addMax = Math.Max(attachmentConfig.checkDistanceAiming, addMax);
}
}
if (ignorePlayer) {
continue;
}
}
var distanceToCheck = (isOutside ? outsideDistance : insideDistance) + addMax;
var violationResult = TestEsp(currentPlayer, distanceToCheck);
var bEnt = violationResult.baseEntity;
if (bEnt == null) continue;
Violation violation = null;
if (bEnt is BasePlayer) {
var sawPlayer = bEnt as BasePlayer;
var distanceToPlayer = bEnt.Distance(currentPlayer);
/***
* Lets check if the player is far enough, if not lets ignore them
*/
if (config.General.ignoreDistance > distanceToPlayer) continue;
/***
* If the player is NPC, and we should ignore bots, then lets skip this player if they are NPC
*/
if (config.General.ignoreNpcBots && (sawPlayer.IsNpc || !sawPlayer.userID.IsSteamId())) continue;
/***
* Let's ignore teammates
*/
if (IsTeamMate(currentPlayer.userID, sawPlayer.userID)) continue;
/***
* Ignore friendly players
*/
if (currentPlayer.IsFriendly(sawPlayer)) continue;
// violation detected.violation = new Violation
{
dateTime = DateTime.Now, distance = distanceToPlayer, rotation = (int)Math.Round(currentPlayer.eyes.rotation.eulerAngles.y), victimName = sawPlayer.displayName, victim = sawPlayer.userID, victimBlip = new Blip {
x = (int)Math.Round(sawPlayer.transform.position.x - currentPlayer.transform.position.x), y = (int)Math.Round(sawPlayer.transform.position.y - currentPlayer.transform.position.y)
}
, breachedLayers = violationResult.breachedLayers
}
;
}
else if (bEnt is BuildingPrivlidge && config.General.trackTcs) {
var tc = bEnt as BuildingPrivlidge;
if (tc.authorizedPlayers.Where(x => x.userid == currentPlayer.userID)?.Any() ?? false) continue;
violation = new Violation {
dateTime = DateTime.Now, distance = bEnt.Distance(currentPlayer), rotation = (int)Math.Round(currentPlayer.eyes.rotation.eulerAngles.y), victim = bEnt.net.ID.Value, isPlayer = false, victimName = bEnt.ShortPrefabName, breachedLayers = violationResult.breachedLayers,
}
;
}
if (violation != null) SaveViolation(currentPlayer.userID, violation);
}
PerfStop(perfTotal, "Total");
timer.Once(config.General.checkInterval, RefreshData);
}
catch (Exception e) {
timer.Once(config.General.checkInterval, RefreshData);
}
}
void DoChecks(BasePlayer currentPlayer) {
if (currentPlayer.userID.IsSteamId()) {
if (!_allViolations.ContainsKey(currentPlayer.userID)) return;
var violations = _allViolations[currentPlayer.userID];
// assign voice data
for (int i = 0; i < violations.Count; i++) {
violations[i].lastVoice = LastVoice(violations[i].victim);
}
LogDebug($"violations: {violations.Count}");
if (CheckViolations(currentPlayer.userID, violations.Count)) {
try {
DiscordSend(currentPlayer, violations);
}
catch (Exception e) {
Puts(e.Message);
// add simple discord webhook.}
}
}
}
#endregion
#region Configuration
[Serializable]
private class Configuration {
[JsonProperty(PropertyName = "General")]
internal GeneralConfig General {
get;
set;
}
[JsonProperty(PropertyName = "Auto Demo Record")]
internal ADRConfig ADR {
get;
set;
}
[JsonProperty(PropertyName = "Discord")]
internal DiscordConfig Discord {
get;
set;
}
[JsonProperty(PropertyName = "Violations")]
internal ViolationsConfig Violations {
get;
set;
}
[JsonProperty(PropertyName = "Weapon Specific Configs")]
internal List<WeaponsConfig> Weapons {
get;
set;
}
[JsonProperty(PropertyName = "Attachment Specific Configs")]
internal List<AttachmentConfig> Attachments {
get;
set;
}
internal class GeneralConfig {
[JsonProperty(PropertyName = "Max distance to check (outside)")]
internal int checkDistanceOutside {
get;
set;
}
[JsonProperty(PropertyName = "Max distance to check (inside)")]
internal int checkDistanceInside {
get;
set;
}
[JsonProperty(PropertyName = "Check Interval (in seconds)")]
internal int checkInterval {
get;
set;
}
[JsonProperty(PropertyName = "Ignore Players Idle (in seconds)")]
internal int ignoreIdleTime {
get;
set;
}
[JsonProperty(PropertyName = "Ignore Team Mates")]
internal bool ignoreFriendly {
get;
set;
}
[JsonProperty(PropertyName = "Only track players with active weapons")]
internal bool trackWeaponsOnly {
get;
set;
}
[JsonProperty(PropertyName = "Ignore NPC's & Bot's")]
internal bool ignoreNpcBots {
get;
set;
}
[JsonProperty(PropertyName = "Track Tc's")]
internal bool trackTcs {
get;
set;
}
[JsonProperty(PropertyName = "Ignore distance shorter than X meters")]
internal float ignoreDistance {
get;
set;
}
}
internal class ViolationsConfig {
[JsonProperty(PropertyName = "Probability %")]
internal int maxViolationsPerc {
get;
set;
}
[JsonProperty(PropertyName = "Samples")]
internal int intervalSamples {
get;
set;
}
}
internal class ADRConfig {
[JsonProperty(PropertyName = "Record Length (in minutes)")]
internal int recordMinutes {
get;
set;
}
[JsonProperty(PropertyName = "Webhook to send recordings")]
internal string webhook {
get;
set;
}
}
internal class WeaponsConfig {
[JsonProperty(PropertyName = "Weapon Shortname")]
internal string shortname {
get;
set;
}
[JsonProperty(PropertyName = "Max distance to check (outside)")]
internal int checkDistanceOutside {
get;
set;
}
[JsonProperty(PropertyName = "Max distance to check (inside)")]
internal int checkDistanceInside {
get;
set;
}
[JsonProperty(PropertyName = "Track")]
internal bool track {
get;
set;
}
}
internal class AttachmentConfig {
[JsonProperty(PropertyName = "Attachment Shortname")]
internal string shortname {
get;
set;
}
[JsonProperty(PropertyName = "Add distance while aiming")]
internal int checkDistanceAiming {
get;
set;
}
[JsonProperty(PropertyName = "Track")]
internal bool track {
get;
set;
}
}
internal class DiscordConfig {
[JsonProperty(PropertyName = "Webhook")]
internal string webhook {
get;
set;
}
[JsonProperty(PropertyName = "Webhook Title")]
internal string title {
get;
set;
}
[JsonProperty(PropertyName = "Convert SteamIDs to Username embed")]
internal bool convertSteamId {
get;
set;
}
[JsonProperty(PropertyName = "Add Radar Image")]
internal bool radarImage {
get;
set;
}
[JsonProperty(PropertyName = "Attach ESP Samples")]
internal bool discordSendSampels {
get;
set;
}
[JsonProperty(PropertyName = "2 columns instead of 3")]
internal bool twoColumn {
get;
set;
}
}
}
protected override void LoadConfig() {
base.LoadConfig();
try {
config = Config.ReadObject<Configuration>();
if (config == null) throw new JsonException();
}
catch {
Config.WriteObject(config, false, $"{Interface.Oxide.ConfigDirectory}/{Name}.json.backup");
PrintError("The configuration file contains an error and has been replaced with a default config.\n" + "The error configuration file was saved in the.json.backup extension");
LoadDefaultConfig();
}
SaveConfig();
}
private Configuration GetDefaultSettings() {
return new Configuration {
General = new Configuration.GeneralConfig {
checkDistanceInside = 100, checkDistanceOutside = 150, checkInterval = 1, ignoreIdleTime = 15, trackWeaponsOnly = true, ignoreFriendly = true, ignoreNpcBots = true, trackTcs = false, ignoreDistance = 5f
}
, ADR = new Configuration.ADRConfig {
recordMinutes = 5
}
, Discord = new Configuration.DiscordConfig {
webhook = string.Empty, title = "ESP Detection", radarImage = true, twoColumn = true, discordSendSampels = true
}
, Violations = new Configuration.ViolationsConfig {
intervalSamples = 30, maxViolationsPerc = 70
}
, Weapons = new List<Configuration.WeaponsConfig>() {
new Configuration.WeaponsConfig {
shortname = "rifle.l96", checkDistanceInside = 50, checkDistanceOutside = 130, track = true
}
, new Configuration.WeaponsConfig {
shortname = "rifle.bolt", checkDistanceInside = 50, checkDistanceOutside = 120, track = true
}
}
, Attachments = new List<Configuration.AttachmentConfig>() {
new Configuration.AttachmentConfig {
shortname = "weapon.mod.8x.scope", checkDistanceAiming = 180, track = true
}
, new Configuration.AttachmentConfig {
shortname = "weapon.mod.small.scope", checkDistanceAiming = 180, track = true
}
}
}
;
}
protected override void LoadDefaultConfig() {
Config.Clear();
config = GetDefaultSettings();
Puts("Creating new configuration file...");
SaveConfig();
}
void LogDebug(string var) {
if (!debug) return;
PrintToChat(var);
PrintToConsole(var);
Puts(var);
}
void PerfStop(Stopwatch w, string tag = "") {
w.Stop();
if (debug) Puts((tag.Length > 0 ? $"{tag} / " : "") + $"Elapsed: {w.ElapsedMilliseconds}ms");
}
protected override void SaveConfig() => Config.WriteObject(config);
#endregion
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
["ADR Message"]
= "Possible ESP: {value}% probability."
}
, this);
}
string GetMsg(string key) => lang.GetMessage(key, this);
#region Classes
bool IsTeamMate(ulong p1, ulong p2) => GetTeamMembers(p1).Contains(p2);
bool IsNpc(BaseEntity ent) => ent.IsNpc || !(ent.ToPlayer() != null && ent.ToPlayer().userID.IsSteamId());
List<ulong> GetTeamMembers(ulong userid) => RelationshipManager.ServerInstance.FindPlayersTeam(userid)?.members ?? new List<ulong>();
void DiscordSend(BasePlayer player, List<Violation> violationsTemp) {
LogDebug("DiscordSend");
var probability = 0d;
if (player && _maxViolation.ContainsKey(player.userID)) probability = _maxViolation[player.userID].maxViolation;
var msgRec = GetMsg("ADR Message").Replace("{value}", probability.ToString());
LogDebug("Record");
Record(player, msgRec);
/* new */
var msg = new DiscordMessageConfig {
Content = string.Empty, Embed = new EmbedConfig {
Title = config.Discord.title, Description = msgRec, Color = "#de8732", Image = string.Empty, Thumbnail = string.Empty, // "https://assets.umod.org/images/icons/user/5e6f8629518b5.jpg",
Fields = new List<FieldConfig> {
}
, Footer = new FooterConfig {
IconUrl = "https://assets.umod.org/images/icons/user/5e6f8629518b5.jpg", Text = $"{Title} V{Version} by {Author}", Enabled = true
}
, Enabled = true
}
}
;
msg.Embed.Fields.Add(new FieldConfig() {
Title = "Culprit", Value = $"{player?.displayName} - {player?.UserIDString}\n[Steam](https://steamcommunity.com/profiles/{player?.UserIDString}) - [SA](https://serverarmour.com/profile/{player?.UserIDString}) - [BM](https://www.battlemetrics.com/rcon/players?filter%5Bsearch%5D={player?.UserIDString})", Inline = false, Order = 1, Enabled = true
}
);
var violations = violationsTemp?.Take(config.Violations.intervalSamples).ToList();
if (config.Discord.discordSendSampels && violationsTemp != null && violationsTemp.Count > 0) {
int i = 1;
for (int index = 0, n = violations.Count; index < n; index++) {
var violation = violations[index];
var appendVoice = violation.lastVoice != null ? $"\n{violation.lastVoice.dateTime.ToLongTimeString()}" : "";
var vicIdent = violation.isPlayer ? violation.victim.ToString() : violation.victimName;
if (config.Discord.convertSteamId && violation.isPlayer) {
var vicBPlayer = covalence.Players.FindPlayer(vicIdent);
if (vicBPlayer != null) vicIdent = $"{vicBPlayer?.Name}(https://steamcommunity.com/profiles/{vicIdent})";
}
msg.Embed.Fields.Add(new FieldConfig() {
Title = $"VID: {i} - {violation.dateTime.ToLongTimeString()}", Value = $"Victim: {vicIdent}" + $"\nDistance: {violation.distance:0}" + $"\nBearing: {violation.rotation}{appendVoice}" + $"\nBreached Layers: {string.Join(", ", violation.breachedLayers.Select(x => x.objectName))}", Inline = true, Order = i, Enabled = true
}
);
if (config.Discord.twoColumn && i % 2 == 0) {
msg.Embed.Fields.Add(new FieldConfig() {
Title = $"** **", Value = $"** **", Inline = false, Order = i, Enabled = true
}
);
}
i++;
}
;
}
Puts($"Before radar, status: {config.Discord.radarImage}");
Puts(JsonConvert.SerializeObject(violations));
string imageUrl = string.Empty;
if (config.Discord.radarImage && violationsTemp != null && violationsTemp.Count > 0) {
Puts($"https://esp.serverarmour.com/api/v1/radar/create");
Puts($"data={JsonConvert.SerializeObject(violations)}");
webrequest.Enqueue($"https://esp.serverarmour.com/api/v1/radar/create", $"data={JsonConvert.SerializeObject(violations)}", (code, rr) => {
if (code <= 204) {
imageUrl = rr.Replace("\"", "").Trim();
msg.Embed.Image = imageUrl;
Puts(imageUrl);
}
else {
Puts($"Radar creation failed: {code} - {rr}");
}
}
, this, RequestMethod.POST);
}
try {
webrequest.Enqueue($"https://steamcommunity.com/profiles/{player.userID}?xml=1", string.Empty, (code2, result) => {
if (code2 >= 200 && code2 <= 204) msg.Embed.Thumbnail = new Regex(@"(?<=<avatarMedium>[\w\W]+)https://.+\.jpg(?=[\w\W]+<\/avatarMedium>)", RegexOptions.Compiled).Match(result).Value;
}
, this);
}
catch (Exception _ignore) {
// we delay so that we are sure the radar image has been created.}
timer.Once(5, () => {
Interface.CallHook("API_EspDetected", JsonConvert.SerializeObject(new {
steamId = player.userID, violations = violationsTemp, radarUrl = Uri.EscapeDataString(imageUrl)
}
));
DiscordMessage message = ParseMessage(msg);
if (config.Discord.webhook.Length == 0 || config.Discord.webhook.Equals("")) return;
SendDiscordMessage(config.Discord.webhook, message);
}
);
}
class Violation {
public DateTime dateTime;
public ulong victim;
public string victimName;
public int rotation;
public float distance;
public Blip victimBlip;
public bool isPlayer = true;
public ViolationSound lastVoice = null;
public List<ViolationBreachedLayer> breachedLayers = new List<ViolationBreachedLayer>();
}
class ViolationTestResult {
public BaseEntity baseEntity;
public List<ViolationBreachedLayer> breachedLayers;
}
class ViolationBreachedLayer {
public int id;
public string layerType;
public string objectName;
public Vector3 position;
}
class Blip {
public int x;
public int y;
}
class ViolationMeta {
public DateTime dateTime;
public double maxViolation;
}
class ViolationSound {
public ulong playerId;
public DateTime dateTime;
public SoundType type;
}
enum SoundType {
Voice
}
#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) {
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";
bool MadeNoiseIn(ulong playerId, int secondsAgo) {
return _allSounds.Count(x => x.playerId == playerId && x.dateTime >= DateTime.Now.AddSeconds(-secondsAgo)) > 0;
}
ViolationSound LastVoice(ulong playerId) {
return _allSounds.LastOrDefault(x => x.type == SoundType.Voice && x.playerId == playerId);
}
void ClearVoices() {
var sounds = _allSounds.FindAll(x => x.dateTime >= DateTime.Now.AddSeconds(-600));
_allSounds = sounds;
}
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>
/// Adds string content to the message
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public DiscordMessage AddContent(string content) {
Content = content;
return this;
}
/// <summary>
/// Changes the username and avatar image for the bot sending the message
/// </summary>
/// <param name="username">username to change</param>
/// <param name="avatarUrl">avatar img url to change</param>
/// <returns>This</returns>
public DiscordMessage AddSender(string username, string avatarUrl) {
Username = username;
AvatarUrl = avatarUrl;
return this;
}
/// <summary>
/// Returns message as JSON to be sent in the web request
/// </summary>
/// <returns></returns>
public string ToJson() => JsonConvert.SerializeObject(this, Formatting.None, new JsonSerializerSettings {
NullValueHandling = NullValueHandling.Ignore
}
);
}
private class Embed {
/// <summary>
/// Color of the left side bar of the embed message
/// </summary>
[JsonProperty("color")]
private int Color {
get;
set;
}
/// <summary>
/// Fields to be added to the embed message
/// </summary>
[JsonProperty("fields")]
private List<Field> Fields {
get;
}
= new List<Field>();
/// <summary>
/// Title of the embed message
/// </summary>
[JsonProperty("title")]
private string Title {
get;
set;
}
/// <summary>
/// Description of the embed message
/// </summary>
[JsonProperty("description")]
private string Description {
get;
set;
}
/// <summary>
/// Image to added to the embed message.Appears at the bottom of the message above the footer
/// </summary>
[JsonProperty("image")]
private Image Image {
get;
set;
}
/// <summary>
/// Thumbnail image added to the embed message.Appears in the top right corner
/// </summary>
[JsonProperty("thumbnail")]
private Image Thumbnail {
get;
set;
}
/// <summary>
/// Author to add to the embed message.Appears above the title./// </summary>
[JsonProperty("author")]
private AuthorInfo Author {
get;
set;
}
/// <summary>
/// Footer
// ... [Code preview truncated due to length. Full source in attached file] ...