// #define DEBUG using Newtonsoft.Json; using Oxide.Core; using Rust; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using UnityEngine; namespace Oxide.Plugins { using EnemyPrefabs = DeathNotes.RemoteConfiguration>; using WeaponPrefabs = DeathNotes.RemoteConfiguration>; using CombatEntityTypes = DeathNotes.RemoteConfiguration>; [Info("Death Notes", "LaserHydra/Mevent", "6.3.9")] [Description("Broadcasts deaths to chat along with detailed information")] class DeathNotes : RustPlugin { #region Fields private const string WildcardCharacter = "*"; private const string CanSeePermission = "deathnotes.cansee"; private static DeathNotes _instance; private PluginConfiguration _configuration; private readonly EnemyPrefabs _enemyPrefabs = new EnemyPrefabs("EnemyPrefabs"); private readonly WeaponPrefabs _weaponPrefabs = new WeaponPrefabs("WeaponPrefabs"); private readonly CombatEntityTypes _combatEntityTypes = new CombatEntityTypes("CombatEntityTypes"); private readonly Regex _colorTagRegex = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly Regex _sizeTagRegex = new Regex(@"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private readonly List _richTextLiterals = new List { "", "", "", "", "", "" }; private readonly Dictionary _previousAttack = new Dictionary(); private readonly Func[] _messageMatchingStages = { (m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) && MatchesCombatEntityType(d.VictimEntityType, m.VictimType) && MatchesDamageType(d.DamageType, m.DamageType), (m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) && MatchesCombatEntityType(d.VictimEntityType, m.VictimType) && m.DamageType == WildcardCharacter, (m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) && m.VictimType == WildcardCharacter && MatchesDamageType(d.DamageType, m.DamageType), (m, d) => m.KillerType == WildcardCharacter && MatchesCombatEntityType(d.VictimEntityType, m.VictimType) && MatchesDamageType(d.DamageType, m.DamageType), (m, d) => MatchesCombatEntityType(d.KillerEntityType, m.KillerType) && m.VictimType == WildcardCharacter && m.DamageType == WildcardCharacter, (m, d) => m.KillerType == WildcardCharacter && MatchesCombatEntityType(d.VictimEntityType, m.VictimType) && m.DamageType == WildcardCharacter, (m, d) => m.KillerType == WildcardCharacter && m.VictimType == WildcardCharacter && MatchesDamageType(d.DamageType, m.DamageType), (m, d) => m.KillerType == WildcardCharacter && m.VictimType == WildcardCharacter && m.DamageType == WildcardCharacter }; #endregion #region Hooks private void Init() { _instance = this; permission.RegisterPermission(CanSeePermission, this); _configuration = Config.ReadObject(); _configuration.LoadDefaults(); Config.WriteObject(_configuration); _enemyPrefabs.Load(); _weaponPrefabs.Load(); _combatEntityTypes.Load(); } private void Unload() { _instance = null; } private void OnEntityTakeDamage(BasePlayer victimEntity, HitInfo hitInfo) { if (victimEntity == null || hitInfo == null) return; // Don't track bleeding if (victimEntity.lastDamage == DamageType.Bleeding) return; var userId = victimEntity.ToPlayer().userID; _previousAttack[userId] = new AttackInfo { HitInfo = hitInfo, Attacker = victimEntity.lastAttacker ?? hitInfo?.Initiator, DamageType = victimEntity.lastDamage }; } private void OnEntityDeath(BaseCombatEntity victimEntity, HitInfo hitInfo) { // Ignore - there is no victim for some reason if (victimEntity == null) return; // Try to avoid error when entity was destroyed if (victimEntity.gameObject == null) return; var data = new DeathData { VictimEntity = victimEntity, KillerEntity = victimEntity.lastAttacker ?? hitInfo?.Initiator, VictimEntityType = GetCombatEntityType(victimEntity), KillerEntityType = GetCombatEntityType(victimEntity.lastAttacker), DamageType = victimEntity.lastDamage, HitInfo = hitInfo }; // Handle inconsistencies/exceptions HandleInconsistencies(ref data); #if DEBUG LogDebug("[DEATHNOTES DEBUG]"); LogDebug( $"VictimEntity: {data.VictimEntity?.GetType().Name ?? "NULL"} / {data.VictimEntity?.ShortPrefabName ?? "NULL"} / {data.VictimEntity?.PrefabName ?? "NULL"}"); LogDebug( $"KillerEntity: {data.KillerEntity?.GetType().Name ?? "NULL"} / {data.KillerEntity?.ShortPrefabName ?? "NULL"} / {data.KillerEntity?.PrefabName ?? "NULL"}"); LogDebug($"VictimEntityType: {data.VictimEntityType}"); LogDebug($"KillerEntityType: {data.KillerEntityType}"); LogDebug($"DamageType: {data.DamageType}"); LogDebug($"Bodypart: {GetCustomizedBodypartName(data.HitInfo)}"); LogDebug($"Weapon: {hitInfo?.WeaponPrefab?.ShortPrefabName ?? "NULL"}"); #endif // Change entity type for dwellers RepairEntityTypes(ref data); // Ignore deaths of other entities if (data.KillerEntityType == CombatEntityType.Other || data.VictimEntityType == CombatEntityType.Other) return; // Ignore deaths which don't involve players or the helicopter which usually does not track a player as killer if (data.VictimEntityType != CombatEntityType.Player && data.KillerEntityType != CombatEntityType.Player && data.VictimEntityType != CombatEntityType.Helicopter) return; // Populate the variables in the message string message = PopulateMessageVariables( // Find the best matching death message for this death GetDeathMessage(data), data ); if (message == null) return; object hookResult = Interface.Call("OnDeathNotice", data.ToDictionary(), message); if (hookResult?.Equals(false) ?? false) return; if (_configuration.ShowInChat) { foreach (var player in BasePlayer.activePlayerList) { if (_configuration.RequirePermission && !permission.UserHasPermission(player.UserIDString, CanSeePermission)) continue; if (_configuration.MessageRadius != -1 && player.Distance(data.VictimEntity) > _configuration.MessageRadius) continue; Player.Reply( player, _configuration.ChatFormat.Replace("{message}", message), ulong.Parse(_configuration.ChatIcon) ); } } if (_configuration.ShowInConsole) Puts(StripRichText(message)); } private void RepairEntityTypes(ref DeathData data) { if (data.VictimEntity != null) { string victimPrefabName = data.VictimEntity.ShortPrefabName.ToLower(); if (victimPrefabName.Contains("underwaterdweller")) { data.VictimEntityType = CombatEntityType.UnderwaterDweller; } if (victimPrefabName.Contains("tunneldweller")) { data.VictimEntityType = CombatEntityType.TunnelDweller; } } if (data.KillerEntity != null) { string killerPrefabName = data.KillerEntity.ShortPrefabName.ToLower(); if (killerPrefabName.Contains("underwaterdweller")) { data.KillerEntityType = CombatEntityType.UnderwaterDweller; } if (killerPrefabName.Contains("tunneldweller")) { data.KillerEntityType = CombatEntityType.TunnelDweller; } } } private void OnFlameThrowerBurn(FlameThrower flameThrower, BaseEntity baseEntity) { if (flameThrower == null || baseEntity == null) return; var flame = baseEntity.gameObject.AddComponent(); flame.Source = Flame.FlameSource.Flamethrower; flame.SourceEntity = flameThrower; flame.Initiator = flameThrower.GetOwnerPlayer(); } private void OnFlameExplosion(FlameExplosive explosive, BaseEntity baseEntity) { if (explosive == null || baseEntity == null) return; var flame = baseEntity.gameObject.AddComponent(); flame.Source = Flame.FlameSource.IncendiaryProjectile; flame.SourceEntity = explosive; flame.Initiator = explosive.creatorEntity; } private void OnFireBallSpread(FireBall fireBall, BaseEntity newFire) { if (fireBall == null) return; var flame = fireBall.GetComponent(); if (flame == null) return; var newFlame = newFire.gameObject.AddComponent(); newFlame.Source = flame.Source; newFlame.SourceEntity = flame.SourceEntity; newFlame.Initiator = flame.Initiator; } private void OnFireBallDamage(FireBall fireBall, BaseCombatEntity target, HitInfo hitInfo) => hitInfo.Initiator = fireBall; #endregion #region Death Messages private string GetDeathMessage(DeathData data) { foreach (var matchingStage in _messageMatchingStages) { var match = _configuration.Translations.Messages.Find(m => matchingStage.Invoke(m, data)); if (match != null) return match.Messages.GetRandom((uint) DateTime.UtcNow.Millisecond); } return null; } private string PopulateMessageVariables(string message, DeathData data) { if (string.IsNullOrEmpty(message)) return null; var replacements = new Dictionary { ["victim"] = GetCustomizedEntityName(data.VictimEntity, data.VictimEntityType) }; if (data.KillerEntityType != CombatEntityType.None) { replacements.Add("killer", GetCustomizedEntityName(data.KillerEntity, data.KillerEntityType)); replacements.Add("bodypart", GetCustomizedBodypartName(data.HitInfo)); if (data.KillerEntity != null) { var distance = data.KillerEntity.Distance(data.VictimEntity); replacements.Add("distance", GetDistance(distance, _configuration.UseMetricDistance)); } if (data.KillerEntityType == CombatEntityType.Player) { replacements.Add("hp", data.KillerEntity.Health().ToString("#0.#")); replacements.Add("weapon", GetCustomizedWeaponName(data)); replacements.Add("attachments", string.Join(", ", GetCustomizedAttachmentNames(data.HitInfo))); } else if (data.KillerEntityType == CombatEntityType.Turret || data.KillerEntityType == CombatEntityType.Lock || data.KillerEntityType == CombatEntityType.Trap) { replacements.Add("owner", covalence.Players.FindPlayerById(data.KillerEntity.OwnerID.ToString())?.Name ?? "unknown owner" ); // TODO: Work on the potential unknown owner case } } message = InsertPlaceholderValues(message, replacements); replacements = null; return message; } private struct DeathData { public CombatEntityType VictimEntityType { get; set; } [JsonIgnore] public BaseCombatEntity VictimEntity { get; set; } public CombatEntityType KillerEntityType { get; set; } [JsonIgnore] public BaseEntity KillerEntity { get; set; } public DamageType DamageType { get; set; } [JsonIgnore] public HitInfo HitInfo { get; set; } public Dictionary ToDictionary() => new Dictionary { ["VictimEntityType"] = VictimEntityType, ["VictimEntity"] = VictimEntity, ["KillerEntityType"] = KillerEntityType, ["KillerEntity"] = KillerEntity, ["DamageType"] = DamageType, ["HitInfo"] = HitInfo }; } #endregion #region Entity Identification private CombatEntityType GetCombatEntityType(BaseEntity entity) { if (entity == null) return CombatEntityType.None; if (_combatEntityTypes.Contents != null) { if (_combatEntityTypes.Contents.ContainsKey(entity.GetType().Name)) return _combatEntityTypes.Contents[entity.GetType().Name]; if (_combatEntityTypes.Contents.ContainsKey(entity.ShortPrefabName)) return _combatEntityTypes.Contents[entity.ShortPrefabName]; } if (entity is BaseOven) return CombatEntityType.HeatSource; if (entity is SimpleBuildingBlock) return CombatEntityType.ExternalWall; if (entity is BaseAnimalNPC) return CombatEntityType.Animal; if (entity is BaseTrap) return CombatEntityType.Trap; if (entity is Barricade) return CombatEntityType.Barricade; if (entity is IOEntity) return CombatEntityType.Trap; if (entity is ScientistNPC) return CombatEntityType.Scientist; if (entity.GetType().Name.Equals("ZombieNPC")) return CombatEntityType.ZombieNPC; return CombatEntityType.Other; } private string GetCustomizedEntityName(BaseEntity entity, CombatEntityType combatEntityType) { var name = GetEntityName(entity, combatEntityType); if (string.IsNullOrEmpty(name)) return null; // Don't load player names into config if (combatEntityType == CombatEntityType.Player) return name; if (!_configuration.Translations.Names.ContainsKey(name)) { _configuration.Translations.Names.Add(name, name); Config.WriteObject(_configuration); } return _configuration.Translations.Names[name]; } private string GetEntityName(BaseEntity entity, CombatEntityType combatEntityType) { // Entity may be null for helicopter or bradley, see HandleExceptions(...) if (entity == null && combatEntityType != CombatEntityType.Helicopter && combatEntityType != CombatEntityType.Bradley) return null; switch (combatEntityType) { case CombatEntityType.Player: return StripRichText(entity.ToPlayer().displayName); case CombatEntityType.Murderer: case CombatEntityType.Scarecrow: case CombatEntityType.Scientist: case CombatEntityType.ZombieNPC: var name = entity.ToPlayer()?.displayName; if (!string.IsNullOrEmpty(name) && name != entity.ToPlayer()?.userID.ToString()) { return name; } if (!_enemyPrefabs.Contents.ContainsKey(entity.ShortPrefabName)) { return combatEntityType.ToString(); } break; case CombatEntityType.TunnelDweller: return "Tunnel Dweller"; case CombatEntityType.UnderwaterDweller: return "Underwater Dweller"; case CombatEntityType.Helicopter: return "Helicopter"; case CombatEntityType.Bradley: return "Bradley APC"; case CombatEntityType.Sentry: return "Sentry"; case CombatEntityType.Fire: return entity.creatorEntity?.ToPlayer()?.displayName ?? "Fire"; } if (_enemyPrefabs.Contents.ContainsKey(entity.ShortPrefabName)) return _enemyPrefabs.Contents[entity.ShortPrefabName]; return HumanizePascalCase(entity.GetType().Name); } internal enum CombatEntityType { Helicopter = 0, Bradley = 1, Animal = 2, Murderer = 3, Scientist = 4, Player = 5, Trap = 6, Turret = 7, Barricade = 8, ExternalWall = 9, HeatSource = 10, Fire = 11, Lock = 12, Sentry = 13, Other = 14, None = 15, Scarecrow = 16, TunnelDweller = 17, UnderwaterDweller = 18, ZombieNPC = 19 } #endregion #region Workarounds and Inconsistency Handling private void HandleInconsistencies(ref DeathData data) { // Deaths of other entity types are not of interest and might cause errors if (data.VictimEntityType == CombatEntityType.Other) return; if (data.KillerEntity is FireBall) data.DamageType = DamageType.Heat; // If the killer entity is null, but a weapon is given, we might be able to fall back to the parent entity of that weapon // Notably for the auto turret after the changes it has had if (data.KillerEntity == null && data.HitInfo?.Weapon != null) { data.KillerEntity = data.HitInfo.Weapon.GetParentEntity(); data.KillerEntityType = GetCombatEntityType(data.KillerEntity); } // Get previous attacker when bleeding out if (data.VictimEntityType == CombatEntityType.Player && (data.DamageType == DamageType.Bleeding || data.HitInfo == null)) { var userId = data.VictimEntity.ToPlayer().userID; if (_previousAttack.ContainsKey(userId)) { var attack = _previousAttack[userId]; data.KillerEntity = attack.Attacker; data.KillerEntityType = GetCombatEntityType(data.KillerEntity); // Restore previous hitInfo for weapon determination if (attack.HitInfo != null) data.HitInfo = attack.HitInfo; // Use previous damagetype if this is a selfinflicted death, // so falling to death etc. is also shown when wounded and bleeding out if (data.KillerEntity == null || data.KillerEntity == data.VictimEntity) data.DamageType = attack.DamageType; else data.DamageType = DamageType.Bleeding; } } if (data.KillerEntityType != CombatEntityType.None && data.KillerEntity != null) { // Workaround for deaths caused by flamethrower or rocket fire var flame = data.KillerEntity.gameObject.GetComponent(); if (flame != null && flame.Initiator != null) { data.KillerEntity = flame.Initiator; data.KillerEntityType = CombatEntityType.Player; return; } } // Bradley kill with main cannon if (data.HitInfo?.WeaponPrefab?.ShortPrefabName == "maincannonshell") { data.KillerEntityType = CombatEntityType.Bradley; return; } if (data.HitInfo?.WeaponPrefab?.ShortPrefabName?.StartsWith("rocket_heli") ?? false) { data.KillerEntityType = CombatEntityType.Helicopter; return; } // Vehicle Kills if (data.KillerEntityType == CombatEntityType.Player && data.DamageType == DamageType.Generic && data.KillerEntity.ToPlayer().isMounted) { data.DamageType = DamageType.Collision; return; } } private struct AttackInfo { public HitInfo HitInfo { get; set; } public DamageType DamageType { get; set; } public BaseEntity Attacker { get; set; } } private class Flame : MonoBehaviour { public FlameSource Source { get; set; } public BaseEntity SourceEntity { get; set; } public BaseEntity Initiator { get; set; } public enum FlameSource { Flamethrower, IncendiaryProjectile } } #endregion #region Weapons private string GetCustomizedWeaponName(DeathData deathData) { var name = GetWeaponName(deathData); if (string.IsNullOrEmpty(name)) return null; if (!_configuration.Translations.Weapons.ContainsKey(name)) { _configuration.Translations.Weapons.Add(name, name); Config.WriteObject(_configuration); } return _configuration.Translations.Weapons[name]; } private string GetWeaponName(DeathData deathData) { if (deathData.HitInfo == null) return null; Item item = deathData.HitInfo.Weapon?.GetItem(); /*var parentEntity = hitInfo.Weapon?.GetParentEntity(); Item item = null; if (parentEntity is BasePlayer) { (parentEntity as BasePlayer).inventory.FindItemUID(hitInfo.Weapon.ownerItemUID); } else if (parentEntity is ContainerIOEntity) { (parentEntity as ContainerIOEntity).inventory.FindItemByUID(hitInfo.Weapon.ownerItemUID); }*/ if (item != null) return item.info.displayName.english; var prefab = deathData.HitInfo.Initiator?.GetComponent()?.SourceEntity?.ShortPrefabName ?? deathData.HitInfo.WeaponPrefab?.ShortPrefabName; if (prefab != null) { if (_weaponPrefabs.Contents.ContainsKey(prefab)) return _weaponPrefabs.Contents[prefab]; return prefab; } // Vehicles are the only thing we classify as a weapon, while not being classified as such by the game. // TODO: Having this here kinda sucks, make this better. if (deathData.DamageType == DamageType.Collision) { return "Vehicle"; } return null; } private string[] GetCustomizedAttachmentNames(HitInfo info) { var items = info?.Weapon?.GetItem()?.contents?.itemList; if (items == null) { return Array.Empty(); } return items.Select(i => GetCustomizedAttachmentName(i.info.displayName.english)).ToArray(); } private string GetCustomizedAttachmentName(string name) { if (!_configuration.Translations.Attachments.ContainsKey(name)) { _configuration.Translations.Attachments.Add(name, name); Config.WriteObject(_configuration); } return _configuration.Translations.Attachments[name]; } #endregion #region Bodyparts private string GetCustomizedBodypartName(HitInfo hitInfo) { var name = GetBodypartName(hitInfo); if (string.IsNullOrEmpty(name)) return null; if (!_configuration.Translations.Bodyparts.ContainsKey(name)) { _configuration.Translations.Bodyparts.Add(name, name); Config.WriteObject(_configuration); } return _configuration.Translations.Bodyparts[name]; } private string GetBodypartName(HitInfo hitInfo) { var hitArea = hitInfo?.boneArea ?? (HitArea) (-1); return (int) hitArea == -1 ? "Body" : hitArea.ToString(); } #endregion #region Helper #if DEBUG private static void LogDebug(string text) { if (BasePlayer.activePlayerList.Count >= 1) { BasePlayer.activePlayerList[0].ConsoleMessage($"{text}"); } } #endif private static string GetDistance(float meters, bool useMetric) { double value = Math.Round(useMetric ? meters : meters * 3.28f, 1); string unit = value == 1 ? _instance.lang.GetMessage("Distance Unit Singular", _instance) : _instance.lang.GetMessage("Distance Unit Plural", _instance); return $"{value} {unit}"; } private static string ApplyVariableFormat(string text, string variableName) { if (_instance._configuration.VariableFormats.ContainsKey(variableName)) { var format = _instance._configuration.VariableFormats[variableName]; text = format.Replace("{value}", text); } return text; } private static string InsertPlaceholderValues(string text, Dictionary values) { foreach (var kvp in values) { string value = ApplyVariableFormat(kvp.Value, kvp.Key); if (string.IsNullOrEmpty(kvp.Value)) { text = text.Replace($"{{{kvp.Key}}}", string.Empty); } else if (_instance._configuration.VariableColors.ContainsKey(kvp.Key)) { var color = _instance._configuration.VariableColors[kvp.Key]; text = text.Replace($"{{{kvp.Key}}}", $"{value}"); color = null; } else { text = text.Replace($"{{{kvp.Key}}}", value); } } return text; } private static string HumanizePascalCase(string text) { if (string.IsNullOrEmpty(text)) return string.Empty; var sb = new StringBuilder(); foreach (char c in text) { if (char.IsUpper(c) && sb.Length != 0 && !char.IsUpper(sb[sb.Length - 1])) sb.Append(" "); sb.Append(c); } return sb.ToString(); } private string StripRichText(string text) { if (string.IsNullOrEmpty(text)) return string.Empty; text = _colorTagRegex.Replace(text, string.Empty); text = _sizeTagRegex.Replace(text, string.Empty); foreach (var richTextLiteral in _richTextLiterals) text = text.Replace(richTextLiteral, string.Empty); return text; } private static bool MatchesCombatEntityType(CombatEntityType combatEntityType, string text) { if (combatEntityType == CombatEntityType.None && text == "-") return true; return combatEntityType.ToString().Equals(text); } private static bool MatchesDamageType(DamageType damageType, string text) { return damageType.ToString().Equals(text); } #endregion #region Configuration protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["Distance Unit Singular"] = "meter", ["Distance Unit Plural"] = "meters" }, this); } protected override void LoadDefaultConfig() => PrintWarning("Generating new configuration file..."); private sealed class PluginConfiguration { [JsonProperty("Translations")] public Translation Translations = new Translation(); [JsonProperty("Variable Formats")] public Dictionary VariableFormats = new Dictionary { ["attachments"] = " ({value})" }; [JsonProperty("Variable Colors")] public Dictionary VariableColors = new Dictionary { ["killer"] = "#C4FF00", ["victim"] = "#C4FF00", ["weapon"] = "#C4FF00", ["attachments"] = "#C4FF00", ["distance"] = "#C4FF00", ["owner"] = "#C4FF00" }; [JsonProperty("Chat Format")] public string ChatFormat = "[DeathNotes] {message}"; [JsonProperty("Chat Icon (SteamID)")] public string ChatIcon = "76561198077847390"; [JsonProperty("Show Kills in Console")] public bool ShowInConsole = true; [JsonProperty("Show Kills in Chat")] public bool ShowInChat = true; [JsonProperty("Message Broadcast Radius (in meters)")] public int MessageRadius = -1; [JsonProperty("Use Metric Distance")] public bool UseMetricDistance = true; [JsonProperty("Require Permission (deathnotes.cansee)")] public bool RequirePermission = false; public void LoadDefaults() { if (Translations.Messages == null) { var defaults = new RemoteConfiguration>("DefaultMessages"); defaults.Load(success => { if (success) { Translations.Messages = defaults.Contents; _instance.Config.WriteObject(this); } }); } } public class DeathMessage { public string KillerType { get; set; } public string VictimType { get; set; } public string DamageType { get; set; } public string[] Messages { get; set; } protected bool Equals(DeathMessage other) => string.Equals(KillerType, other.KillerType) && string.Equals(VictimType, other.VictimType) && string.Equals(DamageType, other.DamageType); } public class Translation { [JsonProperty("Death Messages")] public List Messages; [JsonProperty("Names")] public Dictionary Names = new Dictionary(); [JsonProperty("Bodyparts")] public Dictionary Bodyparts = new Dictionary(); [JsonProperty("Weapons")] public Dictionary Weapons = new Dictionary(); [JsonProperty("Attachments")] public Dictionary Attachments = new Dictionary(); } } internal sealed class RemoteConfiguration { private const string Host = "http://files.laserhydra.com/config/DeathNotes/v6.3.6/"; private readonly string _file; public RemoteConfiguration(string file) { _file = file; } public T Contents { get; private set; } private string ExactUrl => Host + _file; public void Load(Action callback = null) { _instance.webrequest.Enqueue(ExactUrl, string.Empty, (code, response) => { try { if (!IsSuccessStatusCode(code)) throw new Exception($"Status code indicates failure. Code: {code}"); Contents = JsonConvert.DeserializeObject(response); callback?.Invoke(true); Interface.Oxide.DataFileSystem.WriteObject($"{nameof(DeathNotes)}/{_file}", Contents); } catch (Exception ex) { if (Interface.Oxide.DataFileSystem.ExistsDatafile($"{nameof(DeathNotes)}/{_file}")) { Contents = Interface.Oxide.DataFileSystem.ReadObject($"{nameof(DeathNotes)}/{_file}"); _instance.PrintWarning( $"Could not load remote config '{_file}'. The plugin will be using the previously downloaded file."); callback?.Invoke(true); } else { _instance.PrintError( $"Could not load remote config '{_file}'. The plugin will not work properly. Please check whether you can access {ExactUrl} via your browser. If you can, please check the FAQ on how to solve this."); _instance.PrintError($"[Code {code}] {ex.GetType().Name}: {ex.Message}"); _instance.PrintError($"Response: {response}"); callback?.Invoke(false); } } }, _instance); } private bool IsSuccessStatusCode(int code) => code >= 200 && code < 300; } #endregion } }