Иконка ресурса

FREE BetterLoot : 4.2.5 4.2.5

Нет прав для скачивания
C#:
using System;
using System.IO;
using Oxide.Core;
using System.Data;
using System.Linq;
using UnityEngine;
using Rust.Ai.Gen2;
using Newtonsoft.Json;
using Facepunch.Extend;
using Oxide.Core.Plugins;
using System.Collections;
using System.Globalization;
using Newtonsoft.Json.Linq;
using static ConsoleSystem;
using Pool = Facepunch.Pool;
using UnityEngine.Networking;
using Random = System.Random;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using Oxide.Plugins.BetterLootExtensions;
namespace Oxide.Plugins {
    [Info("BetterLoot", "MagicServices.co // TGWA", "4.2.5")]
    [Description("A light loot container modification system with rarity support | Previously maintained and updated by Khan & Tryhard")]
    public class BetterLoot : RustPlugin {
#region Fields
        [PluginReference]
        Plugin? CustomLootSpawns;
        // Static Instance
        private static BetterLoot? _instance;
        private static PluginConfig? _config;
        // System States
        private bool Changed = true;
        private static bool NewConfigGenerated;
        private static bool NewSave;
        private static bool Initialized;
        private static Random? RNG;
        private static Regex? UniqueTagREGEX;
        // Data Instances
        private Dictionary<string, List<string>[]> Items = new Dictionary<string, List<string>[]>();
        // Cached Item Data for each container
        private Dictionary<string, List<string>[]> Blueprints = new Dictionary<string, List<string>[]>();
        // Cached Blueprint Data for each container
        private Dictionary<string, int[]> ItemWeights = new Dictionary<string, int[]>();
        // Item weights for each container
        private Dictionary<string, int[]> BlueprintWeights = new Dictionary<string, int[]>();
        // Blueprint weights for each container
        private Dictionary<string, int> TotalItemWeights = new Dictionary<string, int>();
        // Total sum of item weights for each container
        private Dictionary<string, int> TotalBlueprintWeights = new Dictionary<string, int>();
        // Total sum of blueprint weights for each container
#region Info Caching
        private Dictionary<string, WI_Cache>? WeaponInfoCache;
        // Item info for building table
        private Dictionary<string, ItemSlot>? WeaponModInfoCache;
        // Weapon mod shortname to enum mapping.
        private List<string>? DurabilityItems;
        // Items that need the durability property
        private static ItemDefinition? BlueprintBaseDef;
        private List<string> ItemManagerItemNamesList = new List<string>();
        /// <summary>
        /// Info for each weapon type item
        /// Contains info for building config data for if a item is a weapon (it will be in this dict) as well as how many mods and how much ammo it is allowed to have.
        /// </summary>
        private sealed class WI_Cache {
            public bool IsLiquidWeapon {
                get;
            }
            // is a watergun.
            public int MaxMods {
                get;
            }
            // if > 0 can have mods + max mods property.
            public int MaxAmmo {
                get;
            }
            // vanilla max ammo.
            public ItemSlot ModTypes {
                get;
            }
            // The types of mods that can be applied to this weapon.
            public WI_Cache(bool isLiquidWeapon, int maxMods, int maxAmmo, ItemSlot modTypes) {
                //IsLiquidWeapon = isLiquidWeapon;
                MaxMods = maxMods;
                MaxAmmo = maxAmmo;
                ModTypes = modTypes;
            }
        }
#endregion
#endregion
#region Instance Constants
        private const double BASE_ITEM_RARITY = 2;
        private const string ADMIN_PERM = "betterloot.admin";
#endregion
#region Lang
        private string BLLang(string key, string? id = null) => lang.GetMessage(key, this, id);
        private string BLLang(string key, string? id, params object[] args) => string.Format(BLLang(key, id), args);
        protected override void LoadDefaultMessages() {
            lang.RegisterMessages(new Dictionary<string, string> {
                {
                    "initialized", "Plugin not enabled"
                }
                , {
                    "perm", "You are not authorized to use this command"
                }
                , {
                    "syntax", "Usage: /blacklist [additem|deleteitem] \"ITEMNAME\""
                }
                , {
                    "none", "There are no blacklisted items"
                }
                , {
                    "blocked", "Blacklisted items: {0}"
                }
                , {
                    "notvalid", "Not a valid item: {0}"
                }
                , {
                    "blockedpass", "The item '{0}' is now blacklisted"
                }
                , {
                    "blockedtrue", "The item '{0}' is already blacklisted}"
                }
                , {
                    "unblacklisted", "The item '{0}' has been unblacklisted"
                }
                , {
                    "blockedfalse", "The item '{0}' is not blacklisted"
                }
                , {
                    "lootycmdformat", "Usage: /looty \"looty-id\""
                }
                ,  // Blank code provided.
                {
                    "lootynotfound", "The requested table id was not found. Please ensure youve got the right code."
                }
                // 404 Looty api
            }
            , this);
            //en
        }
#endregion
#region Config
        private class PluginConfig : SerializableConfiguration {
            [JsonProperty("Chat Configuration")]
            public ChatConfiguration ChatConfig = new ChatConfiguration();
            [JsonProperty("General Configuration")]
            public GenericConfiguration Generic = new GenericConfiguration();
            [JsonProperty("Loot Configuration")]
            public LootConfiguration Loot = new LootConfiguration();
            [JsonProperty("Loot Groups Configuration")]
            public LootGroupsConfiguration LootGroupsConfig = new LootGroupsConfiguration();
        }
        private class GenericConfiguration {
            [JsonProperty("Blueprint Weight (0.0 = min bias, 1.0 = max bias, 0.5 = balanced)")]
            public double BlueprintWeight = 0.11;
#region v4.2.1 Configuration Migration
            [JsonProperty("Blueprint Probability")]
            private double? LegacyBlueprintProbability {
                set => LegacyMigrate_BlueprintProbability(value);
            }
            private void LegacyMigrate_BlueprintProbability(double? value) {
                if (value.HasValue) BlueprintWeight = value.Value;
            }
#endregion
            [JsonProperty("Log Updates On Load")]
            public bool ListUpdatesOnLoad = true;
            [JsonProperty("Remove Stacked Containers")]
            public bool RemoveStackedContainers = true;
            [JsonProperty("Only update prefab list on wipe day")]
            public bool OnlyUpdatePrefabListOnWipe = false;
            // Slightly faster start time. (Doesnt check to regenerate prefab watch list from game manifest every startup).
            [JsonProperty("Auto enable new prefabs found on wipe")]
            public bool AutoEnableNewContainers = true;
            [JsonProperty("Watched Container Prefabs (true = monitor container loot, false = disabled)")]
            public Dictionary<string, bool> WatchedPrefabs = new();
#region v4.1.7 Configuration Migration
            [JsonProperty("Watched Prefabs")]
            private HashSet<string>? LegacyWatchedPrefabs {
                set => LegacyMerge_WatchedPrefabs(value);
            }
            private void LegacyMerge_WatchedPrefabs(IEnumerable<string>? values) {
                if (values == null) return;
                foreach (string prefab in values) if (!string.IsNullOrWhiteSpace(prefab)) WatchedPrefabs.TryAdd(prefab, true);
            }
#endregion
        }
        private class LootConfiguration {
            [JsonProperty("Enable Hammer Hit Loot Cycle")]
            public bool EnableHammerLootCycle = false;
            [JsonProperty("Hammer Loot Cycle Time")]
            public double HammerLootCycleTime = 3.0;
            [JsonProperty("Loot Multiplier")]
            public int LootMultiplier = 1;
            [JsonProperty("Scrap Multipler")]
            public int ScrapMultiplier = 1;
            [JsonProperty("Allow duplicate items")]
            public bool AllowDuplicateItems = true;
            [JsonProperty("Enable logging for item attachments auto balancing operations")]
            public bool EnableBonusItemsAutoBalanceLogging = false;
            [JsonProperty("Always allow duplicate items from bonus items list (if set, will override 'Allow duplicate items option')")]
            public bool AllowBonusItemsDuplicateItems = true;
            [JsonProperty("Enable Blueprint Conversion")]
            public bool EnableBlueprintConversion = true;
            [JsonProperty("Allow Duplicate Blueprints")]
            public bool AllowDuplicateBlueprints = false;
            [JsonProperty("Enable Loot Pool Locking System")]
            public bool EnableLootPoolLocking = true;
        }
        private class ChatConfiguration {
            [JsonProperty("Chat Message Prefix")]
            public string Prefix = $"[<color=#00ff00>{nameof(BetterLoot)}</color>]";
            [JsonProperty("Chat Message Icon SteamID (0 = None)")]
            public ulong MessageIcon = 0;
        }
        private class LootGroupsConfiguration {
            [JsonProperty("Enable creation of example loot group on load?")]
            public bool EnableExampleGroupCreation = true;
            [JsonProperty("Enable auto profile probability balancing?")]
            public bool EnableProbabilityBalancing = true;
            [JsonProperty("Always allow duplicate items from loot groups (if true overrides 'Allow duplicate items option')")]
            public bool AllowLootGroupDuplicateItems = true;
            [JsonProperty("Allowed probablity difference to select neighbour during duplicate item resolution.")]
            public double AllowedDuplicateNudgeDifference = 10;
        }
        /// <summary>
        /// As of v4.1.7 this system runs everytime to check for new prefabs as well as maintain the integrity of the current prefab list that is available so it can be enabled / disabled easily at any time.
        /// This behaviour can optionally be disabled in the config for it to only run on wipe day (to update for any new prefab types after a update)
        /// </summary>
        private void CheckWatchedPrefabs() {
            /* Watched Prefabs Auto-Population */
            if (_config.Generic.OnlyUpdatePrefabListOnWipe && !NewSave)  // If it is a new wipe new found prefabs will be auto enabled.
            return;
            NewConfigGenerated = _config.Generic.WatchedPrefabs.Count == 0;
            if (NewConfigGenerated) Log("Checking for missing viable loot containers in prefab watch list. (Currently disabled containers will stay disabled).");
            // Name filtering
            List<string> negativePartialNames = Pool.Get<List<string>>();
            List<string> partialNames = Pool.Get<List<string>>();
            // If does not contain, skip
            negativePartialNames.AddRange(new[] {
                "resource/loot", "misc/supply drop/supply_drop", "/npc/m2bradley/bradley_crate", "/npc/patrol helicopter/heli_crate", "/deployable/chinooklockedcrate/chinooklocked", "/deployable/chinooklockedcrate/codelocked", "prefabs/radtown", "props/roadsigns", "humannpc/scientist", "humannpc/tunneldweller", "humannpc/underwaterdweller", "ptboat.deepsea", "rhib.deepsea", "cache/food"
            }
            );
            // If does contain, skip
            partialNames.AddRange(new[] {
                "radtown/ore", "static", "/spawners", "radtown/desk", "radtown/loot_component_test", "chinooklockedcrate/chinooklockedcrate",  // Specific crate with no spawnable
                "water_puddles_border_fix"  // Weird container prefab from radtown update??
            }
            );
            // Adding default values
            foreach (GameManifest.PrefabProperties category in GameManifest.Current.prefabProperties) {
                string name = category.name;
                if (!negativePartialNames.ContainsPartial(name) || partialNames.ContainsPartial(name)) continue;
                // Add false by default, may have been disabled by user if was not in list from previous version. If is a new wipe and auto-enable is set, containers will be added as enabled prefabs.
                _config.Generic.WatchedPrefabs.TryAdd(name, NewConfigGenerated || (NewSave && _config.Generic.AutoEnableNewContainers));
            }
            if (NewConfigGenerated) {
                Log("Updated configuration with manifest values.");
                AttemptSendLootyLink();
            }
            Pool.FreeUnmanaged(ref negativePartialNames);
            Pool.FreeUnmanaged(ref partialNames);
        }
        protected override void LoadDefaultConfig() => _config = new PluginConfig();
        protected override void LoadConfig() {
            base.LoadConfig();
            try {
                _config = Config.ReadObject<PluginConfig>();
                if (MaybeUpdateConfig(_config)) {
                    Log("Configuration appears to be outdated; updating and saving Better Loot");
                    SaveConfig();
                }
                Log("Loaded configuration!");
            }
            catch (Exception ex) {
                Log("Failed to load Better Loot config file (is the config file corrupt?) (" + ex.Message + ")");
            }
        }
        protected override void SaveConfig() {
            Log($"Configuration changes saved to {nameof(BetterLoot)}.json");
            Config.WriteObject(_config, true);
        }
#region Configuration Updater
        internal class SerializableConfiguration {
            public string ToJson() => JsonConvert.SerializeObject(this);
            public Dictionary<string, object> ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary<string, object>;
        }
        private static class JsonHelper {
            public static object Deserialize(string json) => ToObject(JToken.Parse(json));
            private static object ToObject(JToken token) {
                switch (token.Type) {
                    case JTokenType.Object : return token.Children<JProperty>().ToDictionary(prop => prop.Name, prop => ToObject(prop.Value));
                    case JTokenType.Array : return token.Select(ToObject).ToList();
                    default : return ((JValue)token).Value;
                }
            }
        }
        private bool MaybeUpdateConfig(SerializableConfiguration config) {
            var currentWithDefaults = config.ToDictionary();
            var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value);
            return MaybeUpdateConfigDict(currentWithDefaults, currentRaw);
        }
        private bool MaybeUpdateConfigDict(Dictionary<string, object> currentWithDefaults, Dictionary<string, object> currentRaw) {
            bool changed = false;
            foreach (var key in currentWithDefaults.Keys) {
                if (currentRaw.TryGetValue(key, out object? currentRawValue) && currentRawValue is not null) {
                    var defaultDictValue = currentWithDefaults[key] as Dictionary<string, object>;
                    var currentDictValue = currentRawValue as Dictionary<string, object>;
                    if (defaultDictValue != null) {
                        if (currentDictValue == null) {
                            currentRaw[key] = currentWithDefaults[key];
                            changed = true;
                        }
                        else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue)) changed = true;
                    }
                }
                else {
                    currentRaw[key] = currentWithDefaults[key];
                    changed = true;
                }
            }
            return changed;
        }
#endregion
#endregion
#region Oxide Loaded / Unload / Server Load / New Save
        private void Loaded() {
            _instance = this;
            RNG = new Random();
            UniqueTagREGEX = new Regex(@"\{\d+\}", RegexOptions.Compiled);
            DataSystem.LoadBlacklist();
            DataSystem.LoadLootTables();
            DataSystem.LoadLootGroups();
        }
        private void OnServerInitialized() {
            try {
                CheckWatchedPrefabs();
                // Called so OnNewSave can trigger flag
                ItemManager.Initialize();
                BlueprintBaseDef = ItemManager.FindItemDefinition("blueprintbase");
                BuildWeaponInfoCache();
                permission.RegisterPermission(ADMIN_PERM, this);
                InitLootSystem();
            }
            catch (Exception ex) {
                Puts($"Error initializing plugin. Please ensure you configuration and data files are corrent and that you have used the looty editor to confirm. EX: \n{ex.Message}");
                Server.Command($"o.unload {Name}");
            }
        }
        private void InitLootSystem(bool newData = false) {
            // Ensure empty, is called when looty tables are loaded.
            if (newData) {
                Items.Clear();
                Blueprints.Clear();
                ItemWeights.Clear();
                BlueprintWeights.Clear();
                TotalItemWeights.Clear();
                TotalBlueprintWeights.Clear();
                BuildWeaponInfoCache();
            }
            // Load container data
            LoadAllContainers();
            Pool.FreeUnmanaged(ref WeaponInfoCache);
            Pool.FreeUnmanaged(ref WeaponModInfoCache);
            Pool.FreeUnmanaged(ref DurabilityItems);
            UpdateInternals(_config.Generic.ListUpdatesOnLoad);
        }
        private void Unload() {
            // Static variable instances
            UniqueTagREGEX = null;
            storedBlacklist = null;
            lootTables = null;
            lootGroups = null;
            RNG = null;
            BlueprintBaseDef = null;
            // Reset Static Flags
            NewConfigGenerated = false;
            NewSave = false;
            Initialized = false;
            // Static BetterLoot instance
            _instance = null;
            _config = null;
            foreach (HammerHitLootCycle hhlc in UnityEngine.Object.FindObjectsByType<HammerHitLootCycle>(FindObjectsInactive.Include, FindObjectsSortMode.None).Where(i => i is not null)) UnityEngine.Object.Destroy(hhlc);
        }
        // Set flag so new prefabs can be autoenabled
        private void OnNewSave(string _) => NewSave = true;
#endregion
#region DataFile
        private static LootTableData? lootTables = null;
        private static StoredBlacklist? storedBlacklist = null;
        private static LootGroupsData? lootGroups = null;
        // Looty API Schema
        private sealed record LootyResponse {
            public LootyTarget looty = new ();
            [JsonProperty("LootTable")]
            public Dictionary<string, PrefabLoot> LootTables = new();
            [JsonProperty("Loot Groups")]
            public Dictionary<string, LootProfile>? LootGroups = new();
            public LootyResponse() {
            }
            public sealed record LootyTarget {
                public string target;
                public string id;
            }
        }
        // LootTables.json structure
        private class LootTableData {
            public Dictionary<string, PrefabLoot> LootTables = new Dictionary<string, PrefabLoot>();
            public LootTableData() {
            }
        }
        // Blacklist.json structure
        private class StoredBlacklist {
            public HashSet<string> ItemList = new HashSet<string>();
            public StoredBlacklist() {
            }
        }
        private class LootGroupsData {
            [JsonProperty("Loot Groups", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public Dictionary<string, LootProfile> LootGroups = new Dictionary<string, LootProfile> {
                ["example_group"]
                = new LootProfile(new Dictionary<string, LootProfile.LootRNG> {
                    ["lmg.m249"]
                    = new LootProfile.LootRNG(10, new LootEntry(1, 2))
                }
                , false)
            }
            ;
            public LootGroupsData() {
            }
            public static void ValidateGroups(LootGroupsData? Data) {
                ItemManager.Initialize();
                if (ItemManager.itemDictionaryByName is null) {
                    Log("Error: Failed to initialize ItemDictionary. Unloading");
                    _instance.Server.Command($"o.unload BetterLoot");
                    return;
                }
                if (Data is null || Data.LootGroups is null) {
                    Log($"Error: Invalid data was provided to the {nameof(LootGroupsData)} validator!");
                    return;
                }
                // Attempt to create an example group in the LootGroups file
                TryCreateExampleGroup();
                foreach ((string profileName, LootProfile? profileData) in Data.LootGroups) {
                    Log($"Validating LootGroup: \"{profileName}\"");
                    // NRE Data Check
                    if (profileData.ItemList is null) {
                        Log("- Error: Profile item list is null. Skipping...");
                        continue;
                    }
                    // Ensure items are valid
                    List<string> invalidItemPrefabs = Pool.Get<List<string>>();
                    invalidItemPrefabs.AddRange(profileData.ItemList.Keys.Where(key => !ItemManager.itemDictionaryByName.ContainsKey(UniqueTagREGEX.Replace(key.ToLower(), string.Empty))));
                    if (profileData.ItemList.RemoveAll(invalidItemPrefabs.Contains) is int removeCount && removeCount > 0) Log($"Error - Removing {removeCount} invalid entries. Please check item names that were not found in the games item dictionary: ({string.Join(", ", invalidItemPrefabs)})");
                    Pool.FreeUnmanaged(ref invalidItemPrefabs);
                    string extraReason = string.Empty;
                    if (profileData.ItemList.Count > 0)  // Ensure we still have items
                    {
                        // Balance loot percentages
                        if (_config.LootGroupsConfig.EnableProbabilityBalancing) {
                            double GetSum() => profileData.ItemList.Sum(x => x.Value.Probability);
                            double Round(double x) => Math.Round(x, 2);
                            const double target = 100;
                            double sum = GetSum();
                            if (Math.Abs(target - sum) > 1e-3) {
                                Log($"- Profile probability sum ({sum}) != 100. Balancing profile!");
                                double _ratio = target / sum;
                                // Set first key as largest by default for empty string edgecase
                                string largestKey = profileData.ItemList.Keys.First();
                                double largestValue = profileData.ItemList[largestKey].Probability;
                                foreach (var item in profileData.ItemList) {
                                    double probability = item.Value.Probability;
                                    if (probability > largestValue) {
                                        largestValue = probability;
                                        largestKey = item.Key;
                                    }
                                    item.Value.Probability = Round(probability * _ratio);
                                }
                                var largestEntry = profileData.ItemList[largestKey];
                                largestEntry.Probability = Round(largestEntry.Probability - Round(target - GetSum()));
                            }
                        }
                        else {
                            extraReason = "Probability balance skipped.";
                        }
                    }
                    else {
                        extraReason = "No remaining / valid items in profile, skipped.";
                    }
                    Log($"Profile \"{profileName}\" validation complete. {extraReason}");
                }
            }
            public static void TryCreateExampleGroup() {
                if (!_config.LootGroupsConfig.EnableExampleGroupCreation) return;
                // Create a default group in the first item of the loot table for reference if none exists
                var firstLootTable = lootTables.LootTables.FirstOrDefault();
                if (!firstLootTable.IsDefault() && firstLootTable.Value.LootProfiles.Count == 0) {
                    firstLootTable.Value.LootProfiles.Add(new PrefabLoot.LootProfileImport("example_group", 30, false));
                    Log($"Added LootGroup Import example to \"{firstLootTable.Key}\"");
                    DataSystem.SaveLootTables();
                }
            }
        }
        private static class DataSystem {
#region Public Methods
#region Blacklist
            private const string BL_FN = "Blacklist";
            public static void LoadBlacklist() => LoadFile(BL_FN, (blacklistData) => CheckNull(ref blacklistData, ref storedBlacklist, blacklistData?.ItemList), ref storedBlacklist);
            public static void SaveBlacklist() => SaveFile(BL_FN, (blacklistData) => CheckNull(ref blacklistData, ref storedBlacklist, blacklistData?.ItemList), ref storedBlacklist);
#endregion
#region Loot Tables
            private const string LT_FN = "LootTables";
            public static void LoadLootTables() => LoadFile(LT_FN, (tableData) => CheckNull(ref tableData, ref lootTables, tableData?.LootTables), ref lootTables);
            public static void SaveLootTables() => SaveFile(LT_FN, (tableData) => CheckNull(ref tableData, ref lootTables, tableData?.LootTables), ref lootTables);
#endregion
#region Loot Groups
            private const string LG_FN = "LootGroups";
            public static void LoadLootGroups() => LoadFile(LG_FN, (groupsData) => CheckNull(ref groupsData, ref lootGroups), ref lootGroups, LootGroupsData.ValidateGroups);
            public static void SaveLootGroups() => SaveFile(LG_FN, (groupsData) => CheckNull(ref groupsData, ref lootGroups), ref lootGroups);
#endregion
#endregion
#region DataFile Error Backup
            public static void BakDataFile(string filename, bool isRestoring = false, BasePlayer? msgPlayer = null) {
                // Move these to lang
                const string NoRestoreFileFound = "No backup file to restore.";
                const string NoMainFileFound = "No file found to move to backup, skipping file.";
                const string FileRestored = "Backup restored.";
                const string BackupTaken = "File backed up to bak file.";
                bool sendPlayer = msgPlayer is not null;
                string notifyMessage = string.Format(isRestoring ? "Restoring backup of {0}" : "Attempting to create backup of datafile {0}", $"{filename}.json");
                void Respond(string message) {
                    if (sendPlayer) _instance?.SendMessage(msgPlayer, message);
                    else Log(message);
                }
                Respond(notifyMessage);
                // Rename specified file to *.bak before regenerating a file in place of it
                string dataFilePath = Path.Combine(Interface.Oxide.DataFileSystem.Directory, $"{nameof(BetterLoot)}/{filename}.json");
                string bakFilePath = $"{dataFilePath}.bak";
                if (!isRestoring) {
                    if (!File.Exists(dataFilePath)) {
                        Respond(NoMainFileFound);
                        return;
                    }
                    if (File.Exists(bakFilePath)) File.Delete(bakFilePath);
                    File.Copy(dataFilePath, bakFilePath);
                }
                else {
                    if (!File.Exists(bakFilePath)) {
                        Respond(NoRestoreFileFound);
                        return;
                    }
                    if (File.Exists(dataFilePath)) File.Delete(dataFilePath);
                    File.Copy(bakFilePath, dataFilePath);
                }
                if (isRestoring) {
                    Respond(FileRestored);
                    _instance.InitLootSystem(true);
                }
                else {
                    Respond(BackupTaken);
                }
            }
#endregion
#region Save / Load Methods
            /// <summary>
            /// Load a data from a file within the plugin data directory.
            /// </summary>
            /// <typeparam name="T">The structure of the data being read from the file.</typeparam>
            /// <param name="fileName">The name of the file within the plugin data directory</param>
            /// <param name="validator">A custom data validator. Is nullable.</param>
            /// <param name="loadVar">The variable where the loaded data should be stored.</param>
            private static void LoadFile<T>(string fileName, Func<T, T>? validator, ref T loadVar, Action<T>? postLoadMethod = null) where T : class? {
                // If no validator was provided, set to check if instance is null, if it is create new instance.
                if (validator is null) validator = (data) => data ?? Activator.CreateInstance<T>();
                try {
                    loadVar = validator(Interface.Oxide.DataFileSystem.ReadObject<T>($"{nameof(BetterLoot)}\\{fileName}"));
                    Log($"Loaded file \"{fileName}\" datafile successfully!");
                }
                catch (Exception e) {
                    Log($"ERROR: There was an issue loading your \"{fileName}.json\" datafile, a new one has been created.\n{e.Message}");
                    BakDataFile(fileName);
                    loadVar = Activator.CreateInstance<T>();
                }
                postLoadMethod?.Invoke(loadVar);
                SaveFile(fileName, validator, ref loadVar);
            }
            /// <summary>
            /// Save plugin data to the plugin data directory.
            /// </summary>
            /// <typeparam name="T">The type of the datafile structure</typeparam>
            /// <param name="fileName">The name of the datafile within the plugin data directory.</param>
            /// <param name="validator">A custom data validator. Is nullable.</param>
            /// <param name="saveVar">The variable where this data is currently stored.</param>
            private static void SaveFile<T>(string fileName, Func<T, T>? validator, ref T saveVar) where T : class? {
                if (validator is null) validator = (data) => data ?? Activator.CreateInstance<T>();
                Interface.Oxide.DataFileSystem.WriteObject($"{nameof(BetterLoot)}\\{fileName}", validator(saveVar));
                Log($"Saved {fileName}.json");
            }
#endregion
#region Data Validator
            /// <summary>
            /// Checks if the provided datafile's data is null, if so create a new instance and optionally write it to file.
            /// </summary>
            /// <typeparam name="T">The type of the data structure that is being checked</typeparam>
            /// <param name="obj">The local instance of the data to check</param>
            /// <param name="target">The global instance of where the data is held</param>
            /// <param name="additional">Additional objects to check if null aside from arguement 'obj'</param>
            /// <returns>>Non null type of provided object type</returns>
            private static T CheckNull<T>(ref T? obj, ref T? target, params object?[] additional) where T : class? {
                if (obj is null || additional.Any(x => x is null)) {
                    target = Activator.CreateInstance<T>();
                    obj = target;
                }
                return obj;
            }
#endregion
        }
        private static void AttemptSendLootyLink() {
            Log("--------------------------------------------------------------------------");
            Log("Use the Looty Editor to easily edit and create loot tables for BetterLoot!");
            Log("Find it here -> https://looty.cc/betterloot-v4");
            Log("--------------------------------------------------------------------------");
        }
#endregion
#region Culminative Probabilities Class
        public class ProbalisticRNG {
            [JsonIgnore]
            private List<double> _culminativeProbabilities = new List<double>();
            [JsonIgnore]
            public bool DoProbabilitiesExist => _culminativeProbabilities.Count > 0;
            public void UpdateProbabilities(IEnumerable<double> probabilities) {
                double _culminative = 0;
                foreach (int item in probabilities) {
                    _culminative += item;
                    _culminativeProbabilities.Add(_culminative);
                }
            }
            public int GetRandomIndex() {
                double randomSelect = RNG.NextDouble() * 1e2;
                int elementIndex = _culminativeProbabilities.BinarySearch(randomSelect);
                if (elementIndex < 0) elementIndex = ~elementIndex;
                return elementIndex;
            }
        }
#endregion
#region Loot Classes
        public record ItemConvertInfo(Item Item, bool CanBeBp);
        /// <summary>
        /// Prefab Loot system will be contained in a list. This is the new loot class for loot containers that will
        /// allow the import of custom loot groups allowing for RNG on groups as well as individual items
        /// </summary>
        private class PrefabLoot : ProbalisticRNG {
            [JsonProperty("Is Prefab Enabled?")]
            public bool Enabled;
            [JsonProperty("Loot Profiles", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<LootProfileImport> LootProfiles;
            [JsonProperty("Enable Loot Pool Locking")]
            public bool LootPoolLocking;
            [JsonProperty("Select ungrouped items ignoring rarity bias")]
            public bool IgnoreRarityBias;
            [JsonProperty("Guaranteed Items")]
            public Dictionary<string, LootEntrySettings> GuaranteedItems = new Dictionary<string, LootEntrySettings>();
            [JsonProperty("Ungrouped Items")]
            public Dictionary<string, LootEntry> UngroupedItems;
            [JsonProperty("Item Settings")]
            public ItemProperties ItemSettings;
            public PrefabLoot() {
                LootProfiles = new List<LootProfileImport>();
                UngroupedItems = new Dictionary<string, LootEntry>();
                ItemSettings = new ItemProperties();
            }
            internal class LootProfileImport {
                [JsonProperty("Group Enabled?")]
                public bool Enabled = true;
                [JsonProperty("Loot Profile Name")]
                public string LootProfileName = string.Empty;
                [JsonProperty("Loot Profile Probability (1% - 100%)")]
                public double LootProfileProbability;
                internal LootProfileImport() {
                }
                internal LootProfileImport(string LootProfileName, double LootProfileProbability, bool Enabled = true) {
                    this.LootProfileName = LootProfileName;
                    this.LootProfileProbability = LootProfileProbability;
                    this.Enabled = Enabled;
                }
            }
            internal class ItemProperties {
                [JsonProperty("Minimum Amount of Items")]
                public int ItemsMin;
                [JsonProperty("Maximum Amount of Items")]
                public int ItemsMax;
                [JsonProperty("Minimum Scrap Amount")]
                public int MinScrap;
                [JsonProperty("Maximum Scrap Amount")]
                public int MaxScrap;
                [JsonProperty("Minimum Blueprints")]
                public int MinBlueprints = 0;
                [JsonProperty("Maximum Blueprints")]
                public int MaxBlueprints = 0;
                [JsonProperty("Bonus Items Contribute to Item Count")]
                public bool bonusItemsAddCount;
                [JsonProperty("Guaranteed Items Contribute to Item Count")]
                public bool guaranteedItemsAddCount;
                internal ItemProperties() {
                }
#region v4.0.6  / v4.2.1 Configuration Migration
                [JsonProperty("Scrap Amount", NullValueHandling = NullValueHandling.Ignore)]
                private int? LegacyScrap {
                    get;
                    set;
                }
                [JsonProperty("Max Blueprints", NullValueHandling = NullValueHandling.Ignore)]
                private int? LegacyMaxBPs {
                    get;
                    set;
                }
                [OnDeserialized]
                private void OnDeserialized(StreamingContext _) {
                    if (LegacyScrap.HasValue) {
                        if (MaxScrap == 0) {
                            MaxScrap = LegacyScrap.Value;
                            MinScrap = LegacyScrap.Value;
                        }
                    }
                    if (LegacyMaxBPs.HasValue && MaxBlueprints == 0) {
                        MaxBlueprints = LegacyMaxBPs.Value;
                    }
                    LegacyScrap = null;
                    LegacyMaxBPs = null;
                }
                [OnSerializing]
                private void OnSerializing(StreamingContext _) => LegacyScrap = null;
                // force null, no omit
#endregion
            }
#region Random Profile Selector
            [JsonIgnore]
            private List<int> _enabledProfiles = new ();
            // Map position to index
            /// <summary>
            /// Implemented binary search to select random loot import profile quickly. Returns null if should select from ungrouped items.
            /// </summary>
            /// <param name="tableReference">for reference to use in error message if there is a problem with the selection or config</param>
            /// <returns></returns>
            public LootProfile? GetRandomProfile(string? tableReference) {
                if (LootProfiles is null) return null;
                if (!DoProbabilitiesExist) {
                    // Updates and uses probalistic probability based off of only enabled profiles
                    List<LootProfileImport> _enabledProfiles = new List<LootProfileImport>();
                    for (int i = 0; i < LootProfiles.Count; i++) {
                        var _profile = LootProfiles[i];
                        if (_profile.Enabled) {
                            _enabledProfiles.Add(_profile);
                            this._enabledProfiles.Add(i);
                        }
                    }
                    UpdateProbabilities(_enabledProfiles.Select(x => x.LootProfileProbability));
                }
                int randomProfileIndex = GetRandomIndex();
                if (randomProfileIndex >= _enabledProfiles.Count) return null;
                var importProfile = LootProfiles[_enabledProfiles[randomProfileIndex]];
                if (!lootGroups.LootGroups.TryGetValue(importProfile.LootProfileName, out LootProfile? profile)) {
                    Log($"WARNING: prefab \"{tableReference}\" requested a loot group import with name \"{importProfile.LootProfileName}\". Group does not exist or is disabled in the LootGroups.json!");
                    return null;
                }
                // No Warning
                if (!profile.Enabled) return null;
                return profile;
            }
#endregion
        }
        /// <summary>
        /// LootProfile for containing all items that will be part of a certain profile
        /// Will be referenced by the specified profile name that the user creates within the LootGroups.json
        /// </summary>
        public class LootProfile : ProbalisticRNG {
            [JsonProperty("Enabled?")]
            public bool Enabled = true;
            [JsonProperty("Guaranteed Items")]
            public Dictionary<string, LootEntrySettings> GuaranteedItems = new ();
            [JsonProperty("Item List")]
            public Dictionary<string, LootRNG> ItemList;
            public LootProfile(Dictionary<string, LootRNG> ItemList, bool Enabled = true) {
                this.ItemList = ItemList;
                this.Enabled = Enabled;
            }
            public class LootRNG {
                [JsonProperty("Item Probability (1-100)")]
                public double Probability;
                [JsonProperty("Item Amount")]
                public LootEntry Amount;
                public LootRNG(double Probability, LootEntry Amount) {
                    this.Probability = Probability;
                    this.Amount = Amount;
                }
            }
#region Probalistic Selector Methods
            /// <summary>
            /// Get a random item from this loot group based off of items probabilities
            /// </summary>
            public (ItemConvertInfo?, List<ItemConvertInfo>?) GetItem(HashSet<string> currentItemEntries) {
                int itemIndex = GetRandomIndex();
                // No item found, out of index
                if (itemIndex >= ItemList.Count) return (null, null);
                var bonusItems = new List<ItemConvertInfo>();
                var entry = ItemList.ElementAt(itemIndex);
                if (!entry.Value.Amount.allowDuplicates && currentItemEntries.Contains(entry.Key)) {
                    // Only item in the list and it's a duplicate, else all hope is lost :(
                    if (ItemList.Count == 1) return (null, null);
                    // Prefer the larger-index neighbour, fall back to smaller
                    if (itemIndex < ItemList.Count - 1) itemIndex++;
                    else if (itemIndex > 0 && (entry.Value.Probability - ItemList.ElementAt(itemIndex - 1).Value.Probability) <= _config.LootGroupsConfig.AllowedDuplicateNudgeDifference) itemIndex--;
                    else return (null, null);
                    // Set entry to the entry of the nudged index
                    entry = ItemList.ElementAt(itemIndex);
                }
                entry.Value.Amount.CreateBonusItems(ref bonusItems);
                // Create Item
                string sanitizedName = UniqueTagREGEX.Replace(entry.Key, string.Empty);
                Item item = ItemManager.CreateByPartialName(sanitizedName, GetRNG(entry.Value.Amount.Min, entry.Value.Amount.Max), entry.Value.Amount.SkinId);
                // Apply custom properties
                entry.Value.Amount.ApplyAllProperties(item);
                if (item is null) Log($"ERROR: item \"{entry.Key}\" could not be created! System returned null entry!");
                // Add for future duplicate checking.
                currentItemEntries.Add(entry.Key);
                return (new ItemConvertInfo(item, entry.Value.Amount.CanConvertToBlueprint ?? false), bonusItems);
            }
#endregion
        }
#region Loot Entry
        public class LootEntrySettings {
            [JsonProperty("Skin ID (0 = default)")]
            public ulong SkinId = 0;
            [JsonProperty("Display Name (empty = none)")]
            public string? DisplayName = string.Empty;
            [JsonProperty("Item Minimum")]
            public int Min;
            [JsonProperty("Item Maximum")]
            public int Max;
            [JsonProperty("Can Convert To Blueprint", NullValueHandling = NullValueHandling.Ignore)]
            public bool? CanConvertToBlueprint;
            [JsonProperty("Item Durability", NullValueHandling = NullValueHandling.Ignore)]
            public LootEntryDurability? DurabilitySettings;
            // By default will not exist in item entries. System will add it during table scan.
            [JsonProperty("Item Properties", NullValueHandling = NullValueHandling.Ignore)]
            public ItemEntrySettings? ItemEntryModifications;
            public void ApplyAllProperties(Item item) {
                ApplyAmmo(item);
                ApplyAttachments(item);
                if (!string.IsNullOrWhiteSpace(DisplayName)) item.name = DisplayName;
                if (DurabilitySettings is {
                }
                ) item.ChangeConditionPercentage(GetRNG(DurabilitySettings.MinDurability, DurabilitySettings.MaxDurability));
                item.MarkDirty();
            }
            public void ApplyAttachments(Item item) {
                // No mods to apply
                if (!(ItemEntryModifications?.AttachmentSettings?.itemMods?.Count > 0)) return;
                // Recursively apply
                int total = Math.Clamp(GetRNG(ItemEntryModifications.AttachmentSettings.minModAmount, ItemEntryModifications.AttachmentSettings.maxModAmount), 1, ItemEntryModifications.maxMods);
                for (int i = 0; i < total; i++) {
                    for (int r = 0; r < 5; r++) {
                        Item? itemMod = ItemEntryModifications?.GetRandomItemMod();
                        if (itemMod is null)  // No item selected! This is ok.
                        break;
                        // Attempt to regenerate if there is a duplicate
                        if (itemMod.MoveToContainer(item.contents)) break;
                    }
                }
            }
            public void ApplyAmmo(Item item) {
                if (ItemEntryModifications?.AmmunitionSettings is not ItemEntrySettings.AmmoSettings ammoSettings || string.IsNullOrWhiteSpace(ammoSettings.AmmoItemShortname)) return;
                // if extended mag is applied, add 25% to calculated ammo amount
                ItemDefinition? ammoDef = null;
                int ammoAmount = 1;
                bool applyExtraAmmo = item.contents?.itemList.Any(x => x.info.shortname.Equals("weapon.mod.extendedmags")) ?? false;
                if (ammoSettings.CanHoldMultipleAmmoUnits) {
                    ammoDef = ItemManager.FindDefinitionByPartialName(ammoSettings.AmmoItemShortname);
                    ammoAmount = Math.Clamp(GetRNG(ammoSettings.Min, ammoSettings.Max), 0, ammoSettings.MaxAmmo);
                    if (item.contents?.itemList.Any(
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху