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

FREE BetterLoot : 4.1.9 4.1.9

Нет прав для скачивания
C#:
#nullable enable
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 Newtonsoft.Json.Linq;
using static ConsoleSystem;
using Pool = Facepunch.Pool;
using Random = System.Random;
using UnityEngine.Networking;
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.1.9")]
    [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
        /// <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 Probability")]
            public double BlueprintProbability = 0.11;
            [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 = false;
            [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;
        }
        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;
        }
        /// <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>
        /// <param name="wipeDayBypass"></param>
        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.Any();
            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 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", "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) ? true : false);
            }
            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+\}");
            DataSystem.LoadBlacklist();
            DataSystem.LoadLootTables();
            DataSystem.LoadLootGroups();
        }
        private void OnServerInitialized() {
            CheckWatchedPrefabs();
            // Called so OnNewSave can trigger flag
            ItemManager.Initialize();
            BuildWeaponInfoCache();
            permission.RegisterPermission(ADMIN_PERM, this);
            InitLootSystem();
        }
        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;
            // 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 class LootyResponse {
            [JsonProperty("LootTable")]
            public Dictionary<string, PrefabLoot> LootTables = new();
            [JsonProperty("Loot Groups")]
            public Dictionary<string, LootProfile>? LootGroups = new();
            public LootyResponse() {
            }
        }
        // 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.Any())  // 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.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) {
                // 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";
                bool sendPlayer = msgPlayer is not null;
                string notifyMessage = string.Format(restoreMode ? "Restoring backup of {0}" : "Attempting to create 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";
                string existSearchPath = !restoreMode ? bakPath : path;
                if (File.Exists(existSearchPath)) {
                    File.Delete(existSearchPath);
                }
                else if (restoreMode) {
                    if (sendPlayer) _instance?.SendMessage(msgPlayer, NoRestoreFileFound);
                    else Log(NoRestoreFileFound);
                    return;
                }
                else {
                    if (sendPlayer) _instance?.SendMessage(msgPlayer, NoMainFileFound);
                    else Log(NoMainFileFound);
                    return;
                }
                // Main file does not exist, cannot push to backup
                File.Move(restoreMode ? bakPath : path, existSearchPath);
                if (sendPlayer) _instance?.SendMessage(msgPlayer, FileRestored);
                else Log(FileRestored);
                if (restoreMode) _instance.InitLootSystem(true);
            }
#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 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("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("Max Blueprints")]
                public int MaxBPs;
                internal ItemProperties() {
                }
#region v4.0.6 Configuration Migration
                [JsonProperty("Scrap Amount", NullValueHandling = NullValueHandling.Ignore)]
                private int? LegacyScrap {
                    get;
                    set;
                }
                [OnDeserialized]
                private void OnDeserialized(StreamingContext _) {
                    if (LegacyScrap.HasValue) {
                        if (MaxScrap == 0) {
                            MaxScrap = LegacyScrap.Value;
                            MinScrap = LegacyScrap.Value;
                        }
                    }
                    LegacyScrap = null;
                }
                [OnSerializing]
                private void OnSerializing(StreamingContext _) => LegacyScrap = null;
                // force null, no omit
#endregion
            }
            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("Guaranteed Items")]
            public Dictionary<string, LootEntrySettings> GuaranteedItems = new Dictionary<string, LootEntrySettings>();
            [JsonProperty("Item List")]
            public Dictionary<string, LootRNG> ItemList;
            [JsonIgnore]
            private List<double> _culminativeProbabilities = new List<double>();
            [JsonIgnore]
            public bool DoProbabilitiesExist => _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 LootEntry Amount;
                public LootRNG(double Probability, LootEntry 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?, List<Item>?) GetItem() {
                List<Item> bonusItems = null;
                double randomSelect = RNG.NextDouble() * 1e2;
                int itemIndex = _culminativeProbabilities.BinarySearch(randomSelect);
                if (itemIndex < 0) itemIndex = ~itemIndex;
                // No item found
                if (itemIndex >= ItemList.Count) return (null, null);
                var entry = ItemList.ElementAt(itemIndex);
                entry.Value.Amount.CreateBonusItems(ref bonusItems);
                // Select Amount
                int amount = GetRNG(entry.Value.Amount.Min, entry.Value.Amount.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);
                if (!string.IsNullOrWhiteSpace(customName)) item.name = customName;
                item.skin = skinId;
                entry.Value.Amount.ApplyAttachments(item);
                entry.Value.Amount.ApplyAmmo(item);
                item.MarkDirty();
                if (item is null) Log($"ERROR: item \"{entry.Key}\" could not be created! System returned null entry!");
                return (item, 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("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 ApplyAttachments(Item item) {
                // No mods to apply
                if (!(ItemEntryModifications?.AttachmentSettings?.itemMods.Any() ?? false)) 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(x => x.info.shortname.Equals("weapon.mod.extendedmags")) ?? false) ammoAmount = (int)Math.Ceiling(ammoAmount * 1.25);
                }
                else if (GetRNG(0, 100) <= ammoSettings.Probability) {
                    ammoDef = ItemManager.FindDefinitionByPartialName(ammoSettings.AmmoItemShortname);
                }
                // No ammo defined
                if (ammoDef is null) return;
                // Type check of how should apply
                var heldEntity = item.GetHeldEntity();
                if (heldEntity is BaseProjectile bp) {
                    var magazine = bp.primaryMagazine.ammoType = ammoDef;
                    bp.primaryMagazine.contents = ammoAmount;
                    bp.SendNetworkUpdateImmediate();
                }
                else if (heldEntity is FlameThrower ft) {
                    // Vanilla rust spawns this with full fuel
                    // By default should only be lowgrade fuel but leave the definition open for allowing custom fuel types
                    ft.fuelType = ammoDef;
                    ft.ammo = ammoAmount;
                    ft.SendNetworkUpdateImmediate();
                }
                else if (heldEntity is LiquidWeapon lw) {
                    if (lw.GetContents() is Item _liquid) {
                        _liquid.amount = ammoAmount;
                        _liquid.MarkDirty();
                    }
                    else {
                        Item liquid = ItemManager.Create(ammoDef, ammoAmount);
                        liquid.MoveToContainer(item.contents);
                        item.MarkDirty();
                    }
                }
            }
        }
        public class LootEntryDurability {
            [JsonProperty("Minimum Durability")]
            public int MinDurability = 100;
            [JsonProperty("Maximum Durability")]
            public int MaxDurability = 100;
        }
#region Attachment System
        /* 
            Added v4.1.1
        */
        public class ItemEntrySettings {
            // Should add deserialzier value to set min / max bounds (min <= max ALWAYS)
            public class AmmoSettings  // default state
            {
                [JsonIgnore]
                public bool CanHoldMultipleAmmoUnits = true;
                // Should be set to false when its a single shot weapon (no magazine. Also declares to not serialize the ItemMods list)
                [JsonProperty("Ammo Item Shortname")]
                public string AmmoItemShortname = string.Empty;
                // Item shortname of ammo item
                [JsonIgnore]
                public int MaxAmmo;
                // Maximum ammo the item can take.
                // These should only be added in config if the weapon can hold more than 1 unit of ammunition. (e.g harpoon gun vs ak)
                [JsonProperty("Minimum Amount")]
                public int Min;
                [JsonProperty("Maximum Amount")]
                public int Max;
                // These should only exist if the weapon can only hold 1 unit of ammo
                [JsonProperty("Spawn Probability")]
                public double Probability;
                // conditional serialization
                public bool ShouldSerializeMin() => CanHoldMultipleAmmoUnits;
                public bool ShouldSerializeMax() => CanHoldMultipleAmmoUnits;
                public bool ShouldSerializeProbability() => !CanHoldMultipleAmmoUnits;
            }
            // A weapon always needs ammo
            [JsonProperty("Ammunition Settings")]
            public AmmoSettings AmmunitionSettings = new AmmoSettings();
#region Attachment Related
#region Internal
            [JsonIgnore]
            public int maxMods;
            // Maximum amount of mods the item can have.
            [JsonIgnore]
            private List<double> _culminativeProbabilities = new List<double>();
#endregion
            public class ItemModEntry {
                [JsonProperty("Spawn Probability (0%-100%)")]
                // Culminative probabilty with other items in list. => Implement loot groups like system
                public double Probability;
                [JsonProperty("Durability", NullValueHandling=NullValueHandling.Ignore)]
                public LootEntryDurability? Durability;
            }
            public class ItemModSettings {
                [JsonProperty("Minimum Mod Amount")]
                public int minModAmount = 1;
                [JsonProperty("Maximum Mod Amount")]
                public int maxModAmount = 1;
                [JsonProperty("Available Attachments")]
                public Dictionary<string, ItemModEntry> itemMods = new Dictionary<string, ItemModEntry>();
            }
            // Should only be on weapons that have slots for weapon mods
            [JsonProperty("Weapon Attachments", NullValueHandling = NullValueHandling.Ignore)]
            public ItemModSettings? AttachmentSettings;
            // Item mods / weapon attachments. Null by default. Item shortname: {probabilty, {durability}}
            public void BalanceItemModProbabilities() {
                if (!(AttachmentSettings?.itemMods?.Any() ?? false) || maxMods is 0) return;
                AttachmentSettings.maxModAmount = Math.Clamp(AttachmentSettings.maxModAmount, 1, maxMods);
                double GetSum() => AttachmentSettings.itemMods.Sum(x => x.Value.Probability);
                double Round(double x) => Math.Round(x, 2);
                const double target = 100;
                double sum = GetSum();
                // Initial sum
                if (sum > target)  // Only balance if greater than target
                {
                    if (_config.Loot.EnableBonusItemsAutoBalanceLogging) Log($"- Bonus items probability sum ({sum}) > 100. Balancing list!");
                    double _ratio = target / sum;
                    string largestKey = AttachmentSettings.itemMods.Keys.First();
                    double largestValue = AttachmentSettings.itemMods[largestKey].Probability;
                    foreach(var item in AttachmentSettings.itemMods) {
           
// ... [Code preview truncated due to length. Full source in attached file] ...

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