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

FREE BetterLoot : 4.0.1 4.0.1

Нет прав для скачивания
C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Rust;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using UnityEngine;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Configuration;
using Random = System.Random;
using Oxide.Core;
using Oxide.Core.Plugins;
using Pool = Facepunch.Pool;
using System.Collections;
using Oxide.Plugins.BetterLootExtenstions;
using Facepunch.Extend;
using System.Data;
using System.Text.RegularExpressions;
using Oxide.Core.Libraries.Covalence;
using UnityEngine.Networking;
using static ConsoleSystem;
namespace Oxide.Plugins {
    [Info("BetterLoot", "MagicServices.co // TGWA", "4.0.1")]
    [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 bool initialized;
        private int populatedContainers;
        private static Random? rng;
        private static Regex? uniqueTagRegex;
        private static Regex? lootyIdRegex;
        // 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> totalItemWeight = new Dictionary<string, int>();
        // Total sum of item weights for each container
        private Dictionary<string, int> totalBlueprintWeight = new Dictionary<string, int>();
        // Total sum of blueprint weights for each container
        /// <summary>
        /// Used for when a npc dies that we can check their prefab type and then use that to populate their loot drop./// This has to be done to avoid issues with overhead of directly modifying loot slots (which would have to be restored on unload if anyone wanted to modify them
        /// without having to respawn the npc entity)
        ///
        /// The userid is stored as the SteamID on the NPC corpse, so this can be used to do a reverse check to see what the entity was.When we know the prefab
        /// we can pull the loot table profile and generate our loot to replace the vanilla generated loot./// </summary>
        private Dictionary<string, List<ulong>> npcLootMonitor = new Dictionary<string, List<ulong>>();
        // NPC Prefab : Currently Spawned Ent IDs
#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\""
                }
                , {
                    "lootynotfound", "The requested table id was not found.Please ensure youve got the right code."
                }
                , {
                    "lootyinvalidid", "Invalid looty id format.Please ensure youve got the right code."
                }
            }
            , 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 Probability")]
            public double blueprintProbability = 0.11;
            [JsonProperty("Log Updates On Load")]
            public bool listUpdatesOnLoaded = true;
            [JsonProperty("Remove Stacked Containers")]
            public bool removeStackedContainers = true;
            [JsonProperty("Watched Prefabs")]
            public HashSet<string> WatchedPrefabs = new HashSet<string>();
        }
        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;
        }
        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;
        }
        private void CheckWatchedPrefabs() {
            /* Watched Prefabs Auto-Population */
            if (_config?.Generic.WatchedPrefabs.Any() ?? false) return;
            Log("Updating watched prefabs from manifest...");
            // Name filtering
            List<string> negativePartialNames = Pool.Get<List<string>>();
            List<string> partialNames = Pool.Get<List<string>>();
            // If does not contain, skip
            negativePartialNames.AddRange(new List<string> {
                "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"
            }
            );
            // If does contain, skip
            partialNames.AddRange(new List<string> {
                "radtown/ore", "static", "/spawners", "radtown/desk", "radtown/loot_component_test", "water_puddles_border_fix"  // Weird container prefab from radtown update??
            }
            );
            foreach (GameManifest.PrefabProperties category in GameManifest.Current.prefabProperties) {
                string name = category.name;
                if (!negativePartialNames.ContainsPartial(name) || partialNames.ContainsPartial(name)) continue;
                if (!_config.Generic.WatchedPrefabs.Contains(name)) _config.Generic.WatchedPrefabs.Add(name);
            }
            SaveConfig();
            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 (_config == null) {
                    Log($"Generating Config File for Better Loot");
                    throw new JsonException();
                }
                if (MaybeUpdateConfig(_config)) {
                    Log("Configuration appears to be outdated; updating and saving Better Loot");
                    SaveConfig();
                }
            }
            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
        private void Loaded() {
            _instance = this;
            rng = new Random();
            uniqueTagRegex = new Regex(@"\{\d+\}");
            lootyIdRegex = new Regex(@"^looty-[A-Za-z0-9]{3}$");
            DataSystem.LoadBlacklist();
            DataSystem.LoadLootTables();
            DataSystem.LoadLootGroups();
            CheckWatchedPrefabs();
        }
        private void OnServerInitialized() {
            ItemManager.Initialize();
            permission.RegisterPermission(ADMIN_PERM, this);
            LoadAllContainers();
            UpdateInternals(_config.Generic.listUpdatesOnLoaded);
        }
        private void Unload() {
            // Static variable instances
            uniqueTagRegex = null;
            lootyIdRegex = null;
            storedBlacklist = null;
            lootTables = null;
            lootGroups = null;
            rng = null;
            // Static BetterLoot instance
            _instance = null;
            // Change this to a local list to track HammerHitLootCycle or change to Coroutine
            var gameObjects = UnityEngine.Object.FindObjectsOfType<HammerHitLootCycle>().ToList();
            if (gameObjects.Any()) {
                foreach (var objects in gameObjects) if (objects is not null) UnityEngine.Object.Destroy(objects);
            }
        }
#endregion
#region DataFile
        private static LootTableData? lootTables = null;
        private static StoredBlacklist? storedBlacklist = null;
        private static LootGroupsData? lootGroups = null;
        // 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 LootAmount(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($"- Removed {removeCount} invalid item entries");
                    Pool.FreeUnmanaged(ref invalidItemPrefabs);
                    // 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;
                            string largestKey = string.Empty;
                            double largestValue = 0;
                            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()));
                        }
                    }
                    Log($"Profile \"{profileName}\" validation complete.");
                }
            }
            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.Any()) {
                    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 restoreMode = false, BasePlayer? msgPlayer = null) {
                bool sendPlayer = msgPlayer is not null;
                string notifyMessage = string.Format(restoreMode ? "Restoring backup of {0}" : "Created backup of datafile {0}", $"{filename}.json");
                if (sendPlayer) _instance?.SendMessage(msgPlayer, notifyMessage);
                else Log(notifyMessage);
                // Rename specified file to *.bak before regenerating a file in place of it
                string path = Path.Combine(Interface.Oxide.DataFileSystem.Directory, $"{nameof(BetterLoot)}/{filename}.json");
                string bakPath = $"{path}.bak";
                if (File.Exists(bakPath)) {
                    if (!restoreMode) {
                        File.Delete(bakPath);
                    }
                    else {
                        const string msg = "No backup file to restore.";
                        if (sendPlayer) _instance?.SendMessage(msgPlayer, msg);
                        else Log(msg);
                        return;
                    }
                }
                File.Move(restoreMode ? bakPath : path, $"{path}{(!restoreMode ? ".bak" : string.Empty)}");
            }
#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://editor.looty.cc/betterloot-v2");
            Log("--------------------------------------------------------------------------");
        }
#endregion
#region Loot Classes
        /// <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 {
            [JsonProperty("Is Prefab Enabled?")]
            public bool Enabled;
            [JsonProperty("Loot Profiles", ObjectCreationHandling = ObjectCreationHandling.Replace)]
            public List<LootProfileImport> LootProfiles;
            [JsonProperty("Ungrouped Items")]
            public Dictionary<string, LootAmount> UngroupedItems;
            [JsonProperty("Item Settings")]
            public ItemSettings itemSettings;
            [JsonIgnore]
            public int ItemCount => GetTotalItemCount();
            public PrefabLoot() {
                LootProfiles = new List<LootProfileImport>();
                UngroupedItems = new Dictionary<string, LootAmount>();
                itemSettings = new ItemSettings();
            }
            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 ItemSettings {
                [JsonProperty("Minimum Amount of Items")]
                public int ItemsMin;
                [JsonProperty("Maximum Amount of Items")]
                public int ItemsMax;
                [JsonProperty("Scrap Amount")]
                public int Scrap;
                [JsonProperty("Max Blueprints")]
                public int MaxBPs;
                internal ItemSettings() {
                }
            }
            private int GetTotalItemCount() {
                int count = 0;
                count += UngroupedItems.Count();
                if (lootGroups is not null) foreach (LootProfileImport import in LootProfiles) if (lootGroups.LootGroups.TryGetValue(import.LootProfileName, out LootProfile? profile) && profile is not null) count += profile.ItemList.Count;
                return count;
            }
        }
        /// <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 {
            [JsonProperty("Enabled?")]
            public bool Enabled = true;
            [JsonProperty("Item List")]
            public Dictionary<string, LootRNG> ItemList;
            [JsonIgnore]
            private List<double> _culminativeProbabilities = new List<double>();
            [JsonIgnore]
            public bool probabilitiesExist => _culminativeProbabilities.Any();
            public LootProfile() => ItemList = new Dictionary<string, LootRNG>();
            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 LootAmount Amount;
                public LootRNG(double Probability, LootAmount Amount) {
                    this.Probability = Probability;
                    this.Amount = Amount;
                }
            }
#region Probalistic Selector Methods
            public void UpdateProbabilities() {
                double _culminative = 0;
                foreach(var item in ItemList.Values) {
                    _culminative += item.Probability;
                    _culminativeProbabilities.Add(_culminative);
                }
            }
            /// <summary>
            /// Get a random item from this loot group based off of items probabilities
            /// </summary>
            public Item? GetItem() {
                double randomSelect = rng.NextDouble() * 1e2;
                int itemIndex = _culminativeProbabilities.BinarySearch(randomSelect);
                if (itemIndex < 0) itemIndex = ~itemIndex;
                // No item found
                if (itemIndex >= ItemList.Count) return null;
                var entry = ItemList.ElementAt(itemIndex);
                // Select Amount
                int _min = entry.Value.Amount.Min, _max = entry.Value.Amount.Max;
                int amount = UnityEngine.Random.Range(Mathf.Min(_min, _max), Mathf.Max(_min, _max));
                // Get Custom Properties
                ulong skinId = entry.Value.Amount.skinId;
                string? customName = entry.Value.Amount.displayName;
                // Create Item
                Item item = ItemManager.CreateByPartialName(uniqueTagRegex.Replace(entry.Key, string.Empty), amount);
                item.name = customName ?? string.Empty;
                item.skin = skinId;
                item.MarkDirty();
                if (item is null) Log($"ERROR: item \"{entry.Key}\" could not be created! System returned null entry!");
                return item;
            }
#endregion
        }
        public class LootAmount {
            [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;
            public LootAmount(int Min, int Max) {
                this.Min = Min;
                this.Max = Max;
            }
        }
#endregion
#region Util
        private static void Log(string msg, params object[] args) => _instance?.Puts(msg, args);
        private void SendMessage(BasePlayer player, string message, params object[] args) => Player.Reply(player, message, _config.chatConfig.prefix, _config.chatConfig.messageIcon, args);
#endregion
#region Oxide Hooks
        private object OnLootSpawn(LootContainer container) {
            if ((!initialized || container == null) || (CustomLootSpawns != null && CustomLootSpawns.Call<bool>("IsLootBox", container))) return null;
            if (PopulateContainer(container)) return true;
            return null;
        }
        private void OnEntitySpawned(NPCPlayerCorpse corpse) => NextTick(() => PopulateContainer(corpse));
        private void OnEntitySpawned(NPCPlayer npc) {
            string name = npc.name;
            if (_config.Generic.WatchedPrefabs.Contains(name)) {
                ulong id = npc.userID;
                if (npcLootMonitor.ContainsKey(name)) npcLootMonitor[name].Add(id);
                else npcLootMonitor.Add(name, new List<ulong> {
                    id
                }
                );
            }
        }
#endregion
#region Loot Methods
        private int ItemWeight(double baseRarity, int index) => (int)(Math.Pow(baseRarity, 4 - index) * 1000);
        // OPTIMIZE
        private LootAmount GetAmounts(ItemAmount amount) {
            LootAmount options = new LootAmount((int)amount.amount, ((ItemAmountRanged)amount).maxAmount > 0 && ((ItemAmountRanged)amount).maxAmount > amount.amount ? (int)((ItemAmountRanged)amount).maxAmount : (int)amount.amount );
            return options;
        }
        private void GetLootSpawn(LootSpawn lootSpawn, ref Dictionary<string, LootAmount> items) {
            if (lootSpawn.subSpawn != null && lootSpawn.subSpawn.Any()) {
                foreach (var entry in lootSpawn.subSpawn) GetLootSpawn(entry.category, ref items);
            }
            else if (lootSpawn.items != null && lootSpawn.items.Any()) {
                foreach (var amount in lootSpawn.items) {
                    LootAmount options = GetAmounts(amount);
                    string itemName = amount.itemDef.shortname;
                    if (amount.itemDef.spawnAsBlueprint) itemName += ".blueprint";
                    if (!items.ContainsKey(itemName)) items.Add(itemName, options);
                }
            }
        }
        private void LoadAllContainers() {
            bool wasAdded = false;
            // OPTIMIZE
            foreach (var lootPrefab in _config.Generic.WatchedPrefabs) {
                if (!lootTables.LootTables.ContainsKey(lootPrefab)) {
                    var basePrefab = GameManager.server.FindPrefab(lootPrefab);
                    var npc = basePrefab.GetComponent<HumanNPC>();
                    if (npc is not null) {
                        // is npc
                        var container = new PrefabLoot();
                        container.Enabled = !lootPrefab.Contains("bradley_crate") && !lootPrefab.Contains("heli_crate");
                        container.itemSettings.Scrap = 0;
                        var slotItemCount = 0;
                        var itemList = new Dictionary<string, LootAmount>();
                        foreach (var slot in npc.LootSpawnSlots) {
                            GetLootSpawn(slot.definition, ref itemList);
                            slotItemCount += slot.numberToSpawn;
                        }
                        container.itemSettings.ItemsMin = container.itemSettings.ItemsMax = slotItemCount;
                        container.UngroupedItems = itemList;
                        lootTables.LootTables.Add(lootPrefab, container);
                        wasAdded = true;
                    }
                    else {
                        // is not npc
                        var loot = basePrefab.GetComponent<LootContainer>();
                        if (loot is null) continue;
                        var container = new PrefabLoot();
                        container.Enabled = !lootPrefab.Contains("bradley_crate") && !lootPrefab.Contains("heli_crate");
                        container.itemSettings.Scrap = loot.scrapAmount;
                        int slots = 0;
                        if (loot.LootSpawnSlots.Length > 0) {
                            LootContainer.LootSpawnSlot[] lootSpawnSlots = loot.LootSpawnSlots;
                            for (int i = 0; i < lootSpawnSlots.Length; i++) slots += lootSpawnSlots[i].numberToSpawn;
                        }
                        else slots = loot.maxDefinitionsToSpawn;
                        container.itemSettings.ItemsMin = container.itemSettings.ItemsMax = slots;
                        container.itemSettings.MaxBPs = 1;
                        var itemList = new Dictionary<string, LootAmount>();
                        if (loot.lootDefinition is not null) GetLootSpawn(loot.lootDefinition, ref itemList);
                        else if (loot.LootSpawnSlots.Any()) {
                            LootContainer.LootSpawnSlot[] lootSpawnSlots = loot.LootSpawnSlots;
                            foreach (var lootSpawnSlot in lootSpawnSlots) GetLootSpawn(lootSpawnSlot.definition, ref itemList);
                        }
                        // Default items
                        container.UngroupedItems = itemList;
                        lootTables.LootTables.Add(lootPrefab, container);
                        wasAdded = true;
                    }
                }
            }
            // Write Changes
            if (wasAdded) {
                // Try to create an example loot group within the LootTables.json file for user reference :)
                LootGroupsData.TryCreateExampleGroup();
                DataSystem.SaveLootTables();
            }
            wasAdded = false;
            // Correct any invalid entries
            bool wasRemoved = false;
            int activeTypes = 0;
            foreach (var lootTable in lootTables.LootTables.ToList()) {
                var basePrefab = GameManager.server.FindPrefab(lootTable.Key);
                var npc = basePrefab?.GetComponent<HumanNPC>();
                var loot = basePrefab?.GetComponent<LootContainer>();
                var container = lootTable.Value;
                if (npc is null && loot is null) {
                    lootTables.LootTables.Remove(lootTable.Key);
                    Log($"Removed Invalid Loot Table {lootTable.Key}");
                    wasRemoved = true;
                    continue;
                }
                // Groups items by rarity (weight).Reference: ItemDefinition.Rarity enum
                Items.Add(lootTable.Key, new List<string>[5]);
                Blueprints.Add(lootTable.Key, new List<string>[5]);
                for (var i = 0; i < 5; ++i) {
                    Items[lootTable.Key][i] = new List<string>();
                    Blueprints[lootTable.Key][i] = new List<string>();
                }
                foreach (var itemEntry in container.UngroupedItems) {
                    bool isBP = itemEntry.Key.EndsWith(".blueprint");
                    var def = ItemManager.FindItemDefinition(itemEntry.Key.Replace(".blueprint", ""));
                    if (def is not null) {
                        if (isBP && def.Blueprint is not null && def.Blueprint.isResearchable) {
                            int index = (int)def.rarity;
                            if (!Blueprints[lootTable.Key][index].Contains(def.shortname)) Blueprints[lootTable.Key][index].Add(def.shortname);
                        }
                        else {
                            int index = (int)def.rarity;
                            if (!Items[lootTable.Key][index].Contains(def.shortname)) Items[lootTable.Key][index].Add(def.shortname);
                        }
                    }
                }
                totalItemWeight.Add(lootTable.Key, 0);
                totalBlueprintWeight.Add(lootTable.Key, 0);
                itemWeights.Add(lootTable.Key, new int[5]);
                blueprintWeights.Add(lootTable.Key, new int[5]);
                for (var i = 0; i < 5; ++i) {
                    totalItemWeight[lootTable.Key] += (itemWeights[lootTable.Key][i] = ItemWeight(BASE_ITEM_RARITY, i) * Items[lootTable.Key][i].Count);
                    totalBlueprintWeight[lootTable.Key] += (blueprintWeights[lootTable.Key][i] = ItemWeight(BASE_ITEM_RARITY, i) * Blueprints[lootTable.Key][i].Count);
                }
            }
            if (wasAdded || wasRemoved) DataSystem.SaveLootTables();
            activeTypes = lootTables.LootTables.Count(table => table.Value.Enabled);
            Log($"Using '{activeTypes}' active of '{lootTables.LootTables.Count}' supported containertypes");
        }
#endregion
#region Core
        // NPC Implementation
        private bool PopulateContainer(NPCPlayerCorpse npc) {
            if (lootTables is null) return false;
            // Reverse search entity for prefab
            string prefab = string.Empty;
            foreach (var entry in npcLootMonitor) {
                if (entry.Value.Contains(npc.playerSteamID)) {
                    prefab = entry.Key;
                    break;
                }
            }
            if (string.IsNullOrEmpty(prefab) || (!npc.containers.Any()) || npc.containers[0] is not ItemContainer inventory) return false;
            return PopulateContainer(inventory, prefab);
        }
        private bool PopulateContainer(LootContainer container) {
            if (container is null) return false;
            if (container.inventory is null) {
                container.CreateInventory(true);
                container.OnInventoryFirstCreated(container.inventory);
            }
            return PopulateContainer(container.inventory, container.PrefabName);
        }
        private bool PopulateContainer(ItemContainer container, string prefab) {
            if (container is null || lootTables is null || !lootTables.LootTables.TryGetValue(prefab, out PrefabLoot? con) || con is null || !con.Enabled) return false;
            int lootItemCount = con.ItemCount;
            int min = con.itemSettings.ItemsMin, max = con.itemSettings.ItemsMax;
            int itemCount = UnityEngine.Random.Range(Mathf.Min(min, max), Mathf.Max(min, max));
            if (lootItemCount > 0 && itemCount > lootItemCount && lootItemCount < 36) itemCount = lootItemCount;
            container.Clear();
            container.capacity = 36;
            List<string> itemNames = Pool.Get<List<string>>();
            List<Item> items = Pool.Get<List<Item>>();
            List<int> itemBlueprints = Pool.Get<List<int>>();
            var maxRetry = 10;
            for (int i = 0; i < itemCount; ++i) {
                if (maxRetry is 0) break;
                Item? item = null;
                foreach(var import in con.LootProfiles) {
                    if (!import.Enabled) continue;
                    if (!lootGroups.LootGroups.TryGetValue(import.LootProfileName, out LootProfile? profile) || profile is null) {
                        Log($"WARNING: prefab \"{prefab}\" requested a loot group import with name \"{import.LootProfileName}\".Group does not exist!");
                        continue;
                    }
                    else if (!profile.Enabled) continue;
                    // RNG => Use Profile
                    double rng_pr = rng.NextDouble() * 1e2;
                    // RNG Check Failed
                    if (rng_pr > import.LootProfileProbability) continue;
                    // Ensure probabilities are generated, if not populate them
                    if (!profile.probabilitiesExist) profile.UpdateProbabilities();
                    // Get item
                    if (profile.GetItem() is Item _item) item = _item;
                }
                // Loot import not used, generate from ungrouped items with default rng system
                if (item == null) item = MightyRNG(prefab, itemCount, itemBlueprints.Count >= con.itemSettings.MaxBPs);
                // No item was generated from either system, attempt to regenerate.if (item == null)
                {
                    --maxRetry;
                    --i;
                    continue;
                }
                // Duplicates
                if (itemNames.Contains(item.info.shortname) || (item.IsBlueprint() && itemBlueprints.Contains(item.blueprintTarget))) {
                    item.Remove();
                    --maxRetry;
                    --i;
                    continue;
                }
                else if (item.IsBlueprint()) itemBlueprints.Add(item.blueprintTarget);
                else itemNames.Add(item.info.shortname);
                items.Add(item);
                if (storedBlacklist.ItemList.Contains(item.info.shortname)) {
                    items.Remove(item);
                    item.Remove();
                    // broken item fix
                }
            }
            foreach (var item in items.Where(x => x != null && x.IsValid())) if (!item.MoveToContainer(container, -1, false))  // broken item fix / fixes full container
            item.DoRemove();
            if (con.itemSettings.Scrap > 0) {
                int scrapCount = con.itemSettings.Scrap;
                Item item = ItemManager.CreateByItemID(-932201673, scrapCount * _config.Loot.scrapMultiplier);
                // Scrap item ID
                if (!item.MoveToContainer(container, -1, false)) item.DoRemove();
            }
            container.capacity = container.itemList.Count;
            container.MarkDirty();
            Pool.FreeUnmanaged(ref items);
            Pool.FreeUnmanaged(ref itemNames);
            Pool.FreeUnmanaged(ref itemBlueprints);
            return true;
        }
        private void UpdateInternals(bool doLog) {
            if (Changed) {
                SaveConfig();
                Changed = false;
            }
            if (doLog) Log("Updating internals...");
            populatedContainers = 0;
            int populatedNpcContainers = 0;
            NextTick(() => {
                if (_config.Generic.removeStackedContainers) FixLoot();
                foreach (var container in BaseNetworkable.serverEntities.Where(p => p is (not null) and LootContainer).Cast<LootContainer>()) {
                    // API Check
                    if (CustomLootSpawns is not null && CustomLootSpawns.Call<bool>("IsLootBox", container)) continue;
                    else if (PopulateContainer(container)) populatedContainers++;
                }
                if (doLog) Log($"Populated ({populatedContainers}) supported containers.");
                // NPC Implementation
                foreach (var npcContainer in BaseNetworkable.serverEntities.Where(n => n is (not null) and HumanNPC).Cast<HumanNPC>()) {
                    if (npcLootMonitor.ContainsKey(npcContainer.name)) npcLootMonitor[npcContainer.name].Add(npcContainer.userID);
                    else npcLootMonitor.Add(npcContainer.name, new List<ulong> {
                        npcContainer.userID
                    }
                    );
                    populatedNpcContainers++;
                }
                if (doLog) Log($"Tracking ({populatedNpcCon
// ... [Code preview truncated due to length. Full source in attached file] ...

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