C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("CombatLog Info", "Pho3niX90", "1.0.13")]
[Description("Collects combat log entries, and submits them to SA for analysis.")]
class CombatLogInfo : RustPlugin
{
const bool debug = false;
[PluginReference] Plugin ServerArmour;
List<int> usedHashes = new List<int>();
// Simple stats
DateTime logsSince = DateTime.Now;
int totalLogs = 0;
int totalLogsUploaded = 0;
int pendingGeneration = 0;
int failedUploads = 0;
//
void OnServerInitialized(bool first)
{
Server.Command("server.combatlogsize 45");
}
void OnServerSave()
{
this.cleanupHashes();
}
#region log generation
private void GenCombatLog(BasePlayer forPlayer)
{
if (forPlayer.IsNpc || !forPlayer.userID.IsSteamId())
return;
var pInfo = new PInfo { Name = forPlayer.displayName, SteamId = forPlayer.UserIDString };
var cLog = CombatLog.Get(forPlayer.userID);
pendingGeneration++;
timer.Once(ConVar.Server.combatlogdelay + 1, () =>
{
AddEntries(forPlayer, cLog);
});
}
#endregion
#region commands
[ConsoleCommand("clog.stats")]
void printStats(ConsoleSystem.Arg arg)
{
arg.ReplyWith($"Stats since {logsSince}\n\nTotal Logs: {totalLogs}\nTotal Logs Uploaded: {totalLogsUploaded}\nFailed Logs Upload: {failedUploads}\nPending Log Generation: {pendingGeneration}");
}
[ConsoleCommand("combatlog")]
void printCombatLog(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (!player)
return;
TextTable textTable = new TextTable();
if (config.advancedLogs)
textTable.AddColumns("time", "attacker", "id", "target", "id", "weapon", "ammo", "area", "distance", "old_hp", "new_hp", "info", "desync");
else
textTable.AddColumns("time", "attacker", "target", "weapon", "ammo", "area", "distance", "old_hp", "new_hp", "info", "desync");
foreach (CombatLog.Event evt in CombatLog.Get(player.userID))
{
var entry = CLogEntry.from(player, evt);
if (entry != null)
{
if (config.advancedLogs)
{
textTable.AddRow((Time.realtimeSinceStartup - entry.EventTime).ToString("0.0s"),
GetUsername(player, entry.AttackerSteamId), entry.AttackerSteamId,
GetUsername(player, entry.TargetSteamId), entry.TargetSteamId,
entry.Weapon, entry.Ammo,
entry.Area, entry.Distance.ToString("0.0m"),
entry.HealthOld.ToString("0.0"), entry.HealthNew.ToString("0.0"), entry.EventInfo, entry.Desync.ToString());
}
else
{
textTable.AddRow((Time.realtimeSinceStartup - entry.EventTime).ToString("0.0s"),
entry.AttackerSteamId,
entry.TargetSteamId,
entry.Weapon, entry.Ammo,
entry.Area, entry.Distance.ToString("0.0m"),
entry.HealthOld.ToString("0.0"), entry.HealthNew.ToString("0.0"), entry.EventInfo, entry.Desync.ToString());
}
}
}
player.ConsoleMessage("---------- COMBATLOG ----------\n");
player.ConsoleMessage(textTable.ToString());
}
string GetUsername(BasePlayer player, string id)
{
if (id == player.UserIDString || id == player.net.ID.Value.ToString())
return "you";
BasePlayer fp = null;
try
{
fp = BasePlayer.allPlayerList.FirstOrDefault(x => x.UserIDString == id || x.net.ID.Value.ToString() == id);
}
catch (Exception e) { }
return fp?.displayName ?? "N/A";
}
#endregion
#region hooks
void processingTask()
{
var task = Task.Run(() => { });
}
object OnPlayerDeath(BasePlayer player, HitInfo info)
{
var initiator = info?.InitiatorPlayer;
if (initiator != null && initiator.userID.IsSteamId())
{
GenCombatLog(initiator ?? player);
}
else
{
GenCombatLog(player);
}
return null;
}
private void UploadLogs(Dictionary<int, CLogEntry> logs)
{
pendingGeneration--;
totalLogs += logs.Count;
if (logs.Count > 0)
{
if (ServerArmour == null || !ServerArmour.IsLoaded)
{
// LogDebug("ServerArmour plugin is not loaded.Cannot upload logs.");
failedUploads += logs.Count;
return;
}
try
{
// LogDebug($"Requested upload of {logs.Count} entries");
ServerArmour.Call("UploadCombatEntries", JsonConvert.SerializeObject(logs.Values));
totalLogsUploaded += logs.Count;
}
catch (Exception ex)
{
// LogDebug($"Exception during log upload: {ex}");
failedUploads += logs.Count;
}
}
else
LogDebug($"Nothing to upload");
}
#endregion
#region helpers
private void AddEntries(BasePlayer forPlayer, Queue<CombatLog.Event> cLog)
{
Dictionary<int, CLogEntry> logEntries = new Dictionary<int, CLogEntry>();
foreach (CombatLog.Event evt in cLog)
{
var hash = evt.GetHashCode();
if (!logEntries.ContainsKey(hash) && !usedHashes.Contains(hash))
{
var entry = CreateEntry(forPlayer, evt);
if (entry != null)
{
usedHashes.Add(hash);
logEntries.Add(hash, entry);
}
}
}
UploadLogs(logEntries);
logEntries.Clear();
}
void cleanupHashes()
{
// Check if usedHashes exceeds 100000 entries, then cleanup
if (usedHashes.Count > 100000)
{
// Take the most recent 1000 entries and add a new one
usedHashes = usedHashes.Skip(usedHashes.Count - 1000).ToList();
}
}
private CLogEntry CreateEntry(BasePlayer forPlayer, CombatLog.Event evt)
{
if ((forPlayer is NPCPlayer || forPlayer.IsNpc || !forPlayer.userID.IsSteamId()) && !evt.target_id.IsSteamId())
{
// LogDebug($"Event = {evt.GetHashCode()} bot attacker and bot death.Skipped");
return null;
}
totalLogs++;
// LogDebug($"Event = {evt.GetHashCode()} saved.");
return CLogEntry.from(forPlayer, evt);
}
private void LogDebug(string txt)
{
if (debug) Puts($"DEBUG: {txt}");
}
static private PInfo UintFind(ulong netId)
{
BasePlayer player = null;
try
{
player = BasePlayer.activePlayerList.First(x => x.net.ID.Value == netId);
}
catch (Exception e)
{
}
return player != null ? new PInfo { Name = player.displayName, SteamId = player.UserIDString } : new PInfo { Name = netId.ToString(), SteamId = netId.ToString() };
}
/**
* This avoids scientific notations
*/
static public void RoundOrLimitFloat(ref float value) => value = (value > 1000000) ? value % 1000000 : value;
public class PInfo
{
public string Name;
public string SteamId;
}
#endregion
public class CLogEntry
{
public int EventHash;
public float EventTime;
public string AttackerSteamId;
public string TargetSteamId;
public string Weapon;
public string Ammo;
public string Area;
public float Distance;
public float HealthOld;
public float HealthNew;
public string EventInfo;
public int ProjectileHits;
public float ProjectileIntegrity;
public float ProjectileTravelTime;
public float ProjectileTrajectoryMismatch;
public int Desync;
public static CLogEntry from(BasePlayer forPlayer, CombatLog.Event evt)
{
var pInfo = new PInfo { Name = forPlayer.displayName, SteamId = forPlayer.UserIDString };
var attacker = evt.attacker == "you" ? pInfo : UintFind(evt.attacker_id);
var target = evt.target == "you" ? pInfo : UintFind(evt.target_id);
RoundOrLimitFloat(ref evt.health_new);
RoundOrLimitFloat(ref evt.health_old);
return new CLogEntry
{
EventHash = evt.GetHashCode(),
AttackerSteamId = attacker.SteamId,
TargetSteamId = target.SteamId,
Ammo = evt.ammo,
Area = HitAreaUtil.Format(evt.area).ToLower(),
Distance = evt.distance,
EventInfo = evt.info,
EventTime = evt.time,
HealthNew = evt.health_new,
HealthOld = evt.health_old,
Desync = evt.desync,
ProjectileHits = evt.proj_hits,
ProjectileIntegrity = evt.proj_integrity,
ProjectileTrajectoryMismatch = evt.proj_mismatch,
ProjectileTravelTime = evt.proj_travel,
Weapon = evt.weapon
};
}
}
#region Configuration
private static ConfigData config = new ConfigData();
private class ConfigData
{
[JsonProperty(PropertyName = "Advanced Logs")]
public bool advancedLogs = true;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<ConfigData>();
if (config == null) LoadDefaultConfig();
}
catch
{
PrintError("Your config seems to be corrupted.Will load defaults.");
LoadDefaultConfig();
return;
}
SaveConfig();
}
protected override void LoadDefaultConfig()
{
config = new ConfigData();
}
protected override void SaveConfig() => Config.WriteObject(config);
#endregion
}
}