From e1a9663637150ad26fb7222d463734f6a14bf319 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:03:53 +0200 Subject: [PATCH 01/37] Update to C# 8. --- .../SubnauticaRandomiser.csproj | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 29df245..2235b11 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -9,6 +9,8 @@ SubnauticaRandomiser v4.7.2 true + 8 + enable true @@ -19,16 +21,6 @@ prompt 4 x86 - - - - AfterBuild - ../../CopyToSubnauticaDir.sh - ${ProjectDir} - True - - - true @@ -38,16 +30,15 @@ 4 x86 true - - - - AfterBuild - ../../CopyToSubnauticaDir.sh - ${ProjectDir} - True - - - + + + mkdir $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(OutDir)SubnauticaRandomiser.dll $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(SolutionDir)biomeSlots.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(SolutionDir)mod.json $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(SolutionDir)ReadMe-Documentation.txt $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(SolutionDir)recipeInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser @@ -86,28 +77,36 @@ - ..\..\..\..\..\.steam\steamapps\common\Subnautica\BepInEx\core\0Harmony.dll + Subnautica\BepInEx\core\0Harmony.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\Subnautica_Data\Managed\publicized_assemblies\Assembly-CSharp_publicized.dll + Subnautica\Subnautica_Data\Managed\publicized_assemblies\Assembly-CSharp_publicized.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\Subnautica_Data\Managed\publicized_assemblies\Assembly-CSharp-firstpass_publicized.dll + Subnautica\Subnautica_Data\Managed\publicized_assemblies\Assembly-CSharp-firstpass_publicized.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\BepInEx\plugins\QModManager\QModInstaller.dll + Subnautica\BepInEx\plugins\QModManager\QModInstaller.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\QMods\Modding Helper\SMLHelper.dll + Subnautica\QMods\Modding Helper\SMLHelper.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\Subnautica_Data\Managed\UnityEngine.CoreModule.dll + Subnautica\Subnautica_Data\Managed\UnityEngine.CoreModule.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\Subnautica_Data\Managed\UnityEngine.dll + Subnautica\Subnautica_Data\Managed\UnityEngine.dll + False - ..\..\..\..\..\.steam\steamapps\common\Subnautica\Subnautica_Data\Managed\UnityEngine.UI.dll + Subnautica\Subnautica_Data\Managed\UnityEngine.UI.dll + False From c210a69371660e8ba73efa6f57408cc828b99f58 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:48:03 +0200 Subject: [PATCH 02/37] Docstring and code style updates. --- SubnauticaRandomiser/CSVReader.cs | 211 ++++++++------ SubnauticaRandomiser/DataboxPatcher.cs | 24 +- SubnauticaRandomiser/EntitySerializer.cs | 62 +++-- SubnauticaRandomiser/InitMod.cs | 261 ++++++++++-------- SubnauticaRandomiser/LogHandler.cs | 10 +- SubnauticaRandomiser/Logic/FragmentLogic.cs | 94 +++++-- SubnauticaRandomiser/Logic/Materials.cs | 55 +++- SubnauticaRandomiser/Logic/Mode.cs | 202 ++++++++------ SubnauticaRandomiser/Logic/ModeBalanced.cs | 128 +++++---- SubnauticaRandomiser/Logic/ModeRandom.cs | 55 ++-- SubnauticaRandomiser/Logic/ModeSubstitute.cs | 8 +- SubnauticaRandomiser/Logic/ProgressionTree.cs | 145 +++++++--- SubnauticaRandomiser/Logic/RandomiserLogic.cs | 175 ++++++++---- SubnauticaRandomiser/RandomiserConfig.cs | 68 +++-- .../RandomiserObjects/Biome.cs | 7 +- .../RandomiserObjects/BiomeCollection.cs | 25 +- .../RandomiserObjects/Blueprint.cs | 3 + .../RandomiserObjects/Databox.cs | 3 + .../RandomiserObjects/EBiomeType.cs | 10 +- .../RandomiserObjects/EProgressionNode.cs | 3 +- .../RandomiserObjects/ETechTypeCategory.cs | 11 +- .../RandomiserObjects/EWreckage.cs | 3 +- .../RandomiserObjects/LogicEntity.cs | 39 +-- .../RandomiserObjects/ProgressionPath.cs | 6 +- .../RandomiserObjects/RandomiserBiomeData.cs | 12 +- .../RandomiserObjects/RandomiserIngredient.cs | 3 + .../RandomiserObjects/RandomiserVector.cs | 8 + .../RandomiserObjects/Recipe.cs | 3 + .../RandomiserObjects/SpawnData.cs | 14 +- .../RandomiserObjects/SpoilerLog.cs | 195 ++++++------- .../SubnauticaRandomiser.csproj | 2 +- 31 files changed, 1119 insertions(+), 726 deletions(-) diff --git a/SubnauticaRandomiser/CSVReader.cs b/SubnauticaRandomiser/CSVReader.cs index 9eb0bd3..4d1e079 100644 --- a/SubnauticaRandomiser/CSVReader.cs +++ b/SubnauticaRandomiser/CSVReader.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography; -using SMLHelper.V2.Crafting; +using JetBrains.Annotations; using SubnauticaRandomiser.RandomiserObjects; using UnityEngine; @@ -15,10 +15,16 @@ internal static class CSVReader internal static List s_csvDataboxList; internal static string s_recipeCSVMD5; - private static readonly int s_expectedColumns = 8; - private static readonly int s_expectedRows = 245; - private static readonly int s_expectedWreckColumns = 6; + private const int _ExpectedColumns = 8; + private const int _ExpectedRows = 245; + private const int _ExpectedWreckColumns = 6; + /// + /// Attempt to parse the given file into a list of entities representing recipes. + /// + /// The file to parse. + /// A list of LogicEntities if successful, null otherwise. + [CanBeNull] internal static List ParseRecipeFile(string fileName) { // First, try to find and grab the file containing recipe information. @@ -37,12 +43,11 @@ internal static List ParseRecipeFile(string fileName) return null; } - // If the CSV does not contain the expected amount of rows, it is - // likely that the user added custom items to it. - // If the lines are the same, but the MD5 is not, some values of - // existing entries must have been modified. + // If the CSV does not contain the expected amount of rows, it is likely that the user added custom items + // to it. If the lines are the same, but the MD5 is not, some values of existing entries must have been + // modified. s_recipeCSVMD5 = CalculateMD5(path); - if (csvLines.Length != s_expectedRows) + if (csvLines.Length != _ExpectedRows) { LogHandler.Info("Recipe CSV seems to contain custom entries."); } @@ -51,8 +56,7 @@ internal static List ParseRecipeFile(string fileName) LogHandler.Info("Recipe CSV seems to have been modified."); } - // Second, read each line and try to parse that into a list of - // LogicEntity objects, for later use. + // Second, read each line and try to parse that into a list of LogicEntity objects, for later use. s_csvRecipeList = new List(); int lineCounter = 0; @@ -65,8 +69,7 @@ internal static List ParseRecipeFile(string fileName) continue; } - // ParseRecipeFileLine fails upwards, so this ensures all errors - // are caught in one central location. + // ParseRecipeFileLine fails upwards, so this ensures all errors are caught in one central location. try { s_csvRecipeList.Add(ParseRecipeFileLine(line)); @@ -80,12 +83,16 @@ internal static List ParseRecipeFile(string fileName) return s_csvRecipeList; } - - // Parse one line of a CSV file and attempt to create a LogicEntity. + + /// + /// Parse one line of a CSV file and attempt to create a LogicEntity. + /// + /// A string to parse. + /// The fully processed LogicEntity. + /// If the format of the data is wrong. + /// If a required column is missing or invalid. private static LogicEntity ParseRecipeFileLine(string line) { - LogicEntity entity = null; - TechType type = TechType.None; ETechTypeCategory category = ETechTypeCategory.None; int depth = 0; @@ -102,10 +109,9 @@ private static LogicEntity ParseRecipeFileLine(string line) string[] cells = line.Split(','); - if (cells.Length != s_expectedColumns) - { - throw new InvalidDataException("Unexpected number of columns: " + cells.Length + " instead of " + s_expectedColumns); - } + if (cells.Length != _ExpectedColumns) + throw new InvalidDataException("Unexpected number of columns: " + cells.Length + " instead of " + + _ExpectedColumns); // While ugly, this makes it much easier to react to changes in the // structure of the CSV. Also less prone to accidental oversights. string cellsTechType = cells[0]; @@ -120,42 +126,30 @@ private static LogicEntity ParseRecipeFileLine(string line) // Now to convert the data in each cell to an object we can use. // Column 1: TechType if (string.IsNullOrEmpty(cellsTechType)) - { throw new ArgumentException("TechType is null or empty, but is a required field."); - } type = StringToEnum(cellsTechType); // Column 2: Category if (string.IsNullOrEmpty(cellsCategory)) - { throw new ArgumentException("Category is null or empty, but is a required field."); - } category = StringToEnum(cellsCategory); // Column 3: Depth Difficulty if (!string.IsNullOrEmpty(cellsDepth)) - { depth = StringToInt(cellsDepth, "Depth"); - } // Column 4: Prerequisites if (!string.IsNullOrEmpty(cellsPrereqs)) - { prereqList = ProcessMultipleTechTypes(cellsPrereqs.Split(';')); - } // Column 5: Value if (string.IsNullOrEmpty(cellsValue)) - { throw new ArgumentException("Value is null or empty, but is a required field."); - } value = StringToInt(cellsValue, "Value"); // Column 6: Max Uses Per Game if (!string.IsNullOrEmpty(cellsMaxUses)) - { maxUses = StringToInt(cellsMaxUses, "Max Uses"); - } // Column 7: Blueprint Unlock Conditions if (!string.IsNullOrEmpty(cellsBPUnlock)) @@ -165,49 +159,47 @@ private static LogicEntity ParseRecipeFileLine(string line) foreach (string str in conditions) { if (str.ToLower().Contains("fragment")) - { blueprintFragments.Add(StringToEnum(str)); - } else if (str.ToLower().Contains("databox")) - { blueprintDatabox = true; - } else - { blueprintUnlockConditions.Add(StringToEnum(str)); - } } } // Column 8: Blueprint Unlock Depth if (!string.IsNullOrEmpty(cellsBPDepth)) - { blueprintUnlockDepth = StringToInt(cellsBPDepth, "Blueprint Unlock Depth"); - } - - // Only if any of the blueprint components yielded anything, - // ship the entity with a blueprint. - if ((blueprintUnlockConditions != null && blueprintUnlockConditions.Count > 0) || blueprintUnlockDepth != 0 || !blueprintDatabox || blueprintFragments.Count > 0) + + // Only if any of the blueprint components yielded anything, ship the entity with a blueprint. + if ((blueprintUnlockConditions.Count > 0) || blueprintUnlockDepth != 0 || !blueprintDatabox || blueprintFragments.Count > 0) { - blueprint = new Blueprint(type, blueprintUnlockConditions, blueprintFragments, blueprintDatabox, blueprintUnlockDepth); + blueprint = new Blueprint(type, blueprintUnlockConditions, blueprintFragments, blueprintDatabox, + blueprintUnlockDepth); } - // Only if the category corresponds to a techtype commonly associated - // with a craftable thing, ship the entity with a recipe. + // Only if the category corresponds to a techtype commonly associated with a craftable thing, ship the + // entity with a recipe. if (category.CanHaveRecipe()) - { recipe = new Recipe(type); - } - LogHandler.Debug("Registering entity: " + type.AsString() + ", " + category.ToString() + ", " + depth + ", "+ prereqList.Count + " prerequisites, " + value + ", " + maxUses + ", ..."); + LogHandler.Debug("Registering entity: " + type.AsString() + ", " + category.ToString() + ", " + + depth + ", "+ prereqList.Count + " prerequisites, " + value + ", " + maxUses + ", ..."); - entity = new LogicEntity(type, category, blueprint, recipe, null, prereqList, false, value); - entity.AccessibleDepth = depth; - entity.MaxUsesPerGame = maxUses; + var entity = new LogicEntity(type, category, blueprint, recipe, null, prereqList, false, value) + { + AccessibleDepth = depth, + MaxUsesPerGame = maxUses + }; return entity; } - // This handles everything related to the biome CSV. + /// + /// Attempt to parse the given file into a list of biomes and their stats. + /// + /// The file to parse. + /// A list of BiomeCollection if successful, null otherwise. + [CanBeNull] internal static List ParseBiomeFile(string fileName) { // Try and grab the file containing biome information. @@ -238,14 +230,13 @@ internal static List ParseBiomeFile(string fileName) continue; } - // ParseBiomeFileLine fails upwards, so this ensures all errors - // are caught in one central location. + // ParseBiomeFileLine fails upwards, so this ensures all errors are caught in one central location. try { Biome biome = ParseBiomeFileLine(line); BiomeCollection collection = s_csvBiomeList.Find(x => x.BiomeType.Equals(biome.BiomeType)); - // Initiate a BiomeCollection if it does not alread exist. + // Initiate a BiomeCollection if it does not already exist. if (collection is null) { collection = new BiomeCollection(biome.BiomeType); @@ -264,6 +255,12 @@ internal static List ParseBiomeFile(string fileName) return s_csvBiomeList; } + /// + /// Parse one line of a CSV file and attempt to create a single Biome. + /// + /// A string to parse. + /// The fully processed Biome. + /// If a required column is empty, missing or invalid. private static Biome ParseBiomeFileLine(string line) { Biome biome = null; @@ -309,13 +306,18 @@ private static Biome ParseBiomeFileLine(string line) fragmentRate = StringToFloat(cellsFragmentRate, "fragmentRate"); biome = new Biome(name, biomeType, creatureCount, mediumCount, smallCount, fragmentRate); - LogHandler.Debug("Registering biome: " + name + ", " + biomeType.ToString() + ", " + creatureCount + ", " + mediumCount + ", " + smallCount); + LogHandler.Debug("Registering biome: " + name + ", " + biomeType.ToString() + ", " + creatureCount + + ", " + mediumCount + ", " + smallCount); return biome; } - - // This handles everything related to the wreckage CSV and databoxes. - // Similar in structure to the recipe CSV parser above. + + /// + /// Attempt to parse the given CSV file for wreckage information and extract stats on Databoxes. + /// + /// The file to parse. + /// A list of Databoxes if successful, null otherwise. + [CanBeNull] internal static List ParseWreckageFile(string fileName) { string[] csvLines; @@ -362,23 +364,26 @@ internal static List ParseWreckageFile(string fileName) return s_csvDataboxList; } + /// + /// Parse one line of a CSV file and attempt to create a single Databox. + /// + /// A string to parse. + /// The fully processed Databox. + /// If a required column is empty, missing or invalid. private static Databox ParseWreckageFileLine(string line) { - Databox databox = null; - TechType type = TechType.None; - Vector3 coordinates = Vector3.zero; + Vector3 coordinates; EWreckage wreck = EWreckage.None; - bool isDatabox = false; + bool isDatabox; bool laserCutter = false; bool propulsionCannon = false; string[] cells = line.Split(','); - if (cells.Length != s_expectedWreckColumns) - { - throw new InvalidDataException("Unexpected number of columns: " + cells.Length + " instead of " + s_expectedWreckColumns); - } + if (cells.Length != _ExpectedWreckColumns) + throw new InvalidDataException("Unexpected number of columns: " + cells.Length + " instead of " + + _ExpectedWreckColumns); // As above, it's not the prettiest, but it's flexible. string cellsTechType = cells[0]; string cellsCoordinates = cells[1]; @@ -389,9 +394,7 @@ private static Databox ParseWreckageFileLine(string line) // Column 1: TechType if (string.IsNullOrEmpty(cellsTechType)) - { throw new ArgumentException("TechType is null or empty, but is a required field."); - } type = StringToEnum(cellsTechType); // Column 2: Coordinates @@ -399,9 +402,7 @@ private static Databox ParseWreckageFileLine(string line) { string[] str = cellsCoordinates.Split(';'); if (str.Length != 3) - { throw new ArgumentException("Coordinates are not in a valid format: " + cellsCoordinates); - } float x = StringToFloat(str[0], "Coordinates"); float y = StringToFloat(str[1], "Coordinates"); @@ -417,47 +418,47 @@ private static Databox ParseWreckageFileLine(string line) // Column 3: General location if (!string.IsNullOrEmpty(cellsEWreckage)) - { wreck = StringToEnum(cellsEWreckage); - } // Column 4: Is it a databox? // Redundant until fragments are implemented, so this does nothing. if (!string.IsNullOrEmpty(cellsIsDatabox)) - { isDatabox = StringToBool(cellsIsDatabox, "IsDatabox"); - } // Column 5: Does it need a laser cutter? if (!string.IsNullOrEmpty(cellsLaserCutter)) - { laserCutter = StringToBool(cellsLaserCutter, "NeedsLaserCutter"); - } // Column 6: Does it need a propulsion cannon? if (!string.IsNullOrEmpty(cellsPropulsionCannon)) - { propulsionCannon = StringToBool(cellsPropulsionCannon, "NeedsPropulsionCannon"); - } - LogHandler.Debug("Registering databox: " + type + ", " + coordinates.ToString() + ", " + wreck.ToString() + ", " + laserCutter + ", " + propulsionCannon); - databox = new Databox(type, coordinates, wreck, laserCutter, propulsionCannon); + LogHandler.Debug("Registering databox: " + type + ", " + coordinates.ToString() + ", " + + wreck.ToString() + ", " + laserCutter + ", " + propulsionCannon); + Databox databox = new Databox(type, coordinates, wreck, laserCutter, propulsionCannon); return databox; } + /// + /// Calculate the MD5 hash for a given file. + /// + /// The path to the file to hash. + /// The MD5 hash. internal static string CalculateMD5(string path) { - using (MD5 md5 = MD5.Create()) - { - using (FileStream fileStream = File.OpenRead(path)) - { - var hash = md5.ComputeHash(fileStream); - return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - } - } + using MD5 md5 = MD5.Create(); + using FileStream fileStream = File.OpenRead(path); + var hash = md5.ComputeHash(fileStream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } + /// + /// Turn multiple strings into their TechType equivalents. + /// + /// A list containing all successfully parsed TechTypes. + /// Raised if the parsing fails. + [NotNull] private static List ProcessMultipleTechTypes(string[] str) { List output = new List(); @@ -474,6 +475,11 @@ private static List ProcessMultipleTechTypes(string[] str) return output; } + /// + /// Attempt to parse a given string into an Enum. + /// + /// The parsed Enum. + /// Raised if the parsing fails. private static TEnum StringToEnum(string str) where TEnum : struct { @@ -505,6 +511,13 @@ private static EBiomeType StringToEBiomeType(string str) return EBiomeType.None; } + /// + /// Attempt to parse a string into a boolean value. + /// + /// The value. + /// The name of the column the value was in. + /// The parsed boolean value as appropriate. + /// Raised if the input value is unparseable. private static bool StringToBool(string input, string column) { // If the string is "true" or "false", this just works. @@ -535,6 +548,13 @@ private static bool StringToBool(string input, string column) throw new FormatException(column + " is not a valid boolean value: " + input); } + /// + /// Attempt to parse a string into a floating point value. + /// + /// The value. + /// The name of the column the value was in. + /// The parsed float. + /// Raised if the input value is unparseable. private static float StringToFloat(string input, string column) { float output; @@ -551,6 +571,13 @@ private static float StringToFloat(string input, string column) return output; } + /// + /// Attempt to parse a string into an integer. + /// + /// The value. + /// The name of the column the value was in. + /// The parsed integer. + /// Raised if the input value is unparseable. private static int StringToInt(string input, string column) { int output; diff --git a/SubnauticaRandomiser/DataboxPatcher.cs b/SubnauticaRandomiser/DataboxPatcher.cs index a93b7ad..e2e24b9 100644 --- a/SubnauticaRandomiser/DataboxPatcher.cs +++ b/SubnauticaRandomiser/DataboxPatcher.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using HarmonyLib; using SubnauticaRandomiser.RandomiserObjects; using UnityEngine; @@ -17,7 +16,8 @@ internal static bool PatchDataboxOnSpawn(ref DataboxSpawner __instance) BlueprintHandTarget blueprint = __instance.databoxPrefab.GetComponent(); Vector3 position = __instance.transform.position; - LogHandler.Debug("[OnSpawn] Found blueprint " + blueprint.unlockTechType.AsString() + " at " + position.ToString()); + LogHandler.Debug("[OnSpawn] Found blueprint " + blueprint.unlockTechType.AsString() + " at " + + position.ToString()); ReplaceDatabox(boxDict, position, blueprint); @@ -26,13 +26,14 @@ internal static bool PatchDataboxOnSpawn(ref DataboxSpawner __instance) internal static void ReplaceDatabox(Dictionary boxDict, Vector3 position, BlueprintHandTarget blueprint) { - // Unfortunately it has to be done like this. Building an equal vector - // from CSV has proven elusive, and they're not serialisable anyway. + // Unfortunately it has to be done like this. Building an equal vector from CSV has proven elusive, and + // they're not serialisable anyway. foreach (RandomiserVector vector in boxDict.Keys) { if (vector.EqualsUnityVector(position)) { - LogHandler.Debug("[!] Replacing databox " + position.ToString() + " with " + boxDict[vector].AsString()); + LogHandler.Debug("[!] Replacing databox " + position.ToString() + " with " + + boxDict[vector].AsString()); blueprint.unlockTechType = boxDict[vector]; } } @@ -43,23 +44,20 @@ internal static void ReplaceDatabox(Dictionary boxDi [HarmonyPatch(typeof(ProtobufSerializer), nameof(ProtobufSerializer.DeserializeIntoGameObject))] internal class DataboxSavePatcher { - // This intercepts loading any GameObject from disk, and swaps the blueprint - // of any databoxes it finds. This *needs* to be a fast, lean method or - // else load times and play quality will likely suffer. + /// + /// Intercept loading any GameObject from disk, and swap the blueprint if the GameObject happens to be a + /// databox. Very much suboptimal since this function gets called every single time something spawns in game. + /// [HarmonyPostfix] internal static void PatchDataboxOnLoad(ref ProtobufSerializer __instance, UniqueIdentifier uid) { BlueprintHandTarget blueprint = uid.gameObject.GetComponent(); if (blueprint == null) - { return; - } LogHandler.Debug("[OnLoad] Found blueprint " + blueprint.unlockTechType.AsString()); DataboxPatcher.ReplaceDatabox(InitMod.s_masterDict.Databoxes, uid.transform.position, blueprint); - - return; } } } diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index f9d56c8..35dbecb 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -6,16 +6,24 @@ namespace SubnauticaRandomiser { - // This class does three things. - // - // First, it provides an easy way to store a large amount of recipes or - // spawnables by putting them in a dictionary. - // - // Second, it provides a way to save itself to and restore from disk. - // Because this dictionary eventually contains all randomised entities, - // this makes restoring to a previous state trivial. - // - // Third, the base64 string representing this class also doubles as a seed. + + /// + /// This class does three things. + /// + /// + /// First, it provides an easy way to store a large amount of recipes or spawnables by + /// putting them in a dictionary. + /// + /// + /// Second, it provides a way to save itself to and restore from disk. + /// Because this dictionary eventually contains all randomised entities, + /// this makes restoring to a previous state trivial. + /// + /// + /// Third, the base64 string representing this class also doubles as a seed. + /// + /// + /// [Serializable] public class EntitySerializer { @@ -26,7 +34,9 @@ public class EntitySerializer public bool isDataboxRandomised = false; public static readonly int s_SaveVersion = InitMod.s_expectedSaveVersion; - // Convert this class to a string for saving. + /// + /// Convert this class to a string for saving. + /// public string ToBase64String() { using (MemoryStream ms = new MemoryStream()) @@ -35,8 +45,12 @@ public string ToBase64String() return Convert.ToBase64String(ms.ToArray()); } } - - // Convert a previously saved string back into an instance of this class. + + /// + /// Convert a previously saved string back into an instance of this class. + /// + /// + /// A typecast EntitySerializer, which may or may not be valid. public static EntitySerializer FromBase64String(string base64String) { byte[] bytes = Convert.FromBase64String(base64String); @@ -47,8 +61,13 @@ public static EntitySerializer FromBase64String(string base64String) return (EntitySerializer)(new BinaryFormatter().Deserialize(ms)); } } - - // Try to add an entry to the Recipe dictionary. Returns true if successful. + + /// + /// Try to add an entry to the Recipe dictionary. + /// + /// The TechType to use as key. + /// The Recipe to use as value. + /// True if successful, false if the key already exists in the dictionary. public bool AddRecipe(TechType type, Recipe r) { if (RecipeDict.ContainsKey(type)) @@ -60,7 +79,12 @@ public bool AddRecipe(TechType type, Recipe r) return true; } - // Try to add an entry to the SpawnData dictionary. Returns true if successful. + /// + /// Try to add an entry to the SpawnData dictionary. + /// + /// The TechType to use as key. + /// The SpawnData to use as value. + /// True if successful, false if the key already exists in the dictionary. public bool AddSpawnData(TechType type, SpawnData data) { if (SpawnDataDict.ContainsKey(type)) @@ -71,8 +95,10 @@ public bool AddSpawnData(TechType type, SpawnData data) SpawnDataDict.Add(type, data); return true; } - - // Does the recipe dictionary contain any knife? Used for progression. + + /// + /// Check whether the recipe dictionary contains any kind of knife. Used for progression checks. + /// public bool ContainsKnife() { return RecipeDict.ContainsKey(TechType.Knife) || RecipeDict.ContainsKey(TechType.HeatBlade); diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index a35e59d..a1deb24 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -1,41 +1,42 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using HarmonyLib; -using QModManager.API.ModLoading; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using HarmonyLib; +using QModManager.API.ModLoading; using SMLHelper.V2.Handlers; -using SubnauticaRandomiser.Logic; -using SubnauticaRandomiser.RandomiserObjects; - -namespace SubnauticaRandomiser -{ - [QModCore] - public static class InitMod +using SubnauticaRandomiser.Logic; +using SubnauticaRandomiser.RandomiserObjects; + +namespace SubnauticaRandomiser +{ + [QModCore] + public static class InitMod { - internal static string s_modDirectory; + internal static string s_modDirectory; internal static RandomiserConfig s_config; - internal static readonly string s_biomeFile = "biomeSlots.csv"; - internal static readonly string s_recipeFile = "recipeInformation.csv"; - internal static readonly string s_wreckageFile = "wreckInformation.csv"; - internal static readonly string s_expectedRecipeMD5 = "4ab1b7a019037f76c0d508f1c2aee5f8"; - internal static readonly int s_expectedSaveVersion = 3; + internal const string s_biomeFile = "biomeSlots.csv"; + internal const string s_recipeFile = "recipeInformation.csv"; + internal const string s_wreckageFile = "wreckInformation.csv"; + internal const string s_expectedRecipeMD5 = "4ab1b7a019037f76c0d508f1c2aee5f8"; + internal const int s_expectedSaveVersion = 3; + internal static readonly Dictionary s_versionDict = new Dictionary { [1] = "v0.5.1", [2] = "v0.6.1", [3] = "v0.7.0"}; - // The master list of all recipes that have been modified + // The master list of everything that is modified by the mod. internal static EntitySerializer s_masterDict = new EntitySerializer(); - private static readonly bool _debug_forceRandomise = false; - - [QModPatch] - public static void Initialise() - { - LogHandler.Info("Randomiser starting up!"); - - // Register options menu - s_modDirectory = GetSubnauticaRandomiserDirectory(); - s_config = OptionsPanelHandler.Main.RegisterModOptions(); + private const bool _debug_forceRandomise = false; + + [QModPatch] + public static void Initialise() + { + LogHandler.Info("Randomiser starting up!"); + + // Register options menu + s_modDirectory = GetSubnauticaRandomiserDirectory(); + s_config = OptionsPanelHandler.Main.RegisterModOptions(); LogHandler.Debug("Registered options menu."); // Ensure the user did not update into a save incompatibility, and @@ -44,70 +45,73 @@ public static void Initialise() return; // Try and restore a game state from disk. - try - { - s_masterDict = RestoreGameStateFromDisk(); + try + { + s_masterDict = RestoreGameStateFromDisk(); } - catch (Exception ex) - { + catch (Exception ex) + { LogHandler.Warn("Could not load game state from disk."); - LogHandler.Warn(ex.Message); + LogHandler.Warn(ex.Message); } - // Triple checking things here in case the save got corrupted somehow - if (!_debug_forceRandomise && s_masterDict != null && s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) - { + // Triple checking things here in case the save got corrupted somehow. + if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) + { + // Load recipe changes. RandomiserLogic.ApplyMasterDict(s_masterDict); - - if (s_masterDict.SpawnDataDict != null && s_masterDict.SpawnDataDict.Count > 0) - { + + // Load fragment changes. + if (s_masterDict.SpawnDataDict?.Count > 0) + { FragmentLogic.ApplyMasterDict(s_masterDict); - LogHandler.Info("Loaded fragment state."); - } - + LogHandler.Info("Loaded fragment state."); + } + + // Load databox changes. if (s_masterDict.isDataboxRandomised) EnableHarmonyPatching(); - LogHandler.Info("Successfully loaded game state from disk."); - } - else + LogHandler.Info("Successfully loaded game state from disk."); + } + else { if (_debug_forceRandomise) LogHandler.Warn("Set to forcibly re-randomise recipes."); - else + else LogHandler.Warn("Failed to load game state from disk: dictionary empty."); Randomise(); - if (s_masterDict.isDataboxRandomised) - EnableHarmonyPatching(); + if (s_masterDict?.isDataboxRandomised == true) + EnableHarmonyPatching(); } - LogHandler.Info("Finished loading."); + LogHandler.Info("Finished loading."); } - // Randomise the game, discarding any earlier randomisation data. - internal static void Randomise() + /// + /// Randomise the game, discarding any earlier randomisation data. + /// + internal static void Randomise() { s_masterDict = new EntitySerializer(); s_config.SanitiseConfigValues(); s_config.iSaveVersion = s_expectedSaveVersion; // Attempt to read and parse the CSV with all biome information. - List completeBiomeList; - completeBiomeList = CSVReader.ParseBiomeFile(s_biomeFile); - if (completeBiomeList is null) - { + var completeBiomeList = CSVReader.ParseBiomeFile(s_biomeFile); + if (completeBiomeList is null) + { LogHandler.Fatal("Failed to extract biome information from CSV, aborting."); - return; + return; } - - // Attempt to read and parse the CSV with all recipe information. - List completeMaterialsList; - completeMaterialsList = CSVReader.ParseRecipeFile(s_recipeFile); - if (completeMaterialsList is null) - { - LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); - return; + + // Attempt to read and parse the CSV with all recipe information. + var completeMaterialsList = CSVReader.ParseRecipeFile(s_recipeFile); + if (completeMaterialsList is null) + { + LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); + return; } // Attempt to read and parse the CSV with wreckages and databox info. @@ -118,94 +122,109 @@ internal static void Randomise() // Create a new seed if the current one is just a default Random random; - if (s_config.iSeed == 0) - { - random = new System.Random(); - s_config.iSeed = random.Next(); - } - random = new System.Random(s_config.iSeed); - - RandomiserLogic logic = new RandomiserLogic(random, s_masterDict, s_config, completeMaterialsList, databoxes); + if (s_config.iSeed == 0) + { + random = new Random(); + s_config.iSeed = random.Next(); + } + random = new Random(s_config.iSeed); + + RandomiserLogic logic = new RandomiserLogic(random, s_masterDict, s_config, completeMaterialsList, databoxes); FragmentLogic fragmentLogic = null; - if (s_config.bRandomiseFragments) - { - fragmentLogic = new FragmentLogic(s_config, s_masterDict, completeBiomeList, random); - fragmentLogic.Init(); + if (s_config.bRandomiseFragments) + { + fragmentLogic = new FragmentLogic(s_config, s_masterDict, completeBiomeList, random); + fragmentLogic.Init(); } - logic.RandomSmart(fragmentLogic); + logic.RandomSmart(fragmentLogic); LogHandler.Info("Randomisation successful!"); SaveGameStateToDisk(); SpoilerLog spoiler = new SpoilerLog(s_config); - // This should run async, but we don't need the result here. It's a file. - _ = spoiler.WriteLog(); + // This should run async, but we don't need the result here. It's a file. + _ = spoiler.WriteLog(); } - // Ensure the user did not update into a save incompatibility. - private static bool CheckSaveCompatibility() + /// + /// Ensure the user did not update into a save incompatibility. + /// + private static bool CheckSaveCompatibility() { - if (s_config.iSaveVersion != s_expectedSaveVersion) - { - s_versionDict.TryGetValue(s_config.iSaveVersion, out string version); - if (string.IsNullOrEmpty(version)) - version = "unknown."; - - LogHandler.MainMenuMessage("It seems you updated Subnautica Randomiser. This version is incompatible with your previous savegame."); - LogHandler.MainMenuMessage("The last supported version for your savegame is " + version); - LogHandler.MainMenuMessage("If you wish to continue anyway, randomise again in the options menu or delete your config.json"); - return false; - } - - return true; + if (s_config.iSaveVersion == s_expectedSaveVersion) + return true; + + s_versionDict.TryGetValue(s_config.iSaveVersion, out string version); + if (string.IsNullOrEmpty(version)) + version = "unknown or corrupted."; + + LogHandler.MainMenuMessage("It seems you updated Subnautica Randomiser. This version is incompatible with your previous savegame."); + LogHandler.MainMenuMessage("The last supported version for your savegame is " + version); + LogHandler.MainMenuMessage("To protect your previous savegame, no changes to the game have been made."); + LogHandler.MainMenuMessage("If you wish to continue anyway, randomise again in the options menu or delete your config.json"); + return false; } - internal static void SaveGameStateToDisk() - { - if (s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) - { + /// + /// Serialise the current randomisation state to disk. + /// + internal static void SaveGameStateToDisk() + { + if (s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) + { string base64 = s_masterDict.ToBase64String(); s_config.sBase64Seed = base64; s_config.Save(); - LogHandler.Debug("Saved game state to disk!"); + LogHandler.Debug("Saved game state to disk!"); + } + else + { + LogHandler.Error("Could not save game state to disk: Dictionary empty."); } - else - { - LogHandler.Error("Could not save game state to disk: Dictionary empty."); - } } - internal static EntitySerializer RestoreGameStateFromDisk() + /// + /// Attempt to deserialise a randomisation state from disk. + /// + /// The EntitySerializer as previously written to disk. + /// Raised if the game state is corrupted in some way. + internal static EntitySerializer RestoreGameStateFromDisk() { - if (string.IsNullOrEmpty(s_config.sBase64Seed)) - { - throw new InvalidDataException("base64 seed is empty."); + if (string.IsNullOrEmpty(s_config.sBase64Seed)) + { + throw new InvalidDataException("base64 seed is empty."); } LogHandler.Debug("Trying to decode base64 string..."); EntitySerializer dictionary = EntitySerializer.FromBase64String(s_config.sBase64Seed); - if (dictionary is null || dictionary.RecipeDict is null || dictionary.RecipeDict.Count == 0) - { - throw new InvalidDataException("base64 seed is invalid; could not deserialize Dictionary."); + if (dictionary?.RecipeDict is null || dictionary.RecipeDict.Count == 0) + { + throw new InvalidDataException("base64 seed is invalid; could not deserialize Dictionary."); } - return dictionary; + return dictionary; } + /// + /// Get the installation directory of the mod. + /// internal static string GetSubnauticaRandomiserDirectory() { - return new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName; + return new FileInfo(Assembly.GetExecutingAssembly().Location).Directory?.FullName; } - private static void EnableHarmonyPatching() - { - if (s_masterDict != null && s_masterDict.Databoxes != null && s_masterDict.Databoxes.Count > 0) + /// + /// Enables all necessary harmony patches based on the randomisation state in s_masterDict. + /// + private static void EnableHarmonyPatching() + { + if (s_masterDict?.Databoxes?.Count > 0) { Harmony harmony = new Harmony("SubnauticaRandomiser"); harmony.PatchAll(); - } - } - } -} + } + } + } +} diff --git a/SubnauticaRandomiser/LogHandler.cs b/SubnauticaRandomiser/LogHandler.cs index bf50943..e5fa156 100644 --- a/SubnauticaRandomiser/LogHandler.cs +++ b/SubnauticaRandomiser/LogHandler.cs @@ -1,13 +1,11 @@ -using System; -using QModManager.API; +using QModManager.API; using Logger = QModManager.Utility.Logger; namespace SubnauticaRandomiser { + /// A class for handling all the logging that the main program might ever want to do. + /// Also includes main menu messages for relaying information to the user directly. public static class LogHandler { - // A class for handling all the logging that the main program might ever - // want to do. Also includes main menu messages. - // Unnecessary? Maybe. Cleans up some clutter everywhere else though. internal static void Info(string message) { @@ -34,7 +32,7 @@ internal static void Debug(string message) Logger.Log(Logger.Level.Debug, message); } - // Sending messages through QModManager's main menu system + /// Send a message through QModManager's main menu system. internal static void MainMenuMessage(string message) { QModServices.Main.AddCriticalMessage(message); diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 77b1092..731ee26 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -48,6 +48,9 @@ internal class FragmentLogic }; internal List AllSpawnData; + /// + /// Handle the logic for everything related to fragments. + /// internal FragmentLogic(RandomiserConfig config, EntitySerializer serializer, List biomeList, Random random) { _config = config; @@ -56,13 +59,19 @@ internal FragmentLogic(RandomiserConfig config, EntitySerializer serializer, Lis _random = random; AllSpawnData = new List(); } - - // Randomise the spawn points for a given fragment. + + /// + /// Randomise the spawn points for a given fragment. + /// + /// The fragment entity to randomise. + /// The maximum depth to consider. + /// The SpawnData that was newly added to the EntitySerializer. + /// Raised if the fragment name is invalid. internal SpawnData RandomiseFragment(LogicEntity entity, int depth) { - if (!_classIdDatabase.TryGetValue(entity.TechType, out List idList)){ + if (!_classIdDatabase.TryGetValue(entity.TechType, out List idList)) throw new ArgumentException("Failed to find fragment '" + entity.TechType.AsString() + "' in classId database!"); - } + LogHandler.Debug("Randomising fragment " + entity.TechType.AsString() + " for depth " + depth); // HACK for now, only consider the first entry in the ID list. @@ -77,8 +86,7 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) // Choose a suitable biome which is also accessible at this depth. Biome biome = GetRandom(_availableBiomes.FindAll(x => x.AverageDepth <= depth)); // In case no good biome is available, just choose any. - if (biome is null) - biome = GetRandom(_availableBiomes); + biome ??= GetRandom(_availableBiomes); // Ensure the biome can actually be used for creating valid BiomeData. if (!Enum.TryParse(biome.Name, out BiomeType biomeType)) @@ -107,10 +115,11 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) ApplyRandomisedFragment(entity, spawnData); return spawnData; } - - // Go through all the BiomeData in the game and reset any fragment spawn - // rates to 0.0f, effectively "deleting" them from the game until the - // randomiser has decided on a new distribution. + + /// + /// Go through all the BiomeData in the game and reset any fragment spawn rates to 0.0f, effectively "deleting" + /// them from the game until the randomiser has decided on a new distribution. + /// internal void ResetFragmentSpawns() { LogHandler.Debug("---Resetting vanilla fragment spawn rates---"); @@ -142,10 +151,13 @@ internal void ResetFragmentSpawns() LogHandler.Debug("---Completed resetting vanilla fragment spawn rates---"); } - - // Get all biomes that have fragment rate data, i.e. which contained - // fragments in vanilla. - // TODO: Can be expanded to include non-vanilla ones. + + /// + /// Get all biomes that have fragment rate data, i.e. which contained fragments in vanilla. + /// TODO: Can be expanded to include non-vanilla ones. + /// + /// A list of all biomes in the game. + /// A list of Biomes with active fragment spawn rates. private List GetAvailableFragmentBiomes(List collections) { List biomes = new List(); @@ -164,9 +176,10 @@ private List GetAvailableFragmentBiomes(List collections LogHandler.Debug("---Total biomes suitable for fragments: "+biomes.Count); return biomes; } - - // Assemble a dictionary of all relevant prefabs with their unique classId - // identifier. + + /// + /// Assemble a dictionary of all relevant prefabs with their unique classId identifier. + /// private void PrepareClassIdDatabase() { _classIdDatabase = new Dictionary>(); @@ -193,8 +206,11 @@ private void PrepareClassIdDatabase() //LogHandler.Debug("KEY: " + classId + ", VALUE: " + UWE.PrefabDatabase.prefabFiles[classId] + ", TECHTYPE: " + type.AsString()); } } - - // Rarely, a reversed variant of the classId dictionary is useful. + + /// + /// Reverse the classId dictionary to allow for ID to TechType matching. + /// + /// The inverted dictionary. internal Dictionary ReverseClassIdDatabase() { Dictionary database = new Dictionary(); @@ -214,8 +230,10 @@ internal Dictionary ReverseClassIdDatabase() return database; } - - // Re-apply spawnData from a saved game. + + /// + /// Re-apply spawnData from a saved game. + /// internal static void ApplyMasterDict(EntitySerializer masterDict) { foreach (TechType key in masterDict.SpawnDataDict.Keys) @@ -224,9 +242,12 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); } } - - // Add modified spawnData to the game and any place it needs to go to - // be stored for later use. + + /// + /// Add modified SpawnData to the game and any place it needs to go to be stored for later use. + /// + /// The entity to modify spawn rates for. + /// The modified SpawnData to use. internal void ApplyRandomisedFragment(LogicEntity entity, SpawnData spawnData) { entity.SpawnData = spawnData; @@ -237,17 +258,20 @@ internal void ApplyRandomisedFragment(LogicEntity entity, SpawnData spawnData) LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); } - // This is kinda redundant and a leftover from early testing. - internal void EditBiomeData(string classId, List distribution) - { - LootDistributionHandler.EditLootDistributionData(classId, distribution); - } - + /// + /// Get the classId for the given TechType. + /// internal string GetClassId(TechType type) { return CraftData.GetClassIdForTechType(type); } + /// + /// Get a random element from the given list. + /// + /// The list to choose a member from. + /// The type of the objects contained in the list. + /// A random element of the list. private T GetRandom(List list) { if (list is null || list.Count == 0) @@ -256,6 +280,10 @@ private T GetRandom(List list) return list[_random.Next(0, list.Count)]; } + /// + /// Force Subnautica and SMLHelper to index and cache the classIds, setup the databases, and prepare a blank + /// slate by removing all existing fragment spawns from the game. + /// public void Init() { // This forces SMLHelper (and the game) to cache the classIds. @@ -327,6 +355,12 @@ internal void DumpBiomeDataEntities() } } } + + // This is kinda redundant and a leftover from early testing. + internal void EditBiomeData(string classId, List distribution) + { + LootDistributionHandler.EditLootDistributionData(classId, distribution); + } internal void Test() { diff --git a/SubnauticaRandomiser/Logic/Materials.cs b/SubnauticaRandomiser/Logic/Materials.cs index d4308e1..aa805b8 100644 --- a/SubnauticaRandomiser/Logic/Materials.cs +++ b/SubnauticaRandomiser/Logic/Materials.cs @@ -20,20 +20,33 @@ internal Materials(List allMaterials) _allMaterials = allMaterials; _reachableMaterials = new List(); } - - // Add all recipes that match the given requirements to the list. + + /// + /// Add all recipes that match the given requirements to the list of reachable materials. + /// + /// The category of materials to consider. + /// The maximum depth at which the material is allowed to be available. + /// True if any new entries were added to the list of reachable materials, false otherwise. internal bool AddReachable(ETechTypeCategory[] categories, int maxDepth) { List additions = new List(); - // Use a lambda expression to find every object where the search - // parameters match. + // Use a lambda expression to find every object where the search parameters match. additions.AddRange(_allMaterials.FindAll(x => ContainsCategory(categories, x.Category) && x.AccessibleDepth <= maxDepth)); return AddToReachableList(additions); } - - // Add all recipes where categories, depth, and prerequisites match. + + /// + /// Add all recipes where categories, maximum, depth, and prerequisites match to the list of reachable materials. + /// + /// The category of materials to consider. + /// The maximum depth at which the material is allowed to be available. + /// Only consider materials which require this TechType to be randomised before they + /// are allowed to be considered available. + /// If true, invert the behaviour of the prerequisite to consider exclusively materials + /// which do not require that TechType. + /// True if any new entries were added to the list of reachable materials, false otherwise. internal bool AddReachableWithPrereqs(ETechTypeCategory[] categories, int maxDepth, TechType prerequisite, bool invert = false) { List additions = new List(); @@ -58,6 +71,11 @@ internal bool AddReachableWithPrereqs(ETechTypeCategory[] categories, int maxDep return AddToReachableList(additions); } + /// + /// Add materials to the list of things considered reachable. + /// + /// + /// True if any new entities were added to the list, false otherwise. private bool AddToReachableList(List additions) { // Ensure no duplicates are added to the list. This loop *must* go @@ -79,19 +97,40 @@ private bool AddToReachableList(List additions) return true; } + /// + /// Add a single entity to the list of reachable things. + /// + /// The entity to add. + /// True if successful, false otherwise. internal bool AddReachable(LogicEntity entity) { return AddToReachableList(new List { entity }); } + /// + /// Add all entities matching the category up to a maximum depth to the list of reachable things. + /// + /// The category to consider. + /// The maximum depth at which the entity is allowed to be available. + /// True if any new entities were added to the list, false otherwise. internal bool AddReachable(ETechTypeCategory category, int maxDepth) { - return AddReachable(new ETechTypeCategory[] { category }, maxDepth); + return AddReachable(new[] { category }, maxDepth); } + /// + /// Add all recipes where category, maximum, depth, and prerequisites match to the list of reachable materials. + /// + /// The category of materials to consider. + /// The maximum depth at which the material is allowed to be available. + /// Only consider materials which require this TechType to be randomised before they + /// are allowed to be considered available. + /// If true, invert the behaviour of the prerequisite to consider exclusively materials + /// which do not require that TechType. + /// True if any new entries were added to the list of reachable materials, false otherwise. internal bool AddReachableWithPrereqs(ETechTypeCategory category, int maxDepth, TechType prerequisite, bool invert = false) { - return AddReachableWithPrereqs(new ETechTypeCategory[] { category }, maxDepth, prerequisite, invert); + return AddReachableWithPrereqs(new[] { category }, maxDepth, prerequisite, invert); } // TODO: Generalise this. diff --git a/SubnauticaRandomiser/Logic/Mode.cs b/SubnauticaRandomiser/Logic/Mode.cs index 7bb090a..65af099 100644 --- a/SubnauticaRandomiser/Logic/Mode.cs +++ b/SubnauticaRandomiser/Logic/Mode.cs @@ -1,23 +1,24 @@ using System; -using System.Collections.Generic; -using SMLHelper.V2.Crafting; -using SMLHelper.V2.Handlers; -using SubnauticaRandomiser.RandomiserObjects; - -namespace SubnauticaRandomiser.Logic -{ - internal abstract class Mode - { - protected RandomiserConfig _config; +using System.Collections.Generic; +using JetBrains.Annotations; +using SMLHelper.V2.Crafting; +using SMLHelper.V2.Handlers; +using SubnauticaRandomiser.RandomiserObjects; + +namespace SubnauticaRandomiser.Logic +{ + internal abstract class Mode + { + protected RandomiserConfig _config; protected Materials _materials; protected ProgressionTree _tree; - protected Random _random; - protected List _ingredients = new List(); + protected Random _random; + protected List _ingredients = new List(); protected List _blacklist = new List(); - protected LogicEntity _baseTheme; + protected LogicEntity _baseTheme; - protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) - { + protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) + { _config = config; _materials = materials; _tree = tree; @@ -26,82 +27,106 @@ protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tre _baseTheme = ChooseBaseTheme(100); LogHandler.Debug("Chosen " + _baseTheme.TechType.AsString() + " as base theme."); //InitMod.s_masterDict.DictionaryInstance.Add(TechType.Titanium, _baseTheme.GetSerializableRecipe()); - //ChangeScrapMetalResult(_baseTheme); + //ChangeScrapMetalResult(_baseTheme); } - - internal abstract LogicEntity RandomiseIngredients(LogicEntity entity); - // Add an ingredient to the list of ingredients used to form a recipe, - // but ensure its MaxUses field is respected. + internal abstract LogicEntity RandomiseIngredients(LogicEntity entity); + + /// + /// Add an ingredient to the list of ingredients used to form a recipe, but ensure its MaxUses field is + /// respected. + /// + /// The entity to add. + /// The number of uses to consume. protected void AddIngredientWithMaxUsesCheck(LogicEntity entity, int amount) { // Ensure that limited ingredients are not overused. Particularly // intended for cuddlefish. int remainder = entity.MaxUsesPerGame - entity._usedInRecipes; - if (entity.MaxUsesPerGame != 0 && remainder > 0 && remainder < amount) - amount = remainder; - + if (entity.MaxUsesPerGame != 0 && remainder > 0 && remainder < amount) + amount = remainder; + _ingredients.Add(new RandomiserIngredient(entity.TechType, amount)); - entity._usedInRecipes++; - + entity._usedInRecipes++; + if (!entity.HasUsesLeft()) { _materials.GetReachable().Remove(entity); - LogHandler.Debug("! Removing " + entity.TechType.AsString() + " from materials list due to max uses reached: " + entity._usedInRecipes); + LogHandler.Debug("! Removing " + entity.TechType.AsString() + " from materials list due to " + + "max uses reached: " + entity._usedInRecipes); + } + } + + /// + /// Get a random entity from a list, ensuring that it is not part of a given blacklist. + /// TODO: Install safeguards to prevent infinite loops. + /// + /// The list to get a random element from. + /// The blacklist of forbidden elements to not ever consider. + /// A random, non-blacklisted element from the list. + /// Raised if the list is null or empty. + [NotNull] + protected LogicEntity GetRandom(List list, List blacklist = null) + { + if (list == null || list.Count == 0) + throw new InvalidOperationException("Failed to get valid entity from materials list: list is null or empty."); + + LogicEntity randomEntity = null; + while (true) + { + randomEntity = list[_random.Next(0, list.Count)]; + + if (blacklist != null && blacklist.Count > 0) + { + if (blacklist.Contains(randomEntity.Category)) + continue; + } + break; } + + return randomEntity; } - - protected LogicEntity GetRandom(List list, List blacklist = null) - { - if (list == null || list.Count == 0) - throw new InvalidOperationException("Failed to get valid entity from materials list: list is null or empty."); - - LogicEntity randomEntity = null; - while (true) - { - randomEntity = list[_random.Next(0, list.Count)]; - - if (blacklist != null && blacklist.Count > 0) - { - if (blacklist.Contains(randomEntity.Category)) - continue; - } - break; - } - - return randomEntity; - } - - // If base theming is enabled and this is a base piece, yield the base - // theming ingredient. - protected LogicEntity CheckForBaseTheming(LogicEntity entity) + + /// + /// If base theming is enabled and the given entity is a base piece, return the base theming ingredient. + /// + /// The entity to check. + /// A LogicEntity if the passed entity is a base piece, null otherwise. + [CanBeNull] + protected LogicEntity CheckForBaseTheming(LogicEntity entity) { if (_config.bDoBaseTheming && _baseTheme != null && entity.Category.Equals(ETechTypeCategory.BaseBasePieces)) return _baseTheme; - return null; + return null; } - - // If vanilla upgrade chains are enabled, yield that which this recipe - // upgrades from (e.g. yields Knife when given HeatBlade) - protected LogicEntity CheckForVanillaUpgrades(LogicEntity entity) + + /// + /// If vanilla upgrade chains are enabled, return that which this recipe upgrades from. + /// Returns the basic Knife when given HeatBlade. + /// + /// The entity to check for downgrades. + /// A LogicEntity if the given entity has a predecessor, null otherwise. + [CanBeNull] + protected LogicEntity CheckForVanillaUpgrades(LogicEntity entity) { LogicEntity result = null; - + if (_config.bVanillaUpgradeChains) { TechType basicUpgrade = _tree.GetUpgradeChain(entity.TechType); if (!basicUpgrade.Equals(TechType.None)) - { result = _materials.GetAll().Find(x => x.TechType.Equals(basicUpgrade)); - } } - return result; + return result; } - - // Choose a theming ingredient for the base from among a range of easily - // available options. + + /// + /// Choose a theming ingredient for the base from among a range of easily available options. + /// + /// The maximum depth at which the material must be available. + /// A random LogicEntity from the Raw Materials or (if enabled) Fish categories. private LogicEntity ChooseBaseTheme(int depth) { List options = new List(); @@ -119,21 +144,22 @@ private LogicEntity ChooseBaseTheme(int depth) && !x.HasPrerequisites && x.MaxUsesPerGame == 0 && x.GetItemSize() == 1)); - } + } LogHandler.Debug("LIST OF BASE THEME OPTIONS:"); - foreach (LogicEntity ent in options) - { - LogHandler.Debug(ent.TechType.AsString()); + foreach (LogicEntity ent in options) + { + LogHandler.Debug(ent.TechType.AsString()); } LogHandler.Debug("END LIST"); - + return GetRandom(options); } // This function changes the output of the metal salvage recipe by removing // the titanium one and replacing it with the new one. // As a minor caveat, the new recipe shows up at the bottom of the tree. + // FIXME does not function. internal static void ChangeScrapMetalResult(Recipe replacement) { if (replacement.TechType.Equals(TechType.Titanium)) @@ -170,21 +196,25 @@ internal static void ChangeScrapMetalResult(Recipe replacement) CraftDataHandler.RemoveFromGroup(TechGroup.Resources, TechCategory.BasicMaterials, TechType.Titanium); CraftDataHandler.AddToGroup(TechGroup.Resources, TechCategory.BasicMaterials, yeet); - } - - protected void UpdateBlacklist(LogicEntity entity) - { - _blacklist = new List(); - - if (_config.iEquipmentAsIngredients == 0 || (_config.iEquipmentAsIngredients == 1 && entity.CanFunctionAsIngredient())) - _blacklist.Add(ETechTypeCategory.Equipment); - if (_config.iToolsAsIngredients == 0 || (_config.iToolsAsIngredients == 1 && entity.CanFunctionAsIngredient())) - _blacklist.Add(ETechTypeCategory.Tools); - if (_config.iUpgradesAsIngredients == 0 || (_config.iUpgradesAsIngredients == 1 && entity.CanFunctionAsIngredient())) - { - _blacklist.Add(ETechTypeCategory.VehicleUpgrades); - _blacklist.Add(ETechTypeCategory.WorkBenchUpgrades); - } - } - } -} + } + + /// + /// Set up the blacklist with entities that are not allowed to function as ingredients for the given entity. + /// + /// The entity to build a blacklist against. + protected void UpdateBlacklist(LogicEntity entity) + { + _blacklist = new List(); + + if (_config.iEquipmentAsIngredients == 0 || (_config.iEquipmentAsIngredients == 1 && entity.CanFunctionAsIngredient())) + _blacklist.Add(ETechTypeCategory.Equipment); + if (_config.iToolsAsIngredients == 0 || (_config.iToolsAsIngredients == 1 && entity.CanFunctionAsIngredient())) + _blacklist.Add(ETechTypeCategory.Tools); + if (_config.iUpgradesAsIngredients == 0 || (_config.iUpgradesAsIngredients == 1 && entity.CanFunctionAsIngredient())) + { + _blacklist.Add(ETechTypeCategory.VehicleUpgrades); + _blacklist.Add(ETechTypeCategory.WorkBenchUpgrades); + } + } + } +} diff --git a/SubnauticaRandomiser/Logic/ModeBalanced.cs b/SubnauticaRandomiser/Logic/ModeBalanced.cs index aac4810..3283987 100644 --- a/SubnauticaRandomiser/Logic/ModeBalanced.cs +++ b/SubnauticaRandomiser/Logic/ModeBalanced.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using JetBrains.Annotations; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; @@ -15,10 +16,14 @@ internal ModeBalanced(RandomiserConfig config, Materials materials, ProgressionT _basicOutpostSize = 0; _reachableMaterials = _materials.GetReachable(); } - - // Fill a given recipe with ingredients. This class uses a value arithmetic - // to balance hard to reach materials against easier ones, and tries to - // provide a well-rounded, curated experience. + + /// + /// Fill a given recipe with ingredients in-place. This class uses a value arithmetic to balance hard to reach + /// materials against easier ones, and tries to provide a well-rounded, curated experience. + /// + /// The recipe to randomise ingredients for. + /// The modified entity. + [NotNull] internal override LogicEntity RandomiseIngredients(LogicEntity entity) { _ingredients = new List(); @@ -31,19 +36,18 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) LogHandler.Debug("Figuring out ingredients for " + entity.TechType.AsString()); LogicEntity primaryIngredient = ChoosePrimaryIngredient(entity, targetValue); - - // Disallow the builer tool from being used in base pieces. - if (entity.Category.IsBasePiece() && primaryIngredient.TechType.Equals(TechType.Builder)) - primaryIngredient = ReplaceWithSimilarValue(primaryIngredient); + + // Disallow the builder tool from being used in base pieces. + if (entity.Category.IsBasePiece() && primaryIngredient.TechType.Equals(TechType.Builder)) + primaryIngredient = ReplaceWithSimilarValue(primaryIngredient); AddIngredientWithMaxUsesCheck(primaryIngredient, 1); currentValue += primaryIngredient.Value; LogHandler.Debug(" Adding primary ingredient " + primaryIngredient.TechType.AsString()); - // Now fill up with random materials until the value threshold - // is more or less met, as defined by fuzziness. - // Converted to do-while since we want this to happen at least once. + // Now fill up with random materials until the value threshold is more or less met, as defined by fuzziness. + // Using a do-while since we want this to happen at least once. do { LogicEntity ingredient = GetRandom(_reachableMaterials, _blacklist); @@ -51,10 +55,10 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) // Prevent duplicates. if (_ingredients.Exists(x => x.techType == ingredient.TechType)) continue; - - // Disallow the builder tool from being used in base pieces. - if (entity.Category.IsBasePiece() && ingredient.TechType.Equals(TechType.Builder)) - continue; + + // Disallow the builder tool from being used in base pieces. + if (entity.Category.IsBasePiece() && ingredient.TechType.Equals(TechType.Builder)) + continue; // What's the maximum amount of this ingredient the recipe can // still sustain? @@ -66,7 +70,7 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) // If a recipe starts requiring a lot of inventory space to // complete, try to minimise adding more ingredients. if (totalSize + (ingredient.GetItemSize() * number) > _config.iMaxInventorySizePerRecipe) - number = 1; + number = 1; AddIngredientWithMaxUsesCheck(ingredient, number); currentValue += ingredient.Value * number; @@ -85,11 +89,11 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) { LogHandler.Debug("! Basic outpost size is getting too large, stopping."); break; - } - // Also, respect the maximum number of ingredients set in the config. + } + // Also, respect the maximum number of ingredients set in the config. if (_config.iMaxIngredientsPerRecipe <= _ingredients.Count) { - LogHandler.Debug("! Recipe has reached maximum allowed number of ingredients, stopping."); + LogHandler.Debug("! Recipe has reached maximum allowed number of ingredients, stopping."); break; } } while ((targetValue - currentValue) > (targetValue * _config.dFuzziness / 2)); @@ -105,10 +109,15 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) entity.Recipe.CraftAmount = CraftDataHandler.GetTechData(entity.TechType).craftAmount; return entity; } - - // Find a primary ingredient for the recipe. Its value should be a - // percentage of the total value of the entire recipe as defined in - // the config, +-10%. + + /// + /// Find a primary ingredient for the recipe. Its value should be a percentage of the total value of the entire + /// recipe as defined in the config, +-10%. + /// + /// The recipe to randomise ingredients for. + /// The target value of all ingredients for the recipe. + /// The randomised recipe, modified in-place. + [NotNull] private LogicEntity ChoosePrimaryIngredient(LogicEntity entity, double targetValue) { List pIngredientCandidates = _reachableMaterials.FindAll( @@ -133,18 +142,25 @@ private LogicEntity ChoosePrimaryIngredient(LogicEntity entity, double targetVal return primaryIngredient; } - - // What is the maximum amount of this ingredient the recipe can sustain? + + /// + /// Find the highest number of the given ingredient which the recipe can sustain. + /// + /// The ingredient to consider. + /// The overall target value of the recipe. + /// The current value of all ingredients chosen thus far. + /// A positive integer. private int FindMaximum(LogicEntity ingredient, double targetValue, double currentValue) { int max = (int)((targetValue + targetValue * _config.dFuzziness / 2) - currentValue) / ingredient.Value; max = max > 0 ? max : 1; max = max > _config.iMaxAmountPerIngredient ? _config.iMaxAmountPerIngredient : max; - // Tools and upgrades do not stack, but if the recipe would - // require several and you have more than one in inventory, - // it will consume all of them. - if (ingredient.Category.Equals(ETechTypeCategory.Tools) || ingredient.Category.Equals(ETechTypeCategory.VehicleUpgrades) || ingredient.Category.Equals(ETechTypeCategory.WorkBenchUpgrades)) + // Tools and upgrades do not stack, but if the recipe would require several and you have more than one in + // inventory, it will consume all of them. + if (ingredient.Category.Equals(ETechTypeCategory.Tools) + || ingredient.Category.Equals(ETechTypeCategory.VehicleUpgrades) + || ingredient.Category.Equals(ETechTypeCategory.WorkBenchUpgrades)) max = 1; // Never require more than one (default) egg. That's tedious. @@ -152,36 +168,40 @@ private int FindMaximum(LogicEntity ingredient, double targetValue, double curre max = _config.iMaxEggsAsSingleIngredient; return max; - } - - // Replace an undesirable ingredient with one of similar value. - // Start with a range of 10% in each direction, increasing if no valid - // replacement can be found. + } + + /// + /// Replace an undesirable ingredient with one of similar value. Start with a range of 10% in each direction, + /// increasing if no valid replacement can be found. + /// + /// The ingredient to replace. + /// A different ingredient of roughly similar value, or a random raw material as fallback. + [NotNull] private LogicEntity ReplaceWithSimilarValue(LogicEntity undesirable) { - int value = undesirable.Value; - double range = 0.1; - - List betterOptions = new List(); - LogHandler.Debug("Replacing undesirable ingredient " + undesirable.TechType.AsString()); - - // Progressively increase the search radius if no replacement is found, - // but stop before it gets out of hand. + int value = undesirable.Value; + double range = 0.1; + + List betterOptions = new List(); + LogHandler.Debug("Replacing undesirable ingredient " + undesirable.TechType.AsString()); + + // Progressively increase the search radius if no replacement is found, + // but stop before it gets out of hand. while (betterOptions.Count == 0 && range < 1.0) - { + { // Add all items of the same category with value +- range% - betterOptions.AddRange(_reachableMaterials.FindAll(x => x.Category.Equals(undesirable.Category) - && x.Value < undesirable.Value + undesirable.Value * range - && x.Value > undesirable.Value - undesirable.Value * range - )); + betterOptions.AddRange(_reachableMaterials.FindAll(x => x.Category.Equals(undesirable.Category) + && x.Value < undesirable.Value + undesirable.Value * range + && x.Value > undesirable.Value - undesirable.Value * range + )); range += 0.2; - } - - // If the loop above exited due to the range getting too large, just - // use any unlocked raw material instead. + } + + // If the loop above exited due to the range getting too large, just + // use any unlocked raw material instead. if (betterOptions.Count == 0) - betterOptions.AddRange(_reachableMaterials.FindAll(x => x.Category.Equals(ETechTypeCategory.RawMaterials))); - + betterOptions.AddRange(_reachableMaterials.FindAll(x => x.Category.Equals(ETechTypeCategory.RawMaterials))); + return GetRandom(betterOptions); } } diff --git a/SubnauticaRandomiser/Logic/ModeRandom.cs b/SubnauticaRandomiser/Logic/ModeRandom.cs index 2add0ac..d51a6ab 100644 --- a/SubnauticaRandomiser/Logic/ModeRandom.cs +++ b/SubnauticaRandomiser/Logic/ModeRandom.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; @@ -13,35 +13,38 @@ internal ModeRandom(RandomiserConfig config, Materials materials, ProgressionTre { _reachableMaterials = _materials.GetReachable(); } - - // Fill a given recipe with ingredients. This algorithm mostly uses random - // number generation to fill in the gaps. + + /// + /// Fill a given recipe with ingredients in-place. This algorithm mostly uses pure RNG to fill in the gaps. + /// + /// The recipe to randomise ingredients for. + /// The modified entity. internal override LogicEntity RandomiseIngredients(LogicEntity entity) { int number = _random.Next(1, _config.iMaxIngredientsPerRecipe + 1); - int totalInvSize = 0; + int totalInvSize = 0; _ingredients = new List(); - UpdateBlacklist(entity); + UpdateBlacklist(entity); for (int i = 1; i <= number; i++) { - LogicEntity ingredientEntity = GetRandom(_reachableMaterials, _blacklist); + LogicEntity ingredientEntity = GetRandom(_reachableMaterials, _blacklist); // Prevent duplicates. if (_ingredients.Exists(x => x.techType == ingredientEntity.TechType)) { i--; continue; - } - - // Disallow the builder tool from being used in base pieces. + } + + // Disallow the builder tool from being used in base pieces. if (entity.Category.IsBasePiece() && ingredientEntity.TechType.Equals(TechType.Builder)) { - i--; + i--; continue; - } - - int max = FindMaximum(ingredientEntity); + } + + int max = FindMaximum(ingredientEntity); RandomiserIngredient ingredient = new RandomiserIngredient(ingredientEntity.TechType, _random.Next(1, max + 1)); @@ -60,22 +63,28 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) entity.Recipe.Ingredients = _ingredients; entity.Recipe.CraftAmount = CraftDataHandler.GetTechData(entity.TechType).craftAmount; return entity; - } - + } + + /// + /// Find the highest number allowed for the given ingredient. + /// + /// The ingredient to consider. + /// A positive integer. private int FindMaximum(LogicEntity entity) { - int max = _config.iMaxAmountPerIngredient; + int max = _config.iMaxAmountPerIngredient; - // Tools and upgrades do not stack, but if the recipe would - // require several and you have more than one in inventory, - // it will consume all of them. - if (entity.Category.Equals(ETechTypeCategory.Tools) || entity.Category.Equals(ETechTypeCategory.VehicleUpgrades) || entity.Category.Equals(ETechTypeCategory.WorkBenchUpgrades)) + // Tools and upgrades do not stack, but if the recipe would require several and you have more than one in + // inventory, it will consume all of them. + if (entity.Category.Equals(ETechTypeCategory.Tools) + || entity.Category.Equals(ETechTypeCategory.VehicleUpgrades) + || entity.Category.Equals(ETechTypeCategory.WorkBenchUpgrades)) max = 1; // Never require more than one (default) egg. That's tedious. if (entity.Category.Equals(ETechTypeCategory.Eggs)) - max = _config.iMaxEggsAsSingleIngredient; - + max = _config.iMaxEggsAsSingleIngredient; + return max; } } diff --git a/SubnauticaRandomiser/Logic/ModeSubstitute.cs b/SubnauticaRandomiser/Logic/ModeSubstitute.cs index 3b42666..a5133e9 100644 --- a/SubnauticaRandomiser/Logic/ModeSubstitute.cs +++ b/SubnauticaRandomiser/Logic/ModeSubstitute.cs @@ -1,8 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser.Logic { + /// + /// This is a legacy class, originally a revised implementation of the first Randomizer's approach to randomisation. + /// It is kept here for potential future repurposing. + /// + [Obsolete("This is a legacy class and no longer intended to be used.")] internal class ModeSubstitute { private EntitySerializer _masterDict; diff --git a/SubnauticaRandomiser/Logic/ProgressionTree.cs b/SubnauticaRandomiser/Logic/ProgressionTree.cs index 7bc3abc..374f942 100644 --- a/SubnauticaRandomiser/Logic/ProgressionTree.cs +++ b/SubnauticaRandomiser/Logic/ProgressionTree.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser.Logic @@ -21,19 +22,16 @@ public ProgressionTree() DepthProgressionItems = new Dictionary(); } + /// + /// Set up a progression tree with all the vanilla roadblocks and checkpoints. It includes the major depth + /// milestones and the aurora, as well as which vehicles let you reach them. Getting to places on foot is + /// handled by the depth calculcation logic elsewhere. + /// If mod support ever becomes a thing there will likely have to be more flexible solutions than this. + /// public void SetupVanillaTree() { - // This is where the progression tree with all the vanilla roadblocks - // and checkpoints gets set up. It includes the major depth milestones - // and the aurora, as well as which vehicles let you reach them. - // Getting to places on foot is handled by the depth calculcation - // logic in ProgressionManager. - // If mod support ever becomes a thing there will likely have to be - // more flexible solutions than this. - - ProgressionPath path; // Aurora. Radiation suit required, unless you're fast. - path = new ProgressionPath(EProgressionNode.Aurora); + var path = new ProgressionPath(EProgressionNode.Aurora); path.AddPath(TechType.RadiationSuit); SetProgressionPath(EProgressionNode.Aurora, path); @@ -53,42 +51,42 @@ public void SetupVanillaTree() // 300m. Requires Seamoth I. path = new ProgressionPath(EProgressionNode.Depth300m); - path.AddPath(new TechType[] { TechType.Seamoth, TechType.VehicleHullModule1 }); - path.AddPath(new TechType[] { TechType.Seamoth, TechType.VehicleHullModule2 }); - path.AddPath(new TechType[] { TechType.Seamoth, TechType.VehicleHullModule3 }); + path.AddPath(new [] { TechType.Seamoth, TechType.VehicleHullModule1 }); + path.AddPath(new [] { TechType.Seamoth, TechType.VehicleHullModule2 }); + path.AddPath(new [] { TechType.Seamoth, TechType.VehicleHullModule3 }); path.AddPath(TechType.Exosuit); path.AddPath(TechType.Cyclops); SetProgressionPath(EProgressionNode.Depth300m, path); // 500m. Reachable with Seamoth II, Prawn, or Cyclops. path = new ProgressionPath(EProgressionNode.Depth500m); - path.AddPath(new TechType[] { TechType.Seamoth, TechType.VehicleHullModule2 }); - path.AddPath(new TechType[] { TechType.Seamoth, TechType.VehicleHullModule3 }); + path.AddPath(new [] { TechType.Seamoth, TechType.VehicleHullModule2 }); + path.AddPath(new [] { TechType.Seamoth, TechType.VehicleHullModule3 }); path.AddPath(TechType.Exosuit); path.AddPath(TechType.Cyclops); SetProgressionPath(EProgressionNode.Depth500m, path); // 900m. Reachable with Seamoth III, Prawn, or Cyclops I. path = new ProgressionPath(EProgressionNode.Depth900m); - path.AddPath(new TechType[] {TechType.Seamoth, TechType.VehicleHullModule3 }); + path.AddPath(new [] {TechType.Seamoth, TechType.VehicleHullModule3 }); path.AddPath(TechType.Exosuit); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule1 }); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule2 }); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule3 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule1 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule2 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule3 }); SetProgressionPath(EProgressionNode.Depth900m, path); // 1300m. Reachable with Prawn I or Cyclops II. path = new ProgressionPath(EProgressionNode.Depth1300m); - path.AddPath(new TechType[] { TechType.Exosuit, TechType.ExoHullModule1 }); - path.AddPath(new TechType[] { TechType.Exosuit, TechType.ExoHullModule2 }); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule2 }); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule3 }); + path.AddPath(new [] { TechType.Exosuit, TechType.ExoHullModule1 }); + path.AddPath(new [] { TechType.Exosuit, TechType.ExoHullModule2 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule2 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule3 }); SetProgressionPath(EProgressionNode.Depth1300m, path); // 1700m. Only Prawn II and Cyclops III can reach here. path = new ProgressionPath(EProgressionNode.Depth1700m); - path.AddPath(new TechType[] { TechType.Exosuit, TechType.ExoHullModule2 }); - path.AddPath(new TechType[] { TechType.Cyclops, TechType.CyclopsHullModule3 }); + path.AddPath(new [] { TechType.Exosuit, TechType.ExoHullModule2 }); + path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule3 }); SetProgressionPath(EProgressionNode.Depth1700m, path); @@ -145,11 +143,11 @@ public void SetupVanillaTree() // From among these, at least one has to be accessible by the provided // depth level. Ensures e.g. at least one power source by 200m. - AddElectiveItems(EProgressionNode.Depth100m, new TechType[] { TechType.Battery, TechType.BatteryCharger }); + AddElectiveItems(EProgressionNode.Depth100m, new [] { TechType.Battery, TechType.BatteryCharger }); - AddElectiveItems(EProgressionNode.Depth200m, new TechType[] { TechType.BaseBioReactor, TechType.SolarPanel }); - AddElectiveItems(EProgressionNode.Depth200m, new TechType[] { TechType.PowerCell, TechType.PowerCellCharger, TechType.SeamothSolarCharge }); - AddElectiveItems(EProgressionNode.Depth200m, new TechType[] { TechType.BaseBulkhead, TechType.BaseFoundation, TechType.BaseReinforcement }); + AddElectiveItems(EProgressionNode.Depth200m, new [] { TechType.BaseBioReactor, TechType.SolarPanel }); + AddElectiveItems(EProgressionNode.Depth200m, new [] { TechType.PowerCell, TechType.PowerCellCharger, TechType.SeamothSolarCharge }); + AddElectiveItems(EProgressionNode.Depth200m, new [] { TechType.BaseBulkhead, TechType.BaseFoundation, TechType.BaseReinforcement }); // Assemble a vanilla upgrade chain. These are the upgrades as the @@ -169,9 +167,12 @@ public void SetupVanillaTree() AddUpgradeChain(TechType.PlasteelTank, TechType.DoubleTank); AddUpgradeChain(TechType.HighCapacityTank, TechType.DoubleTank); } - - // Make upgrade chains a part of those items' prerequisites to ensure the - // continuity is respected. + + /// + /// Add early elements of an upgrade chain as prerequisites of the later pieces to ensure that they are always + /// randomised in order, and no Knife can require a Heatblade as ingredient. + /// + /// The list of all materials in the game. public void ApplyUpgradeChainToPrerequisites(List materials) { if (materials == null || materials.Count == 0 || _upgradeChains == null || _upgradeChains.Count == 0) @@ -188,6 +189,12 @@ public void ApplyUpgradeChainToPrerequisites(List materials) } } + /// + /// Get all possible ways to progress past the given progression node. + /// + /// The node to progress past, commonly a depth. + /// The paths to progress, or null if the node or path do not exist. + [CanBeNull] public ProgressionPath GetProgressionPath(EProgressionNode node) { if (_depthDifficulties.TryGetValue(node, out ProgressionPath path)) @@ -196,6 +203,11 @@ public ProgressionPath GetProgressionPath(EProgressionNode node) return null; } + /// + /// Set a pathway of progression for the given progression node. + /// + /// The node to set a path for. + /// The path to set. public void SetProgressionPath(EProgressionNode node, ProgressionPath path) { if (_depthDifficulties.ContainsKey(node)) @@ -205,6 +217,11 @@ public void SetProgressionPath(EProgressionNode node, ProgressionPath path) _depthDifficulties.Add(node, path); } + /// + /// Add a pathway of progression to an existing one ProgressionPath. + /// + /// The node to add a path for. + /// The TechType that allows for progression. public void AddToProgressionPath(EProgressionNode node, TechType path) { if (_depthDifficulties.TryGetValue(node, out ProgressionPath pathways)) @@ -219,6 +236,11 @@ public void AddToProgressionPath(EProgressionNode node, TechType path) } } + /// + /// Add an entity that absolutely must be accessible by the time of the given progression node. + /// + /// The node representing the latest point at which the entity must be accessible. + /// The entity which must be accessible. public void AddEssentialItem(EProgressionNode node, TechType type) { if (_essentialItems.TryGetValue(node, out List items)) @@ -232,6 +254,11 @@ public void AddEssentialItem(EProgressionNode node, TechType type) } } + /// + /// Add a range of entities at least one of which must be accessible by the time of the given progression node. + /// + /// The node representing the latest point at which at least one entity must be accessible. + /// The entities to choose from. public void AddElectiveItems(EProgressionNode node, TechType[] types) { if (_electiveItems.TryGetValue(node, out List existingItems)) @@ -245,19 +272,27 @@ public void AddElectiveItems(EProgressionNode node, TechType[] types) } } + /// + /// Define one entity as a direct upgrade of another. + /// + /// The "Tier 2", or higher order entity. + /// The "Tier 1", or lower order entity. + /// True if successful, false if the upgrade is already head of an existing chain. public bool AddUpgradeChain(TechType upgrade, TechType ingredient) { if (_upgradeChains.ContainsKey(upgrade)) - { return false; - } - else - { - _upgradeChains.Add(upgrade, ingredient); - return true; - } + + _upgradeChains.Add(upgrade, ingredient); + return true; } + /// + /// Get the essential items for the given progression node. + /// + /// The node. + /// The list of essential items, or null if it doesn't exist or the node is invalid. + [CanBeNull] public List GetEssentialItems(EProgressionNode node) { if (_essentialItems.TryGetValue(node, out List items)) @@ -266,8 +301,16 @@ public List GetEssentialItems(EProgressionNode node) return null; } + /// + /// Get essential items for the given depth. + /// + /// The maximum depth to look for. + /// The list of essential items, or null if it doesn't exist or the given depth does not resolve to + /// a progression node. + [CanBeNull] public List GetEssentialItems(int depth) { + // FIXME pretty sure this always yields the first match only. foreach (EProgressionNode node in _essentialItems.Keys) { if ((int)node < depth && _essentialItems[node].Count > 0) @@ -276,6 +319,12 @@ public List GetEssentialItems(int depth) return null; } + /// + /// Get the list of lists of elective items for the given progression node. + /// + /// The node. + /// The list of elective items, or null if it doesn't exist or the node is invalid. + [CanBeNull] public List GetElectiveItems(EProgressionNode node) { if (_electiveItems.TryGetValue(node, out List items)) @@ -284,8 +333,16 @@ public List GetElectiveItems(EProgressionNode node) return null; } + /// + /// Get the list of lists of elective items for the given depth. + /// + /// The maximum depth to look for. + /// The list of elective items, or null if it doesn't exist or the given depth does not resolve to + /// a progression node. + [CanBeNull] public List GetElectiveItems(int depth) { + // FIXME pretty sure this always yields the first match only. foreach (EProgressionNode node in _electiveItems.Keys) { if ((int)node < depth && _electiveItems[node].Count > 0) @@ -293,9 +350,13 @@ public List GetElectiveItems(int depth) } return null; } - - // Takes an advanced upgrade, and returns a required ingredient, if any. - // E.g. Seamoth Depth MK2 will return MK1. + + /// + /// Get the ingredient required for a given upgrade, if any. E.g. Seamoth Depth MK2 will return MK1. + /// + /// The "Tier 2" entity to investigate for ingredients. + /// The TechType of the required "Tier 1" ingredient, or TechType.None if no such requirement exists. + /// public TechType GetUpgradeChain(TechType upgrade) { if (_upgradeChains.TryGetValue(upgrade, out TechType type)) diff --git a/SubnauticaRandomiser/Logic/RandomiserLogic.cs b/SubnauticaRandomiser/Logic/RandomiserLogic.cs index 017cb60..f17a0e8 100644 --- a/SubnauticaRandomiser/Logic/RandomiserLogic.cs +++ b/SubnauticaRandomiser/Logic/RandomiserLogic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; using UnityEngine; @@ -74,15 +75,13 @@ internal void RandomSmart(FragmentLogic fragmentLogic) // If databox randomising is enabled, go and do that. if (_config.bRandomiseDataboxes && _databoxes != null) - { _databoxes = RandomiseDataboxes(_masterDict, _databoxes); - } foreach (LogicEntity e in _materials.GetAll().FindAll(x => - !x.Category.Equals(ETechTypeCategory.RawMaterials) - && !x.Category.Equals(ETechTypeCategory.Fish) - && !x.Category.Equals(ETechTypeCategory.Seeds) - && !x.Category.Equals(ETechTypeCategory.Eggs))) + !x.Category.Equals(ETechTypeCategory.RawMaterials) + && !x.Category.Equals(ETechTypeCategory.Fish) + && !x.Category.Equals(ETechTypeCategory.Seeds) + && !x.Category.Equals(ETechTypeCategory.Eggs))) { toBeRandomised.Add(e); } @@ -109,7 +108,8 @@ internal void RandomSmart(FragmentLogic fragmentLogic) newDepth = CalculateReachableDepth(_tree, unlockedProgressionItems, _config.iDepthSearchTime); if (SpoilerLog.s_progression.Count > 0) { - KeyValuePair valuePair = new KeyValuePair(SpoilerLog.s_progression[SpoilerLog.s_progression.Count - 1].Key, newDepth); + KeyValuePair valuePair = new KeyValuePair + (SpoilerLog.s_progression[SpoilerLog.s_progression.Count - 1].Key, newDepth); SpoilerLog.s_progression.RemoveAt(SpoilerLog.s_progression.Count - 1); SpoilerLog.s_progression.Add(valuePair); } @@ -123,14 +123,13 @@ internal void RandomSmart(FragmentLogic fragmentLogic) UpdateReachableMaterials(reachableDepth); } - LogicEntity nextEntity = null; newProgressionItem = false; bool isPriority = false; // Make sure the list of absolutely essential items is done first, // for each depth level. This guarantees certain recipes are done // by a certain depth, e.g. waterparks by 500m. - nextEntity = GetPriorityEntity(reachableDepth); + LogicEntity nextEntity = GetPriorityEntity(reachableDepth); // Once all essentials and electives are done, grab a random entity // which has not yet been randomised. @@ -173,10 +172,14 @@ internal void RandomSmart(FragmentLogic fragmentLogic) LogHandler.Info("Finished randomising within " + circuitbreaker + " cycles!"); } - - // Handle everything related to actually randomising the recipe itself, - // and ensure all special cases are covered. - // Returns true if a new progression item was unlocked. + + /// + /// Handle everything related to actually randomising the recipe itself, and ensure all special cases are covered. + /// + /// The recipe to randomise. + /// The available materials to use as potential ingredients. + /// The currently reachable depth. + /// True if a new progression item was unlocked, false otherwise. private bool RandomiseRecipeEntity(LogicEntity entity, Dictionary unlockedProgressionItems, int reachableDepth) { bool newProgressionItem = false; @@ -216,8 +219,14 @@ private bool RandomiseRecipeEntity(LogicEntity entity, Dictionary + /// Randomise (shuffle) the blueprints found inside databoxes. + /// + /// The master dictionary. + /// A list of all databoxes. + /// The list of newly randomised databoxes. + [NotNull] internal List RandomiseDataboxes(EntitySerializer masterDict, List databoxes) { masterDict.Databoxes = new Dictionary(); @@ -234,17 +243,24 @@ internal List RandomiseDataboxes(EntitySerializer masterDict, List x.Coordinates.Equals(toBeRandomised[next])); - randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); + randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, + replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); masterDict.Databoxes.Add(new RandomiserVector(toBeRandomised[next]), originalBox.TechType); - LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); + LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " + + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); toBeRandomised.RemoveAt(next); } masterDict.isDataboxRandomised = true; return randomDataboxes; } - - // Grab an essential or elective entity for the currently reachable depth. + + /// + /// Get an essential or elective entity for the currently reachable depth, prioritising essential ones. + /// + /// The maximum depth to consider. + /// A LogicEntity, or null if all have been processed already. + [CanBeNull] private LogicEntity GetPriorityEntity(int depth) { List essentialItems = _tree.GetEssentialItems(depth); @@ -287,19 +303,17 @@ private LogicEntity GetPriorityEntity(int depth) return entity; } - - // Add all reachable materials to the list, taking into account depth and - // any config options. + + /// + /// Add all reachable materials to the list, taking into account depth and any config options. + /// + /// The maximum depth to consider. internal void UpdateReachableMaterials(int depth) { if (_masterDict.ContainsKnife()) - { _materials.AddReachable(ETechTypeCategory.RawMaterials, depth); - } else - { _materials.AddReachableWithPrereqs(ETechTypeCategory.RawMaterials, depth, TechType.Knife, true); - } if (_config.bUseFish) _materials.AddReachable(ETechTypeCategory.Fish, depth); @@ -308,11 +322,17 @@ internal void UpdateReachableMaterials(int depth) if (_config.bUseEggs && _masterDict.RecipeDict.ContainsKey(TechType.BaseWaterPark)) _materials.AddReachable(ETechTypeCategory.Eggs, depth); } - - // This function calculates the maximum reachable depth based on - // what vehicles the player has attained, as well as how much - // further they can go "on foot" - // TODO: Simplify this. + + /// + /// This function calculates the maximum reachable depth based on what vehicles the player has attained, as well + /// as how much further they can go "on foot" + /// TODO: Simplify this. + /// + /// The progression tree. + /// A list of all currently reachable items relevant for progression. + /// The minimum time that it must be possible to spend at the reachable depth before + /// resurfacing. + /// The reachable depth. internal static int CalculateReachableDepth(ProgressionTree tree, Dictionary progressionItems, int depthTime = 15) { double swimmingSpeed = 4.7; // Assuming player is holding a tool. @@ -322,14 +342,13 @@ internal static int CalculateReachableDepth(ProgressionTree tree, Dictionary playerDepthRaw ? depth : playerDepthRaw; } - // The vehicle depth and whether or not the player has a rebreather - // can modify the raw achievable diving depth. + // The vehicle depth and whether or not the player has a rebreather can modify the raw achievable diving depth. if (progressionItems.ContainsKey(TechType.Rebreather)) { totalDepth = vehicleDepth + (playerDepthRaw > maxSoloDepth ? maxSoloDepth : playerDepthRaw); @@ -456,6 +474,12 @@ internal static int CalculateReachableDepth(ProgressionTree tree, Dictionary + /// Check whether all TechTypes given in the array are present in the given dictionary. + /// + /// The dictionary to check. + /// The array of TechTypes. + /// True if all TechTypes are present in the dictionary, false otherwise. private static bool CheckDictForAllTechTypes(Dictionary dict, TechType[] types) { bool allItemsPresent = true; @@ -469,18 +493,28 @@ private static bool CheckDictForAllTechTypes(Dictionary dict, Te return allItemsPresent; } - - // Check if this recipe fulfills all conditions to have its blueprint be unlocked + + /// + /// Check if this recipe fulfills all conditions to have its blueprint be unlocked. + /// + /// The master dictionary. + /// The list of all databoxes. + /// The recipe to check. + /// The maximum depth to consider. + /// True if the recipe fulfills all conditions, false otherwise. private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List databoxes, LogicEntity entity, int depth) { bool fulfilled = true; - if (entity.Blueprint == null || (entity.Blueprint.UnlockConditions == null && entity.Blueprint.UnlockDepth == 0)) + if (entity.Blueprint == null || (entity.Blueprint.UnlockConditions == null + && entity.Blueprint.UnlockDepth == 0)) return true; // If the databox was randomised, do work to account for new locations. // Cyclops hull modules need extra special treatment. - if (entity.Blueprint.NeedsDatabox && databoxes != null && databoxes.Count > 0 && !entity.TechType.Equals(TechType.CyclopsHullModule2) && !entity.TechType.Equals(TechType.CyclopsHullModule3)) + if (entity.Blueprint.NeedsDatabox && databoxes?.Count > 0 + && !entity.TechType.Equals(TechType.CyclopsHullModule2) + && !entity.TechType.Equals(TechType.CyclopsHullModule3)) { int total = 0; int number = 0; @@ -503,8 +537,10 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List entity.Blueprint.UnlockDepth = total / number; if (entity.TechType.Equals(TechType.CyclopsHullModule1)) { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)).Blueprint.UnlockDepth = total / number; - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)).Blueprint.UnlockDepth = total / number; + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockDepth = total / number; + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockDepth = total / number; } // If more than half of all locations of this databox require a @@ -514,8 +550,10 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List entity.Blueprint.UnlockConditions.Add(TechType.LaserCutter); if (entity.TechType.Equals(TechType.CyclopsHullModule1)) { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)).Blueprint.UnlockConditions.Add(TechType.LaserCutter); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)).Blueprint.UnlockConditions.Add(TechType.LaserCutter); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockConditions.Add(TechType.LaserCutter); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockConditions.Add(TechType.LaserCutter); } } @@ -524,8 +562,10 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List entity.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); if (entity.TechType.Equals(TechType.CyclopsHullModule1)) { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)).Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)).Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); } } } @@ -546,7 +586,8 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List if (!_config.bUseSeeds && conditionEntity.Category.Equals(ETechTypeCategory.Seeds)) continue; - fulfilled &= (masterDict.RecipeDict.ContainsKey(condition) || _materials.GetReachable().Exists(x => x.TechType.Equals(condition))); + fulfilled &= (masterDict.RecipeDict.ContainsKey(condition) + || _materials.GetReachable().Exists(x => x.TechType.Equals(condition))); if (!fulfilled) return false; @@ -559,7 +600,8 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List { if (!_masterDict.SpawnDataDict.ContainsKey(fragment)) { - LogHandler.Debug("[B] Entity " + entity.TechType.AsString() + " missing fragment " + fragment.AsString()); + LogHandler.Debug("[B] Entity " + entity.TechType.AsString() + " missing fragment " + + fragment.AsString()); return false; } } @@ -572,6 +614,12 @@ private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List return fulfilled; } + /// + /// Check whether all prerequisites for this recipe have already been randomised. + /// + /// The master dictionary. + /// The recipe to check. + /// True if all conditions are fulfilled, false otherwise. private static bool CheckRecipeForPrerequisites(EntitySerializer masterDict, LogicEntity entity) { bool fulfilled = true; @@ -594,6 +642,12 @@ private static bool CheckRecipeForPrerequisites(EntitySerializer masterDict, Log return fulfilled; } + /// + /// Check wether any of the given TechTypes have already been randomised. + /// + /// The master dictionary. + /// The TechTypes. + /// True if any TechType in the array has been randomised, false otherwise. private bool ContainsAny(EntitySerializer masterDict, TechType[] types) { foreach (TechType type in types) @@ -604,6 +658,9 @@ private bool ContainsAny(EntitySerializer masterDict, TechType[] types) return false; } + /// + /// Get a random element from a list. + /// private T GetRandom(List list) { if (list == null || list.Count == 0) @@ -613,9 +670,11 @@ private T GetRandom(List list) return list[_random.Next(0, list.Count)]; } - - // Grab a collection of all keys in the dictionary, then use them to - // apply every single one as a recipe change in the game. + + /// + /// Apply all recipe changes stored in the masterDict to the game. + /// + /// The master dictionary. internal static void ApplyMasterDict(EntitySerializer masterDict) { Dictionary.KeyCollection keys = masterDict.RecipeDict.Keys; @@ -625,13 +684,15 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) CraftDataHandler.SetTechData(key, masterDict.RecipeDict[key]); } - // TODO Once scrap metal is working, un-commenting this will apply the - // change on every startup. + // TODO Once scrap metal is working, un-commenting this will apply the change on every startup. //ChangeScrapMetalResult(masterDict.DictionaryInstance[TechType.Titanium]); } - - // This function handles applying a randomised recipe to the in-game - // craft data, and stores a copy in the master dictionary. + + /// + /// Apply a randomised recipe to the in-game craft data, and store a copy in the master dictionary. + /// + /// The master dictionary. + /// The recipe to change. internal static void ApplyRandomisedRecipe(EntitySerializer masterDict, Recipe recipe) { CraftDataHandler.SetTechData(recipe.TechType, recipe); diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index ff29fd7..9fdd30f 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -7,11 +7,10 @@ namespace SubnauticaRandomiser public class RandomiserConfig : ConfigFile { private DateTime _timeButtonPressed = new DateTime(); - private readonly int _confirmInterval = 5; + private const int _confirmInterval = 5; - // Every public variable listed here will end up in the config file - // Additionally, adding the relevant Attributes will also make them - // show up in the in-game options menu + // Every public variable listed here will end up in the config file. + // Additionally, adding the relevant Attributes will also make them show up in the in-game options menu. public int iSeed = 0; [Choice("Mode", "Balanced", "Chaotic")] @@ -59,14 +58,12 @@ public class RandomiserConfig : ConfigFile [Button("Randomise with new seed")] public void NewRandomNewSeed() { - // Re-randomising everything is a serious request, and it should not - // happen accidentally. This here ensures the button is pressed twice - // within a certain timeframe before actually randomising. + // Re-randomising everything is a serious request, and it should not happen accidentally. This ensures + // the button is pressed twice within a certain timeframe before actually randomising. if (EnsureButtonTime()) { - Random ran = new Random(); - iSeed = ran.Next(); - ran = new Random(iSeed); + Random random = new Random(); + iSeed = random.Next(); LogHandler.MainMenuMessage("Changed seed to " + iSeed); LogHandler.MainMenuMessage("Randomising..."); InitMod.Randomise(); @@ -133,11 +130,12 @@ public void SanitiseConfigValues() fFragmentSpawnChance = ConfigDefaults.fFragmentSpawnChance; } + /// + /// Ensure the button is pressed twice within a certain timeframe before actually randomising. + /// + /// True if the button was pressed for the second time, false if not. private bool EnsureButtonTime() { - // Re-randomising everything is a serious request, and it should not - // happen accidentally. This here ensures the button is pressed twice - // within a certain timeframe before actually randomising. if (DateTime.UtcNow.Subtract(_timeButtonPressed).TotalSeconds < _confirmInterval) { _timeButtonPressed = DateTime.MinValue; @@ -149,31 +147,31 @@ private bool EnsureButtonTime() } } - // Mostly used so that the spoiler log can tell which settings to include. + /// Mostly used so that the spoiler log can tell which settings to include. internal static class ConfigDefaults { - internal static readonly int iRandomiserMode = 0; - internal static readonly bool bUseFish = true; - internal static readonly bool bUseEggs = false; - internal static readonly bool bUseSeeds = true; - internal static readonly bool bRandomiseDataboxes = true; - internal static readonly bool bRandomiseFragments = true; - internal static readonly bool bVanillaUpgradeChains = false; - internal static readonly bool bDoBaseTheming = false; - internal static readonly int iEquipmentAsIngredients = 1; - internal static readonly int iToolsAsIngredients = 1; - internal static readonly int iUpgradesAsIngredients = 1; - internal static readonly int iMaxAmountPerIngredient = 5; - internal static readonly int iMaxIngredientsPerRecipe = 7; - internal static readonly int iMaxBiomesPerFragment = 3; + internal const int iRandomiserMode = 0; + internal const bool bUseFish = true; + internal const bool bUseEggs = false; + internal const bool bUseSeeds = true; + internal const bool bRandomiseDataboxes = true; + internal const bool bRandomiseFragments = true; + internal const bool bVanillaUpgradeChains = false; + internal const bool bDoBaseTheming = false; + internal const int iEquipmentAsIngredients = 1; + internal const int iToolsAsIngredients = 1; + internal const int iUpgradesAsIngredients = 1; + internal const int iMaxAmountPerIngredient = 5; + internal const int iMaxIngredientsPerRecipe = 7; + internal const int iMaxBiomesPerFragment = 3; // Advanced setting defaults start here. - internal static readonly int iDepthSearchTime = 15; - internal static readonly int iMaxBasicOutpostSize = 24; - internal static readonly int iMaxEggsAsSingleIngredient = 1; - internal static readonly int iMaxInventorySizePerRecipe = 24; - internal static readonly double dFuzziness = 0.2; - internal static readonly double dIngredientRatio = 0.45; - internal static readonly float fFragmentSpawnChance = 0.1f; + internal const int iDepthSearchTime = 15; + internal const int iMaxBasicOutpostSize = 24; + internal const int iMaxEggsAsSingleIngredient = 1; + internal const int iMaxInventorySizePerRecipe = 24; + internal const double dFuzziness = 0.2; + internal const double dIngredientRatio = 0.45; + internal const float fFragmentSpawnChance = 0.1f; } } diff --git a/SubnauticaRandomiser/RandomiserObjects/Biome.cs b/SubnauticaRandomiser/RandomiserObjects/Biome.cs index 3582d8e..368e25f 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Biome.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Biome.cs @@ -1,6 +1,9 @@ -using System; -namespace SubnauticaRandomiser.RandomiserObjects +namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A class representing a single Biome as the game handles it, along with detailed info on spawn slots. + /// These individual biomes can get very detailed, such as BloodKelp_Floor, BloodKelp_CaveWall, etc. + /// public class Biome { public readonly int CreatureSlots; diff --git a/SubnauticaRandomiser/RandomiserObjects/BiomeCollection.cs b/SubnauticaRandomiser/RandomiserObjects/BiomeCollection.cs index 61572ca..88b6127 100644 --- a/SubnauticaRandomiser/RandomiserObjects/BiomeCollection.cs +++ b/SubnauticaRandomiser/RandomiserObjects/BiomeCollection.cs @@ -1,8 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A class representing a biome as the average player might know it. E.g. BloodKelp being made up of 14 smaller, + /// more detailed biomes. + /// public class BiomeCollection { public List BiomeList = new List(); @@ -16,10 +19,12 @@ public BiomeCollection(EBiomeType biomeType) BiomeType = biomeType; AverageDepth = biomeType.GetAccessibleDepth(); } - - // Calculate the average depth of the biomes contained in this collection. - // Intended as a more fine-tuneable way of depth control, but largely - // unused due to the hardcoded depths of EBiomeType. + + /// + /// Calculate the average depth of the biomes contained in this collection. Intended as a more fine-tuneable + /// way of depth control, but largely unused due to the hardcoded depths of EBiomeType. + /// + /// The average depth. public int CalculateAverageDepth() { if (BiomeList is null || BiomeList.Count == 0) @@ -37,8 +42,12 @@ public int CalculateAverageDepth() AverageDepth = total / BiomeList.Count; return AverageDepth; } - - // Ensure that no duplicates can be added to the collection. + + /// + /// Add a biome to the collection if it does not already exist. + /// + /// The biome to add. + /// True if successful, false if the collection already contained the biome. public bool Add(Biome biome) { if (BiomeList.Contains(biome)) diff --git a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs index fab9d72..61853e6 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs @@ -3,6 +3,9 @@ namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A class representing the knowledge required for an entity to appear in the player's PDA. + /// [Serializable] public class Blueprint { diff --git a/SubnauticaRandomiser/RandomiserObjects/Databox.cs b/SubnauticaRandomiser/RandomiserObjects/Databox.cs index b9118c2..c0eaeb8 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Databox.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Databox.cs @@ -3,6 +3,9 @@ namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A databox containing a blueprint it unlocks. + /// [Serializable] public class Databox { diff --git a/SubnauticaRandomiser/RandomiserObjects/EBiomeType.cs b/SubnauticaRandomiser/RandomiserObjects/EBiomeType.cs index 9bd0151..a87f6c3 100644 --- a/SubnauticaRandomiser/RandomiserObjects/EBiomeType.cs +++ b/SubnauticaRandomiser/RandomiserObjects/EBiomeType.cs @@ -1,5 +1,4 @@ -using System; -namespace SubnauticaRandomiser.RandomiserObjects +namespace SubnauticaRandomiser.RandomiserObjects { public enum EBiomeType { @@ -38,8 +37,11 @@ public enum EBiomeType public static class BiomeTypeExtensions { - // Hardcoded, rough approximations of the depth at which the biome - // in general becomes broadly accessible, i.e. comfortably explorable. + /// + /// Returns a hardcoded, rough approximation of the depth at which the biome in general becomes broadly + /// accessible, i.e. comfortably explorable. + /// + /// The accessible depth. public static int GetAccessibleDepth(this EBiomeType biomeType) { switch (biomeType) diff --git a/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs b/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs index 2a7d041..3d5e7f6 100644 --- a/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs +++ b/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs @@ -1,5 +1,4 @@ -using System; -namespace SubnauticaRandomiser.RandomiserObjects +namespace SubnauticaRandomiser.RandomiserObjects { public enum EProgressionNode { diff --git a/SubnauticaRandomiser/RandomiserObjects/ETechTypeCategory.cs b/SubnauticaRandomiser/RandomiserObjects/ETechTypeCategory.cs index 93795fa..95b3328 100644 --- a/SubnauticaRandomiser/RandomiserObjects/ETechTypeCategory.cs +++ b/SubnauticaRandomiser/RandomiserObjects/ETechTypeCategory.cs @@ -1,5 +1,4 @@ -using System; -namespace SubnauticaRandomiser.RandomiserObjects +namespace SubnauticaRandomiser.RandomiserObjects { public enum ETechTypeCategory { @@ -31,6 +30,10 @@ public enum ETechTypeCategory public static class TTCategoryExtensions { + /// + /// Checks whether this category is made up of base pieces. + /// + /// True if the category belongs to base pieces, falls otherwise. public static bool IsBasePiece(this ETechTypeCategory category) { switch (category) @@ -46,6 +49,10 @@ public static bool IsBasePiece(this ETechTypeCategory category) } } + /// + /// Checks whether this category is capable of showing up in the PDA as a craftable item. + /// + /// True if the category can be craftable, false otherwise. public static bool CanHaveRecipe(this ETechTypeCategory category) { switch (category) diff --git a/SubnauticaRandomiser/RandomiserObjects/EWreckage.cs b/SubnauticaRandomiser/RandomiserObjects/EWreckage.cs index 3d3cbe1..417dfa6 100644 --- a/SubnauticaRandomiser/RandomiserObjects/EWreckage.cs +++ b/SubnauticaRandomiser/RandomiserObjects/EWreckage.cs @@ -1,5 +1,4 @@ -using System; -namespace SubnauticaRandomiser.RandomiserObjects +namespace SubnauticaRandomiser.RandomiserObjects { public enum EWreckage { diff --git a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs index da073fe..07fd308 100644 --- a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs +++ b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs @@ -1,8 +1,12 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// This class acts an abstract representation of anything that could or should be considered while randomising. + /// The Randomiser will pass over every one of these entities and only consider itself done once each of them has + /// the InLogic flag - meaning that it is considered accessible within the game. + /// public class LogicEntity { public readonly TechType TechType; @@ -22,14 +26,6 @@ public class LogicEntity public bool HasRecipe { get { return !(Recipe is null); } } public bool HasSpawnData { get { return !(SpawnData is null); } } - /* - * This class acts an abstract representation of anything that could or - * should be considered while randomising. - * The Randomiser will pass over every one of these entities and only - * consider itself done once each of them has the InLogic flag - meaning - * that it is considered accessible within the game. - */ - public LogicEntity(TechType type, ETechTypeCategory category, Blueprint blueprint = null, Recipe recipe = null, SpawnData spawnData = null, List prerequisites = null, bool inLogic = false, int value = 0) { TechType = type; @@ -45,9 +41,12 @@ public LogicEntity(TechType type, ETechTypeCategory category, Blueprint blueprin MaxUsesPerGame = 0; _usedInRecipes = 0; } - - // Base pieces and vehicles obviously cannot act as ingredients for - // recipes, so this function detects and filters them. + + /// + /// Check whether this entity can act as an ingredient in crafting. Base pieces and vehicles are obviously + /// excluded. + /// + /// True if it can act as an ingredient, false if not. public bool CanFunctionAsIngredient() { ETechTypeCategory[] bad = { ETechTypeCategory.BaseBasePieces, @@ -69,8 +68,11 @@ public bool CanFunctionAsIngredient() return true; } - - // How big is this entity in the inventory? + + /// + /// Get the number of slots this entity occupies in an inventory. + /// + /// The number of slots, or 0 if the entity cannot exist in the inventory. public int GetItemSize() { int size = 0; @@ -79,8 +81,11 @@ public int GetItemSize() return size; } - - // Can this entity still be used in the recipe for a different entity? + + /// + /// Checks whether this entity can still be used in the recipe for a different entity, + /// + /// True if it can be used, false if not. public bool HasUsesLeft() { if (MaxUsesPerGame <= 0) diff --git a/SubnauticaRandomiser/RandomiserObjects/ProgressionPath.cs b/SubnauticaRandomiser/RandomiserObjects/ProgressionPath.cs index 5fc2ccb..436f3d6 100644 --- a/SubnauticaRandomiser/RandomiserObjects/ProgressionPath.cs +++ b/SubnauticaRandomiser/RandomiserObjects/ProgressionPath.cs @@ -1,8 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// This class represents all the ways a progression roadblock (node) can be surpassed. + /// public class ProgressionPath { public EProgressionNode Node; diff --git a/SubnauticaRandomiser/RandomiserObjects/RandomiserBiomeData.cs b/SubnauticaRandomiser/RandomiserObjects/RandomiserBiomeData.cs index 2bb13dc..1c298cf 100644 --- a/SubnauticaRandomiser/RandomiserObjects/RandomiserBiomeData.cs +++ b/SubnauticaRandomiser/RandomiserObjects/RandomiserBiomeData.cs @@ -3,8 +3,9 @@ namespace SubnauticaRandomiser.RandomiserObjects { - // This is a wrapper class around the original BiomeData to make it serializable. - + /// + /// A wrapper for the game's BiomeData class to make it serializable. + /// [Serializable] public class RandomiserBiomeData { @@ -18,8 +19,11 @@ public RandomiserBiomeData() Count = 0; Probability = 0f; } - - // Get the non-serializable equivalent. + + /// + /// Get the non-serializable in-game equivalent of this class. + /// + /// This class, converted to the game's equivalent. public BiomeData GetBaseBiomeData() { BiomeData data = new BiomeData diff --git a/SubnauticaRandomiser/RandomiserObjects/RandomiserIngredient.cs b/SubnauticaRandomiser/RandomiserObjects/RandomiserIngredient.cs index 0d51fa9..25cb96f 100644 --- a/SubnauticaRandomiser/RandomiserObjects/RandomiserIngredient.cs +++ b/SubnauticaRandomiser/RandomiserObjects/RandomiserIngredient.cs @@ -1,6 +1,9 @@ using System; namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A wrapper for the game's Ingredient class to make it serializable. + /// [Serializable] public class RandomiserIngredient : IIngredient { diff --git a/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs b/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs index 605fa4e..7e9d9a4 100644 --- a/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs +++ b/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs @@ -3,6 +3,9 @@ namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A wrapper for Unity's Vector3 class to make it serializable. + /// [Serializable] public class RandomiserVector { @@ -23,6 +26,11 @@ public RandomiserVector(Vector3 vector) z = (int)vector.z; } + /// + /// Check whether this vector is the same as an in-game Unity vector with negligible differences. + /// + /// The Unity vector to compare against. + /// True if they're equal, false if not. public bool EqualsUnityVector(Vector3 vector) { if (Math.Abs(x - vector.x) < 1 && Math.Abs(y - vector.y) < 1 && Math.Abs(z - vector.z) < 1) diff --git a/SubnauticaRandomiser/RandomiserObjects/Recipe.cs b/SubnauticaRandomiser/RandomiserObjects/Recipe.cs index 184b5a1..708c2df 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Recipe.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Recipe.cs @@ -5,6 +5,9 @@ namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A wrapper for the game's TechData class to make it serializable. + /// [Serializable] public class Recipe : ITechData { diff --git a/SubnauticaRandomiser/RandomiserObjects/SpawnData.cs b/SubnauticaRandomiser/RandomiserObjects/SpawnData.cs index 12382a0..332a040 100644 --- a/SubnauticaRandomiser/RandomiserObjects/SpawnData.cs +++ b/SubnauticaRandomiser/RandomiserObjects/SpawnData.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; -using SMLHelper.V2.Handlers; +using JetBrains.Annotations; using static LootDistributionData; namespace SubnauticaRandomiser.RandomiserObjects { + /// + /// A wrapper for the game's SpawnData class to make it serializable. + /// [Serializable] public class SpawnData { @@ -19,6 +22,10 @@ public SpawnData(string classId, int depth = 0) BiomeDataList = new List(); } + /// + /// Add BiomeData to the SpawnData. Will throw out any duplicates. + /// + /// The data to add. public void AddBiomeData(RandomiserBiomeData bd) { if (BiomeDataList.Find(x => x.Biome.Equals(bd.Biome)) != null) @@ -29,6 +36,11 @@ public void AddBiomeData(RandomiserBiomeData bd) BiomeDataList.Add(bd); } + /// + /// Get a list of this object's BiomeData converted to the game's base form. + /// + /// A list of BiomeData. + [NotNull] public List GetBaseBiomeData() { List list = new List(); diff --git a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs index b999496..620285a 100644 --- a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs +++ b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs @@ -1,31 +1,36 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading.Tasks; - -namespace SubnauticaRandomiser.RandomiserObjects -{ - public class SpoilerLog - { - internal static readonly string s_fileName = "spoilerlog.txt"; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace SubnauticaRandomiser.RandomiserObjects +{ + /// + /// Handles everything related to the spoiler log generated during randomisation. + /// + public class SpoilerLog + { + internal const string _FileName = "spoilerlog.txt"; private RandomiserConfig _config; internal static List> s_progression = new List>(); - private List _basicOptions; + private List _basicOptions; private string[] _contentHeader; private string[] _contentBasics; private string[] _contentAdvanced; private string[] _contentDataboxes; - internal SpoilerLog(RandomiserConfig config) - { - _config = config; + internal SpoilerLog(RandomiserConfig config) + { + _config = config; } - - // Prepare most of the fluff of the log. - private void PrepareStrings() - { + + /// + /// Prepare the more basic aspects of the log. + /// + private void PrepareStrings() + { _basicOptions = new List() { "iSeed", @@ -39,16 +44,16 @@ private void PrepareStrings() "iMaxIngredientsPerRecipe", "iMaxAmountPerIngredient", "bMaxBiomesPerFragments" }; - _contentHeader = new string[] - { + _contentHeader = new [] + { "*************************************************", "***** SUBNAUTICA RANDOMISER SPOILER LOG *****", "*************************************************", "", "Generated on " + DateTime.Now + " with " + InitMod.s_versionDict[InitMod.s_expectedSaveVersion] }; - _contentBasics = new string[] - { + _contentBasics = new [] + { "", "", "///// Basic Information /////", @@ -64,64 +69,68 @@ private void PrepareStrings() "Max Biomes per Fragment: " + _config.iMaxBiomesPerFragment, "" }; - _contentAdvanced = new string[] - { + _contentAdvanced = new [] + { "", "", "///// Depth Progression Path /////" }; - _contentDataboxes = new string[] - { + _contentDataboxes = new [] + { "", "", "///// Databox Locations /////" - }; + }; } - - // Add advanced settings to the spoiler log, but only if they have been - // modified. - private string[] PrepareAdvancedSettings() - { + + /// + /// Add advanced settings to the spoiler log, but only if they have been modified. + /// + /// An array of modified settings. + private string[] PrepareAdvancedSettings() + { List preparedAdvSettings = new List(); FieldInfo[] defaultFieldInfoArray = typeof(ConfigDefaults).GetFields(BindingFlags.NonPublic | BindingFlags.Static); FieldInfo[] fieldInfoArray = typeof(RandomiserConfig).GetFields(BindingFlags.Public | BindingFlags.Instance); LogHandler.Debug("Number of fields in default, instance: " + defaultFieldInfoArray.Length + ", " + fieldInfoArray.Length); - foreach (FieldInfo defaultField in defaultFieldInfoArray) + foreach (FieldInfo defaultField in defaultFieldInfoArray) { - // Check whether this field is an advanced config option or not. + // Check whether this field is an advanced config option or not. if (_basicOptions.Contains(defaultField.Name)) continue; - foreach (FieldInfo field in fieldInfoArray) - { + foreach (FieldInfo field in fieldInfoArray) + { if (!field.Name.Equals(defaultField.Name)) continue; var value = field.GetValue(_config); - // If the value of a config field does not correspond to its - // default value, the user must have modified it. Add it to - // the list in that case. + // If the value of a config field does not correspond to its default value, the user must have + // modified it. Add it to the list in that case. if (!value.Equals(defaultField.GetValue(null))) preparedAdvSettings.Add(field.Name + ": " + value); - break; - } + break; + } } if (preparedAdvSettings.Count == 0) preparedAdvSettings.Add("No advanced settings were modified."); - return preparedAdvSettings.ToArray(); + return preparedAdvSettings.ToArray(); } - - // Grab the randomised boxes from masterDict, and sort them alphabetically. - private string[] PrepareDataboxes() + + /// + /// Grab the randomised boxes from masterDict, and sort them alphabetically. + /// + /// The prepared log entries. + private string[] PrepareDataboxes() { if (!InitMod.s_masterDict.isDataboxRandomised) - return new string[] { "Not randomised, all in vanilla locations." }; + return new [] { "Not randomised, all in vanilla locations." }; List preparedDataboxes = new List(); @@ -132,48 +141,49 @@ private string[] PrepareDataboxes() preparedDataboxes.Sort(); - return preparedDataboxes.ToArray(); + return preparedDataboxes.ToArray(); } - - // Compare the MD5 of the recipe CSV and try to see if it's still the same. - // Since this is done while parsing the CSV anyway, grab the value from there. - private string PrepareMD5() + + /// + /// Compare the MD5 of the recipe CSV and try to see if it's still the same. + /// Since this is done while parsing the CSV anyway, grab the value from there. + /// + /// The prepared log entry. + private string PrepareMD5() { - if (!InitMod.s_expectedRecipeMD5.Equals(CSVReader.s_recipeCSVMD5)) - { - return "recipeInformation.csv has been modified: " + CSVReader.s_recipeCSVMD5; - } - else - { - return "recipeInformation.csv is unmodified."; - } + if (!InitMod.s_expectedRecipeMD5.Equals(CSVReader.s_recipeCSVMD5)) + return "recipeInformation.csv has been modified: " + CSVReader.s_recipeCSVMD5; + + return "recipeInformation.csv is unmodified."; } - - // Make the data gathered during randomising a bit nicer for human eyes. - private string[] PrepareProgressionPath() + + /// + /// Prepare a human readable way to tell what must be crafted to reach greater depths. + /// + /// The prepared log entries. + private string[] PrepareProgressionPath() { List preparedProgressionPath = new List(); int lastDepth = 0; - - foreach (KeyValuePair pair in s_progression) + + foreach (KeyValuePair pair in s_progression) { - if (pair.Value > lastDepth) - { - preparedProgressionPath.Add("Craft " + pair.Key.AsString() + " to reach " + pair.Value + "m"); - } - else - { - preparedProgressionPath.Add("Unlocked " + pair.Key.AsString() + "."); - } - - lastDepth = pair.Value; + if (pair.Value > lastDepth) + preparedProgressionPath.Add("Craft " + pair.Key.AsString() + " to reach " + pair.Value + "m"); + else + preparedProgressionPath.Add("Unlocked " + pair.Key.AsString() + "."); + + lastDepth = pair.Value; } - return preparedProgressionPath.ToArray(); + return preparedProgressionPath.ToArray(); } - - internal async Task WriteLog() - { + + /// + /// Write the log to disk. + /// + internal async Task WriteLog() + { List lines = new List(); PrepareStrings(); @@ -187,26 +197,21 @@ internal async Task WriteLog() lines.AddRange(PrepareProgressionPath()); lines.AddRange(_contentDataboxes); lines.AddRange(PrepareDataboxes()); - - using (StreamWriter file = new StreamWriter(Path.Combine(InitMod.s_modDirectory, s_fileName))) + + using (StreamWriter file = new StreamWriter(Path.Combine(InitMod.s_modDirectory, _FileName))) { - await WriteTextToLog(file, lines.ToArray()); + await WriteTextToLog(file, lines.ToArray()); } - LogHandler.Info("Wrote spoiler log to disk."); + LogHandler.Info("Wrote spoiler log to disk."); } - private async Task WriteTextToLog(StreamWriter file, string text) - { - await file.WriteLineAsync(text); + private async Task WriteTextToLog(StreamWriter file, string[] text) + { + foreach (string line in text) + { + await file.WriteLineAsync(line); + } } - - private async Task WriteTextToLog(StreamWriter file, string[] text) - { - foreach (string line in text) - { - await file.WriteLineAsync(line); - } - } - } -} + } +} diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 2235b11..de88e5f 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -10,7 +10,7 @@ v4.7.2 true 8 - enable + warnings true From 4a9ea52a77a73e9f12e85572f1ee0bd1fa443ef2 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Thu, 4 Aug 2022 11:51:00 +0200 Subject: [PATCH 03/37] Consistent LF line endings. --- SubnauticaRandomiser/InitMod.cs | 460 +++++++++--------- SubnauticaRandomiser/Logic/Mode.cs | 440 ++++++++--------- .../RandomiserObjects/SpoilerLog.cs | 434 ++++++++--------- 3 files changed, 667 insertions(+), 667 deletions(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index a1deb24..13a425d 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -1,230 +1,230 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using HarmonyLib; -using QModManager.API.ModLoading; -using SMLHelper.V2.Handlers; -using SubnauticaRandomiser.Logic; -using SubnauticaRandomiser.RandomiserObjects; - -namespace SubnauticaRandomiser -{ - [QModCore] - public static class InitMod - { - internal static string s_modDirectory; - internal static RandomiserConfig s_config; - internal const string s_biomeFile = "biomeSlots.csv"; - internal const string s_recipeFile = "recipeInformation.csv"; - internal const string s_wreckageFile = "wreckInformation.csv"; - internal const string s_expectedRecipeMD5 = "4ab1b7a019037f76c0d508f1c2aee5f8"; - internal const int s_expectedSaveVersion = 3; - - internal static readonly Dictionary s_versionDict = new Dictionary { [1] = "v0.5.1", - [2] = "v0.6.1", - [3] = "v0.7.0"}; - - // The master list of everything that is modified by the mod. - internal static EntitySerializer s_masterDict = new EntitySerializer(); - private const bool _debug_forceRandomise = false; - - [QModPatch] - public static void Initialise() - { - LogHandler.Info("Randomiser starting up!"); - - // Register options menu - s_modDirectory = GetSubnauticaRandomiserDirectory(); - s_config = OptionsPanelHandler.Main.RegisterModOptions(); - LogHandler.Debug("Registered options menu."); - - // Ensure the user did not update into a save incompatibility, and - // abort if they did to preserve a prior version's state. - if (!CheckSaveCompatibility()) - return; - - // Try and restore a game state from disk. - try - { - s_masterDict = RestoreGameStateFromDisk(); - } - catch (Exception ex) - { - LogHandler.Warn("Could not load game state from disk."); - LogHandler.Warn(ex.Message); - } - - // Triple checking things here in case the save got corrupted somehow. - if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) - { - // Load recipe changes. - RandomiserLogic.ApplyMasterDict(s_masterDict); - - // Load fragment changes. - if (s_masterDict.SpawnDataDict?.Count > 0) - { - FragmentLogic.ApplyMasterDict(s_masterDict); - LogHandler.Info("Loaded fragment state."); - } - - // Load databox changes. - if (s_masterDict.isDataboxRandomised) - EnableHarmonyPatching(); - - LogHandler.Info("Successfully loaded game state from disk."); - } - else - { - if (_debug_forceRandomise) - LogHandler.Warn("Set to forcibly re-randomise recipes."); - else - LogHandler.Warn("Failed to load game state from disk: dictionary empty."); - - Randomise(); - if (s_masterDict?.isDataboxRandomised == true) - EnableHarmonyPatching(); - } - - LogHandler.Info("Finished loading."); - } - - /// - /// Randomise the game, discarding any earlier randomisation data. - /// - internal static void Randomise() - { - s_masterDict = new EntitySerializer(); - s_config.SanitiseConfigValues(); - s_config.iSaveVersion = s_expectedSaveVersion; - - // Attempt to read and parse the CSV with all biome information. - var completeBiomeList = CSVReader.ParseBiomeFile(s_biomeFile); - if (completeBiomeList is null) - { - LogHandler.Fatal("Failed to extract biome information from CSV, aborting."); - return; - } - - // Attempt to read and parse the CSV with all recipe information. - var completeMaterialsList = CSVReader.ParseRecipeFile(s_recipeFile); - if (completeMaterialsList is null) - { - LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); - return; - } - - // Attempt to read and parse the CSV with wreckages and databox info. - List databoxes; - databoxes = CSVReader.ParseWreckageFile(s_wreckageFile); - if (databoxes is null || databoxes.Count == 0) - LogHandler.Error("Failed to extract databox information from CSV."); - - // Create a new seed if the current one is just a default - Random random; - if (s_config.iSeed == 0) - { - random = new Random(); - s_config.iSeed = random.Next(); - } - random = new Random(s_config.iSeed); - - RandomiserLogic logic = new RandomiserLogic(random, s_masterDict, s_config, completeMaterialsList, databoxes); - FragmentLogic fragmentLogic = null; - if (s_config.bRandomiseFragments) - { - fragmentLogic = new FragmentLogic(s_config, s_masterDict, completeBiomeList, random); - fragmentLogic.Init(); - } - - logic.RandomSmart(fragmentLogic); - LogHandler.Info("Randomisation successful!"); - - SaveGameStateToDisk(); - - SpoilerLog spoiler = new SpoilerLog(s_config); - // This should run async, but we don't need the result here. It's a file. - _ = spoiler.WriteLog(); - } - - /// - /// Ensure the user did not update into a save incompatibility. - /// - private static bool CheckSaveCompatibility() - { - if (s_config.iSaveVersion == s_expectedSaveVersion) - return true; - - s_versionDict.TryGetValue(s_config.iSaveVersion, out string version); - if (string.IsNullOrEmpty(version)) - version = "unknown or corrupted."; - - LogHandler.MainMenuMessage("It seems you updated Subnautica Randomiser. This version is incompatible with your previous savegame."); - LogHandler.MainMenuMessage("The last supported version for your savegame is " + version); - LogHandler.MainMenuMessage("To protect your previous savegame, no changes to the game have been made."); - LogHandler.MainMenuMessage("If you wish to continue anyway, randomise again in the options menu or delete your config.json"); - return false; - } - - /// - /// Serialise the current randomisation state to disk. - /// - internal static void SaveGameStateToDisk() - { - if (s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) - { - string base64 = s_masterDict.ToBase64String(); - s_config.sBase64Seed = base64; - s_config.Save(); - LogHandler.Debug("Saved game state to disk!"); - } - else - { - LogHandler.Error("Could not save game state to disk: Dictionary empty."); - } - } - - /// - /// Attempt to deserialise a randomisation state from disk. - /// - /// The EntitySerializer as previously written to disk. - /// Raised if the game state is corrupted in some way. - internal static EntitySerializer RestoreGameStateFromDisk() - { - if (string.IsNullOrEmpty(s_config.sBase64Seed)) - { - throw new InvalidDataException("base64 seed is empty."); - } - - LogHandler.Debug("Trying to decode base64 string..."); - EntitySerializer dictionary = EntitySerializer.FromBase64String(s_config.sBase64Seed); - - if (dictionary?.RecipeDict is null || dictionary.RecipeDict.Count == 0) - { - throw new InvalidDataException("base64 seed is invalid; could not deserialize Dictionary."); - } - - return dictionary; - } - - /// - /// Get the installation directory of the mod. - /// - internal static string GetSubnauticaRandomiserDirectory() - { - return new FileInfo(Assembly.GetExecutingAssembly().Location).Directory?.FullName; - } - - /// - /// Enables all necessary harmony patches based on the randomisation state in s_masterDict. - /// - private static void EnableHarmonyPatching() - { - if (s_masterDict?.Databoxes?.Count > 0) - { - Harmony harmony = new Harmony("SubnauticaRandomiser"); - harmony.PatchAll(); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using HarmonyLib; +using QModManager.API.ModLoading; +using SMLHelper.V2.Handlers; +using SubnauticaRandomiser.Logic; +using SubnauticaRandomiser.RandomiserObjects; + +namespace SubnauticaRandomiser +{ + [QModCore] + public static class InitMod + { + internal static string s_modDirectory; + internal static RandomiserConfig s_config; + internal const string s_biomeFile = "biomeSlots.csv"; + internal const string s_recipeFile = "recipeInformation.csv"; + internal const string s_wreckageFile = "wreckInformation.csv"; + internal const string s_expectedRecipeMD5 = "4ab1b7a019037f76c0d508f1c2aee5f8"; + internal const int s_expectedSaveVersion = 3; + + internal static readonly Dictionary s_versionDict = new Dictionary { [1] = "v0.5.1", + [2] = "v0.6.1", + [3] = "v0.7.0"}; + + // The master list of everything that is modified by the mod. + internal static EntitySerializer s_masterDict = new EntitySerializer(); + private const bool _debug_forceRandomise = false; + + [QModPatch] + public static void Initialise() + { + LogHandler.Info("Randomiser starting up!"); + + // Register options menu + s_modDirectory = GetSubnauticaRandomiserDirectory(); + s_config = OptionsPanelHandler.Main.RegisterModOptions(); + LogHandler.Debug("Registered options menu."); + + // Ensure the user did not update into a save incompatibility, and + // abort if they did to preserve a prior version's state. + if (!CheckSaveCompatibility()) + return; + + // Try and restore a game state from disk. + try + { + s_masterDict = RestoreGameStateFromDisk(); + } + catch (Exception ex) + { + LogHandler.Warn("Could not load game state from disk."); + LogHandler.Warn(ex.Message); + } + + // Triple checking things here in case the save got corrupted somehow. + if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) + { + // Load recipe changes. + RandomiserLogic.ApplyMasterDict(s_masterDict); + + // Load fragment changes. + if (s_masterDict.SpawnDataDict?.Count > 0) + { + FragmentLogic.ApplyMasterDict(s_masterDict); + LogHandler.Info("Loaded fragment state."); + } + + // Load databox changes. + if (s_masterDict.isDataboxRandomised) + EnableHarmonyPatching(); + + LogHandler.Info("Successfully loaded game state from disk."); + } + else + { + if (_debug_forceRandomise) + LogHandler.Warn("Set to forcibly re-randomise recipes."); + else + LogHandler.Warn("Failed to load game state from disk: dictionary empty."); + + Randomise(); + if (s_masterDict?.isDataboxRandomised == true) + EnableHarmonyPatching(); + } + + LogHandler.Info("Finished loading."); + } + + /// + /// Randomise the game, discarding any earlier randomisation data. + /// + internal static void Randomise() + { + s_masterDict = new EntitySerializer(); + s_config.SanitiseConfigValues(); + s_config.iSaveVersion = s_expectedSaveVersion; + + // Attempt to read and parse the CSV with all biome information. + var completeBiomeList = CSVReader.ParseBiomeFile(s_biomeFile); + if (completeBiomeList is null) + { + LogHandler.Fatal("Failed to extract biome information from CSV, aborting."); + return; + } + + // Attempt to read and parse the CSV with all recipe information. + var completeMaterialsList = CSVReader.ParseRecipeFile(s_recipeFile); + if (completeMaterialsList is null) + { + LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); + return; + } + + // Attempt to read and parse the CSV with wreckages and databox info. + List databoxes; + databoxes = CSVReader.ParseWreckageFile(s_wreckageFile); + if (databoxes is null || databoxes.Count == 0) + LogHandler.Error("Failed to extract databox information from CSV."); + + // Create a new seed if the current one is just a default + Random random; + if (s_config.iSeed == 0) + { + random = new Random(); + s_config.iSeed = random.Next(); + } + random = new Random(s_config.iSeed); + + RandomiserLogic logic = new RandomiserLogic(random, s_masterDict, s_config, completeMaterialsList, databoxes); + FragmentLogic fragmentLogic = null; + if (s_config.bRandomiseFragments) + { + fragmentLogic = new FragmentLogic(s_config, s_masterDict, completeBiomeList, random); + fragmentLogic.Init(); + } + + logic.RandomSmart(fragmentLogic); + LogHandler.Info("Randomisation successful!"); + + SaveGameStateToDisk(); + + SpoilerLog spoiler = new SpoilerLog(s_config); + // This should run async, but we don't need the result here. It's a file. + _ = spoiler.WriteLog(); + } + + /// + /// Ensure the user did not update into a save incompatibility. + /// + private static bool CheckSaveCompatibility() + { + if (s_config.iSaveVersion == s_expectedSaveVersion) + return true; + + s_versionDict.TryGetValue(s_config.iSaveVersion, out string version); + if (string.IsNullOrEmpty(version)) + version = "unknown or corrupted."; + + LogHandler.MainMenuMessage("It seems you updated Subnautica Randomiser. This version is incompatible with your previous savegame."); + LogHandler.MainMenuMessage("The last supported version for your savegame is " + version); + LogHandler.MainMenuMessage("To protect your previous savegame, no changes to the game have been made."); + LogHandler.MainMenuMessage("If you wish to continue anyway, randomise again in the options menu or delete your config.json"); + return false; + } + + /// + /// Serialise the current randomisation state to disk. + /// + internal static void SaveGameStateToDisk() + { + if (s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) + { + string base64 = s_masterDict.ToBase64String(); + s_config.sBase64Seed = base64; + s_config.Save(); + LogHandler.Debug("Saved game state to disk!"); + } + else + { + LogHandler.Error("Could not save game state to disk: Dictionary empty."); + } + } + + /// + /// Attempt to deserialise a randomisation state from disk. + /// + /// The EntitySerializer as previously written to disk. + /// Raised if the game state is corrupted in some way. + internal static EntitySerializer RestoreGameStateFromDisk() + { + if (string.IsNullOrEmpty(s_config.sBase64Seed)) + { + throw new InvalidDataException("base64 seed is empty."); + } + + LogHandler.Debug("Trying to decode base64 string..."); + EntitySerializer dictionary = EntitySerializer.FromBase64String(s_config.sBase64Seed); + + if (dictionary?.RecipeDict is null || dictionary.RecipeDict.Count == 0) + { + throw new InvalidDataException("base64 seed is invalid; could not deserialize Dictionary."); + } + + return dictionary; + } + + /// + /// Get the installation directory of the mod. + /// + internal static string GetSubnauticaRandomiserDirectory() + { + return new FileInfo(Assembly.GetExecutingAssembly().Location).Directory?.FullName; + } + + /// + /// Enables all necessary harmony patches based on the randomisation state in s_masterDict. + /// + private static void EnableHarmonyPatching() + { + if (s_masterDict?.Databoxes?.Count > 0) + { + Harmony harmony = new Harmony("SubnauticaRandomiser"); + harmony.PatchAll(); + } + } + } +} diff --git a/SubnauticaRandomiser/Logic/Mode.cs b/SubnauticaRandomiser/Logic/Mode.cs index 65af099..2cb8004 100644 --- a/SubnauticaRandomiser/Logic/Mode.cs +++ b/SubnauticaRandomiser/Logic/Mode.cs @@ -1,220 +1,220 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using SMLHelper.V2.Crafting; -using SMLHelper.V2.Handlers; -using SubnauticaRandomiser.RandomiserObjects; - -namespace SubnauticaRandomiser.Logic -{ - internal abstract class Mode - { - protected RandomiserConfig _config; - protected Materials _materials; - protected ProgressionTree _tree; - protected Random _random; - protected List _ingredients = new List(); - protected List _blacklist = new List(); - protected LogicEntity _baseTheme; - - protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) - { - _config = config; - _materials = materials; - _tree = tree; - _random = random; - - _baseTheme = ChooseBaseTheme(100); - LogHandler.Debug("Chosen " + _baseTheme.TechType.AsString() + " as base theme."); - //InitMod.s_masterDict.DictionaryInstance.Add(TechType.Titanium, _baseTheme.GetSerializableRecipe()); - //ChangeScrapMetalResult(_baseTheme); - } - - internal abstract LogicEntity RandomiseIngredients(LogicEntity entity); - - /// - /// Add an ingredient to the list of ingredients used to form a recipe, but ensure its MaxUses field is - /// respected. - /// - /// The entity to add. - /// The number of uses to consume. - protected void AddIngredientWithMaxUsesCheck(LogicEntity entity, int amount) - { - // Ensure that limited ingredients are not overused. Particularly - // intended for cuddlefish. - int remainder = entity.MaxUsesPerGame - entity._usedInRecipes; - if (entity.MaxUsesPerGame != 0 && remainder > 0 && remainder < amount) - amount = remainder; - - _ingredients.Add(new RandomiserIngredient(entity.TechType, amount)); - entity._usedInRecipes++; - - if (!entity.HasUsesLeft()) - { - _materials.GetReachable().Remove(entity); - LogHandler.Debug("! Removing " + entity.TechType.AsString() + " from materials list due to " + - "max uses reached: " + entity._usedInRecipes); - } - } - - /// - /// Get a random entity from a list, ensuring that it is not part of a given blacklist. - /// TODO: Install safeguards to prevent infinite loops. - /// - /// The list to get a random element from. - /// The blacklist of forbidden elements to not ever consider. - /// A random, non-blacklisted element from the list. - /// Raised if the list is null or empty. - [NotNull] - protected LogicEntity GetRandom(List list, List blacklist = null) - { - if (list == null || list.Count == 0) - throw new InvalidOperationException("Failed to get valid entity from materials list: list is null or empty."); - - LogicEntity randomEntity = null; - while (true) - { - randomEntity = list[_random.Next(0, list.Count)]; - - if (blacklist != null && blacklist.Count > 0) - { - if (blacklist.Contains(randomEntity.Category)) - continue; - } - break; - } - - return randomEntity; - } - - /// - /// If base theming is enabled and the given entity is a base piece, return the base theming ingredient. - /// - /// The entity to check. - /// A LogicEntity if the passed entity is a base piece, null otherwise. - [CanBeNull] - protected LogicEntity CheckForBaseTheming(LogicEntity entity) - { - if (_config.bDoBaseTheming && _baseTheme != null && entity.Category.Equals(ETechTypeCategory.BaseBasePieces)) - return _baseTheme; - - return null; - } - - /// - /// If vanilla upgrade chains are enabled, return that which this recipe upgrades from. - /// Returns the basic Knife when given HeatBlade. - /// - /// The entity to check for downgrades. - /// A LogicEntity if the given entity has a predecessor, null otherwise. - [CanBeNull] - protected LogicEntity CheckForVanillaUpgrades(LogicEntity entity) - { - LogicEntity result = null; - - if (_config.bVanillaUpgradeChains) - { - TechType basicUpgrade = _tree.GetUpgradeChain(entity.TechType); - if (!basicUpgrade.Equals(TechType.None)) - result = _materials.GetAll().Find(x => x.TechType.Equals(basicUpgrade)); - } - - return result; - } - - /// - /// Choose a theming ingredient for the base from among a range of easily available options. - /// - /// The maximum depth at which the material must be available. - /// A random LogicEntity from the Raw Materials or (if enabled) Fish categories. - private LogicEntity ChooseBaseTheme(int depth) - { - List options = new List(); - - options.AddRange(_materials.GetAll().FindAll(x => x.Category.Equals(ETechTypeCategory.RawMaterials) - && x.AccessibleDepth < depth - && !x.HasPrerequisites - && x.MaxUsesPerGame == 0 - && x.GetItemSize() == 1)); - - if (_config.bUseFish) - { - options.AddRange(_materials.GetAll().FindAll(x => x.Category.Equals(ETechTypeCategory.Fish) - && x.AccessibleDepth < depth - && !x.HasPrerequisites - && x.MaxUsesPerGame == 0 - && x.GetItemSize() == 1)); - } - - LogHandler.Debug("LIST OF BASE THEME OPTIONS:"); - foreach (LogicEntity ent in options) - { - LogHandler.Debug(ent.TechType.AsString()); - } - LogHandler.Debug("END LIST"); - - return GetRandom(options); - } - - // This function changes the output of the metal salvage recipe by removing - // the titanium one and replacing it with the new one. - // As a minor caveat, the new recipe shows up at the bottom of the tree. - // FIXME does not function. - internal static void ChangeScrapMetalResult(Recipe replacement) - { - if (replacement.TechType.Equals(TechType.Titanium)) - return; - - // This techdata was used as a futile and desparate attempt to get things - // working. It acts just like a RandomiserRecipe would though. - TechData td = new TechData(); - td.Ingredients = new List(); - td.Ingredients.Add(new Ingredient(TechType.ScrapMetal, 1)); - td.craftAmount = 1; - TechType yeet = TechType.GasPod; - - replacement.Ingredients = new List(); - replacement.Ingredients.Add(new RandomiserIngredient(TechType.ScrapMetal, 1)); - replacement.CraftAmount = 4; - - //CraftDataHandler.SetTechData(replacement.TechType, replacement); - CraftDataHandler.SetTechData(yeet, td); - - LogHandler.Debug("!!! TechType contained in replacement: " + replacement.TechType.AsString()); - foreach (RandomiserIngredient i in replacement.Ingredients) - { - LogHandler.Debug("!!! Ingredient: " + i.techType.AsString() + ", " + i.amount); - } - - // FIXME for whatever reason, this code works for some items, but not for others???? - // Fish seem to work, and so does lead, but every other raw material does not? - // What's worse, CC2 has no issues with this at all despite apparently doing nothing different??? - CraftTreeHandler.RemoveNode(CraftTree.Type.Fabricator, "Resources", "BasicMaterials", "Titanium"); - - //CraftTreeHandler.AddCraftingNode(CraftTree.Type.Fabricator, replacement.TechType, "Resources", "BasicMaterials"); - CraftTreeHandler.AddCraftingNode(CraftTree.Type.Fabricator, yeet, "Resources", "BasicMaterials"); - - CraftDataHandler.RemoveFromGroup(TechGroup.Resources, TechCategory.BasicMaterials, TechType.Titanium); - CraftDataHandler.AddToGroup(TechGroup.Resources, TechCategory.BasicMaterials, yeet); - } - - /// - /// Set up the blacklist with entities that are not allowed to function as ingredients for the given entity. - /// - /// The entity to build a blacklist against. - protected void UpdateBlacklist(LogicEntity entity) - { - _blacklist = new List(); - - if (_config.iEquipmentAsIngredients == 0 || (_config.iEquipmentAsIngredients == 1 && entity.CanFunctionAsIngredient())) - _blacklist.Add(ETechTypeCategory.Equipment); - if (_config.iToolsAsIngredients == 0 || (_config.iToolsAsIngredients == 1 && entity.CanFunctionAsIngredient())) - _blacklist.Add(ETechTypeCategory.Tools); - if (_config.iUpgradesAsIngredients == 0 || (_config.iUpgradesAsIngredients == 1 && entity.CanFunctionAsIngredient())) - { - _blacklist.Add(ETechTypeCategory.VehicleUpgrades); - _blacklist.Add(ETechTypeCategory.WorkBenchUpgrades); - } - } - } -} +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using SMLHelper.V2.Crafting; +using SMLHelper.V2.Handlers; +using SubnauticaRandomiser.RandomiserObjects; + +namespace SubnauticaRandomiser.Logic +{ + internal abstract class Mode + { + protected RandomiserConfig _config; + protected Materials _materials; + protected ProgressionTree _tree; + protected Random _random; + protected List _ingredients = new List(); + protected List _blacklist = new List(); + protected LogicEntity _baseTheme; + + protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) + { + _config = config; + _materials = materials; + _tree = tree; + _random = random; + + _baseTheme = ChooseBaseTheme(100); + LogHandler.Debug("Chosen " + _baseTheme.TechType.AsString() + " as base theme."); + //InitMod.s_masterDict.DictionaryInstance.Add(TechType.Titanium, _baseTheme.GetSerializableRecipe()); + //ChangeScrapMetalResult(_baseTheme); + } + + internal abstract LogicEntity RandomiseIngredients(LogicEntity entity); + + /// + /// Add an ingredient to the list of ingredients used to form a recipe, but ensure its MaxUses field is + /// respected. + /// + /// The entity to add. + /// The number of uses to consume. + protected void AddIngredientWithMaxUsesCheck(LogicEntity entity, int amount) + { + // Ensure that limited ingredients are not overused. Particularly + // intended for cuddlefish. + int remainder = entity.MaxUsesPerGame - entity._usedInRecipes; + if (entity.MaxUsesPerGame != 0 && remainder > 0 && remainder < amount) + amount = remainder; + + _ingredients.Add(new RandomiserIngredient(entity.TechType, amount)); + entity._usedInRecipes++; + + if (!entity.HasUsesLeft()) + { + _materials.GetReachable().Remove(entity); + LogHandler.Debug("! Removing " + entity.TechType.AsString() + " from materials list due to " + + "max uses reached: " + entity._usedInRecipes); + } + } + + /// + /// Get a random entity from a list, ensuring that it is not part of a given blacklist. + /// TODO: Install safeguards to prevent infinite loops. + /// + /// The list to get a random element from. + /// The blacklist of forbidden elements to not ever consider. + /// A random, non-blacklisted element from the list. + /// Raised if the list is null or empty. + [NotNull] + protected LogicEntity GetRandom(List list, List blacklist = null) + { + if (list == null || list.Count == 0) + throw new InvalidOperationException("Failed to get valid entity from materials list: list is null or empty."); + + LogicEntity randomEntity = null; + while (true) + { + randomEntity = list[_random.Next(0, list.Count)]; + + if (blacklist != null && blacklist.Count > 0) + { + if (blacklist.Contains(randomEntity.Category)) + continue; + } + break; + } + + return randomEntity; + } + + /// + /// If base theming is enabled and the given entity is a base piece, return the base theming ingredient. + /// + /// The entity to check. + /// A LogicEntity if the passed entity is a base piece, null otherwise. + [CanBeNull] + protected LogicEntity CheckForBaseTheming(LogicEntity entity) + { + if (_config.bDoBaseTheming && _baseTheme != null && entity.Category.Equals(ETechTypeCategory.BaseBasePieces)) + return _baseTheme; + + return null; + } + + /// + /// If vanilla upgrade chains are enabled, return that which this recipe upgrades from. + /// Returns the basic Knife when given HeatBlade. + /// + /// The entity to check for downgrades. + /// A LogicEntity if the given entity has a predecessor, null otherwise. + [CanBeNull] + protected LogicEntity CheckForVanillaUpgrades(LogicEntity entity) + { + LogicEntity result = null; + + if (_config.bVanillaUpgradeChains) + { + TechType basicUpgrade = _tree.GetUpgradeChain(entity.TechType); + if (!basicUpgrade.Equals(TechType.None)) + result = _materials.GetAll().Find(x => x.TechType.Equals(basicUpgrade)); + } + + return result; + } + + /// + /// Choose a theming ingredient for the base from among a range of easily available options. + /// + /// The maximum depth at which the material must be available. + /// A random LogicEntity from the Raw Materials or (if enabled) Fish categories. + private LogicEntity ChooseBaseTheme(int depth) + { + List options = new List(); + + options.AddRange(_materials.GetAll().FindAll(x => x.Category.Equals(ETechTypeCategory.RawMaterials) + && x.AccessibleDepth < depth + && !x.HasPrerequisites + && x.MaxUsesPerGame == 0 + && x.GetItemSize() == 1)); + + if (_config.bUseFish) + { + options.AddRange(_materials.GetAll().FindAll(x => x.Category.Equals(ETechTypeCategory.Fish) + && x.AccessibleDepth < depth + && !x.HasPrerequisites + && x.MaxUsesPerGame == 0 + && x.GetItemSize() == 1)); + } + + LogHandler.Debug("LIST OF BASE THEME OPTIONS:"); + foreach (LogicEntity ent in options) + { + LogHandler.Debug(ent.TechType.AsString()); + } + LogHandler.Debug("END LIST"); + + return GetRandom(options); + } + + // This function changes the output of the metal salvage recipe by removing + // the titanium one and replacing it with the new one. + // As a minor caveat, the new recipe shows up at the bottom of the tree. + // FIXME does not function. + internal static void ChangeScrapMetalResult(Recipe replacement) + { + if (replacement.TechType.Equals(TechType.Titanium)) + return; + + // This techdata was used as a futile and desparate attempt to get things + // working. It acts just like a RandomiserRecipe would though. + TechData td = new TechData(); + td.Ingredients = new List(); + td.Ingredients.Add(new Ingredient(TechType.ScrapMetal, 1)); + td.craftAmount = 1; + TechType yeet = TechType.GasPod; + + replacement.Ingredients = new List(); + replacement.Ingredients.Add(new RandomiserIngredient(TechType.ScrapMetal, 1)); + replacement.CraftAmount = 4; + + //CraftDataHandler.SetTechData(replacement.TechType, replacement); + CraftDataHandler.SetTechData(yeet, td); + + LogHandler.Debug("!!! TechType contained in replacement: " + replacement.TechType.AsString()); + foreach (RandomiserIngredient i in replacement.Ingredients) + { + LogHandler.Debug("!!! Ingredient: " + i.techType.AsString() + ", " + i.amount); + } + + // FIXME for whatever reason, this code works for some items, but not for others???? + // Fish seem to work, and so does lead, but every other raw material does not? + // What's worse, CC2 has no issues with this at all despite apparently doing nothing different??? + CraftTreeHandler.RemoveNode(CraftTree.Type.Fabricator, "Resources", "BasicMaterials", "Titanium"); + + //CraftTreeHandler.AddCraftingNode(CraftTree.Type.Fabricator, replacement.TechType, "Resources", "BasicMaterials"); + CraftTreeHandler.AddCraftingNode(CraftTree.Type.Fabricator, yeet, "Resources", "BasicMaterials"); + + CraftDataHandler.RemoveFromGroup(TechGroup.Resources, TechCategory.BasicMaterials, TechType.Titanium); + CraftDataHandler.AddToGroup(TechGroup.Resources, TechCategory.BasicMaterials, yeet); + } + + /// + /// Set up the blacklist with entities that are not allowed to function as ingredients for the given entity. + /// + /// The entity to build a blacklist against. + protected void UpdateBlacklist(LogicEntity entity) + { + _blacklist = new List(); + + if (_config.iEquipmentAsIngredients == 0 || (_config.iEquipmentAsIngredients == 1 && entity.CanFunctionAsIngredient())) + _blacklist.Add(ETechTypeCategory.Equipment); + if (_config.iToolsAsIngredients == 0 || (_config.iToolsAsIngredients == 1 && entity.CanFunctionAsIngredient())) + _blacklist.Add(ETechTypeCategory.Tools); + if (_config.iUpgradesAsIngredients == 0 || (_config.iUpgradesAsIngredients == 1 && entity.CanFunctionAsIngredient())) + { + _blacklist.Add(ETechTypeCategory.VehicleUpgrades); + _blacklist.Add(ETechTypeCategory.WorkBenchUpgrades); + } + } + } +} diff --git a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs index 620285a..e1b917a 100644 --- a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs +++ b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs @@ -1,217 +1,217 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading.Tasks; - -namespace SubnauticaRandomiser.RandomiserObjects -{ - /// - /// Handles everything related to the spoiler log generated during randomisation. - /// - public class SpoilerLog - { - internal const string _FileName = "spoilerlog.txt"; - private RandomiserConfig _config; - internal static List> s_progression = new List>(); - - private List _basicOptions; - private string[] _contentHeader; - private string[] _contentBasics; - private string[] _contentAdvanced; - private string[] _contentDataboxes; - - internal SpoilerLog(RandomiserConfig config) - { - _config = config; - } - - /// - /// Prepare the more basic aspects of the log. - /// - private void PrepareStrings() - { - _basicOptions = new List() - { - "iSeed", - "iRandomiserMode", - "bUseFish", "bUseEggs", "bUseSeeds", - "bRandomiseDataboxes", - "bRandomiseFragments", - "bVanillaUpgradeChains", - "bDoBaseTheming", - "iEquipmentAsIngredients", "iToolsAsIngredients", "iUpgradesAsIngredients", - "iMaxIngredientsPerRecipe", "iMaxAmountPerIngredient", - "bMaxBiomesPerFragments" - }; - _contentHeader = new [] - { - "*************************************************", - "***** SUBNAUTICA RANDOMISER SPOILER LOG *****", - "*************************************************", - "", - "Generated on " + DateTime.Now + " with " + InitMod.s_versionDict[InitMod.s_expectedSaveVersion] - }; - _contentBasics = new [] - { - "", - "", - "///// Basic Information /////", - "Seed: " + _config.iSeed, - "Mode: " + _config.iRandomiserMode, - "Fish, Eggs, Seeds: " + _config.bUseFish + ", " + _config.bUseEggs + ", " + _config.bUseSeeds, - "Random Databoxes: " + _config.bRandomiseDataboxes, - "Random Fragments: " + _config.bRandomiseFragments, - "Vanilla Upgrade Chains: " + _config.bVanillaUpgradeChains, - "Base Theming: " + _config.bDoBaseTheming, - "Equipment, Tools, Upgrades: " + _config.iEquipmentAsIngredients + ", " + _config.iToolsAsIngredients + ", " + _config.iUpgradesAsIngredients, - "Max Ingredients: " + _config.iMaxIngredientsPerRecipe + " per recipe, " + _config.iMaxAmountPerIngredient + " per ingredient", - "Max Biomes per Fragment: " + _config.iMaxBiomesPerFragment, - "" - }; - _contentAdvanced = new [] - { - "", - "", - "///// Depth Progression Path /////" - }; - _contentDataboxes = new [] - { - "", - "", - "///// Databox Locations /////" - }; - } - - /// - /// Add advanced settings to the spoiler log, but only if they have been modified. - /// - /// An array of modified settings. - private string[] PrepareAdvancedSettings() - { - List preparedAdvSettings = new List(); - FieldInfo[] defaultFieldInfoArray = typeof(ConfigDefaults).GetFields(BindingFlags.NonPublic | BindingFlags.Static); - FieldInfo[] fieldInfoArray = typeof(RandomiserConfig).GetFields(BindingFlags.Public | BindingFlags.Instance); - - LogHandler.Debug("Number of fields in default, instance: " + defaultFieldInfoArray.Length + ", " + fieldInfoArray.Length); - - foreach (FieldInfo defaultField in defaultFieldInfoArray) - { - // Check whether this field is an advanced config option or not. - if (_basicOptions.Contains(defaultField.Name)) - continue; - - foreach (FieldInfo field in fieldInfoArray) - { - if (!field.Name.Equals(defaultField.Name)) - continue; - - var value = field.GetValue(_config); - - // If the value of a config field does not correspond to its default value, the user must have - // modified it. Add it to the list in that case. - if (!value.Equals(defaultField.GetValue(null))) - preparedAdvSettings.Add(field.Name + ": " + value); - - break; - } - } - - if (preparedAdvSettings.Count == 0) - preparedAdvSettings.Add("No advanced settings were modified."); - - return preparedAdvSettings.ToArray(); - } - - /// - /// Grab the randomised boxes from masterDict, and sort them alphabetically. - /// - /// The prepared log entries. - private string[] PrepareDataboxes() - { - if (!InitMod.s_masterDict.isDataboxRandomised) - return new [] { "Not randomised, all in vanilla locations." }; - - List preparedDataboxes = new List(); - - foreach (KeyValuePair entry in InitMod.s_masterDict.Databoxes) - { - preparedDataboxes.Add(entry.Value.AsString() + " can be found at " + entry.Key); - } - - preparedDataboxes.Sort(); - - return preparedDataboxes.ToArray(); - } - - /// - /// Compare the MD5 of the recipe CSV and try to see if it's still the same. - /// Since this is done while parsing the CSV anyway, grab the value from there. - /// - /// The prepared log entry. - private string PrepareMD5() - { - if (!InitMod.s_expectedRecipeMD5.Equals(CSVReader.s_recipeCSVMD5)) - return "recipeInformation.csv has been modified: " + CSVReader.s_recipeCSVMD5; - - return "recipeInformation.csv is unmodified."; - } - - /// - /// Prepare a human readable way to tell what must be crafted to reach greater depths. - /// - /// The prepared log entries. - private string[] PrepareProgressionPath() - { - List preparedProgressionPath = new List(); - int lastDepth = 0; - - foreach (KeyValuePair pair in s_progression) - { - if (pair.Value > lastDepth) - preparedProgressionPath.Add("Craft " + pair.Key.AsString() + " to reach " + pair.Value + "m"); - else - preparedProgressionPath.Add("Unlocked " + pair.Key.AsString() + "."); - - lastDepth = pair.Value; - } - - return preparedProgressionPath.ToArray(); - } - - /// - /// Write the log to disk. - /// - internal async Task WriteLog() - { - List lines = new List(); - PrepareStrings(); - - lines.AddRange(_contentHeader); - lines.Add(PrepareMD5()); - - lines.AddRange(_contentBasics); - lines.AddRange(PrepareAdvancedSettings()); - lines.AddRange(_contentAdvanced); - - lines.AddRange(PrepareProgressionPath()); - lines.AddRange(_contentDataboxes); - lines.AddRange(PrepareDataboxes()); - - using (StreamWriter file = new StreamWriter(Path.Combine(InitMod.s_modDirectory, _FileName))) - { - await WriteTextToLog(file, lines.ToArray()); - } - - LogHandler.Info("Wrote spoiler log to disk."); - } - - private async Task WriteTextToLog(StreamWriter file, string[] text) - { - foreach (string line in text) - { - await file.WriteLineAsync(line); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +namespace SubnauticaRandomiser.RandomiserObjects +{ + /// + /// Handles everything related to the spoiler log generated during randomisation. + /// + public class SpoilerLog + { + internal const string _FileName = "spoilerlog.txt"; + private RandomiserConfig _config; + internal static List> s_progression = new List>(); + + private List _basicOptions; + private string[] _contentHeader; + private string[] _contentBasics; + private string[] _contentAdvanced; + private string[] _contentDataboxes; + + internal SpoilerLog(RandomiserConfig config) + { + _config = config; + } + + /// + /// Prepare the more basic aspects of the log. + /// + private void PrepareStrings() + { + _basicOptions = new List() + { + "iSeed", + "iRandomiserMode", + "bUseFish", "bUseEggs", "bUseSeeds", + "bRandomiseDataboxes", + "bRandomiseFragments", + "bVanillaUpgradeChains", + "bDoBaseTheming", + "iEquipmentAsIngredients", "iToolsAsIngredients", "iUpgradesAsIngredients", + "iMaxIngredientsPerRecipe", "iMaxAmountPerIngredient", + "bMaxBiomesPerFragments" + }; + _contentHeader = new [] + { + "*************************************************", + "***** SUBNAUTICA RANDOMISER SPOILER LOG *****", + "*************************************************", + "", + "Generated on " + DateTime.Now + " with " + InitMod.s_versionDict[InitMod.s_expectedSaveVersion] + }; + _contentBasics = new [] + { + "", + "", + "///// Basic Information /////", + "Seed: " + _config.iSeed, + "Mode: " + _config.iRandomiserMode, + "Fish, Eggs, Seeds: " + _config.bUseFish + ", " + _config.bUseEggs + ", " + _config.bUseSeeds, + "Random Databoxes: " + _config.bRandomiseDataboxes, + "Random Fragments: " + _config.bRandomiseFragments, + "Vanilla Upgrade Chains: " + _config.bVanillaUpgradeChains, + "Base Theming: " + _config.bDoBaseTheming, + "Equipment, Tools, Upgrades: " + _config.iEquipmentAsIngredients + ", " + _config.iToolsAsIngredients + ", " + _config.iUpgradesAsIngredients, + "Max Ingredients: " + _config.iMaxIngredientsPerRecipe + " per recipe, " + _config.iMaxAmountPerIngredient + " per ingredient", + "Max Biomes per Fragment: " + _config.iMaxBiomesPerFragment, + "" + }; + _contentAdvanced = new [] + { + "", + "", + "///// Depth Progression Path /////" + }; + _contentDataboxes = new [] + { + "", + "", + "///// Databox Locations /////" + }; + } + + /// + /// Add advanced settings to the spoiler log, but only if they have been modified. + /// + /// An array of modified settings. + private string[] PrepareAdvancedSettings() + { + List preparedAdvSettings = new List(); + FieldInfo[] defaultFieldInfoArray = typeof(ConfigDefaults).GetFields(BindingFlags.NonPublic | BindingFlags.Static); + FieldInfo[] fieldInfoArray = typeof(RandomiserConfig).GetFields(BindingFlags.Public | BindingFlags.Instance); + + LogHandler.Debug("Number of fields in default, instance: " + defaultFieldInfoArray.Length + ", " + fieldInfoArray.Length); + + foreach (FieldInfo defaultField in defaultFieldInfoArray) + { + // Check whether this field is an advanced config option or not. + if (_basicOptions.Contains(defaultField.Name)) + continue; + + foreach (FieldInfo field in fieldInfoArray) + { + if (!field.Name.Equals(defaultField.Name)) + continue; + + var value = field.GetValue(_config); + + // If the value of a config field does not correspond to its default value, the user must have + // modified it. Add it to the list in that case. + if (!value.Equals(defaultField.GetValue(null))) + preparedAdvSettings.Add(field.Name + ": " + value); + + break; + } + } + + if (preparedAdvSettings.Count == 0) + preparedAdvSettings.Add("No advanced settings were modified."); + + return preparedAdvSettings.ToArray(); + } + + /// + /// Grab the randomised boxes from masterDict, and sort them alphabetically. + /// + /// The prepared log entries. + private string[] PrepareDataboxes() + { + if (!InitMod.s_masterDict.isDataboxRandomised) + return new [] { "Not randomised, all in vanilla locations." }; + + List preparedDataboxes = new List(); + + foreach (KeyValuePair entry in InitMod.s_masterDict.Databoxes) + { + preparedDataboxes.Add(entry.Value.AsString() + " can be found at " + entry.Key); + } + + preparedDataboxes.Sort(); + + return preparedDataboxes.ToArray(); + } + + /// + /// Compare the MD5 of the recipe CSV and try to see if it's still the same. + /// Since this is done while parsing the CSV anyway, grab the value from there. + /// + /// The prepared log entry. + private string PrepareMD5() + { + if (!InitMod.s_expectedRecipeMD5.Equals(CSVReader.s_recipeCSVMD5)) + return "recipeInformation.csv has been modified: " + CSVReader.s_recipeCSVMD5; + + return "recipeInformation.csv is unmodified."; + } + + /// + /// Prepare a human readable way to tell what must be crafted to reach greater depths. + /// + /// The prepared log entries. + private string[] PrepareProgressionPath() + { + List preparedProgressionPath = new List(); + int lastDepth = 0; + + foreach (KeyValuePair pair in s_progression) + { + if (pair.Value > lastDepth) + preparedProgressionPath.Add("Craft " + pair.Key.AsString() + " to reach " + pair.Value + "m"); + else + preparedProgressionPath.Add("Unlocked " + pair.Key.AsString() + "."); + + lastDepth = pair.Value; + } + + return preparedProgressionPath.ToArray(); + } + + /// + /// Write the log to disk. + /// + internal async Task WriteLog() + { + List lines = new List(); + PrepareStrings(); + + lines.AddRange(_contentHeader); + lines.Add(PrepareMD5()); + + lines.AddRange(_contentBasics); + lines.AddRange(PrepareAdvancedSettings()); + lines.AddRange(_contentAdvanced); + + lines.AddRange(PrepareProgressionPath()); + lines.AddRange(_contentDataboxes); + lines.AddRange(PrepareDataboxes()); + + using (StreamWriter file = new StreamWriter(Path.Combine(InitMod.s_modDirectory, _FileName))) + { + await WriteTextToLog(file, lines.ToArray()); + } + + LogHandler.Info("Wrote spoiler log to disk."); + } + + private async Task WriteTextToLog(StreamWriter file, string[] text) + { + foreach (string line in text) + { + await file.WriteLineAsync(line); + } + } + } +} From 126946ff74655e0954aaea5f590a0de14615d125 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 5 Aug 2022 20:00:02 +0200 Subject: [PATCH 04/37] Modularise logic systems. --- SubnauticaRandomiser/InitMod.cs | 24 +- SubnauticaRandomiser/Logic/CoreLogic.cs | 417 +++++++++++ SubnauticaRandomiser/Logic/FragmentLogic.cs | 20 +- SubnauticaRandomiser/Logic/ProgressionTree.cs | 16 + SubnauticaRandomiser/Logic/RandomiserLogic.cs | 702 ------------------ .../Logic/{ => Recipes}/Materials.cs | 28 +- .../Logic/{ => Recipes}/Mode.cs | 19 +- .../Logic/{ => Recipes}/ModeBalanced.cs | 6 +- .../Logic/{ => Recipes}/ModeRandom.cs | 4 +- .../Logic/{ => Recipes}/ModeSubstitute.cs | 4 +- .../Logic/Recipes/RecipeLogic.cs | 315 ++++++++ .../RandomiserObjects/LogicEntity.cs | 12 +- .../RandomiserObjects/SpoilerLog.cs | 41 +- .../SubnauticaRandomiser.csproj | 13 +- 14 files changed, 866 insertions(+), 755 deletions(-) create mode 100644 SubnauticaRandomiser/Logic/CoreLogic.cs delete mode 100644 SubnauticaRandomiser/Logic/RandomiserLogic.cs rename SubnauticaRandomiser/Logic/{ => Recipes}/Materials.cs (88%) rename SubnauticaRandomiser/Logic/{ => Recipes}/Mode.cs (95%) rename SubnauticaRandomiser/Logic/{ => Recipes}/ModeBalanced.cs (98%) rename SubnauticaRandomiser/Logic/{ => Recipes}/ModeRandom.cs (94%) rename SubnauticaRandomiser/Logic/{ => Recipes}/ModeSubstitute.cs (98%) create mode 100644 SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 13a425d..bc00d82 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -34,13 +34,13 @@ public static void Initialise() { LogHandler.Info("Randomiser starting up!"); - // Register options menu + // Register options menu. s_modDirectory = GetSubnauticaRandomiserDirectory(); s_config = OptionsPanelHandler.Main.RegisterModOptions(); LogHandler.Debug("Registered options menu."); - // Ensure the user did not update into a save incompatibility, and - // abort if they did to preserve a prior version's state. + // Ensure the user did not update into a save incompatibility, and abort if they did to preserve a prior + // version's state. if (!CheckSaveCompatibility()) return; @@ -59,7 +59,7 @@ public static void Initialise() if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) { // Load recipe changes. - RandomiserLogic.ApplyMasterDict(s_masterDict); + CoreLogic.ApplyMasterDict(s_masterDict); // Load fragment changes. if (s_masterDict.SpawnDataDict?.Count > 0) @@ -129,22 +129,12 @@ internal static void Randomise() } random = new Random(s_config.iSeed); - RandomiserLogic logic = new RandomiserLogic(random, s_masterDict, s_config, completeMaterialsList, databoxes); - FragmentLogic fragmentLogic = null; - if (s_config.bRandomiseFragments) - { - fragmentLogic = new FragmentLogic(s_config, s_masterDict, completeBiomeList, random); - fragmentLogic.Init(); - } - - logic.RandomSmart(fragmentLogic); + // Randomise! + CoreLogic logic = new CoreLogic(random, s_masterDict, s_config, completeMaterialsList, completeBiomeList, databoxes); + logic.Randomise(); LogHandler.Info("Randomisation successful!"); SaveGameStateToDisk(); - - SpoilerLog spoiler = new SpoilerLog(s_config); - // This should run async, but we don't need the result here. It's a file. - _ = spoiler.WriteLog(); } /// diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs new file mode 100644 index 0000000..63697f5 --- /dev/null +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using SMLHelper.V2.Handlers; +using SubnauticaRandomiser.Logic.Recipes; +using SubnauticaRandomiser.RandomiserObjects; +using UnityEngine; + +namespace SubnauticaRandomiser.Logic +{ + /// + /// Acts as the core for handling all randomising logic in the mod, and turning modules on/off as needed. + /// + internal class CoreLogic + { + internal readonly RandomiserConfig _config; + internal readonly List _databoxes; + internal readonly EntitySerializer _masterDict; + internal readonly Materials _materials; + internal readonly System.Random _random; + internal readonly SpoilerLog _spoilerLog; + internal readonly ProgressionTree _tree; + + private readonly FragmentLogic _fragmentLogic; + private readonly RecipeLogic _recipeLogic; + + public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserConfig config, + List allMaterials, List biomes = null, List databoxes = null) + { + _config = config; + _databoxes = databoxes; + _masterDict = masterDict; + _materials = new Materials(allMaterials); + _random = random; + _spoilerLog = new SpoilerLog(config); + + // TODO: Respect config options. + _fragmentLogic = new FragmentLogic(this, biomes); + _recipeLogic = new RecipeLogic(this); + _tree = new ProgressionTree(); + } + + /// + /// Set up all the necessary structures for later. + /// + private void Setup(List notRandomised, Dictionary unlockedProgressionItems) + { + if (_recipeLogic != null) + { + _recipeLogic.UpdateReachableMaterials(0); + // Queue up all craftables to be randomised. + notRandomised.AddRange(_materials.GetAllCraftables()); + + // Init the progression tree. + _tree.SetupVanillaTree(); + if (_config.bVanillaUpgradeChains) + _tree.ApplyUpgradeChainToPrerequisites(_materials.GetAll()); + } + + if (_fragmentLogic != null) + { + // Initialise the fragment cache and remove vanilla spawns. + _fragmentLogic.Init(); + // Queue up all fragments to be randomised. + notRandomised.AddRange(_materials.GetAllFragments()); + } + } + + internal void Randomise() + { + LogHandler.Info("Randomising using logic-based system..."); + + List notRandomised = new List(); + Dictionary unlockedProgressionItems = new Dictionary(); + + // Set up basic structures. + Setup(notRandomised, unlockedProgressionItems); + + int circuitbreaker = 0; + int currentDepth = 0; + int numProgressionItems = unlockedProgressionItems.Count; + while (notRandomised.Count > 0) + { + circuitbreaker++; + if (circuitbreaker > 3000) + { + LogHandler.MainMenuMessage("Failed to randomise items: stuck in infinite loop!"); + LogHandler.Fatal("Encountered infinite loop, aborting!"); + // TODO: Throw exception. + break; + } + + // Update depth and reachable materials. + currentDepth = UpdateReachableDepth(currentDepth, unlockedProgressionItems, numProgressionItems); + numProgressionItems = unlockedProgressionItems.Count; + + LogicEntity nextEntity = ChooseNextEntity(notRandomised, currentDepth); + + // Choose a logic appropriate to the entity. + if (nextEntity.IsFragment) + { + // TODO implement proper depth restrictions and config options. + if (_config.bRandomiseFragments && _fragmentLogic != null) + _fragmentLogic.RandomiseFragment(nextEntity, currentDepth); + + notRandomised.Remove(nextEntity); + nextEntity.InLogic = true; + continue; + } + + if (nextEntity.HasRecipe) + { + bool success = _recipeLogic.RandomiseRecipe(nextEntity, unlockedProgressionItems, currentDepth); + if (success) + { + notRandomised.Remove(nextEntity); + nextEntity.InLogic = true; + } + + continue; + } + + LogHandler.Warn("Unsupported entity in loop: " + nextEntity); + } + + _spoilerLog.WriteLog(); + LogHandler.Info("Finished randomising within " + circuitbreaker + " cycles!"); + } + + /// + /// Get the next entity to be randomised, prioritising essential or elective ones. + /// + /// The next entity. + private LogicEntity ChooseNextEntity(List notRandomised, int depth) + { + // Make sure the list of absolutely essential items is done first, for each depth level. This guarantees + // certain recipes are done by a certain depth, e.g. waterparks by 500m. + // Automatically fails if recipes do not get randomised. + LogicEntity next = _recipeLogic?.GetPriorityEntity(depth); + next ??= GetRandom(notRandomised); + + return next; + } + + /// + /// Randomise (shuffle) the blueprints found inside databoxes. + /// + /// The master dictionary. + /// A list of all databoxes. + /// The list of newly randomised databoxes. + [NotNull] + internal List RandomiseDataboxes(EntitySerializer masterDict, List databoxes) + { + masterDict.Databoxes = new Dictionary(); + List randomDataboxes = new List(); + List toBeRandomised = new List(); + + foreach (Databox dbox in databoxes) + { + toBeRandomised.Add(dbox.Coordinates); + } + + foreach (Databox originalBox in databoxes) + { + int next = _random.Next(0, toBeRandomised.Count); + Databox replacementBox = databoxes.Find(x => x.Coordinates.Equals(toBeRandomised[next])); + + randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, + replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); + masterDict.Databoxes.Add(new RandomiserVector(toBeRandomised[next]), originalBox.TechType); + LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " + + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); + toBeRandomised.RemoveAt(next); + } + masterDict.isDataboxRandomised = true; + + return randomDataboxes; + } + + /// + /// This function calculates the maximum reachable depth based on what vehicles the player has attained, as well + /// as how much further they can go "on foot" + /// TODO: Simplify this. + /// + /// A list of all currently reachable items relevant for progression. + /// The minimum time that it must be possible to spend at the reachable depth before + /// resurfacing. + /// The reachable depth. + internal int CalculateReachableDepth(Dictionary progressionItems, int depthTime = 15) + { + double swimmingSpeed = 4.7; // Assuming player is holding a tool. + double seaglideSpeed = 11.0; + bool seaglide = progressionItems.ContainsKey(TechType.Seaglide); + double finSpeed = 0.0; + double tankPenalty = 0.0; + int breathTime = 45; + + // How long should the player be able to remain at this depth and still make it back just fine? + int searchTime = depthTime; + // Never assume the player has to go deeper than this on foot. + int maxSoloDepth = 300; + int vehicleDepth = 0; + double playerDepthRaw; + double totalDepth; + + LogHandler.Debug("===== Recalculating reachable depth ====="); + + // This feels like it could be simplified. + // Also, this trusts that the tree is set up correctly. + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth200m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 200; + } + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth300m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 300; + } + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth500m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 500; + } + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth900m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 900; + } + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth1300m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 1300; + } + foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth1700m).Pathways) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = 1700; + } + + if (progressionItems.ContainsKey(TechType.Fins)) + finSpeed = 1.41; + if (progressionItems.ContainsKey(TechType.UltraGlideFins)) + finSpeed = 1.88; + + // How deep can the player go without any tanks? + playerDepthRaw = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed)) / 2; + + // But can they go deeper with a tank? (Yes.) + if (progressionItems.ContainsKey(TechType.Tank)) + { + breathTime = 75; + tankPenalty = 0.4; + double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; + playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; + } + + if (progressionItems.ContainsKey(TechType.DoubleTank)) + { + breathTime = 135; + tankPenalty = 0.47; + double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; + playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; + } + + if (progressionItems.ContainsKey(TechType.HighCapacityTank)) + { + breathTime = 225; + tankPenalty = 0.6; + double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; + playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; + } + + if (progressionItems.ContainsKey(TechType.PlasteelTank)) + { + breathTime = 135; + tankPenalty = 0.1; + double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; + playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; + } + + // The vehicle depth and whether or not the player has a rebreather can modify the raw achievable diving depth. + if (progressionItems.ContainsKey(TechType.Rebreather)) + { + totalDepth = vehicleDepth + (playerDepthRaw > maxSoloDepth ? maxSoloDepth : playerDepthRaw); + } + else + { + // Below 100 meters, air is consumed three times as fast. + // Below 200 meters, it is consumed five times as fast. + double depth = 0.0; + + if (vehicleDepth == 0) + { + if (playerDepthRaw <= 100) + { + depth = playerDepthRaw; + } + else + { + depth += 100; + playerDepthRaw -= 100; + + // For anything between 100-200 meters, triple air consumption + if (playerDepthRaw <= 100) + { + depth += playerDepthRaw / 3; + } + else + { + depth += 33.3; + playerDepthRaw -= 100; + // For anything below 200 meters, quintuple it. + depth += playerDepthRaw / 5; + } + } + } + else + { + depth = playerDepthRaw / 5; + } + + totalDepth = vehicleDepth + (depth > maxSoloDepth ? maxSoloDepth : depth); + } + LogHandler.Debug("===== New reachable depth: " + totalDepth + " ====="); + + return (int)totalDepth; + } + + /// + /// Update the depth that can be reached and trigger any changes that need to happen if a new significant + /// threshold has been passed. + /// + /// The previously reachable depth. + /// The unlocked progression items. + /// The number of progression items on the previous cycle. + /// The new maximum depth. + private int UpdateReachableDepth(int currentDepth, Dictionary progressionItems, int numItems) + { + if (progressionItems.Count <= numItems) + return currentDepth; + + int newDepth = CalculateReachableDepth(progressionItems); + _spoilerLog.UpdateLastProgressionEntry(newDepth); + currentDepth = Math.Max(currentDepth, newDepth); + _recipeLogic?.UpdateReachableMaterials(currentDepth); + + return currentDepth; + } + + /// + /// Check whether all TechTypes given in the array are present in the given dictionary. + /// + /// The dictionary to check. + /// The array of TechTypes. + /// True if all TechTypes are present in the dictionary, false otherwise. + private static bool CheckDictForAllTechTypes(Dictionary dict, TechType[] types) + { + bool allItemsPresent = true; + + foreach (TechType t in types) + { + allItemsPresent &= dict.ContainsKey(t); + if (!allItemsPresent) + break; + } + + return allItemsPresent; + } + + /// + /// Check wether any of the given TechTypes have already been randomised. + /// + /// The master dictionary. + /// The TechTypes. + /// True if any TechType in the array has been randomised, false otherwise. + public bool ContainsAny(EntitySerializer masterDict, TechType[] types) + { + foreach (TechType type in types) + { + if (masterDict.RecipeDict.ContainsKey(type)) + return true; + } + return false; + } + + /// + /// Get a random element from a list. + /// + public T GetRandom(List list) + { + if (list == null || list.Count == 0) + { + return default(T); + } + + return list[_random.Next(0, list.Count)]; + } + + /// + /// Apply all recipe changes stored in the masterDict to the game. + /// + /// The master dictionary. + internal static void ApplyMasterDict(EntitySerializer masterDict) + { + Dictionary.KeyCollection keys = masterDict.RecipeDict.Keys; + + foreach (TechType key in keys) + { + CraftDataHandler.SetTechData(key, masterDict.RecipeDict[key]); + } + + // TODO Once scrap metal is working, un-commenting this will apply the change on every startup. + //ChangeScrapMetalResult(masterDict.DictionaryInstance[TechType.Titanium]); + } + } +} diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 731ee26..c9ee47f 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -7,12 +7,17 @@ namespace SubnauticaRandomiser.Logic { + /// + /// Handles everything related to randomising fragments. + /// internal class FragmentLogic { + private readonly CoreLogic _logic; + private Dictionary> _classIdDatabase; - private readonly RandomiserConfig _config; - private readonly EntitySerializer _entitySerializer; - private readonly Random _random; + private RandomiserConfig _config { get { return _logic._config; } } + private EntitySerializer _masterDict { get { return _logic._masterDict; } } + private Random _random { get { return _logic._random; } } private List _availableBiomes; private readonly Dictionary _fragmentDataPaths = new Dictionary { @@ -51,12 +56,11 @@ internal class FragmentLogic /// /// Handle the logic for everything related to fragments. /// - internal FragmentLogic(RandomiserConfig config, EntitySerializer serializer, List biomeList, Random random) + internal FragmentLogic(CoreLogic coreLogic, List biomeList) { - _config = config; - _entitySerializer = serializer; + _logic = coreLogic; + _availableBiomes = GetAvailableFragmentBiomes(biomeList); - _random = random; AllSpawnData = new List(); } @@ -253,7 +257,7 @@ internal void ApplyRandomisedFragment(LogicEntity entity, SpawnData spawnData) entity.SpawnData = spawnData; AllSpawnData.Add(spawnData); - _entitySerializer.AddSpawnData(entity.TechType, spawnData); + _masterDict.AddSpawnData(entity.TechType, spawnData); LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); } diff --git a/SubnauticaRandomiser/Logic/ProgressionTree.cs b/SubnauticaRandomiser/Logic/ProgressionTree.cs index 374f942..83f9f8e 100644 --- a/SubnauticaRandomiser/Logic/ProgressionTree.cs +++ b/SubnauticaRandomiser/Logic/ProgressionTree.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using SubnauticaRandomiser.RandomiserObjects; @@ -364,5 +365,20 @@ public TechType GetUpgradeChain(TechType upgrade) return TechType.None; } + + /// + /// Check whether the given entity is part of any essential or elective items in any node. + /// + /// The entity to check. + /// True if the entity is part of essential or elective items, false otherwise. + public bool IsPriorityEntity(LogicEntity entity) + { + if (_essentialItems.Values.Any(list => list.Contains(entity.TechType))) + return true; + if (_electiveItems.Values.Any(list => list.Any(arr => arr.Contains(entity.TechType)))) + return true; + + return false; + } } } diff --git a/SubnauticaRandomiser/Logic/RandomiserLogic.cs b/SubnauticaRandomiser/Logic/RandomiserLogic.cs deleted file mode 100644 index f17a0e8..0000000 --- a/SubnauticaRandomiser/Logic/RandomiserLogic.cs +++ /dev/null @@ -1,702 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using SMLHelper.V2.Handlers; -using SubnauticaRandomiser.RandomiserObjects; -using UnityEngine; - -namespace SubnauticaRandomiser.Logic -{ - internal class RandomiserLogic - { - private readonly System.Random _random; - - private readonly EntitySerializer _masterDict; - private readonly RandomiserConfig _config; - private Materials _materials; - private ProgressionTree _tree; - private List _databoxes; - private Mode _mode; - - public RandomiserLogic(System.Random random, EntitySerializer masterDict, RandomiserConfig config, List allMaterials, List databoxes = null) - { - _random = random; - _masterDict = masterDict; - _config = config; - _materials = new Materials(allMaterials); - _databoxes = databoxes; - _mode = null; - } - - internal void RandomSmart(FragmentLogic fragmentLogic) - { - // This function uses the progression tree to randomise materials - // and game progression in an intelligent way. - // - // Basic structure looks something like this: - // - Set up vanilla progression tree - // - Calculate reachable depth - // - Put reachable raw materials and fish into _reachableMaterials - // - Pick a random item from _allMaterials - // - Check if it has all dependencies, both as an item and as a - // blueprint, fulfilled. Abort and skip if not. - // - Randomise its ingredients using available materials - // - Add it to the list of reachable materials - // - If it's a knife, also add all seeds and chunks - // - If it's not an item (like a base piece), skip this step - // - Add it to the master dictionary - // - Recalculate reachable depth - // - Repeat - // - Once all items have been randomised, do an integrity check for - // safety. Rocket, vehicles, and hatching enzymes must be on the list. - - LogHandler.Info("Randomising using logic-based system..."); - - List toBeRandomised = new List(); - Dictionary unlockedProgressionItems = new Dictionary(); - _tree = new ProgressionTree(); - int reachableDepth = 0; - - // Init the progression tree - _tree.SetupVanillaTree(); - if (_config.bVanillaUpgradeChains) - _tree.ApplyUpgradeChainToPrerequisites(_materials.GetAll()); - - // Init the mode that will be used - switch (_config.iRandomiserMode) - { - case (0): - _mode = new ModeBalanced(_config, _materials, _tree, _random); - break; - case (1): - _mode = new ModeRandom(_config, _materials, _tree, _random); - break; - } - - // If databox randomising is enabled, go and do that. - if (_config.bRandomiseDataboxes && _databoxes != null) - _databoxes = RandomiseDataboxes(_masterDict, _databoxes); - - foreach (LogicEntity e in _materials.GetAll().FindAll(x => - !x.Category.Equals(ETechTypeCategory.RawMaterials) - && !x.Category.Equals(ETechTypeCategory.Fish) - && !x.Category.Equals(ETechTypeCategory.Seeds) - && !x.Category.Equals(ETechTypeCategory.Eggs))) - { - toBeRandomised.Add(e); - } - - // Iterate over every single entity in the game until all of them - // are considered randomised. - bool newProgressionItem = true; - int circuitbreaker = 0; - while (toBeRandomised.Count > 0) - { - circuitbreaker++; - if (circuitbreaker > 3000) - { - LogHandler.MainMenuMessage("Failed to randomise items: stuck in infinite loop!"); - LogHandler.Fatal("Encountered infinite loop, aborting!"); - break; - } - - int newDepth = 0; - // If the previous cycle randomised an entity that was critical - // and possibly allows for reaching greater depths, recalculate. - if (newProgressionItem) - { - newDepth = CalculateReachableDepth(_tree, unlockedProgressionItems, _config.iDepthSearchTime); - if (SpoilerLog.s_progression.Count > 0) - { - KeyValuePair valuePair = new KeyValuePair - (SpoilerLog.s_progression[SpoilerLog.s_progression.Count - 1].Key, newDepth); - SpoilerLog.s_progression.RemoveAt(SpoilerLog.s_progression.Count - 1); - SpoilerLog.s_progression.Add(valuePair); - } - } - - // If the most recently randomised entity opened up some new paths - // to progress, update the list of reachable materials. - if (newProgressionItem || (newDepth > reachableDepth)) - { - reachableDepth = newDepth > reachableDepth ? newDepth : reachableDepth; - UpdateReachableMaterials(reachableDepth); - } - - newProgressionItem = false; - bool isPriority = false; - - // Make sure the list of absolutely essential items is done first, - // for each depth level. This guarantees certain recipes are done - // by a certain depth, e.g. waterparks by 500m. - LogicEntity nextEntity = GetPriorityEntity(reachableDepth); - - // Once all essentials and electives are done, grab a random entity - // which has not yet been randomised. - if (nextEntity is null) - nextEntity = GetRandom(toBeRandomised); - else - isPriority = true; - - // If the entity is a fragment, go handle that. - // TODO implement proper depth restrictions and config options. - if (nextEntity.Category.Equals(ETechTypeCategory.Fragments)) - { - if (_config.bRandomiseFragments && fragmentLogic != null) - fragmentLogic.RandomiseFragment(nextEntity, reachableDepth); - - toBeRandomised.Remove(nextEntity); - nextEntity.InLogic = true; - continue; - } - - // HACK improve this. Currently makes logic only consider recipes. - if (!nextEntity.HasRecipe) - continue; - - // Does this recipe have all of its prerequisites fulfilled? - // Skip this check if the recipe is a priority (essential or elective) - if (isPriority || (CheckRecipeForBlueprint(_masterDict, _databoxes, nextEntity, reachableDepth) && CheckRecipeForPrerequisites(_masterDict, nextEntity))) - { - // Found a good recipe! Randomise it. - toBeRandomised.Remove(nextEntity); - newProgressionItem = RandomiseRecipeEntity(nextEntity, unlockedProgressionItems, reachableDepth); - - LogHandler.Debug("[+] Randomised recipe for [" + nextEntity.TechType.AsString() + "]."); - } - else - { - LogHandler.Debug("--- Recipe [" + nextEntity.TechType.AsString() + "] did not fulfill requirements, skipping."); - } - } - - LogHandler.Info("Finished randomising within " + circuitbreaker + " cycles!"); - } - - /// - /// Handle everything related to actually randomising the recipe itself, and ensure all special cases are covered. - /// - /// The recipe to randomise. - /// The available materials to use as potential ingredients. - /// The currently reachable depth. - /// True if a new progression item was unlocked, false otherwise. - private bool RandomiseRecipeEntity(LogicEntity entity, Dictionary unlockedProgressionItems, int reachableDepth) - { - bool newProgressionItem = false; - - entity = _mode.RandomiseIngredients(entity); - ApplyRandomisedRecipe(_masterDict, entity.Recipe); - - // Only add this entity to the materials list if it can be an ingredient. - if (entity.CanFunctionAsIngredient()) - _materials.AddReachable(entity); - - // Knives are a special case that open up a lot of new materials. - if ((entity.TechType.Equals(TechType.Knife) || entity.TechType.Equals(TechType.HeatBlade)) && !unlockedProgressionItems.ContainsKey(TechType.Knife)) - { - unlockedProgressionItems.Add(TechType.Knife, true); - newProgressionItem = true; - } - - // Similarly, Alien Containment is a special case for eggs. - if (entity.TechType.Equals(TechType.BaseWaterPark) && _config.bUseEggs) - { - unlockedProgressionItems.Add(TechType.BaseWaterPark, true); - newProgressionItem = true; - } - - // If it is a central depth progression item, consider it unlocked. - if (_tree.DepthProgressionItems.ContainsKey(entity.TechType) && !unlockedProgressionItems.ContainsKey(entity.TechType)) - { - unlockedProgressionItems.Add(entity.TechType, true); - SpoilerLog.s_progression.Add(new KeyValuePair(entity.TechType, 0)); - newProgressionItem = true; - - LogHandler.Debug("[+] Added " + entity.TechType.AsString() + " to progression items."); - } - - entity.InLogic = true; - - return newProgressionItem; - } - - /// - /// Randomise (shuffle) the blueprints found inside databoxes. - /// - /// The master dictionary. - /// A list of all databoxes. - /// The list of newly randomised databoxes. - [NotNull] - internal List RandomiseDataboxes(EntitySerializer masterDict, List databoxes) - { - masterDict.Databoxes = new Dictionary(); - List randomDataboxes = new List(); - List toBeRandomised = new List(); - - foreach (Databox dbox in databoxes) - { - toBeRandomised.Add(dbox.Coordinates); - } - - foreach (Databox originalBox in databoxes) - { - int next = _random.Next(0, toBeRandomised.Count); - Databox replacementBox = databoxes.Find(x => x.Coordinates.Equals(toBeRandomised[next])); - - randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, - replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); - masterDict.Databoxes.Add(new RandomiserVector(toBeRandomised[next]), originalBox.TechType); - LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " - + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); - toBeRandomised.RemoveAt(next); - } - masterDict.isDataboxRandomised = true; - - return randomDataboxes; - } - - /// - /// Get an essential or elective entity for the currently reachable depth, prioritising essential ones. - /// - /// The maximum depth to consider. - /// A LogicEntity, or null if all have been processed already. - [CanBeNull] - private LogicEntity GetPriorityEntity(int depth) - { - List essentialItems = _tree.GetEssentialItems(depth); - List electiveItems = _tree.GetElectiveItems(depth); - LogicEntity entity = null; - - // Always get one of the essential items first, if available. - if (essentialItems != null && essentialItems.Count > 0) - { - entity = _materials.GetAll().Find(x => x.TechType.Equals(essentialItems[0])); - essentialItems.RemoveAt(0); - LogHandler.Debug("Prioritising essential item " + entity.TechType.AsString() + " for depth " + depth); - - // If this has already been randomised, all the better. - if (_masterDict.RecipeDict.ContainsKey(entity.TechType)) - { - entity = null; - LogHandler.Debug("Priority item was already randomised, skipping."); - } - } - - // Similarly, if all essential items are done, grab one from among - // the elective items and leave the rest up to chance. - if (entity is null && electiveItems != null && electiveItems.Count > 0) - { - TechType[] electiveTypes = electiveItems[0]; - electiveItems.RemoveAt(0); - - if (ContainsAny(_masterDict, electiveTypes)) - { - LogHandler.Debug("Priority elective containing " + electiveTypes[0].AsString() + " was already randomised, skipping."); - } - else - { - TechType nextType = GetRandom(new List(electiveTypes)); - entity = _materials.GetAll().Find(x => x.TechType.Equals(nextType)); - LogHandler.Debug("Prioritising elective item " + entity.TechType.AsString() + " for depth " + depth); - } - } - - return entity; - } - - /// - /// Add all reachable materials to the list, taking into account depth and any config options. - /// - /// The maximum depth to consider. - internal void UpdateReachableMaterials(int depth) - { - if (_masterDict.ContainsKnife()) - _materials.AddReachable(ETechTypeCategory.RawMaterials, depth); - else - _materials.AddReachableWithPrereqs(ETechTypeCategory.RawMaterials, depth, TechType.Knife, true); - - if (_config.bUseFish) - _materials.AddReachable(ETechTypeCategory.Fish, depth); - if (_config.bUseSeeds && _masterDict.ContainsKnife()) - _materials.AddReachable(ETechTypeCategory.Seeds, depth); - if (_config.bUseEggs && _masterDict.RecipeDict.ContainsKey(TechType.BaseWaterPark)) - _materials.AddReachable(ETechTypeCategory.Eggs, depth); - } - - /// - /// This function calculates the maximum reachable depth based on what vehicles the player has attained, as well - /// as how much further they can go "on foot" - /// TODO: Simplify this. - /// - /// The progression tree. - /// A list of all currently reachable items relevant for progression. - /// The minimum time that it must be possible to spend at the reachable depth before - /// resurfacing. - /// The reachable depth. - internal static int CalculateReachableDepth(ProgressionTree tree, Dictionary progressionItems, int depthTime = 15) - { - double swimmingSpeed = 4.7; // Assuming player is holding a tool. - double seaglideSpeed = 11.0; - bool seaglide = progressionItems.ContainsKey(TechType.Seaglide); - double finSpeed = 0.0; - double tankPenalty = 0.0; - int breathTime = 45; - - // How long should the player be able to remain at this depth and still make it back just fine? - int searchTime = depthTime; - // Never assume the player has to go deeper than this on foot. - int maxSoloDepth = 300; - int vehicleDepth = 0; - double playerDepthRaw; - double totalDepth; - - LogHandler.Debug("===== Recalculating reachable depth ====="); - - // This feels like it could be simplified. - // Also, this trusts that the tree is set up correctly. - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth200m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 200; - } - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth300m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 300; - } - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth500m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 500; - } - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth900m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 900; - } - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth1300m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 1300; - } - foreach (TechType[] path in tree.GetProgressionPath(EProgressionNode.Depth1700m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 1700; - } - - if (progressionItems.ContainsKey(TechType.Fins)) - finSpeed = 1.41; - if (progressionItems.ContainsKey(TechType.UltraGlideFins)) - finSpeed = 1.88; - - // How deep can the player go without any tanks? - playerDepthRaw = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed)) / 2; - - // But can they go deeper with a tank? (Yes.) - if (progressionItems.ContainsKey(TechType.Tank)) - { - breathTime = 75; - tankPenalty = 0.4; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } - - if (progressionItems.ContainsKey(TechType.DoubleTank)) - { - breathTime = 135; - tankPenalty = 0.47; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } - - if (progressionItems.ContainsKey(TechType.HighCapacityTank)) - { - breathTime = 225; - tankPenalty = 0.6; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } - - if (progressionItems.ContainsKey(TechType.PlasteelTank)) - { - breathTime = 135; - tankPenalty = 0.1; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } - - // The vehicle depth and whether or not the player has a rebreather can modify the raw achievable diving depth. - if (progressionItems.ContainsKey(TechType.Rebreather)) - { - totalDepth = vehicleDepth + (playerDepthRaw > maxSoloDepth ? maxSoloDepth : playerDepthRaw); - } - else - { - // Below 100 meters, air is consumed three times as fast. - // Below 200 meters, it is consumed five times as fast. - double depth = 0.0; - - if (vehicleDepth == 0) - { - if (playerDepthRaw <= 100) - { - depth = playerDepthRaw; - } - else - { - depth += 100; - playerDepthRaw -= 100; - - // For anything between 100-200 meters, triple air consumption - if (playerDepthRaw <= 100) - { - depth += playerDepthRaw / 3; - } - else - { - depth += 33.3; - playerDepthRaw -= 100; - // For anything below 200 meters, quintuple it. - depth += playerDepthRaw / 5; - } - } - } - else - { - depth = playerDepthRaw / 5; - } - - totalDepth = vehicleDepth + (depth > maxSoloDepth ? maxSoloDepth : depth); - } - LogHandler.Debug("===== New reachable depth: " + totalDepth + " ====="); - - return (int)totalDepth; - } - - /// - /// Check whether all TechTypes given in the array are present in the given dictionary. - /// - /// The dictionary to check. - /// The array of TechTypes. - /// True if all TechTypes are present in the dictionary, false otherwise. - private static bool CheckDictForAllTechTypes(Dictionary dict, TechType[] types) - { - bool allItemsPresent = true; - - foreach (TechType t in types) - { - allItemsPresent &= dict.ContainsKey(t); - if (!allItemsPresent) - break; - } - - return allItemsPresent; - } - - /// - /// Check if this recipe fulfills all conditions to have its blueprint be unlocked. - /// - /// The master dictionary. - /// The list of all databoxes. - /// The recipe to check. - /// The maximum depth to consider. - /// True if the recipe fulfills all conditions, false otherwise. - private bool CheckRecipeForBlueprint(EntitySerializer masterDict, List databoxes, LogicEntity entity, int depth) - { - bool fulfilled = true; - - if (entity.Blueprint == null || (entity.Blueprint.UnlockConditions == null - && entity.Blueprint.UnlockDepth == 0)) - return true; - - // If the databox was randomised, do work to account for new locations. - // Cyclops hull modules need extra special treatment. - if (entity.Blueprint.NeedsDatabox && databoxes?.Count > 0 - && !entity.TechType.Equals(TechType.CyclopsHullModule2) - && !entity.TechType.Equals(TechType.CyclopsHullModule3)) - { - int total = 0; - int number = 0; - int lasercutter = 0; - int propulsioncannon = 0; - - foreach (Databox box in databoxes.FindAll(x => x.TechType.Equals(entity.TechType))) - { - total += (int)Math.Abs(box.Coordinates.y); - number++; - - if (box.RequiresLaserCutter) - lasercutter++; - if (box.RequiresPropulsionCannon) - propulsioncannon++; - } - - LogHandler.Debug("[B] Found " + number + " databoxes for " + entity.TechType.AsString()); - - entity.Blueprint.UnlockDepth = total / number; - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockDepth = total / number; - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockDepth = total / number; - } - - // If more than half of all locations of this databox require a - // tool to access the box, add it to the requirements for the recipe - if (lasercutter / number >= 0.5) - { - entity.Blueprint.UnlockConditions.Add(TechType.LaserCutter); - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockConditions.Add(TechType.LaserCutter); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockConditions.Add(TechType.LaserCutter); - } - } - - if (propulsioncannon / number >= 0.5) - { - entity.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - } - } - } - - foreach (TechType condition in entity.Blueprint.UnlockConditions) - { - LogicEntity conditionEntity = _materials.GetAll().Find(x => x.TechType.Equals(condition)); - - // Without this piece, the Air bladder will hang if fish are not - // enabled for the logic, as it fruitlessly searches for a bladderfish - // which never enters its algorithm. - // Eggs and seeds are never problematic in vanilla, but are covered - // in case users add their own modded items with those. - if (!_config.bUseFish && conditionEntity.Category.Equals(ETechTypeCategory.Fish)) - continue; - if (!_config.bUseEggs && conditionEntity.Category.Equals(ETechTypeCategory.Eggs)) - continue; - if (!_config.bUseSeeds && conditionEntity.Category.Equals(ETechTypeCategory.Seeds)) - continue; - - fulfilled &= (masterDict.RecipeDict.ContainsKey(condition) - || _materials.GetReachable().Exists(x => x.TechType.Equals(condition))); - - if (!fulfilled) - return false; - } - - // Ensure that necessary fragments have already been randomised. - if (_config.bRandomiseFragments && entity.Blueprint.Fragments != null && entity.Blueprint.Fragments.Count > 0) - { - foreach (TechType fragment in entity.Blueprint.Fragments) - { - if (!_masterDict.SpawnDataDict.ContainsKey(fragment)) - { - LogHandler.Debug("[B] Entity " + entity.TechType.AsString() + " missing fragment " - + fragment.AsString()); - return false; - } - } - } - else if (entity.Blueprint.UnlockDepth > depth) - { - fulfilled = false; - } - - return fulfilled; - } - - /// - /// Check whether all prerequisites for this recipe have already been randomised. - /// - /// The master dictionary. - /// The recipe to check. - /// True if all conditions are fulfilled, false otherwise. - private static bool CheckRecipeForPrerequisites(EntitySerializer masterDict, LogicEntity entity) - { - bool fulfilled = true; - - // The builder tool must always be randomised before any base pieces - // ever become accessible. - if (entity.Category.IsBasePiece() && !masterDict.RecipeDict.ContainsKey(TechType.Builder)) - return false; - - if (entity.Prerequisites == null) - return true; - - foreach (TechType t in entity.Prerequisites) - { - fulfilled &= masterDict.RecipeDict.ContainsKey(t); - if (!fulfilled) - break; - } - - return fulfilled; - } - - /// - /// Check wether any of the given TechTypes have already been randomised. - /// - /// The master dictionary. - /// The TechTypes. - /// True if any TechType in the array has been randomised, false otherwise. - private bool ContainsAny(EntitySerializer masterDict, TechType[] types) - { - foreach (TechType type in types) - { - if (masterDict.RecipeDict.ContainsKey(type)) - return true; - } - return false; - } - - /// - /// Get a random element from a list. - /// - private T GetRandom(List list) - { - if (list == null || list.Count == 0) - { - return default(T); - } - - return list[_random.Next(0, list.Count)]; - } - - /// - /// Apply all recipe changes stored in the masterDict to the game. - /// - /// The master dictionary. - internal static void ApplyMasterDict(EntitySerializer masterDict) - { - Dictionary.KeyCollection keys = masterDict.RecipeDict.Keys; - - foreach (TechType key in keys) - { - CraftDataHandler.SetTechData(key, masterDict.RecipeDict[key]); - } - - // TODO Once scrap metal is working, un-commenting this will apply the change on every startup. - //ChangeScrapMetalResult(masterDict.DictionaryInstance[TechType.Titanium]); - } - - /// - /// Apply a randomised recipe to the in-game craft data, and store a copy in the master dictionary. - /// - /// The master dictionary. - /// The recipe to change. - internal static void ApplyRandomisedRecipe(EntitySerializer masterDict, Recipe recipe) - { - CraftDataHandler.SetTechData(recipe.TechType, recipe); - masterDict.AddRecipe(recipe.TechType, recipe); - } - } -} diff --git a/SubnauticaRandomiser/Logic/Materials.cs b/SubnauticaRandomiser/Logic/Recipes/Materials.cs similarity index 88% rename from SubnauticaRandomiser/Logic/Materials.cs rename to SubnauticaRandomiser/Logic/Recipes/Materials.cs index aa805b8..37fef6f 100644 --- a/SubnauticaRandomiser/Logic/Materials.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Materials.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using SubnauticaRandomiser.RandomiserObjects; -namespace SubnauticaRandomiser.Logic +namespace SubnauticaRandomiser.Logic.Recipes { internal class Materials { @@ -155,5 +155,31 @@ private bool ContainsTechType(TechType[] array, TechType target) return false; } + + /// + /// Get all entities that are capable of being crafted. + /// + internal List GetAllCraftables() + { + var craftables = _allMaterials.FindAll(x => + !x.Category.Equals(ETechTypeCategory.RawMaterials) + && !x.Category.Equals(ETechTypeCategory.Fish) + && !x.Category.Equals(ETechTypeCategory.Seeds) + && !x.Category.Equals(ETechTypeCategory.Eggs) + && !x.Category.Equals(ETechTypeCategory.Fragments)); + + return craftables; + } + + /// + /// Get all entities that are considered fragments. + /// + internal List GetAllFragments() + { + var fragments = _allMaterials.FindAll(x => + x.Category.Equals(ETechTypeCategory.Fragments)); + + return fragments; + } } } diff --git a/SubnauticaRandomiser/Logic/Mode.cs b/SubnauticaRandomiser/Logic/Recipes/Mode.cs similarity index 95% rename from SubnauticaRandomiser/Logic/Mode.cs rename to SubnauticaRandomiser/Logic/Recipes/Mode.cs index 2cb8004..9df3033 100644 --- a/SubnauticaRandomiser/Logic/Mode.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Mode.cs @@ -5,24 +5,23 @@ using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; -namespace SubnauticaRandomiser.Logic +namespace SubnauticaRandomiser.Logic.Recipes { internal abstract class Mode { - protected RandomiserConfig _config; - protected Materials _materials; - protected ProgressionTree _tree; - protected Random _random; + protected readonly CoreLogic _logic; + protected RandomiserConfig _config => _logic._config; + protected Materials _materials => _logic._materials; + protected ProgressionTree _tree => _logic._tree; + protected Random _random => _logic._random; + protected List _ingredients = new List(); protected List _blacklist = new List(); protected LogicEntity _baseTheme; - protected Mode(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) + protected Mode(CoreLogic logic) { - _config = config; - _materials = materials; - _tree = tree; - _random = random; + _logic = logic; _baseTheme = ChooseBaseTheme(100); LogHandler.Debug("Chosen " + _baseTheme.TechType.AsString() + " as base theme."); diff --git a/SubnauticaRandomiser/Logic/ModeBalanced.cs b/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs similarity index 98% rename from SubnauticaRandomiser/Logic/ModeBalanced.cs rename to SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs index 3283987..4eb1078 100644 --- a/SubnauticaRandomiser/Logic/ModeBalanced.cs +++ b/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs @@ -4,14 +4,14 @@ using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; -namespace SubnauticaRandomiser.Logic +namespace SubnauticaRandomiser.Logic.Recipes { internal class ModeBalanced : Mode { private int _basicOutpostSize; private List _reachableMaterials; - internal ModeBalanced(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) : base(config, materials, tree, random) + internal ModeBalanced(CoreLogic logic) : base(logic) { _basicOutpostSize = 0; _reachableMaterials = _materials.GetReachable(); @@ -84,12 +84,14 @@ internal override LogicEntity RandomiseIngredients(LogicEntity entity) LogHandler.Debug("! Recipe is getting too large, stopping."); break; } + // Same thing for special case of outpost base parts. if (_tree.BasicOutpostPieces.ContainsKey(entity.TechType) && _basicOutpostSize > _config.iMaxBasicOutpostSize * 0.6) { LogHandler.Debug("! Basic outpost size is getting too large, stopping."); break; } + // Also, respect the maximum number of ingredients set in the config. if (_config.iMaxIngredientsPerRecipe <= _ingredients.Count) { diff --git a/SubnauticaRandomiser/Logic/ModeRandom.cs b/SubnauticaRandomiser/Logic/Recipes/ModeRandom.cs similarity index 94% rename from SubnauticaRandomiser/Logic/ModeRandom.cs rename to SubnauticaRandomiser/Logic/Recipes/ModeRandom.cs index d51a6ab..f7318c5 100644 --- a/SubnauticaRandomiser/Logic/ModeRandom.cs +++ b/SubnauticaRandomiser/Logic/Recipes/ModeRandom.cs @@ -3,13 +3,13 @@ using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; -namespace SubnauticaRandomiser.Logic +namespace SubnauticaRandomiser.Logic.Recipes { internal class ModeRandom : Mode { private List _reachableMaterials; - internal ModeRandom(RandomiserConfig config, Materials materials, ProgressionTree tree, Random random) : base(config, materials, tree, random) + internal ModeRandom(CoreLogic logic) : base(logic) { _reachableMaterials = _materials.GetReachable(); } diff --git a/SubnauticaRandomiser/Logic/ModeSubstitute.cs b/SubnauticaRandomiser/Logic/Recipes/ModeSubstitute.cs similarity index 98% rename from SubnauticaRandomiser/Logic/ModeSubstitute.cs rename to SubnauticaRandomiser/Logic/Recipes/ModeSubstitute.cs index a5133e9..7341c50 100644 --- a/SubnauticaRandomiser/Logic/ModeSubstitute.cs +++ b/SubnauticaRandomiser/Logic/Recipes/ModeSubstitute.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using SubnauticaRandomiser.RandomiserObjects; -namespace SubnauticaRandomiser.Logic +namespace SubnauticaRandomiser.Logic.Recipes { /// /// This is a legacy class, originally a revised implementation of the first Randomizer's approach to randomisation. @@ -113,7 +113,7 @@ internal void RandomSubstituteMaterials(EntitySerializer masterDict, bool useFis } } - RandomiserLogic.ApplyRandomisedRecipe(masterDict, randomiseMe.Recipe); + //RecipeLogic.ApplyRandomisedRecipe(randomiseMe.Recipe); } LogHandler.Info("Finished randomising."); diff --git a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs new file mode 100644 index 0000000..379899c --- /dev/null +++ b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using SMLHelper.V2.Handlers; +using SubnauticaRandomiser.RandomiserObjects; + +namespace SubnauticaRandomiser.Logic.Recipes +{ + /// + /// Handles everything related to randomising recipes. + /// + internal class RecipeLogic + { + private readonly CoreLogic _logic; + private readonly Mode _mode; + + private RandomiserConfig _config => _logic._config; + private EntitySerializer _masterDict => _logic._masterDict; + private Materials _materials => _logic._materials; + private ProgressionTree _tree => _logic._tree; + + public RecipeLogic(CoreLogic coreLogic) + { + _logic = coreLogic; + _mode = null; + + // Init the mode that will be used. + switch (_config.iRandomiserMode) + { + case (0): + _mode = new ModeBalanced(_logic); + break; + case (1): + _mode = new ModeRandom(_logic); + break; + default: + LogHandler.Error("Invalid recipe mode: " + _config.iRandomiserMode); + break; + } + } + + /// + /// Handle everything related to actually randomising the recipe itself, and ensure all special cases are covered. + /// + /// The recipe to randomise. + /// The available materials to use as potential ingredients. + /// The currently reachable depth. + /// True if the recipe was randomised, false otherwise. + internal bool RandomiseRecipe(LogicEntity entity, Dictionary unlockedProgressionItems, int reachableDepth) + { + // Does this recipe have all of its prerequisites fulfilled? Skip this check if the recipe is a priority. + if (!(_tree.IsPriorityEntity(entity) + || (CheckRecipeForBlueprint(entity, reachableDepth) && CheckRecipeForPrerequisites(entity)))) + { + LogHandler.Debug("--- Recipe [" + entity.TechType.AsString() + "] did not fulfill requirements, skipping."); + return false; + } + + entity = _mode.RandomiseIngredients(entity); + ApplyRandomisedRecipe(entity.Recipe); + + // Only add this entity to the materials list if it can be an ingredient. + if (entity.CanFunctionAsIngredient()) + _materials.AddReachable(entity); + + // Knives are a special case that open up a lot of new materials. + if ((entity.TechType.Equals(TechType.Knife) || entity.TechType.Equals(TechType.HeatBlade)) + && !unlockedProgressionItems.ContainsKey(TechType.Knife)) + unlockedProgressionItems.Add(TechType.Knife, true); + + // Similarly, Alien Containment is a special case for eggs. + if (entity.TechType.Equals(TechType.BaseWaterPark) && _config.bUseEggs) + unlockedProgressionItems.Add(TechType.BaseWaterPark, true); + + // If it is a central depth progression item, consider it unlocked. + if (_tree.DepthProgressionItems.ContainsKey(entity.TechType) && !unlockedProgressionItems.ContainsKey(entity.TechType)) + { + unlockedProgressionItems.Add(entity.TechType, true); + _logic._spoilerLog.AddProgressionEntry(entity.TechType, 0); + + LogHandler.Debug("[+] Added " + entity.TechType.AsString() + " to progression items."); + } + + entity.InLogic = true; + LogHandler.Debug("[+] Randomised recipe for [" + entity.TechType.AsString() + "]."); + + return true; + } + + /// + /// Get an essential or elective entity for the currently reachable depth, prioritising essential ones. + /// + /// The maximum depth to consider. + /// A LogicEntity, or null if all have been processed already. + [CanBeNull] + internal LogicEntity GetPriorityEntity(int depth) + { + List essentialItems = _tree.GetEssentialItems(depth); + List electiveItems = _tree.GetElectiveItems(depth); + LogicEntity entity = null; + + // Always get one of the essential items first, if available. + if (essentialItems != null && essentialItems.Count > 0) + { + entity = _materials.GetAll().Find(x => x.TechType.Equals(essentialItems[0])); + essentialItems.RemoveAt(0); + LogHandler.Debug("Prioritising essential item " + entity.TechType.AsString() + " for depth " + depth); + + // If this has already been randomised, all the better. + if (_masterDict.RecipeDict.ContainsKey(entity.TechType)) + { + entity = null; + LogHandler.Debug("Priority item was already randomised, skipping."); + } + } + + // Similarly, if all essential items are done, grab one from among + // the elective items and leave the rest up to chance. + if (entity is null && electiveItems != null && electiveItems.Count > 0) + { + TechType[] electiveTypes = electiveItems[0]; + electiveItems.RemoveAt(0); + + if (_logic.ContainsAny(_masterDict, electiveTypes)) + { + LogHandler.Debug("Priority elective containing " + electiveTypes[0].AsString() + " was already randomised, skipping."); + } + else + { + TechType nextType = _logic.GetRandom(new List(electiveTypes)); + entity = _materials.GetAll().Find(x => x.TechType.Equals(nextType)); + LogHandler.Debug("Prioritising elective item " + entity.TechType.AsString() + " for depth " + depth); + } + } + + return entity; + } + + /// + /// Add all reachable materials to the list, taking into account depth and any config options. + /// + /// The maximum depth to consider. + internal void UpdateReachableMaterials(int depth) + { + if (_masterDict.ContainsKnife()) + _materials.AddReachable(ETechTypeCategory.RawMaterials, depth); + else + _materials.AddReachableWithPrereqs(ETechTypeCategory.RawMaterials, depth, TechType.Knife, true); + + if (_config.bUseFish) + _materials.AddReachable(ETechTypeCategory.Fish, depth); + if (_config.bUseSeeds && _masterDict.ContainsKnife()) + _materials.AddReachable(ETechTypeCategory.Seeds, depth); + if (_config.bUseEggs && _masterDict.RecipeDict.ContainsKey(TechType.BaseWaterPark)) + _materials.AddReachable(ETechTypeCategory.Eggs, depth); + } + + /// + /// Check if this recipe fulfills all conditions to have its blueprint be unlocked. + /// + /// The master dictionary. + /// The list of all databoxes. + /// The recipe to check. + /// The maximum depth to consider. + /// True if the recipe fulfills all conditions, false otherwise. + private bool CheckRecipeForBlueprint(LogicEntity entity, int depth) + { + bool fulfilled = true; + + if (entity.Blueprint == null || (entity.Blueprint.UnlockConditions == null + && entity.Blueprint.UnlockDepth == 0)) + return true; + + // If the databox was randomised, do work to account for new locations. + // Cyclops hull modules need extra special treatment. + if (entity.Blueprint.NeedsDatabox && _logic._databoxes?.Count > 0 + && !entity.TechType.Equals(TechType.CyclopsHullModule2) + && !entity.TechType.Equals(TechType.CyclopsHullModule3)) + { + int total = 0; + int number = 0; + int lasercutter = 0; + int propulsioncannon = 0; + + foreach (Databox box in _logic._databoxes.FindAll(x => x.TechType.Equals(entity.TechType))) + { + total += (int)Math.Abs(box.Coordinates.y); + number++; + + if (box.RequiresLaserCutter) + lasercutter++; + if (box.RequiresPropulsionCannon) + propulsioncannon++; + } + + LogHandler.Debug("[B] Found " + number + " databoxes for " + entity.TechType.AsString()); + + entity.Blueprint.UnlockDepth = total / number; + if (entity.TechType.Equals(TechType.CyclopsHullModule1)) + { + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockDepth = total / number; + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockDepth = total / number; + } + + // If more than half of all locations of this databox require a + // tool to access the box, add it to the requirements for the recipe + if (lasercutter / number >= 0.5) + { + entity.Blueprint.UnlockConditions.Add(TechType.LaserCutter); + if (entity.TechType.Equals(TechType.CyclopsHullModule1)) + { + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockConditions.Add(TechType.LaserCutter); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockConditions.Add(TechType.LaserCutter); + } + } + + if (propulsioncannon / number >= 0.5) + { + entity.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + if (entity.TechType.Equals(TechType.CyclopsHullModule1)) + { + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) + .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) + .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + } + } + } + + foreach (TechType condition in entity.Blueprint.UnlockConditions) + { + LogicEntity conditionEntity = _materials.GetAll().Find(x => x.TechType.Equals(condition)); + + // Without this piece, the Air bladder will hang if fish are not + // enabled for the logic, as it fruitlessly searches for a bladderfish + // which never enters its algorithm. + // Eggs and seeds are never problematic in vanilla, but are covered + // in case users add their own modded items with those. + if (!_config.bUseFish && conditionEntity.Category.Equals(ETechTypeCategory.Fish)) + continue; + if (!_config.bUseEggs && conditionEntity.Category.Equals(ETechTypeCategory.Eggs)) + continue; + if (!_config.bUseSeeds && conditionEntity.Category.Equals(ETechTypeCategory.Seeds)) + continue; + + fulfilled &= (_masterDict.RecipeDict.ContainsKey(condition) + || _materials.GetReachable().Exists(x => x.TechType.Equals(condition))); + + if (!fulfilled) + return false; + } + + // Ensure that necessary fragments have already been randomised. + if (_config.bRandomiseFragments && entity.Blueprint.Fragments != null && entity.Blueprint.Fragments.Count > 0) + { + foreach (TechType fragment in entity.Blueprint.Fragments) + { + if (!_masterDict.SpawnDataDict.ContainsKey(fragment)) + { + LogHandler.Debug("[B] Entity " + entity.TechType.AsString() + " missing fragment " + + fragment.AsString()); + return false; + } + } + } + else if (entity.Blueprint.UnlockDepth > depth) + { + fulfilled = false; + } + + return fulfilled; + } + + /// + /// Check whether all prerequisites for this recipe have already been randomised. + /// + /// The recipe to check. + /// True if all conditions are fulfilled, false otherwise. + private bool CheckRecipeForPrerequisites(LogicEntity entity) + { + bool fulfilled = true; + + // The builder tool must always be randomised before any base pieces + // ever become accessible. + if (entity.Category.IsBasePiece() && !_masterDict.RecipeDict.ContainsKey(TechType.Builder)) + return false; + + if (entity.Prerequisites == null) + return true; + + foreach (TechType t in entity.Prerequisites) + { + fulfilled &= _masterDict.RecipeDict.ContainsKey(t); + if (!fulfilled) + break; + } + + return fulfilled; + } + + /// + /// Apply a randomised recipe to the in-game craft data, and store a copy in the master dictionary. + /// + /// The recipe to change. + internal void ApplyRandomisedRecipe(Recipe recipe) + { + CraftDataHandler.SetTechData(recipe.TechType, recipe); + _masterDict.AddRecipe(recipe.TechType, recipe); + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs index 07fd308..21f309c 100644 --- a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs +++ b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs @@ -22,9 +22,10 @@ public class LogicEntity public int MaxUsesPerGame; // How often can this get used in recipes? internal int _usedInRecipes; // How often did this get used in recipes? - public bool HasPrerequisites { get { return !(Prerequisites is null) && Prerequisites.Count > 0; } } - public bool HasRecipe { get { return !(Recipe is null); } } - public bool HasSpawnData { get { return !(SpawnData is null); } } + public bool HasPrerequisites => !(Prerequisites is null) && Prerequisites.Count > 0; + public bool HasRecipe => !(Recipe is null); + public bool HasSpawnData => !(SpawnData is null); + public bool IsFragment => Category.Equals(ETechTypeCategory.Fragments); public LogicEntity(TechType type, ETechTypeCategory category, Blueprint blueprint = null, Recipe recipe = null, SpawnData spawnData = null, List prerequisites = null, bool inLogic = false, int value = 0) { @@ -96,5 +97,10 @@ public bool HasUsesLeft() return false; } + + public override string ToString() + { + return TechType.AsString(); + } } } diff --git a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs index e1b917a..758c69f 100644 --- a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs +++ b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs @@ -13,7 +13,7 @@ public class SpoilerLog { internal const string _FileName = "spoilerlog.txt"; private RandomiserConfig _config; - internal static List> s_progression = new List>(); + private readonly List> _progression = new List>(); private List _basicOptions; private string[] _contentHeader; @@ -166,7 +166,7 @@ private string[] PrepareProgressionPath() List preparedProgressionPath = new List(); int lastDepth = 0; - foreach (KeyValuePair pair in s_progression) + foreach (KeyValuePair pair in _progression) { if (pair.Value > lastDepth) preparedProgressionPath.Add("Craft " + pair.Key.AsString() + " to reach " + pair.Value + "m"); @@ -179,6 +179,43 @@ private string[] PrepareProgressionPath() return preparedProgressionPath.ToArray(); } + /// + /// Add an entry for a progression item to the spoiler log. + /// + /// The progression item. + /// The depth it unlocks or was unlocked at. + /// True if successful, false if the entry already exists. + internal bool AddProgressionEntry(TechType type, int depth) + { + if (_progression.Exists(x => x.Key.Equals(type))) + { + LogHandler.Warn("Tried to add duplicate progression item to spoiler log: " + type.AsString()); + return false; + } + + var kvpair = new KeyValuePair(type, depth); + _progression.Add(kvpair); + return true; + } + + /// + /// When a progression item first gets unlocked, its depth reflects the depth required to reach it, rather than + /// what it makes accessible; update that here. + /// Always changes the latest addition to the spoiler log. + /// + /// The new depth to update the entry with. + /// True if successful, false if the update failed, e.g. if there are no entries in the list. + internal bool UpdateLastProgressionEntry(int depth) + { + if (_progression.Count == 0) + return false; + + // Since this is a list of immutable k-v pairs, it must be removed and replaced entirely. + TechType type = _progression[_progression.Count - 1].Key; + _progression.RemoveAt(_progression.Count - 1); + return AddProgressionEntry(type, depth); + } + /// /// Write the log to disk. /// diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index de88e5f..58ff899 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -42,6 +42,12 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis + + + + + + @@ -56,13 +62,8 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis - + - - - - - From 608aa9f7ca0aa9a4b7cb204edb67511250993eab Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 5 Aug 2022 20:15:56 +0200 Subject: [PATCH 05/37] Fix fragments not purging vanilla spawns on reload. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 2 +- SubnauticaRandomiser/Logic/FragmentLogic.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 63697f5..fd13acb 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -60,7 +60,7 @@ private void Setup(List notRandomised, Dictionary u if (_fragmentLogic != null) { // Initialise the fragment cache and remove vanilla spawns. - _fragmentLogic.Init(); + FragmentLogic.Init(); // Queue up all fragments to be randomised. notRandomised.AddRange(_materials.GetAllFragments()); } diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index c9ee47f..2d1f97a 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -14,12 +14,12 @@ internal class FragmentLogic { private readonly CoreLogic _logic; - private Dictionary> _classIdDatabase; + private static Dictionary> _classIdDatabase; private RandomiserConfig _config { get { return _logic._config; } } private EntitySerializer _masterDict { get { return _logic._masterDict; } } private Random _random { get { return _logic._random; } } private List _availableBiomes; - private readonly Dictionary _fragmentDataPaths = new Dictionary + private static readonly Dictionary _fragmentDataPaths = new Dictionary { { "BaseBioReactor_Fragment", TechType.BaseBioReactorFragment }, { "BaseNuclearReactor_Fragment", TechType.BaseNuclearReactorFragment }, @@ -124,7 +124,7 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) /// Go through all the BiomeData in the game and reset any fragment spawn rates to 0.0f, effectively "deleting" /// them from the game until the randomiser has decided on a new distribution. /// - internal void ResetFragmentSpawns() + internal static void ResetFragmentSpawns() { LogHandler.Debug("---Resetting vanilla fragment spawn rates---"); @@ -184,7 +184,7 @@ private List GetAvailableFragmentBiomes(List collections /// /// Assemble a dictionary of all relevant prefabs with their unique classId identifier. /// - private void PrepareClassIdDatabase() + private static void PrepareClassIdDatabase() { _classIdDatabase = new Dictionary>(); @@ -215,7 +215,7 @@ private void PrepareClassIdDatabase() /// Reverse the classId dictionary to allow for ID to TechType matching. /// /// The inverted dictionary. - internal Dictionary ReverseClassIdDatabase() + internal static Dictionary ReverseClassIdDatabase() { Dictionary database = new Dictionary(); @@ -236,10 +236,13 @@ internal Dictionary ReverseClassIdDatabase() } /// - /// Re-apply spawnData from a saved game. + /// Re-apply spawnData from a saved game. This will fail to catch all existing fragment spawns if called in a + /// previously randomised game. /// internal static void ApplyMasterDict(EntitySerializer masterDict) { + Init(); + foreach (TechType key in masterDict.SpawnDataDict.Keys) { SpawnData spawnData = masterDict.SpawnDataDict[key]; @@ -265,7 +268,7 @@ internal void ApplyRandomisedFragment(LogicEntity entity, SpawnData spawnData) /// /// Get the classId for the given TechType. /// - internal string GetClassId(TechType type) + private static string GetClassId(TechType type) { return CraftData.GetClassIdForTechType(type); } @@ -288,7 +291,7 @@ private T GetRandom(List list) /// Force Subnautica and SMLHelper to index and cache the classIds, setup the databases, and prepare a blank /// slate by removing all existing fragment spawns from the game. /// - public void Init() + public static void Init() { // This forces SMLHelper (and the game) to cache the classIds. // Without this, anything below will fail. From ca93b330cfa43e148ac0061c1edf351cfd1800df Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 5 Aug 2022 20:35:02 +0200 Subject: [PATCH 06/37] Re-enable databoxes. --- SubnauticaRandomiser/InitMod.cs | 3 +- SubnauticaRandomiser/Logic/CoreLogic.cs | 61 +++---------------- SubnauticaRandomiser/Logic/DataboxLogic.cs | 54 ++++++++++++++++ .../Logic/Recipes/RecipeLogic.cs | 17 ++++++ .../SubnauticaRandomiser.csproj | 1 + 5 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 SubnauticaRandomiser/Logic/DataboxLogic.cs diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index bc00d82..0faec2e 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -6,6 +6,7 @@ using QModManager.API.ModLoading; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.Logic; +using SubnauticaRandomiser.Logic.Recipes; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser @@ -59,7 +60,7 @@ public static void Initialise() if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) { // Load recipe changes. - CoreLogic.ApplyMasterDict(s_masterDict); + RecipeLogic.ApplyMasterDict(s_masterDict); // Load fragment changes. if (s_masterDict.SpawnDataDict?.Count > 0) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index fd13acb..8f47673 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -5,6 +5,7 @@ using SubnauticaRandomiser.Logic.Recipes; using SubnauticaRandomiser.RandomiserObjects; using UnityEngine; +using Random = System.Random; namespace SubnauticaRandomiser.Logic { @@ -21,6 +22,7 @@ internal class CoreLogic internal readonly SpoilerLog _spoilerLog; internal readonly ProgressionTree _tree; + private readonly DataboxLogic _databoxLogic; private readonly FragmentLogic _fragmentLogic; private readonly RecipeLogic _recipeLogic; @@ -35,6 +37,7 @@ public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserCo _spoilerLog = new SpoilerLog(config); // TODO: Respect config options. + _databoxLogic = new DataboxLogic(this); _fragmentLogic = new FragmentLogic(this, biomes); _recipeLogic = new RecipeLogic(this); _tree = new ProgressionTree(); @@ -57,6 +60,12 @@ private void Setup(List notRandomised, Dictionary u _tree.ApplyUpgradeChainToPrerequisites(_materials.GetAll()); } + if (_databoxLogic != null) + { + // Just randomise those flat out for now, instead of including them in the core loop. + _databoxLogic.RandomiseDataboxes(); + } + if (_fragmentLogic != null) { // Initialise the fragment cache and remove vanilla spawns. @@ -142,41 +151,6 @@ private LogicEntity ChooseNextEntity(List notRandomised, int depth) return next; } - /// - /// Randomise (shuffle) the blueprints found inside databoxes. - /// - /// The master dictionary. - /// A list of all databoxes. - /// The list of newly randomised databoxes. - [NotNull] - internal List RandomiseDataboxes(EntitySerializer masterDict, List databoxes) - { - masterDict.Databoxes = new Dictionary(); - List randomDataboxes = new List(); - List toBeRandomised = new List(); - - foreach (Databox dbox in databoxes) - { - toBeRandomised.Add(dbox.Coordinates); - } - - foreach (Databox originalBox in databoxes) - { - int next = _random.Next(0, toBeRandomised.Count); - Databox replacementBox = databoxes.Find(x => x.Coordinates.Equals(toBeRandomised[next])); - - randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, - replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); - masterDict.Databoxes.Add(new RandomiserVector(toBeRandomised[next]), originalBox.TechType); - LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " - + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); - toBeRandomised.RemoveAt(next); - } - masterDict.isDataboxRandomised = true; - - return randomDataboxes; - } - /// /// This function calculates the maximum reachable depth based on what vehicles the player has attained, as well /// as how much further they can go "on foot" @@ -396,22 +370,5 @@ public T GetRandom(List list) return list[_random.Next(0, list.Count)]; } - - /// - /// Apply all recipe changes stored in the masterDict to the game. - /// - /// The master dictionary. - internal static void ApplyMasterDict(EntitySerializer masterDict) - { - Dictionary.KeyCollection keys = masterDict.RecipeDict.Keys; - - foreach (TechType key in keys) - { - CraftDataHandler.SetTechData(key, masterDict.RecipeDict[key]); - } - - // TODO Once scrap metal is working, un-commenting this will apply the change on every startup. - //ChangeScrapMetalResult(masterDict.DictionaryInstance[TechType.Titanium]); - } } } diff --git a/SubnauticaRandomiser/Logic/DataboxLogic.cs b/SubnauticaRandomiser/Logic/DataboxLogic.cs new file mode 100644 index 0000000..6cb502a --- /dev/null +++ b/SubnauticaRandomiser/Logic/DataboxLogic.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using SubnauticaRandomiser.RandomiserObjects; +using UnityEngine; + +namespace SubnauticaRandomiser.Logic +{ + internal class DataboxLogic + { + private readonly CoreLogic _logic; + + private List _databoxes => _logic._databoxes; + private EntitySerializer _masterDict => _logic._masterDict; + private System.Random _random => _logic._random; + + internal DataboxLogic(CoreLogic logic) + { + _logic = logic; + } + + /// + /// Randomise (shuffle) the blueprints found inside databoxes. + /// + /// The list of newly randomised databoxes. + [NotNull] + internal List RandomiseDataboxes() + { + _masterDict.Databoxes = new Dictionary(); + List randomDataboxes = new List(); + List toBeRandomised = new List(); + + foreach (Databox dbox in _databoxes) + { + toBeRandomised.Add(dbox.Coordinates); + } + + foreach (Databox originalBox in _databoxes) + { + int next = _random.Next(0, toBeRandomised.Count); + Databox replacementBox = _databoxes.Find(x => x.Coordinates.Equals(toBeRandomised[next])); + + randomDataboxes.Add(new Databox(originalBox.TechType, toBeRandomised[next], replacementBox.Wreck, + replacementBox.RequiresLaserCutter, replacementBox.RequiresPropulsionCannon)); + _masterDict.Databoxes.Add(new RandomiserVector(toBeRandomised[next]), originalBox.TechType); + LogHandler.Debug("Databox " + toBeRandomised[next].ToString() + " with " + + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); + toBeRandomised.RemoveAt(next); + } + _masterDict.isDataboxRandomised = true; + + return randomDataboxes; + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs index 379899c..378e294 100644 --- a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs +++ b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs @@ -311,5 +311,22 @@ internal void ApplyRandomisedRecipe(Recipe recipe) CraftDataHandler.SetTechData(recipe.TechType, recipe); _masterDict.AddRecipe(recipe.TechType, recipe); } + + /// + /// Apply all recipe changes stored in the masterDict to the game. + /// + /// The master dictionary. + internal static void ApplyMasterDict(EntitySerializer masterDict) + { + Dictionary.KeyCollection keys = masterDict.RecipeDict.Keys; + + foreach (TechType key in keys) + { + CraftDataHandler.SetTechData(key, masterDict.RecipeDict[key]); + } + + // TODO Once scrap metal is working, un-commenting this will apply the change on every startup. + //ChangeScrapMetalResult(masterDict.DictionaryInstance[TechType.Titanium]); + } } } \ No newline at end of file diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 58ff899..ae4b5f4 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -42,6 +42,7 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis + From 8dbe7ebdb04447218596fba202c59f9e7016e5f5 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 5 Aug 2022 21:07:15 +0200 Subject: [PATCH 07/37] Add config option, fix save/loading. --- SubnauticaRandomiser/InitMod.cs | 48 +++++++++++++++--------- SubnauticaRandomiser/Logic/CoreLogic.cs | 40 ++++++++++---------- SubnauticaRandomiser/RandomiserConfig.cs | 4 ++ 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 0faec2e..e7b468e 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -57,22 +57,9 @@ public static void Initialise() } // Triple checking things here in case the save got corrupted somehow. - if (!_debug_forceRandomise && s_masterDict?.RecipeDict?.Count > 0) + if (!_debug_forceRandomise && s_masterDict != null) { - // Load recipe changes. - RecipeLogic.ApplyMasterDict(s_masterDict); - - // Load fragment changes. - if (s_masterDict.SpawnDataDict?.Count > 0) - { - FragmentLogic.ApplyMasterDict(s_masterDict); - LogHandler.Info("Loaded fragment state."); - } - - // Load databox changes. - if (s_masterDict.isDataboxRandomised) - EnableHarmonyPatching(); - + ApplyAllChanges(); LogHandler.Info("Successfully loaded game state from disk."); } else @@ -138,6 +125,31 @@ internal static void Randomise() SaveGameStateToDisk(); } + /// + /// Apply all changes contained within the serialiser. + /// + /// If the serialiser is null or invalid. + internal static void ApplyAllChanges() + { + if (s_masterDict is null) + throw new InvalidDataException("Cannot apply randomisation changes: MasterDict is null!"); + + // Load recipe changes. + if (s_masterDict.RecipeDict?.Count > 0) + RecipeLogic.ApplyMasterDict(s_masterDict); + + // Load fragment changes. + if (s_masterDict.SpawnDataDict?.Count > 0) + { + FragmentLogic.ApplyMasterDict(s_masterDict); + LogHandler.Info("Loaded fragment state."); + } + + // Load databox changes. + if (s_masterDict.isDataboxRandomised) + EnableHarmonyPatching(); + } + /// /// Ensure the user did not update into a save incompatibility. /// @@ -162,7 +174,7 @@ private static bool CheckSaveCompatibility() /// internal static void SaveGameStateToDisk() { - if (s_masterDict.RecipeDict != null && s_masterDict.RecipeDict.Count > 0) + if (s_masterDict != null) { string base64 = s_masterDict.ToBase64String(); s_config.sBase64Seed = base64; @@ -171,7 +183,7 @@ internal static void SaveGameStateToDisk() } else { - LogHandler.Error("Could not save game state to disk: Dictionary empty."); + LogHandler.Error("Could not save game state to disk: invalid data."); } } @@ -190,7 +202,7 @@ internal static EntitySerializer RestoreGameStateFromDisk() LogHandler.Debug("Trying to decode base64 string..."); EntitySerializer dictionary = EntitySerializer.FromBase64String(s_config.sBase64Seed); - if (dictionary?.RecipeDict is null || dictionary.RecipeDict.Count == 0) + if (dictionary?.SpawnDataDict is null || dictionary.RecipeDict is null) { throw new InvalidDataException("base64 seed is invalid; could not deserialize Dictionary."); } diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 8f47673..78bb429 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; -using JetBrains.Annotations; -using SMLHelper.V2.Handlers; using SubnauticaRandomiser.Logic.Recipes; using SubnauticaRandomiser.RandomiserObjects; -using UnityEngine; -using Random = System.Random; namespace SubnauticaRandomiser.Logic { @@ -35,11 +31,13 @@ public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserCo _materials = new Materials(allMaterials); _random = random; _spoilerLog = new SpoilerLog(config); - - // TODO: Respect config options. - _databoxLogic = new DataboxLogic(this); - _fragmentLogic = new FragmentLogic(this, biomes); - _recipeLogic = new RecipeLogic(this); + + if (_config.bRandomiseDataboxes) + _databoxLogic = new DataboxLogic(this); + if (_config.bRandomiseFragments) + _fragmentLogic = new FragmentLogic(this, biomes); + if (_config.bRandomiseRecipes) + _recipeLogic = new RecipeLogic(this); _tree = new ProgressionTree(); } @@ -48,18 +46,6 @@ public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserCo /// private void Setup(List notRandomised, Dictionary unlockedProgressionItems) { - if (_recipeLogic != null) - { - _recipeLogic.UpdateReachableMaterials(0); - // Queue up all craftables to be randomised. - notRandomised.AddRange(_materials.GetAllCraftables()); - - // Init the progression tree. - _tree.SetupVanillaTree(); - if (_config.bVanillaUpgradeChains) - _tree.ApplyUpgradeChainToPrerequisites(_materials.GetAll()); - } - if (_databoxLogic != null) { // Just randomise those flat out for now, instead of including them in the core loop. @@ -73,6 +59,18 @@ private void Setup(List notRandomised, Dictionary u // Queue up all fragments to be randomised. notRandomised.AddRange(_materials.GetAllFragments()); } + + if (_recipeLogic != null) + { + _recipeLogic.UpdateReachableMaterials(0); + // Queue up all craftables to be randomised. + notRandomised.AddRange(_materials.GetAllCraftables()); + + // Init the progression tree. + _tree.SetupVanillaTree(); + if (_config.bVanillaUpgradeChains) + _tree.ApplyUpgradeChainToPrerequisites(_materials.GetAll()); + } } internal void Randomise() diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index 9fdd30f..55a0363 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -31,6 +31,9 @@ public class RandomiserConfig : ConfigFile [Toggle("Randomise fragments?")] public bool bRandomiseFragments = ConfigDefaults.bRandomiseFragments; + [Toggle("Randomise recipes?")] + public bool bRandomiseRecipes = ConfigDefaults.bRandomiseRecipes; + [Toggle("Respect vanilla upgrade chains?")] public bool bVanillaUpgradeChains = ConfigDefaults.bVanillaUpgradeChains; @@ -156,6 +159,7 @@ internal static class ConfigDefaults internal const bool bUseSeeds = true; internal const bool bRandomiseDataboxes = true; internal const bool bRandomiseFragments = true; + internal const bool bRandomiseRecipes = true; internal const bool bVanillaUpgradeChains = false; internal const bool bDoBaseTheming = false; internal const int iEquipmentAsIngredients = 1; From 059bf44c720ce1a68f95bafc0c082ec61a002d2c Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Sat, 6 Aug 2022 13:13:24 +0200 Subject: [PATCH 08/37] More verbosity when randomising fails. --- SubnauticaRandomiser/CSVReader.cs | 77 +++++++++++++++---------- SubnauticaRandomiser/InitMod.cs | 21 ++++--- SubnauticaRandomiser/Logic/CoreLogic.cs | 8 ++- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/SubnauticaRandomiser/CSVReader.cs b/SubnauticaRandomiser/CSVReader.cs index 4d1e079..18d7e3d 100644 --- a/SubnauticaRandomiser/CSVReader.cs +++ b/SubnauticaRandomiser/CSVReader.cs @@ -8,24 +8,31 @@ namespace SubnauticaRandomiser { - internal static class CSVReader + internal class CSVReader { - internal static List s_csvRecipeList; - internal static List s_csvBiomeList; - internal static List s_csvDataboxList; + internal List _csvBiomeList; + internal List _csvDataboxList; + internal List _csvRecipeList; internal static string s_recipeCSVMD5; private const int _ExpectedColumns = 8; private const int _ExpectedRows = 245; private const int _ExpectedWreckColumns = 6; + internal CSVReader() + { + _csvBiomeList = new List(); + _csvDataboxList = new List(); + _csvRecipeList = new List(); + } + /// /// Attempt to parse the given file into a list of entities representing recipes. /// /// The file to parse. /// A list of LogicEntities if successful, null otherwise. [CanBeNull] - internal static List ParseRecipeFile(string fileName) + internal List ParseRecipeFile(string fileName) { // First, try to find and grab the file containing recipe information. string[] csvLines; @@ -57,7 +64,7 @@ internal static List ParseRecipeFile(string fileName) } // Second, read each line and try to parse that into a list of LogicEntity objects, for later use. - s_csvRecipeList = new List(); + _csvRecipeList = new List(); int lineCounter = 0; foreach (string line in csvLines) @@ -72,7 +79,7 @@ internal static List ParseRecipeFile(string fileName) // ParseRecipeFileLine fails upwards, so this ensures all errors are caught in one central location. try { - s_csvRecipeList.Add(ParseRecipeFileLine(line)); + _csvRecipeList.Add(ParseRecipeFileLine(line)); } catch (Exception ex) { @@ -81,7 +88,7 @@ internal static List ParseRecipeFile(string fileName) } } - return s_csvRecipeList; + return _csvRecipeList; } /// @@ -91,14 +98,14 @@ internal static List ParseRecipeFile(string fileName) /// The fully processed LogicEntity. /// If the format of the data is wrong. /// If a required column is missing or invalid. - private static LogicEntity ParseRecipeFileLine(string line) + private LogicEntity ParseRecipeFileLine(string line) { - TechType type = TechType.None; - ETechTypeCategory category = ETechTypeCategory.None; + TechType type; + ETechTypeCategory category; int depth = 0; Recipe recipe = null; List prereqList = new List(); - int value = 0; + int value; int maxUses = 0; Blueprint blueprint = null; @@ -172,7 +179,9 @@ private static LogicEntity ParseRecipeFileLine(string line) blueprintUnlockDepth = StringToInt(cellsBPDepth, "Blueprint Unlock Depth"); // Only if any of the blueprint components yielded anything, ship the entity with a blueprint. - if ((blueprintUnlockConditions.Count > 0) || blueprintUnlockDepth != 0 || !blueprintDatabox || blueprintFragments.Count > 0) + if ((blueprintUnlockConditions.Count > 0) || blueprintUnlockDepth != 0 + || !blueprintDatabox + || blueprintFragments.Count > 0) { blueprint = new Blueprint(type, blueprintUnlockConditions, blueprintFragments, blueprintDatabox, blueprintUnlockDepth); @@ -200,7 +209,7 @@ private static LogicEntity ParseRecipeFileLine(string line) /// The file to parse. /// A list of BiomeCollection if successful, null otherwise. [CanBeNull] - internal static List ParseBiomeFile(string fileName) + internal List ParseBiomeFile(string fileName) { // Try and grab the file containing biome information. string[] csvLines; @@ -218,7 +227,7 @@ internal static List ParseBiomeFile(string fileName) return null; } - s_csvBiomeList = new List(); + _csvBiomeList = new List(); int lineCounter = 0; foreach (string line in csvLines) @@ -234,13 +243,13 @@ internal static List ParseBiomeFile(string fileName) try { Biome biome = ParseBiomeFileLine(line); - BiomeCollection collection = s_csvBiomeList.Find(x => x.BiomeType.Equals(biome.BiomeType)); + BiomeCollection collection = _csvBiomeList.Find(x => x.BiomeType.Equals(biome.BiomeType)); // Initiate a BiomeCollection if it does not already exist. if (collection is null) { collection = new BiomeCollection(biome.BiomeType); - s_csvBiomeList.Add(collection); + _csvBiomeList.Add(collection); } collection.Add(biome); @@ -252,7 +261,7 @@ internal static List ParseBiomeFile(string fileName) } } - return s_csvBiomeList; + return _csvBiomeList; } /// @@ -261,12 +270,12 @@ internal static List ParseBiomeFile(string fileName) /// A string to parse. /// The fully processed Biome. /// If a required column is empty, missing or invalid. - private static Biome ParseBiomeFileLine(string line) + private Biome ParseBiomeFileLine(string line) { - Biome biome = null; - int smallCount = 0; - int mediumCount = 0; - int creatureCount = 0; + Biome biome; + int smallCount; + int mediumCount; + int creatureCount; float? fragmentRate = null; string[] cells = line.Split(','); @@ -318,7 +327,7 @@ private static Biome ParseBiomeFileLine(string line) /// The file to parse. /// A list of Databoxes if successful, null otherwise. [CanBeNull] - internal static List ParseWreckageFile(string fileName) + internal List ParseWreckageFile(string fileName) { string[] csvLines; string path = Path.Combine(InitMod.s_modDirectory, fileName); @@ -335,7 +344,7 @@ internal static List ParseWreckageFile(string fileName) return null; } - s_csvDataboxList = new List(); + _csvDataboxList = new List(); int lineCounter = 0; foreach (string line in csvLines) @@ -352,7 +361,7 @@ internal static List ParseWreckageFile(string fileName) { Databox databox = ParseWreckageFileLine(line); if (databox != null) - s_csvDataboxList.Add(databox); + _csvDataboxList.Add(databox); } catch (Exception ex) { @@ -361,7 +370,7 @@ internal static List ParseWreckageFile(string fileName) } } - return s_csvDataboxList; + return _csvDataboxList; } /// @@ -370,9 +379,9 @@ internal static List ParseWreckageFile(string fileName) /// A string to parse. /// The fully processed Databox. /// If a required column is empty, missing or invalid. - private static Databox ParseWreckageFileLine(string line) + private Databox ParseWreckageFileLine(string line) { - TechType type = TechType.None; + TechType type; Vector3 coordinates; EWreckage wreck = EWreckage.None; bool isDatabox; @@ -594,4 +603,14 @@ private static int StringToInt(string input, string column) return output; } } + + /// + /// The exception that is thrown when an input file cannot be parsed properly into the expected objects. + /// + public class ParsingException : Exception + { + public ParsingException() {} + + public ParsingException(string message) : base(message) {} + } } diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index e7b468e..265869a 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -85,28 +85,31 @@ internal static void Randomise() s_masterDict = new EntitySerializer(); s_config.SanitiseConfigValues(); s_config.iSaveVersion = s_expectedSaveVersion; + var csvReader = new CSVReader(); // Attempt to read and parse the CSV with all biome information. - var completeBiomeList = CSVReader.ParseBiomeFile(s_biomeFile); - if (completeBiomeList is null) + var biomes = csvReader.ParseBiomeFile(s_biomeFile); + if (biomes is null) { LogHandler.Fatal("Failed to extract biome information from CSV, aborting."); - return; + throw new ParsingException("Failed to extract biome information: null"); } // Attempt to read and parse the CSV with all recipe information. - var completeMaterialsList = CSVReader.ParseRecipeFile(s_recipeFile); - if (completeMaterialsList is null) + var materials = csvReader.ParseRecipeFile(s_recipeFile); + if (materials is null) { LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); - return; + throw new ParsingException("Failed to extract recipe information: null"); } // Attempt to read and parse the CSV with wreckages and databox info. - List databoxes; - databoxes = CSVReader.ParseWreckageFile(s_wreckageFile); + var databoxes = csvReader.ParseWreckageFile(s_wreckageFile); if (databoxes is null || databoxes.Count == 0) + { LogHandler.Error("Failed to extract databox information from CSV."); + throw new ParsingException("Failed to extract databox information: null"); + } // Create a new seed if the current one is just a default Random random; @@ -118,7 +121,7 @@ internal static void Randomise() random = new Random(s_config.iSeed); // Randomise! - CoreLogic logic = new CoreLogic(random, s_masterDict, s_config, completeMaterialsList, completeBiomeList, databoxes); + CoreLogic logic = new CoreLogic(random, s_masterDict, s_config, materials, biomes, databoxes); logic.Randomise(); LogHandler.Info("Randomisation successful!"); diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 78bb429..f328a52 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -73,6 +73,11 @@ private void Setup(List notRandomised, Dictionary u } } + /// + /// Start the randomisation process. + /// + /// Raised to prevent infinite loops if the core loop takes too long to find + /// a valid solution. internal void Randomise() { LogHandler.Info("Randomising using logic-based system..."); @@ -93,8 +98,7 @@ internal void Randomise() { LogHandler.MainMenuMessage("Failed to randomise items: stuck in infinite loop!"); LogHandler.Fatal("Encountered infinite loop, aborting!"); - // TODO: Throw exception. - break; + throw new TimeoutException("Encountered infinite loop while randomising!"); } // Update depth and reachable materials. From c1ba7a0b0a11f0dfde301aef19aca5517b56c627 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Sat, 6 Aug 2022 16:10:16 +0200 Subject: [PATCH 09/37] Refactor depth calculations. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 185 +++++++----------- .../RandomiserObjects/EProgressionNode.cs | 17 +- 2 files changed, 86 insertions(+), 116 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index f328a52..c6e6f90 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using SubnauticaRandomiser.Logic.Recipes; using SubnauticaRandomiser.RandomiserObjects; @@ -44,7 +45,7 @@ public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserCo /// /// Set up all the necessary structures for later. /// - private void Setup(List notRandomised, Dictionary unlockedProgressionItems) + private void Setup(List notRandomised) { if (_databoxLogic != null) { @@ -86,7 +87,7 @@ internal void Randomise() Dictionary unlockedProgressionItems = new Dictionary(); // Set up basic structures. - Setup(notRandomised, unlockedProgressionItems); + Setup(notRandomised); int circuitbreaker = 0; int currentDepth = 0; @@ -156,7 +157,6 @@ private LogicEntity ChooseNextEntity(List notRandomised, int depth) /// /// This function calculates the maximum reachable depth based on what vehicles the player has attained, as well /// as how much further they can go "on foot" - /// TODO: Simplify this. /// /// A list of all currently reachable items relevant for progression. /// The minimum time that it must be possible to spend at the reachable depth before @@ -164,54 +164,29 @@ private LogicEntity ChooseNextEntity(List notRandomised, int depth) /// The reachable depth. internal int CalculateReachableDepth(Dictionary progressionItems, int depthTime = 15) { - double swimmingSpeed = 4.7; // Assuming player is holding a tool. - double seaglideSpeed = 11.0; + const double swimmingSpeed = 4.7; // Always assume that the player is holding a tool. + const double seaglideSpeed = 11.0; bool seaglide = progressionItems.ContainsKey(TechType.Seaglide); double finSpeed = 0.0; - double tankPenalty = 0.0; - int breathTime = 45; - - // How long should the player be able to remain at this depth and still make it back just fine? - int searchTime = depthTime; - // Never assume the player has to go deeper than this on foot. - int maxSoloDepth = 300; int vehicleDepth = 0; - double playerDepthRaw; - double totalDepth; + Dictionary tanks = new Dictionary + { + { TechType.Tank, new[] { 75, 0.4 } }, // Tank type, oxygen, weight factor. + { TechType.DoubleTank, new[] { 135, 0.47 } }, + { TechType.HighCapacityTank, new[] { 225, 0.6 } }, + { TechType.PlasteelTank, new[] { 135, 0.1 } } + }; LogHandler.Debug("===== Recalculating reachable depth ====="); - // This feels like it could be simplified. - // Also, this trusts that the tree is set up correctly. - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth200m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 200; - } - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth300m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 300; - } - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth500m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 500; - } - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth900m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 900; - } - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth1300m).Pathways) - { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 1300; - } - foreach (TechType[] path in _tree.GetProgressionPath(EProgressionNode.Depth1700m).Pathways) + // Get the deepest depth that can be reached by vehicle. + foreach (EProgressionNode node in EProgressionNodeExtensions.AllDepthNodes) { - if (CheckDictForAllTechTypes(progressionItems, path)) - vehicleDepth = 1700; + foreach (TechType[] path in _tree?.GetProgressionPath(node)?.Pathways ?? Enumerable.Empty()) + { + if (CheckDictForAllTechTypes(progressionItems, path)) + vehicleDepth = Math.Max(vehicleDepth, (int)node); + } } if (progressionItems.ContainsKey(TechType.Fins)) @@ -220,87 +195,67 @@ internal int CalculateReachableDepth(Dictionary progressionItems finSpeed = 1.88; // How deep can the player go without any tanks? - playerDepthRaw = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed)) / 2; + double soloDepthRaw = (45 - depthTime) * (seaglide ? seaglideSpeed : swimmingSpeed + finSpeed) / 2; - // But can they go deeper with a tank? (Yes.) - if (progressionItems.ContainsKey(TechType.Tank)) + // How deep can they go with tanks? + foreach (var kv in tanks) { - breathTime = 75; - tankPenalty = 0.4; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; + if (progressionItems.ContainsKey(kv.Key)) + { + // Value[0] is the oxygen granted by the tank, Value[1] its weight factor. + double depth = (kv.Value[0] - depthTime) + * (seaglide ? seaglideSpeed : swimmingSpeed + finSpeed - kv.Value[1]) / 2; + soloDepthRaw = Math.Max(soloDepthRaw, depth); + } } - if (progressionItems.ContainsKey(TechType.DoubleTank)) - { - breathTime = 135; - tankPenalty = 0.47; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } + // Given everything above, calculate the total. + int totalDepth = CalculateTotalDepth(progressionItems, vehicleDepth, (int)soloDepthRaw); + + LogHandler.Debug("===== New reachable depth: " + totalDepth + " ====="); - if (progressionItems.ContainsKey(TechType.HighCapacityTank)) - { - breathTime = 225; - tankPenalty = 0.6; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } + return totalDepth; + } - if (progressionItems.ContainsKey(TechType.PlasteelTank)) + /// + /// Calculate the depth that can be comfortably reached on foot. + /// + /// The depth reachable by vehicle. + /// The raw depth reachable on foot given no depth restrictions. + /// The depth that can be covered on foot in addition to the depth reachable by vehicle. + private int CalculateSoloDepth(int vehicleDepth, int soloDepthRaw) + { + // Ensure that a number stays between a lower and upper bound. (e.g. 0 < x < 100) + double limit(double x, double upperBound) => Math.Max(0, Math.Min(x, upperBound)); + // Calculate how much of the 0-100m and 100-200m range is already covered by vehicles. + double[] vehicleDepths = { limit(vehicleDepth, 100), limit(vehicleDepth - 100, 100) }; + double[] soloDepths = { - breathTime = 135; - tankPenalty = 0.1; - double depth = (breathTime - searchTime) * (seaglide ? seaglideSpeed : (swimmingSpeed + finSpeed - tankPenalty)) / 2; - playerDepthRaw = depth > playerDepthRaw ? depth : playerDepthRaw; - } + limit(soloDepthRaw, 100 - vehicleDepths[0]), + limit(soloDepthRaw + vehicleDepths[0] - 100, 100 - vehicleDepths[1]), + limit(soloDepthRaw + vehicleDepths[1] - 200, 10000) + }; + + // Below 100 meters, air is consumed three times as fast. + // Below 200 meters, it is consumed five times as fast. + return (int)(soloDepths[0] + soloDepths[1] / 3 + soloDepths[2] / 5); + } - // The vehicle depth and whether or not the player has a rebreather can modify the raw achievable diving depth. + /// + /// Calculate the total depth that can be reached given the available equipment. + /// + /// The unlocked progression items. + /// The depth reachable by vehicle. + /// The raw depth reachable on foot given no oxygen restrictions. + /// The total depth coverable by extending vehicle depth with a solo journey. + private int CalculateTotalDepth(Dictionary progressionItems, int vehicleDepth, int soloDepthRaw) + { + const int maxSoloDepth = 300; // Never make the player go deeper than this on foot. + // If there is a rebreather, all the funky calculations are redundant. if (progressionItems.ContainsKey(TechType.Rebreather)) - { - totalDepth = vehicleDepth + (playerDepthRaw > maxSoloDepth ? maxSoloDepth : playerDepthRaw); - } - else - { - // Below 100 meters, air is consumed three times as fast. - // Below 200 meters, it is consumed five times as fast. - double depth = 0.0; - - if (vehicleDepth == 0) - { - if (playerDepthRaw <= 100) - { - depth = playerDepthRaw; - } - else - { - depth += 100; - playerDepthRaw -= 100; - - // For anything between 100-200 meters, triple air consumption - if (playerDepthRaw <= 100) - { - depth += playerDepthRaw / 3; - } - else - { - depth += 33.3; - playerDepthRaw -= 100; - // For anything below 200 meters, quintuple it. - depth += playerDepthRaw / 5; - } - } - } - else - { - depth = playerDepthRaw / 5; - } - - totalDepth = vehicleDepth + (depth > maxSoloDepth ? maxSoloDepth : depth); - } - LogHandler.Debug("===== New reachable depth: " + totalDepth + " ====="); + return vehicleDepth + Math.Min(soloDepthRaw, maxSoloDepth); - return (int)totalDepth; + return vehicleDepth + Math.Min(CalculateSoloDepth(vehicleDepth, soloDepthRaw), maxSoloDepth); } /// diff --git a/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs b/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs index 3d5e7f6..310a10a 100644 --- a/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs +++ b/SubnauticaRandomiser/RandomiserObjects/EProgressionNode.cs @@ -11,6 +11,21 @@ public enum EProgressionNode Depth500m = 500, Depth900m = 900, Depth1300m = 1300, - Depth1700m = 1700, + Depth1700m = 1700 + } + + public static class EProgressionNodeExtensions + { + public static EProgressionNode[] AllDepthNodes => new[] + { + EProgressionNode.Depth0m, + EProgressionNode.Depth100m, + EProgressionNode.Depth200m, + EProgressionNode.Depth300m, + EProgressionNode.Depth500m, + EProgressionNode.Depth900m, + EProgressionNode.Depth1300m, + EProgressionNode.Depth1700m + }; } } From dbc7a9ddee5ed6376f57097f3b256f8ce52aa44d Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Sat, 6 Aug 2022 16:32:16 +0200 Subject: [PATCH 10/37] Cleanup. --- .../Logic/Recipes/Materials.cs | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/SubnauticaRandomiser/Logic/Recipes/Materials.cs b/SubnauticaRandomiser/Logic/Recipes/Materials.cs index 37fef6f..bf619f2 100644 --- a/SubnauticaRandomiser/Logic/Recipes/Materials.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Materials.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser.Logic.Recipes @@ -32,7 +33,7 @@ internal bool AddReachable(ETechTypeCategory[] categories, int maxDepth) List additions = new List(); // Use a lambda expression to find every object where the search parameters match. - additions.AddRange(_allMaterials.FindAll(x => ContainsCategory(categories, x.Category) && x.AccessibleDepth <= maxDepth)); + additions.AddRange(_allMaterials.FindAll(x => categories.Contains(x.Category) && x.AccessibleDepth <= maxDepth)); return AddToReachableList(additions); } @@ -53,7 +54,7 @@ internal bool AddReachableWithPrereqs(ETechTypeCategory[] categories, int maxDep if (invert) { - additions.AddRange(_allMaterials.FindAll(x => ContainsCategory(categories, x.Category) + additions.AddRange(_allMaterials.FindAll(x => categories.Contains(x.Category) && x.AccessibleDepth <= maxDepth && (!x.HasPrerequisites || !x.Prerequisites.Contains(prerequisite)) @@ -61,7 +62,7 @@ internal bool AddReachableWithPrereqs(ETechTypeCategory[] categories, int maxDep } else { - additions.AddRange(_allMaterials.FindAll(x => ContainsCategory(categories, x.Category) + additions.AddRange(_allMaterials.FindAll(x => categories.Contains(x.Category) && x.AccessibleDepth <= maxDepth && x.HasPrerequisites && x.Prerequisites.Contains(prerequisite) @@ -133,29 +134,6 @@ internal bool AddReachableWithPrereqs(ETechTypeCategory category, int maxDepth, return AddReachableWithPrereqs(new[] { category }, maxDepth, prerequisite, invert); } - // TODO: Generalise this. - private bool ContainsCategory(ETechTypeCategory[] array, ETechTypeCategory target) - { - foreach (ETechTypeCategory category in array) - { - if (category.Equals(target)) - return true; - } - - return false; - } - - private bool ContainsTechType(TechType[] array, TechType target) - { - foreach (TechType type in array) - { - if (type.Equals(target)) - return true; - } - - return false; - } - /// /// Get all entities that are capable of being crafted. /// From ff75f71282e3341a661e0988431677f96d8a7580 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Sat, 6 Aug 2022 20:50:51 +0200 Subject: [PATCH 11/37] Fix priority items always choosing the first in the list. --- SubnauticaRandomiser/Logic/ProgressionTree.cs | 53 ++++++++++--------- .../Logic/Recipes/RecipeLogic.cs | 36 +++++-------- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/SubnauticaRandomiser/Logic/ProgressionTree.cs b/SubnauticaRandomiser/Logic/ProgressionTree.cs index 83f9f8e..79134ae 100644 --- a/SubnauticaRandomiser/Logic/ProgressionTree.cs +++ b/SubnauticaRandomiser/Logic/ProgressionTree.cs @@ -90,7 +90,6 @@ public void SetupVanillaTree() path.AddPath(new [] { TechType.Cyclops, TechType.CyclopsHullModule3 }); SetProgressionPath(EProgressionNode.Depth1700m, path); - // Putting every item or vehicle that can help the player achieve // lower depths in one dictionary. DepthProgressionItems.Add(TechType.Fins, true); @@ -115,7 +114,6 @@ public void SetupVanillaTree() DepthProgressionItems.Add(TechType.CyclopsHullModule2, true); DepthProgressionItems.Add(TechType.CyclopsHullModule3, true); - // Assemble a dictionary of what's considered basic outpost pieces // which together should not exceed the cost of config.iMaxBasicOutpostSize BasicOutpostPieces.Add(TechType.BaseCorridorI, 1); @@ -125,7 +123,6 @@ public void SetupVanillaTree() BasicOutpostPieces.Add(TechType.Beacon, 1); BasicOutpostPieces.Add(TechType.SolarPanel, 2); - // The scanner and repair tool are absolutely required to get the // early game going, without the others it can get tedious. AddEssentialItem(EProgressionNode.Depth0m, TechType.Scanner); @@ -141,7 +138,6 @@ public void SetupVanillaTree() AddEssentialItem(EProgressionNode.Depth300m, TechType.BaseWaterPark); - // From among these, at least one has to be accessible by the provided // depth level. Ensures e.g. at least one power source by 200m. AddElectiveItems(EProgressionNode.Depth100m, new [] { TechType.Battery, TechType.BatteryCharger }); @@ -150,7 +146,6 @@ public void SetupVanillaTree() AddElectiveItems(EProgressionNode.Depth200m, new [] { TechType.PowerCell, TechType.PowerCellCharger, TechType.SeamothSolarCharge }); AddElectiveItems(EProgressionNode.Depth200m, new [] { TechType.BaseBulkhead, TechType.BaseFoundation, TechType.BaseReinforcement }); - // Assemble a vanilla upgrade chain. These are the upgrades as the // base game intends you to progress through them. _upgradeChains = new Dictionary(); @@ -294,7 +289,7 @@ public bool AddUpgradeChain(TechType upgrade, TechType ingredient) /// The node. /// The list of essential items, or null if it doesn't exist or the node is invalid. [CanBeNull] - public List GetEssentialItems(EProgressionNode node) + public List GetEssentialNodeItems(EProgressionNode node) { if (_essentialItems.TryGetValue(node, out List items)) return items; @@ -303,21 +298,25 @@ public List GetEssentialItems(EProgressionNode node) } /// - /// Get essential items for the given depth. + /// Get all essential items up to the given depth. /// /// The maximum depth to look for. - /// The list of essential items, or null if it doesn't exist or the given depth does not resolve to - /// a progression node. - [CanBeNull] + /// The list of essential items, or null if it doesn't exist. + [NotNull] public List GetEssentialItems(int depth) { - // FIXME pretty sure this always yields the first match only. - foreach (EProgressionNode node in _essentialItems.Keys) + var essentials = new List(); + + foreach (EProgressionNode node in EProgressionNodeExtensions.AllDepthNodes) { - if ((int)node < depth && _essentialItems[node].Count > 0) - return _essentialItems[node]; + if ((int)node > depth) + break; + + if (_essentialItems.TryGetValue(node, out var list)) + essentials.AddRange(list); } - return null; + + return essentials; } /// @@ -326,7 +325,7 @@ public List GetEssentialItems(int depth) /// The node. /// The list of elective items, or null if it doesn't exist or the node is invalid. [CanBeNull] - public List GetElectiveItems(EProgressionNode node) + public List GetElectiveNodeItems(EProgressionNode node) { if (_electiveItems.TryGetValue(node, out List items)) return items; @@ -335,21 +334,25 @@ public List GetElectiveItems(EProgressionNode node) } /// - /// Get the list of lists of elective items for the given depth. + /// Get the list of lists of elective items up to the given depth. /// /// The maximum depth to look for. - /// The list of elective items, or null if it doesn't exist or the given depth does not resolve to - /// a progression node. - [CanBeNull] + /// The list of elective items, or null if it doesn't exist. + [NotNull] public List GetElectiveItems(int depth) { - // FIXME pretty sure this always yields the first match only. - foreach (EProgressionNode node in _electiveItems.Keys) + var electives = new List(); + + foreach (EProgressionNode node in EProgressionNodeExtensions.AllDepthNodes) { - if ((int)node < depth && _electiveItems[node].Count > 0) - return _electiveItems[node]; + if ((int)node > depth) + break; + + if (_electiveItems.TryGetValue(node, out var list)) + electives.AddRange(list); } - return null; + + return electives; } /// diff --git a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs index 378e294..c7113a0 100644 --- a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs +++ b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; @@ -100,35 +101,26 @@ internal LogicEntity GetPriorityEntity(int depth) LogicEntity entity = null; // Always get one of the essential items first, if available. - if (essentialItems != null && essentialItems.Count > 0) + if (essentialItems.Count > 0) { - entity = _materials.GetAll().Find(x => x.TechType.Equals(essentialItems[0])); - essentialItems.RemoveAt(0); - LogHandler.Debug("Prioritising essential item " + entity.TechType.AsString() + " for depth " + depth); - - // If this has already been randomised, all the better. - if (_masterDict.RecipeDict.ContainsKey(entity.TechType)) + TechType type = essentialItems.Find(x => !_masterDict.RecipeDict.ContainsKey(x)); + if (!type.Equals(TechType.None)) { - entity = null; - LogHandler.Debug("Priority item was already randomised, skipping."); + entity = _materials.GetAll().Find(e => e.TechType.Equals(type)); + LogHandler.Debug("Prioritising essential item " + entity.TechType.AsString() + " for depth " + depth); } } - // Similarly, if all essential items are done, grab one from among - // the elective items and leave the rest up to chance. - if (entity is null && electiveItems != null && electiveItems.Count > 0) + // Similarly, if all essential items are done, grab one from among the elective items and leave the rest + // up to chance. + if (entity is null && electiveItems.Count > 0) { - TechType[] electiveTypes = electiveItems[0]; - electiveItems.RemoveAt(0); - - if (_logic.ContainsAny(_masterDict, electiveTypes)) - { - LogHandler.Debug("Priority elective containing " + electiveTypes[0].AsString() + " was already randomised, skipping."); - } - else + TechType[] types = electiveItems.Find(arr => arr.All(x => !_masterDict.RecipeDict.ContainsKey(x))); + + if (types?.Length > 0) { - TechType nextType = _logic.GetRandom(new List(electiveTypes)); - entity = _materials.GetAll().Find(x => x.TechType.Equals(nextType)); + TechType nextType = _logic.GetRandom(new List(types)); + entity = _materials.GetAll().Find(e => e.TechType.Equals(nextType)); LogHandler.Debug("Prioritising elective item " + entity.TechType.AsString() + " for depth " + depth); } } From 1027f1f3df5d9d007dbcecca2f2ee10f5480ec8f Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Sat, 6 Aug 2022 23:12:49 +0200 Subject: [PATCH 12/37] Clean up blueprint and prerequisite checking. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 69 +++++++- .../Logic/Recipes/Materials.cs | 12 ++ .../Logic/Recipes/RecipeLogic.cs | 158 +----------------- .../RandomiserObjects/Blueprint.cs | 52 ++++++ .../RandomiserObjects/LogicEntity.cs | 79 ++++++++- 5 files changed, 215 insertions(+), 155 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index c6e6f90..bd74c7b 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using SubnauticaRandomiser.Logic.Recipes; using SubnauticaRandomiser.RandomiserObjects; @@ -9,7 +10,7 @@ namespace SubnauticaRandomiser.Logic /// /// Acts as the core for handling all randomising logic in the mod, and turning modules on/off as needed. /// - internal class CoreLogic + public class CoreLogic { internal readonly RandomiserConfig _config; internal readonly List _databoxes; @@ -51,6 +52,7 @@ private void Setup(List notRandomised) { // Just randomise those flat out for now, instead of including them in the core loop. _databoxLogic.RandomiseDataboxes(); + LinkCyclopsHullModules(); } if (_fragmentLogic != null) @@ -278,6 +280,71 @@ private int UpdateReachableDepth(int currentDepth, Dictionary pr return currentDepth; } + + /// + /// Cyclops hull modules are linked and unlock together once the blueprint for module1 is found. Do the work + /// for module1 and synchronise them. + /// + /// If the LogicEntity or databox for one of the hull modules cannot be + /// found. + private void LinkCyclopsHullModules() + { + if (!(_databoxes?.Count > 0)) + { + LogHandler.Debug("Skipped linking Cyclops Hull Modules: Databoxes not randomised."); + return; + } + + LogicEntity mod1 = _materials.Find(TechType.CyclopsHullModule1); + LogicEntity mod2 = _materials.Find(TechType.CyclopsHullModule2); + LogicEntity mod3 = _materials.Find(TechType.CyclopsHullModule3); + + if (mod1 is null || mod2 is null || mod3 is null) + throw new InvalidDataException("Tried to link Cyclops Hull Modules, but found null."); + + int total = 0; + int number = 0; + int lasercutter = 0; + int propulsioncannon = 0; + + foreach (Databox box in _databoxes.FindAll(x => x.TechType.Equals(mod1.TechType))) + { + total += (int)Math.Abs(box.Coordinates.y); + number++; + + if (box.RequiresLaserCutter) + lasercutter++; + if (box.RequiresPropulsionCannon) + propulsioncannon++; + } + + if (number == 0) + throw new InvalidDataException("Entity " + this + " requires a databox, but 0 were found!"); + + mod1.Blueprint.UnlockDepth = total / number; + mod2.Blueprint.UnlockDepth = total / number; + mod3.Blueprint.UnlockDepth = total / number; + + if (lasercutter / number >= 0.5) + { + mod1.Blueprint.UnlockConditions.Add(TechType.LaserCutter); + mod2.Blueprint.UnlockConditions.Add(TechType.LaserCutter); + mod3.Blueprint.UnlockConditions.Add(TechType.LaserCutter); + } + + if (propulsioncannon / number >= 0.5) + { + mod1.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + mod2.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + mod3.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); + } + + mod1.Blueprint.WasUpdated = true; + mod2.Blueprint.WasUpdated = true; + mod3.Blueprint.WasUpdated = true; + + LogHandler.Debug("Linked Cyclops Hull Modules."); + } /// /// Check whether all TechTypes given in the array are present in the given dictionary. diff --git a/SubnauticaRandomiser/Logic/Recipes/Materials.cs b/SubnauticaRandomiser/Logic/Recipes/Materials.cs index bf619f2..674b7cf 100644 --- a/SubnauticaRandomiser/Logic/Recipes/Materials.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Materials.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser.Logic.Recipes @@ -134,6 +135,17 @@ internal bool AddReachableWithPrereqs(ETechTypeCategory category, int maxDepth, return AddReachableWithPrereqs(new[] { category }, maxDepth, prerequisite, invert); } + /// + /// Get the corresponding LogicEntity to the given TechType. + /// + /// The TechType. + /// The LogicEntity if found, null otherwise. + [CanBeNull] + internal LogicEntity Find(TechType type) + { + return _allMaterials.Find(x => x.TechType.Equals(type)); + } + /// /// Get all entities that are capable of being crafted. /// diff --git a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs index c7113a0..923501a 100644 --- a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs +++ b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -51,7 +50,7 @@ internal bool RandomiseRecipe(LogicEntity entity, Dictionary unl { // Does this recipe have all of its prerequisites fulfilled? Skip this check if the recipe is a priority. if (!(_tree.IsPriorityEntity(entity) - || (CheckRecipeForBlueprint(entity, reachableDepth) && CheckRecipeForPrerequisites(entity)))) + || (entity.CheckBlueprintFulfilled(_logic, reachableDepth) && entity.CheckPrerequisitesFulfilled(_logic)))) { LogHandler.Debug("--- Recipe [" + entity.TechType.AsString() + "] did not fulfill requirements, skipping."); return false; @@ -106,8 +105,8 @@ internal LogicEntity GetPriorityEntity(int depth) TechType type = essentialItems.Find(x => !_masterDict.RecipeDict.ContainsKey(x)); if (!type.Equals(TechType.None)) { - entity = _materials.GetAll().Find(e => e.TechType.Equals(type)); - LogHandler.Debug("Prioritising essential item " + entity.TechType.AsString() + " for depth " + depth); + entity = _materials.Find(type); + LogHandler.Debug("Prioritising essential item " + entity + " for depth " + depth); } } @@ -120,8 +119,8 @@ internal LogicEntity GetPriorityEntity(int depth) if (types?.Length > 0) { TechType nextType = _logic.GetRandom(new List(types)); - entity = _materials.GetAll().Find(e => e.TechType.Equals(nextType)); - LogHandler.Debug("Prioritising elective item " + entity.TechType.AsString() + " for depth " + depth); + entity = _materials.Find(nextType); + LogHandler.Debug("Prioritising elective item " + entity + " for depth " + depth); } } @@ -147,153 +146,6 @@ internal void UpdateReachableMaterials(int depth) _materials.AddReachable(ETechTypeCategory.Eggs, depth); } - /// - /// Check if this recipe fulfills all conditions to have its blueprint be unlocked. - /// - /// The master dictionary. - /// The list of all databoxes. - /// The recipe to check. - /// The maximum depth to consider. - /// True if the recipe fulfills all conditions, false otherwise. - private bool CheckRecipeForBlueprint(LogicEntity entity, int depth) - { - bool fulfilled = true; - - if (entity.Blueprint == null || (entity.Blueprint.UnlockConditions == null - && entity.Blueprint.UnlockDepth == 0)) - return true; - - // If the databox was randomised, do work to account for new locations. - // Cyclops hull modules need extra special treatment. - if (entity.Blueprint.NeedsDatabox && _logic._databoxes?.Count > 0 - && !entity.TechType.Equals(TechType.CyclopsHullModule2) - && !entity.TechType.Equals(TechType.CyclopsHullModule3)) - { - int total = 0; - int number = 0; - int lasercutter = 0; - int propulsioncannon = 0; - - foreach (Databox box in _logic._databoxes.FindAll(x => x.TechType.Equals(entity.TechType))) - { - total += (int)Math.Abs(box.Coordinates.y); - number++; - - if (box.RequiresLaserCutter) - lasercutter++; - if (box.RequiresPropulsionCannon) - propulsioncannon++; - } - - LogHandler.Debug("[B] Found " + number + " databoxes for " + entity.TechType.AsString()); - - entity.Blueprint.UnlockDepth = total / number; - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockDepth = total / number; - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockDepth = total / number; - } - - // If more than half of all locations of this databox require a - // tool to access the box, add it to the requirements for the recipe - if (lasercutter / number >= 0.5) - { - entity.Blueprint.UnlockConditions.Add(TechType.LaserCutter); - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockConditions.Add(TechType.LaserCutter); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockConditions.Add(TechType.LaserCutter); - } - } - - if (propulsioncannon / number >= 0.5) - { - entity.Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - if (entity.TechType.Equals(TechType.CyclopsHullModule1)) - { - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule2)) - .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - _materials.GetAll().Find(x => x.TechType.Equals(TechType.CyclopsHullModule3)) - .Blueprint.UnlockConditions.Add(TechType.PropulsionCannon); - } - } - } - - foreach (TechType condition in entity.Blueprint.UnlockConditions) - { - LogicEntity conditionEntity = _materials.GetAll().Find(x => x.TechType.Equals(condition)); - - // Without this piece, the Air bladder will hang if fish are not - // enabled for the logic, as it fruitlessly searches for a bladderfish - // which never enters its algorithm. - // Eggs and seeds are never problematic in vanilla, but are covered - // in case users add their own modded items with those. - if (!_config.bUseFish && conditionEntity.Category.Equals(ETechTypeCategory.Fish)) - continue; - if (!_config.bUseEggs && conditionEntity.Category.Equals(ETechTypeCategory.Eggs)) - continue; - if (!_config.bUseSeeds && conditionEntity.Category.Equals(ETechTypeCategory.Seeds)) - continue; - - fulfilled &= (_masterDict.RecipeDict.ContainsKey(condition) - || _materials.GetReachable().Exists(x => x.TechType.Equals(condition))); - - if (!fulfilled) - return false; - } - - // Ensure that necessary fragments have already been randomised. - if (_config.bRandomiseFragments && entity.Blueprint.Fragments != null && entity.Blueprint.Fragments.Count > 0) - { - foreach (TechType fragment in entity.Blueprint.Fragments) - { - if (!_masterDict.SpawnDataDict.ContainsKey(fragment)) - { - LogHandler.Debug("[B] Entity " + entity.TechType.AsString() + " missing fragment " - + fragment.AsString()); - return false; - } - } - } - else if (entity.Blueprint.UnlockDepth > depth) - { - fulfilled = false; - } - - return fulfilled; - } - - /// - /// Check whether all prerequisites for this recipe have already been randomised. - /// - /// The recipe to check. - /// True if all conditions are fulfilled, false otherwise. - private bool CheckRecipeForPrerequisites(LogicEntity entity) - { - bool fulfilled = true; - - // The builder tool must always be randomised before any base pieces - // ever become accessible. - if (entity.Category.IsBasePiece() && !_masterDict.RecipeDict.ContainsKey(TechType.Builder)) - return false; - - if (entity.Prerequisites == null) - return true; - - foreach (TechType t in entity.Prerequisites) - { - fulfilled &= _masterDict.RecipeDict.ContainsKey(t); - if (!fulfilled) - break; - } - - return fulfilled; - } - /// /// Apply a randomised recipe to the in-game craft data, and store a copy in the master dictionary. /// diff --git a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs index 61853e6..c3e0512 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.IO; +using SubnauticaRandomiser.Logic; namespace SubnauticaRandomiser.RandomiserObjects { @@ -14,6 +16,7 @@ public class Blueprint public List Fragments; public bool NeedsDatabox; public int UnlockDepth; + public bool WasUpdated; // Was this one updated to account for changes in databox locations? public Blueprint(TechType techType, List unlockConditions = null, TechType fragment = TechType.None, bool databox = false, int unlockDepth = 0) { @@ -34,5 +37,54 @@ public Blueprint(TechType techType, List unlockConditions = null, List NeedsDatabox = databox; UnlockDepth = unlockDepth; } + + /// + /// If databoxes were randomised, ensure the unlock requirements are updated to reflect the new positions. + /// + /// The core logic. + /// Raised if databoxes weren't randomised in the given Logic. + /// Raised if the blueprint requires a databox, but no databoxes + /// containing it were found. + public void UpdateDataboxUnlocks(CoreLogic logic) + { + if (!NeedsDatabox || WasUpdated) + return; + if (!(logic?._databoxes?.Count > 0)) + throw new ArgumentException("Cannot update databox unlocks: Databox list is null or invalid."); + + int total = 0; + int number = 0; + int lasercutter = 0; + int propulsioncannon = 0; + + foreach (Databox box in logic._databoxes.FindAll(x => x.TechType.Equals(TechType))) + { + total += (int)Math.Abs(box.Coordinates.y); + number++; + + if (box.RequiresLaserCutter) + lasercutter++; + if (box.RequiresPropulsionCannon) + propulsioncannon++; + } + + if (number == 0) + throw new InvalidDataException("Entity " + TechType.AsString() + " requires a databox, but 0 were found!"); + + LogHandler.Debug("[B] Found " + number + " databoxes for " + TechType.AsString()); + + UnlockDepth = total / number; + UnlockConditions ??= new List(); + + // If more than half of all locations of this databox require a tool to access the box, add it to + // the requirements for the recipe. + if (lasercutter / number >= 0.5) + UnlockConditions.Add(TechType.LaserCutter); + + if (propulsioncannon / number >= 0.5) + UnlockConditions.Add(TechType.PropulsionCannon); + + WasUpdated = true; + } } } diff --git a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs index 21f309c..6c3de04 100644 --- a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs +++ b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs @@ -1,4 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using SubnauticaRandomiser.Logic; +using SubnauticaRandomiser.Logic.Recipes; namespace SubnauticaRandomiser.RandomiserObjects { @@ -70,6 +75,78 @@ public bool CanFunctionAsIngredient() return true; } + /// + /// Check if this recipe fulfills all conditions to have its blueprint be unlocked. + /// + /// An instance of the core logic. + /// The maximum depth to consider. + /// True if the recipe has no blueprint or fulfills all conditions, false otherwise. + public bool CheckBlueprintFulfilled(CoreLogic logic, int depth) + { + if (Blueprint is null || (Blueprint.UnlockConditions is null && Blueprint.UnlockDepth == 0)) + return true; + + // If the databox was randomised, do work to account for new locations. + if (logic._config.bRandomiseDataboxes && Blueprint.NeedsDatabox && !Blueprint.WasUpdated && logic._databoxes?.Count > 0) + Blueprint.UpdateDataboxUnlocks(logic); + + foreach (TechType condition in Blueprint.UnlockConditions ?? Enumerable.Empty()) + { + LogicEntity conditionEntity = logic._materials.Find(condition); + if (conditionEntity is null) + continue; + + // Without this piece, the Air bladder will hang if fish are not enabled for the logic, as it + // fruitlessly searches for a bladderfish which never enters its algorithm. + // Eggs and seeds are never problematic in vanilla, but are covered in case users add their own + // modded items with those. + if ((!logic._config.bUseFish && conditionEntity.Category.Equals(ETechTypeCategory.Fish)) + || (!logic._config.bUseEggs && conditionEntity.Category.Equals(ETechTypeCategory.Eggs)) + || (!logic._config.bUseSeeds && conditionEntity.Category.Equals(ETechTypeCategory.Seeds))) + continue; + + if (logic._masterDict.RecipeDict.ContainsKey(condition) + || logic._materials.GetReachable().Exists(x => x.TechType.Equals(condition))) + continue; + + return false; + } + + // Ensure that necessary fragments have already been randomised. + if (logic._config.bRandomiseFragments && Blueprint.Fragments?.Count > 0) + { + foreach (TechType fragment in Blueprint.Fragments) + { + if (!logic._masterDict.SpawnDataDict.ContainsKey(fragment)) + { + LogHandler.Debug("[B] Entity " + this + " missing fragment " + fragment.AsString()); + return false; + } + } + + return true; + } + + return depth >= Blueprint.UnlockDepth; + } + + /// + /// Check whether all prerequisites for this recipe have already been randomised. + /// + /// The core logic. + /// True if all conditions are fulfilled, false otherwise. + public bool CheckPrerequisitesFulfilled(CoreLogic logic) + { + // The builder tool must always be randomised before any base pieces ever become accessible. + if (Category.IsBasePiece() && !logic._masterDict.RecipeDict.ContainsKey(TechType.Builder)) + return false; + + if (Prerequisites is null || Prerequisites.Count == 0) + return true; + + return Prerequisites.All(type => logic._masterDict.RecipeDict.ContainsKey(type)); + } + /// /// Get the number of slots this entity occupies in an inventory. /// From 23cb9226310b71b2dc492e7fa2835c58c4ebac2e Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Mon, 8 Aug 2022 08:07:05 +0200 Subject: [PATCH 13/37] Fix randomising not happening on first load. --- SubnauticaRandomiser/InitMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 265869a..34c4510 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -27,7 +27,7 @@ public static class InitMod [3] = "v0.7.0"}; // The master list of everything that is modified by the mod. - internal static EntitySerializer s_masterDict = new EntitySerializer(); + internal static EntitySerializer s_masterDict; private const bool _debug_forceRandomise = false; [QModPatch] From fd090ce3099f7edb42976a77180f3c8d587bce39 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Mon, 8 Aug 2022 08:47:08 +0200 Subject: [PATCH 14/37] Fix #29. Make fragments approximate vanilla spawn rates. --- SubnauticaRandomiser/Logic/FragmentLogic.cs | 29 ++++++++++++++++----- SubnauticaRandomiser/RandomiserConfig.cs | 18 ++++++++----- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 2d1f97a..2b04844 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -18,7 +18,8 @@ internal class FragmentLogic private RandomiserConfig _config { get { return _logic._config; } } private EntitySerializer _masterDict { get { return _logic._masterDict; } } private Random _random { get { return _logic._random; } } - private List _availableBiomes; + private readonly List _allBiomes; + private readonly List _availableBiomes; private static readonly Dictionary _fragmentDataPaths = new Dictionary { { "BaseBioReactor_Fragment", TechType.BaseBioReactorFragment }, @@ -60,6 +61,7 @@ internal FragmentLogic(CoreLogic coreLogic, List biomeList) { _logic = coreLogic; + _allBiomes = GetAvailableFragmentBiomes(biomeList); _availableBiomes = GetAvailableFragmentBiomes(biomeList); AllSpawnData = new List(); } @@ -74,23 +76,23 @@ internal FragmentLogic(CoreLogic coreLogic, List biomeList) internal SpawnData RandomiseFragment(LogicEntity entity, int depth) { if (!_classIdDatabase.TryGetValue(entity.TechType, out List idList)) - throw new ArgumentException("Failed to find fragment '" + entity.TechType.AsString() + "' in classId database!"); + throw new ArgumentException("Failed to find fragment '" + entity + "' in classId database!"); - LogHandler.Debug("Randomising fragment " + entity.TechType.AsString() + " for depth " + depth); + LogHandler.Debug("Randomising fragment " + entity + " for depth " + depth); // HACK for now, only consider the first entry in the ID list. string classId = idList[0]; SpawnData spawnData = new SpawnData(classId); // Determine how many different biomes the fragment should spawn in. - int biomeCount = _random.Next(1, _config.iMaxBiomesPerFragment); + int biomeCount = _random.Next(3, _config.iMaxBiomesPerFragment); for (int i = 0; i < biomeCount; i++) { // Choose a suitable biome which is also accessible at this depth. Biome biome = GetRandom(_availableBiomes.FindAll(x => x.AverageDepth <= depth)); - // In case no good biome is available, just choose any. - biome ??= GetRandom(_availableBiomes); + // In case no good biome is available, ignore overpopulation restrictions and choose any. + biome ??= GetRandom(_allBiomes.FindAll(x => x.AverageDepth <= depth)); // Ensure the biome can actually be used for creating valid BiomeData. if (!Enum.TryParse(biome.Name, out BiomeType biomeType)) @@ -109,7 +111,7 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) { Biome = biomeType, Count = 1, - Probability = (float)_random.NextDouble() * _config.fFragmentSpawnChance + Probability = CalcFragmentSpawnRate(biome) }; spawnData.AddBiomeData(data); @@ -180,6 +182,19 @@ private List GetAvailableFragmentBiomes(List collections LogHandler.Debug("---Total biomes suitable for fragments: "+biomes.Count); return biomes; } + + /// + /// Calculate the spawn rate for an entity in the given biome. + /// + /// The biome. + /// The spawn rate. + private float CalcFragmentSpawnRate(Biome biome) + { + // Set a percentage between Min and Max% of the biome's combined original spawn rates. + float percentage = _config.fFragmentSpawnChanceMin + (float)_random.NextDouble() + * (_config.fFragmentSpawnChanceMax - _config.fFragmentSpawnChanceMax); + return percentage * biome.FragmentRate ?? 0.0f; + } /// /// Assemble a dictionary of all relevant prefabs with their unique classId identifier. diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index 55a0363..58cd118 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -55,7 +55,7 @@ public class RandomiserConfig : ConfigFile [Slider("Max ingredients per recipe", 1, 10, DefaultValue = 7)] public int iMaxIngredientsPerRecipe = ConfigDefaults.iMaxIngredientsPerRecipe; - [Slider("Max biomes to spawn each fragment in", 1, 5, DefaultValue = 3)] + [Slider("Max biomes to spawn each fragment in", 3, 10, DefaultValue = 5)] public int iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; [Button("Randomise with new seed")] @@ -95,7 +95,8 @@ public void NewRandomOldSeed() public int iMaxInventorySizePerRecipe = ConfigDefaults.iMaxInventorySizePerRecipe; public double dFuzziness = ConfigDefaults.dFuzziness; public double dIngredientRatio = ConfigDefaults.dIngredientRatio; - public float fFragmentSpawnChance = ConfigDefaults.fFragmentSpawnChance; + public float fFragmentSpawnChanceMin = ConfigDefaults.fFragmentSpawnChanceMin; + public float fFragmentSpawnChanceMax = ConfigDefaults.fFragmentSpawnChanceMax; // Way down here since it tends to take up some space and scrolling is annoying. public string sBase64Seed = ""; @@ -115,7 +116,7 @@ public void SanitiseConfigValues() iMaxAmountPerIngredient = ConfigDefaults.iMaxAmountPerIngredient; if (iMaxIngredientsPerRecipe > 10 || iMaxIngredientsPerRecipe < 1) iMaxIngredientsPerRecipe = ConfigDefaults.iMaxIngredientsPerRecipe; - if (iMaxBiomesPerFragment > 10 || iMaxBiomesPerFragment < 1) + if (iMaxBiomesPerFragment > 10 || iMaxBiomesPerFragment < 3) iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; // Advanced settings below. @@ -129,8 +130,10 @@ public void SanitiseConfigValues() dFuzziness = ConfigDefaults.dFuzziness; if (dIngredientRatio > 1 || dIngredientRatio < 0) dIngredientRatio = ConfigDefaults.dIngredientRatio; - if (fFragmentSpawnChance > 1.0f || fFragmentSpawnChance < 0.01f) - fFragmentSpawnChance = ConfigDefaults.fFragmentSpawnChance; + if (fFragmentSpawnChanceMin > 10.0f || fFragmentSpawnChanceMin < 0.01f) + fFragmentSpawnChanceMin = ConfigDefaults.fFragmentSpawnChanceMin; + if (fFragmentSpawnChanceMax > 10.0f || fFragmentSpawnChanceMax < 0.01f) + fFragmentSpawnChanceMax = ConfigDefaults.fFragmentSpawnChanceMax; } /// @@ -167,7 +170,7 @@ internal static class ConfigDefaults internal const int iUpgradesAsIngredients = 1; internal const int iMaxAmountPerIngredient = 5; internal const int iMaxIngredientsPerRecipe = 7; - internal const int iMaxBiomesPerFragment = 3; + internal const int iMaxBiomesPerFragment = 5; // Advanced setting defaults start here. internal const int iDepthSearchTime = 15; @@ -176,6 +179,7 @@ internal static class ConfigDefaults internal const int iMaxInventorySizePerRecipe = 24; internal const double dFuzziness = 0.2; internal const double dIngredientRatio = 0.45; - internal const float fFragmentSpawnChance = 0.1f; + internal const float fFragmentSpawnChanceMin = 0.3f; + internal const float fFragmentSpawnChanceMax = 0.6f; } } From e41cd747e86a5114eb273d477f8c354c33a2bbf4 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Mon, 8 Aug 2022 10:44:44 +0200 Subject: [PATCH 15/37] Utilise the full range of fragment prefabs for variety. --- SubnauticaRandomiser/EntitySerializer.cs | 4 +- SubnauticaRandomiser/Logic/FragmentLogic.cs | 96 ++++++++++++++----- .../RandomiserObjects/LogicEntity.cs | 5 +- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index 35dbecb..12c4fd8 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -28,7 +28,7 @@ namespace SubnauticaRandomiser public class EntitySerializer { public Dictionary RecipeDict = new Dictionary(); - public Dictionary SpawnDataDict = new Dictionary(); + public Dictionary> SpawnDataDict = new Dictionary>(); public Dictionary Databoxes = new Dictionary(); public bool isDataboxRandomised = false; @@ -85,7 +85,7 @@ public bool AddRecipe(TechType type, Recipe r) /// The TechType to use as key. /// The SpawnData to use as value. /// True if successful, false if the key already exists in the dictionary. - public bool AddSpawnData(TechType type, SpawnData data) + public bool AddSpawnData(TechType type, List data) { if (SpawnDataDict.ContainsKey(type)) { diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 2b04844..36ab097 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using SMLHelper.V2.Handlers; using SubnauticaRandomiser.RandomiserObjects; using static LootDistributionData; @@ -52,7 +53,6 @@ internal class FragmentLogic { "ThermalPlant_Fragment", TechType.ThermalPlantFragment }, { "Workbench_Fragment", TechType.WorkbenchFragment } }; - internal List AllSpawnData; /// /// Handle the logic for everything related to fragments. @@ -63,7 +63,6 @@ internal FragmentLogic(CoreLogic coreLogic, List biomeList) _allBiomes = GetAvailableFragmentBiomes(biomeList); _availableBiomes = GetAvailableFragmentBiomes(biomeList); - AllSpawnData = new List(); } /// @@ -73,7 +72,7 @@ internal FragmentLogic(CoreLogic coreLogic, List biomeList) /// The maximum depth to consider. /// The SpawnData that was newly added to the EntitySerializer. /// Raised if the fragment name is invalid. - internal SpawnData RandomiseFragment(LogicEntity entity, int depth) + internal List RandomiseFragment(LogicEntity entity, int depth) { if (!_classIdDatabase.TryGetValue(entity.TechType, out List idList)) throw new ArgumentException("Failed to find fragment '" + entity + "' in classId database!"); @@ -81,8 +80,9 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) LogHandler.Debug("Randomising fragment " + entity + " for depth " + depth); // HACK for now, only consider the first entry in the ID list. - string classId = idList[0]; - SpawnData spawnData = new SpawnData(classId); + //string classId = idList[0]; + //SpawnData spawnList = new SpawnData(classId); + List spawnList = new List(); // Determine how many different biomes the fragment should spawn in. int biomeCount = _random.Next(3, _config.iMaxBiomesPerFragment); @@ -106,22 +106,32 @@ internal SpawnData RandomiseFragment(LogicEntity entity, int depth) // Remove the biome from the pool if it gets too populated. if (biome.Used >= 5) _availableBiomes.Remove(biome); + + // Calculate spawn rate. + float spawnRate = CalcFragmentSpawnRate(biome); + float[] splitRates = SplitFragmentSpawnRate(spawnRate, idList.Count); - RandomiserBiomeData data = new RandomiserBiomeData + // Split the spawn rate among each variation of the fragment. + for (int j = 0; j < idList.Count; j++) { - Biome = biomeType, - Count = 1, - Probability = CalcFragmentSpawnRate(biome) - }; - - spawnData.AddBiomeData(data); - LogHandler.Debug(" Adding fragment to biome: " + data.Biome.AsString() + ", " + data.Probability); + SpawnData spawnData = new SpawnData(idList[j]); + RandomiserBiomeData data = new RandomiserBiomeData + { + Biome = biomeType, + Count = 1, + Probability = splitRates[j] + }; + spawnData.AddBiomeData(data); + spawnList.Add(spawnData); + } + + LogHandler.Debug(" Adding fragment to biome: " + biomeType.AsString() + ", " + spawnRate); } - ApplyRandomisedFragment(entity, spawnData); - return spawnData; + ApplyRandomisedFragment(entity, spawnList); + return spawnList; } - + /// /// Go through all the BiomeData in the game and reset any fragment spawn rates to 0.0f, effectively "deleting" /// them from the game until the randomiser has decided on a new distribution. @@ -251,7 +261,38 @@ internal static Dictionary ReverseClassIdDatabase() } /// - /// Re-apply spawnData from a saved game. This will fail to catch all existing fragment spawns if called in a + /// Split a fragment's spawn rate into a number of randomly sized parts. + /// + /// The spawn rate. + /// The number of parts to split into. + /// An array containing each part's spawn rate. + /// Raised if parts is smaller than 1. + private float[] SplitFragmentSpawnRate(float spawnRate, int parts) + { + if (parts < 1) + throw new ArgumentException("Cannot split spawn rate into less than one pieces!"); + if (parts == 1) + return new[] { spawnRate }; + + // Initially, get some random values. + float[] result = new float[parts]; + for (int i = 0; i < result.Length; i++) + { + result[i] = (float)_random.NextDouble(); + } + + // Adjust the values so they sum up to spawnRate. + float adjust = spawnRate / result.Sum(); + for (int i = 0; i < result.Length; i++) + { + result[i] *= adjust; + } + + return result; + } + + /// + /// Re-apply spawnList from a saved game. This will fail to catch all existing fragment spawns if called in a /// previously randomised game. /// internal static void ApplyMasterDict(EntitySerializer masterDict) @@ -260,8 +301,10 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) foreach (TechType key in masterDict.SpawnDataDict.Keys) { - SpawnData spawnData = masterDict.SpawnDataDict[key]; - LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); + foreach (SpawnData spawnData in masterDict.SpawnDataDict[key]) + { + LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); + } } } @@ -269,15 +312,16 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) /// Add modified SpawnData to the game and any place it needs to go to be stored for later use. /// /// The entity to modify spawn rates for. - /// The modified SpawnData to use. - internal void ApplyRandomisedFragment(LogicEntity entity, SpawnData spawnData) + /// The list of modified SpawnData to use. + internal void ApplyRandomisedFragment(LogicEntity entity, List spawnList) { - entity.SpawnData = spawnData; - - AllSpawnData.Add(spawnData); - _masterDict.AddSpawnData(entity.TechType, spawnData); + entity.SpawnData = spawnList; + _masterDict.AddSpawnData(entity.TechType, spawnList); - LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); + foreach (SpawnData data in spawnList) + { + LootDistributionHandler.EditLootDistributionData(data.ClassId, data.GetBaseBiomeData()); + } } /// diff --git a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs index 6c3de04..4b4a929 100644 --- a/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs +++ b/SubnauticaRandomiser/RandomiserObjects/LogicEntity.cs @@ -18,7 +18,7 @@ public class LogicEntity public readonly ETechTypeCategory Category; public Blueprint Blueprint; // For making it show up in the PDA public Recipe Recipe; // For actually crafting it - public SpawnData SpawnData; // For spawning it naturally in the world + public List SpawnData; // For spawning it naturally in the world public List Prerequisites; // What is absolutely mandatory before getting this? public bool InLogic; // Is this available for randomising other entities? public int AccessibleDepth; // How deep down must you reach to get to this? @@ -32,7 +32,8 @@ public class LogicEntity public bool HasSpawnData => !(SpawnData is null); public bool IsFragment => Category.Equals(ETechTypeCategory.Fragments); - public LogicEntity(TechType type, ETechTypeCategory category, Blueprint blueprint = null, Recipe recipe = null, SpawnData spawnData = null, List prerequisites = null, bool inLogic = false, int value = 0) + public LogicEntity(TechType type, ETechTypeCategory category, Blueprint blueprint = null, Recipe recipe = null, + List spawnData = null, List prerequisites = null, bool inLogic = false, int value = 0) { TechType = type; Category = category; From acf2fbaa167ffb64e57ce83c125bac07d86583a9 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Tue, 9 Aug 2022 11:31:14 +0200 Subject: [PATCH 16/37] Close #5. Randomise needed fragment number. --- SubnauticaRandomiser/EntitySerializer.cs | 20 +++++++++++- SubnauticaRandomiser/Logic/FragmentLogic.cs | 32 +++++++++++++++---- SubnauticaRandomiser/RandomiserConfig.cs | 14 ++++++++ .../RandomiserObjects/Blueprint.cs | 5 ++- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index 12c4fd8..c4b79cf 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -30,9 +30,10 @@ public class EntitySerializer public Dictionary RecipeDict = new Dictionary(); public Dictionary> SpawnDataDict = new Dictionary>(); public Dictionary Databoxes = new Dictionary(); + public Dictionary NumFragmentsToUnlock = new Dictionary(); public bool isDataboxRandomised = false; - public static readonly int s_SaveVersion = InitMod.s_expectedSaveVersion; + public const int SaveVersion = InitMod.s_expectedSaveVersion; /// /// Convert this class to a string for saving. @@ -61,6 +62,23 @@ public static EntitySerializer FromBase64String(string base64String) return (EntitySerializer)(new BinaryFormatter().Deserialize(ms)); } } + + /// + /// Try to add an entry to the FragmentUnlockNumber dictionary. + /// + /// The TechType to use as key. + /// The number to use as value. + /// True if successful, false if the key already exists in the dictionary. + public bool AddFragmentUnlockNum(TechType type, int number) + { + if (NumFragmentsToUnlock.ContainsKey(type)) + { + LogHandler.Warn("Tried to add duplicate key " + type.AsString() + " to FragmentNum master dictionary!"); + return false; + } + NumFragmentsToUnlock.Add(type, number); + return true; + } /// /// Try to add an entry to the Recipe dictionary. diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 36ab097..dac71e7 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -78,14 +78,11 @@ internal List RandomiseFragment(LogicEntity entity, int depth) throw new ArgumentException("Failed to find fragment '" + entity + "' in classId database!"); LogHandler.Debug("Randomising fragment " + entity + " for depth " + depth); - - // HACK for now, only consider the first entry in the ID list. - //string classId = idList[0]; - //SpawnData spawnList = new SpawnData(classId); + List spawnList = new List(); // Determine how many different biomes the fragment should spawn in. - int biomeCount = _random.Next(3, _config.iMaxBiomesPerFragment); + int biomeCount = _random.Next(3, _config.iMaxBiomesPerFragment + 1); for (int i = 0; i < biomeCount; i++) { @@ -124,10 +121,12 @@ internal List RandomiseFragment(LogicEntity entity, int depth) spawnData.AddBiomeData(data); spawnList.Add(spawnData); } - + LogHandler.Debug(" Adding fragment to biome: " + biomeType.AsString() + ", " + spawnRate); } + // Change the number of fragments required to unlock the blueprint. + ChangeNumFragmentsToUnlock(entity); ApplyRandomisedFragment(entity, spawnList); return spawnList; } @@ -205,6 +204,22 @@ private float CalcFragmentSpawnRate(Biome biome) * (_config.fFragmentSpawnChanceMax - _config.fFragmentSpawnChanceMax); return percentage * biome.FragmentRate ?? 0.0f; } + + /// + /// Change the number of fragments needed to unlock the blueprint to the given entity. + /// + /// The entity that is unlocked on scan completion. + private void ChangeNumFragmentsToUnlock(LogicEntity entity) + { + int numFragments = _random.Next(_config.iMinFragmentsToUnlock, _config.iMaxFragmentsToUnlock + 1); + // Exosuit fragments are worth a lot more and the vanilla blueprint has the highest cost of all, at 20. + if (entity.TechType.Equals(TechType.ExosuitFragment)) + numFragments *= 6; + + LogHandler.Debug(" New number of fragments required: " + numFragments); + _masterDict.AddFragmentUnlockNum(entity.TechType, numFragments); + PDAHandler.EditFragmentsToScan(entity.TechType, numFragments); + } /// /// Assemble a dictionary of all relevant prefabs with their unique classId identifier. @@ -306,6 +321,11 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); } } + + foreach (TechType key in masterDict.NumFragmentsToUnlock.Keys) + { + PDAHandler.EditFragmentsToScan(key, masterDict.NumFragmentsToUnlock[key]); + } } /// diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index 58cd118..80e72af 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -31,6 +31,9 @@ public class RandomiserConfig : ConfigFile [Toggle("Randomise fragments?")] public bool bRandomiseFragments = ConfigDefaults.bRandomiseFragments; + [Toggle("Randomise number of fragments to unlock something?")] + public bool bRandomiseNumFragments = ConfigDefaults.bRandomiseNumFragments; + [Toggle("Randomise recipes?")] public bool bRandomiseRecipes = ConfigDefaults.bRandomiseRecipes; @@ -58,6 +61,9 @@ public class RandomiserConfig : ConfigFile [Slider("Max biomes to spawn each fragment in", 3, 10, DefaultValue = 5)] public int iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; + [Slider("Max number of fragments to scan to unlock something", 1, 20, DefaultValue = 5)] + public int iMaxFragmentsToUnlock = ConfigDefaults.iMaxFragmentsToUnlock; + [Button("Randomise with new seed")] public void NewRandomNewSeed() { @@ -89,6 +95,7 @@ public void NewRandomOldSeed() } public string ADVANCED_SETTINGS_BELOW_THIS_POINT = "ADVANCED_SETTINGS_BELOW_THIS_POINT"; + public int iMinFragmentsToUnlock = ConfigDefaults.iMinFragmentsToUnlock; public int iDepthSearchTime = ConfigDefaults.iDepthSearchTime; public int iMaxBasicOutpostSize = ConfigDefaults.iMaxBasicOutpostSize; public int iMaxEggsAsSingleIngredient = ConfigDefaults.iMaxEggsAsSingleIngredient; @@ -118,8 +125,12 @@ public void SanitiseConfigValues() iMaxIngredientsPerRecipe = ConfigDefaults.iMaxIngredientsPerRecipe; if (iMaxBiomesPerFragment > 10 || iMaxBiomesPerFragment < 3) iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; + if (iMaxFragmentsToUnlock > 30 || iMaxFragmentsToUnlock < 1) + iMaxFragmentsToUnlock = ConfigDefaults.iMaxFragmentsToUnlock; // Advanced settings below. + if (iMinFragmentsToUnlock > iMaxFragmentsToUnlock || iMinFragmentsToUnlock < 1) + iMinFragmentsToUnlock = ConfigDefaults.iMinFragmentsToUnlock; if (iMaxBasicOutpostSize > 48 || iMaxBasicOutpostSize < 4) iMaxBasicOutpostSize = ConfigDefaults.iMaxBasicOutpostSize; if (iMaxEggsAsSingleIngredient > 10 || iMaxEggsAsSingleIngredient < 1) @@ -162,6 +173,7 @@ internal static class ConfigDefaults internal const bool bUseSeeds = true; internal const bool bRandomiseDataboxes = true; internal const bool bRandomiseFragments = true; + internal const bool bRandomiseNumFragments = true; internal const bool bRandomiseRecipes = true; internal const bool bVanillaUpgradeChains = false; internal const bool bDoBaseTheming = false; @@ -171,8 +183,10 @@ internal static class ConfigDefaults internal const int iMaxAmountPerIngredient = 5; internal const int iMaxIngredientsPerRecipe = 7; internal const int iMaxBiomesPerFragment = 5; + internal const int iMaxFragmentsToUnlock = 5; // Advanced setting defaults start here. + internal const int iMinFragmentsToUnlock = 2; internal const int iDepthSearchTime = 15; internal const int iMaxBasicOutpostSize = 24; internal const int iMaxEggsAsSingleIngredient = 1; diff --git a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs index c3e0512..f094fd0 100644 --- a/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs +++ b/SubnauticaRandomiser/RandomiserObjects/Blueprint.cs @@ -14,17 +14,20 @@ public class Blueprint public TechType TechType; public List UnlockConditions; public List Fragments; + public int NumFragments; public bool NeedsDatabox; public int UnlockDepth; public bool WasUpdated; // Was this one updated to account for changes in databox locations? - public Blueprint(TechType techType, List unlockConditions = null, TechType fragment = TechType.None, bool databox = false, int unlockDepth = 0) + public Blueprint(TechType techType, List unlockConditions = null, TechType fragment = TechType.None, + int numFragments = 3, bool databox = false, int unlockDepth = 0) { Fragments = new List(); TechType = techType; UnlockConditions = unlockConditions; Fragments.Add(fragment); + NumFragments = numFragments; NeedsDatabox = databox; UnlockDepth = unlockDepth; } From 2986a0ab8a65e6f99ab43fde3cac6a21cf9b486c Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Tue, 9 Aug 2022 12:06:18 +0200 Subject: [PATCH 17/37] Streamline applying changes to the game. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 1 + SubnauticaRandomiser/Logic/FragmentLogic.cs | 8 +------- SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index bd74c7b..967ccc5 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -138,6 +138,7 @@ internal void Randomise() } _spoilerLog.WriteLog(); + InitMod.ApplyAllChanges(); LogHandler.Info("Finished randomising within " + circuitbreaker + " cycles!"); } diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index dac71e7..ab6195f 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -218,7 +218,6 @@ private void ChangeNumFragmentsToUnlock(LogicEntity entity) LogHandler.Debug(" New number of fragments required: " + numFragments); _masterDict.AddFragmentUnlockNum(entity.TechType, numFragments); - PDAHandler.EditFragmentsToScan(entity.TechType, numFragments); } /// @@ -321,7 +320,7 @@ internal static void ApplyMasterDict(EntitySerializer masterDict) LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); } } - + foreach (TechType key in masterDict.NumFragmentsToUnlock.Keys) { PDAHandler.EditFragmentsToScan(key, masterDict.NumFragmentsToUnlock[key]); @@ -337,11 +336,6 @@ internal void ApplyRandomisedFragment(LogicEntity entity, List spawnL { entity.SpawnData = spawnList; _masterDict.AddSpawnData(entity.TechType, spawnList); - - foreach (SpawnData data in spawnList) - { - LootDistributionHandler.EditLootDistributionData(data.ClassId, data.GetBaseBiomeData()); - } } /// diff --git a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs index 923501a..9fe616e 100644 --- a/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs +++ b/SubnauticaRandomiser/Logic/Recipes/RecipeLogic.cs @@ -152,7 +152,6 @@ internal void UpdateReachableMaterials(int depth) /// The recipe to change. internal void ApplyRandomisedRecipe(Recipe recipe) { - CraftDataHandler.SetTechData(recipe.TechType, recipe); _masterDict.AddRecipe(recipe.TechType, recipe); } From f1abd6a7c179232bdbf74150724e185f5fe1c103 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Tue, 9 Aug 2022 23:03:32 +0200 Subject: [PATCH 18/37] Implement generic way to sanitise config values. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 2 +- SubnauticaRandomiser/Logic/FragmentLogic.cs | 3 + SubnauticaRandomiser/RandomiserConfig.cs | 199 +++++++++++--------- 3 files changed, 111 insertions(+), 93 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 967ccc5..7cab0fd 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -274,7 +274,7 @@ private int UpdateReachableDepth(int currentDepth, Dictionary pr if (progressionItems.Count <= numItems) return currentDepth; - int newDepth = CalculateReachableDepth(progressionItems); + int newDepth = CalculateReachableDepth(progressionItems, _config.iDepthSearchTime); _spoilerLog.UpdateLastProgressionEntry(newDepth); currentDepth = Math.Max(currentDepth, newDepth); _recipeLogic?.UpdateReachableMaterials(currentDepth); diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index ab6195f..3517517 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -211,6 +211,9 @@ private float CalcFragmentSpawnRate(Biome biome) /// The entity that is unlocked on scan completion. private void ChangeNumFragmentsToUnlock(LogicEntity entity) { + if (!_config.bRandomiseNumFragments) + return; + int numFragments = _random.Next(_config.iMinFragmentsToUnlock, _config.iMaxFragmentsToUnlock + 1); // Exosuit fragments are worth a lot more and the vanilla blueprint has the highest cost of all, at 20. if (entity.TechType.Equals(TechType.ExosuitFragment)) diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index 80e72af..288df3d 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; +using System.Collections.Generic; using SMLHelper.V2.Json; using SMLHelper.V2.Options.Attributes; + namespace SubnauticaRandomiser { [Menu("Randomiser")] @@ -14,55 +17,55 @@ public class RandomiserConfig : ConfigFile public int iSeed = 0; [Choice("Mode", "Balanced", "Chaotic")] - public int iRandomiserMode = ConfigDefaults.iRandomiserMode; + public int iRandomiserMode = (int)ConfigDefaults.GetDefault("iRandomiserMode"); [Toggle("Use fish in logic?")] - public bool bUseFish = ConfigDefaults.bUseFish; + public bool bUseFish = (bool)ConfigDefaults.GetDefault("bUseFish"); [Toggle("Use eggs in logic?")] - public bool bUseEggs = ConfigDefaults.bUseEggs; + public bool bUseEggs = (bool)ConfigDefaults.GetDefault("bUseEggs"); [Toggle("Use seeds in logic?")] - public bool bUseSeeds = ConfigDefaults.bUseSeeds; + public bool bUseSeeds = (bool)ConfigDefaults.GetDefault("bUseSeeds"); [Toggle("Randomise blueprints in databoxes?")] - public bool bRandomiseDataboxes = ConfigDefaults.bRandomiseDataboxes; + public bool bRandomiseDataboxes = (bool)ConfigDefaults.GetDefault("bRandomiseDataboxes"); - [Toggle("Randomise fragments?")] - public bool bRandomiseFragments = ConfigDefaults.bRandomiseFragments; + [Toggle("Randomise fragment locations?")] + public bool bRandomiseFragments = (bool)ConfigDefaults.GetDefault("bRandomiseFragments"); - [Toggle("Randomise number of fragments to unlock something?")] - public bool bRandomiseNumFragments = ConfigDefaults.bRandomiseNumFragments; + [Toggle("Randomise number of fragments needed?")] + public bool bRandomiseNumFragments = (bool)ConfigDefaults.GetDefault("bRandomiseNumFragments"); [Toggle("Randomise recipes?")] - public bool bRandomiseRecipes = ConfigDefaults.bRandomiseRecipes; + public bool bRandomiseRecipes = (bool)ConfigDefaults.GetDefault("bRandomiseRecipes"); [Toggle("Respect vanilla upgrade chains?")] - public bool bVanillaUpgradeChains = ConfigDefaults.bVanillaUpgradeChains; + public bool bVanillaUpgradeChains = (bool)ConfigDefaults.GetDefault("bVanillaUpgradeChains"); [Toggle("Theme base parts around a common ingredient?")] - public bool bDoBaseTheming = ConfigDefaults.bDoBaseTheming; + public bool bDoBaseTheming = (bool)ConfigDefaults.GetDefault("bDoBaseTheming"); [Choice("Include equipment as ingredients?", "Never", "Top-level recipes only", "Unrestricted")] - public int iEquipmentAsIngredients = ConfigDefaults.iEquipmentAsIngredients; + public int iEquipmentAsIngredients = (int)ConfigDefaults.GetDefault("iEquipmentAsIngredients"); [Choice("Include tools as ingredients?", "Never", "Top-level recipes only", "Unrestricted")] - public int iToolsAsIngredients = ConfigDefaults.iToolsAsIngredients; + public int iToolsAsIngredients = (int)ConfigDefaults.GetDefault("iToolsAsIngredients"); [Choice("Include upgrades as ingredients?", "Never", "Top-level recipes only", "Unrestricted")] - public int iUpgradesAsIngredients = ConfigDefaults.iUpgradesAsIngredients; + public int iUpgradesAsIngredients = (int)ConfigDefaults.GetDefault("iUpgradesAsIngredients"); [Slider("Max number of a single ingredient", 1, 10, DefaultValue = 5)] - public int iMaxAmountPerIngredient = ConfigDefaults.iMaxAmountPerIngredient; + public int iMaxAmountPerIngredient = (int)ConfigDefaults.GetDefault("iMaxAmountPerIngredient"); [Slider("Max ingredients per recipe", 1, 10, DefaultValue = 7)] - public int iMaxIngredientsPerRecipe = ConfigDefaults.iMaxIngredientsPerRecipe; + public int iMaxIngredientsPerRecipe = (int)ConfigDefaults.GetDefault("iMaxIngredientsPerRecipe"); [Slider("Max biomes to spawn each fragment in", 3, 10, DefaultValue = 5)] - public int iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; + public int iMaxBiomesPerFragment = (int)ConfigDefaults.GetDefault("iMaxBiomesPerFragment"); - [Slider("Max number of fragments to scan to unlock something", 1, 20, DefaultValue = 5)] - public int iMaxFragmentsToUnlock = ConfigDefaults.iMaxFragmentsToUnlock; + [Slider("Max number of fragments needed", 1, 20, DefaultValue = 5)] + public int iMaxFragmentsToUnlock = (int)ConfigDefaults.GetDefault("iMaxFragmentsToUnlock"); [Button("Randomise with new seed")] public void NewRandomNewSeed() @@ -95,15 +98,15 @@ public void NewRandomOldSeed() } public string ADVANCED_SETTINGS_BELOW_THIS_POINT = "ADVANCED_SETTINGS_BELOW_THIS_POINT"; - public int iMinFragmentsToUnlock = ConfigDefaults.iMinFragmentsToUnlock; - public int iDepthSearchTime = ConfigDefaults.iDepthSearchTime; - public int iMaxBasicOutpostSize = ConfigDefaults.iMaxBasicOutpostSize; - public int iMaxEggsAsSingleIngredient = ConfigDefaults.iMaxEggsAsSingleIngredient; - public int iMaxInventorySizePerRecipe = ConfigDefaults.iMaxInventorySizePerRecipe; - public double dFuzziness = ConfigDefaults.dFuzziness; - public double dIngredientRatio = ConfigDefaults.dIngredientRatio; - public float fFragmentSpawnChanceMin = ConfigDefaults.fFragmentSpawnChanceMin; - public float fFragmentSpawnChanceMax = ConfigDefaults.fFragmentSpawnChanceMax; + public int iMinFragmentsToUnlock = (int)ConfigDefaults.GetDefault("iMinFragmentsToUnlock"); + public int iDepthSearchTime = (int)ConfigDefaults.GetDefault("iDepthSearchTime"); + public int iMaxBasicOutpostSize = (int)ConfigDefaults.GetDefault("iMaxBasicOutpostSize"); + public int iMaxEggsAsSingleIngredient = (int)ConfigDefaults.GetDefault("iMaxEgssAsSingleIngredient"); + public int iMaxInventorySizePerRecipe = (int)ConfigDefaults.GetDefault("iMaxInventorySizePerRecipe"); + public double dFuzziness = (double)ConfigDefaults.GetDefault("dFuzziness"); + public double dIngredientRatio = (double)ConfigDefaults.GetDefault("dIngredientRatio"); + public float fFragmentSpawnChanceMin = (float)ConfigDefaults.GetDefault("fFragmentSpawnChanceMin"); + public float fFragmentSpawnChanceMax = (float)ConfigDefaults.GetDefault("fFragmentSpawnChanceMax"); // Way down here since it tends to take up some space and scrolling is annoying. public string sBase64Seed = ""; @@ -111,40 +114,28 @@ public void NewRandomOldSeed() public void SanitiseConfigValues() { - if (iRandomiserMode > 1 || iRandomiserMode < 0) - iRandomiserMode = ConfigDefaults.iRandomiserMode; - if (iToolsAsIngredients > 2 || iToolsAsIngredients < 0) - iToolsAsIngredients = ConfigDefaults.iToolsAsIngredients; - if (iUpgradesAsIngredients > 2 || iUpgradesAsIngredients < 0) - iUpgradesAsIngredients = ConfigDefaults.iUpgradesAsIngredients; - if (iDepthSearchTime > 45 || iDepthSearchTime < 0) - iDepthSearchTime = ConfigDefaults.iDepthSearchTime; - if (iMaxAmountPerIngredient > 10 || iMaxAmountPerIngredient < 1) - iMaxAmountPerIngredient = ConfigDefaults.iMaxAmountPerIngredient; - if (iMaxIngredientsPerRecipe > 10 || iMaxIngredientsPerRecipe < 1) - iMaxIngredientsPerRecipe = ConfigDefaults.iMaxIngredientsPerRecipe; - if (iMaxBiomesPerFragment > 10 || iMaxBiomesPerFragment < 3) - iMaxBiomesPerFragment = ConfigDefaults.iMaxBiomesPerFragment; - if (iMaxFragmentsToUnlock > 30 || iMaxFragmentsToUnlock < 1) - iMaxFragmentsToUnlock = ConfigDefaults.iMaxFragmentsToUnlock; - - // Advanced settings below. - if (iMinFragmentsToUnlock > iMaxFragmentsToUnlock || iMinFragmentsToUnlock < 1) - iMinFragmentsToUnlock = ConfigDefaults.iMinFragmentsToUnlock; - if (iMaxBasicOutpostSize > 48 || iMaxBasicOutpostSize < 4) - iMaxBasicOutpostSize = ConfigDefaults.iMaxBasicOutpostSize; - if (iMaxEggsAsSingleIngredient > 10 || iMaxEggsAsSingleIngredient < 1) - iMaxEggsAsSingleIngredient = ConfigDefaults.iMaxEggsAsSingleIngredient; - if (iMaxInventorySizePerRecipe > 100 || iMaxInventorySizePerRecipe < 4) - iMaxInventorySizePerRecipe = ConfigDefaults.iMaxInventorySizePerRecipe; - if (dFuzziness > 1 || dFuzziness < 0) - dFuzziness = ConfigDefaults.dFuzziness; - if (dIngredientRatio > 1 || dIngredientRatio < 0) - dIngredientRatio = ConfigDefaults.dIngredientRatio; - if (fFragmentSpawnChanceMin > 10.0f || fFragmentSpawnChanceMin < 0.01f) - fFragmentSpawnChanceMin = ConfigDefaults.fFragmentSpawnChanceMin; - if (fFragmentSpawnChanceMax > 10.0f || fFragmentSpawnChanceMax < 0.01f) - fFragmentSpawnChanceMax = ConfigDefaults.fFragmentSpawnChanceMax; + // Iterate through every variable of the config. + foreach (var field in typeof(RandomiserConfig).GetFields()) + { + string name = field.Name; + Type type = field.FieldType; + // Skip clamping values for special cases, and for non-numeric options. + if (!ConfigDefaults.Contains(name) || type == typeof(bool)) + { + // LogHandler.Debug("Skipping config sanity check for variable " + name); + continue; + } + + var value = (IComparable)field.GetValue(this); + + // If the variable is outside the range of acceptable values, reset it. + if (value.CompareTo(ConfigDefaults.GetMin(name)) < 0 + || value.CompareTo(ConfigDefaults.GetMax(name)) > 0) + { + LogHandler.Debug("Resetting invalid config value for " + name); + field.SetValue(this, ConfigDefaults.GetDefault(name)); + } + } } /// @@ -167,33 +158,57 @@ private bool EnsureButtonTime() /// Mostly used so that the spoiler log can tell which settings to include. internal static class ConfigDefaults { - internal const int iRandomiserMode = 0; - internal const bool bUseFish = true; - internal const bool bUseEggs = false; - internal const bool bUseSeeds = true; - internal const bool bRandomiseDataboxes = true; - internal const bool bRandomiseFragments = true; - internal const bool bRandomiseNumFragments = true; - internal const bool bRandomiseRecipes = true; - internal const bool bVanillaUpgradeChains = false; - internal const bool bDoBaseTheming = false; - internal const int iEquipmentAsIngredients = 1; - internal const int iToolsAsIngredients = 1; - internal const int iUpgradesAsIngredients = 1; - internal const int iMaxAmountPerIngredient = 5; - internal const int iMaxIngredientsPerRecipe = 7; - internal const int iMaxBiomesPerFragment = 5; - internal const int iMaxFragmentsToUnlock = 5; - - // Advanced setting defaults start here. - internal const int iMinFragmentsToUnlock = 2; - internal const int iDepthSearchTime = 15; - internal const int iMaxBasicOutpostSize = 24; - internal const int iMaxEggsAsSingleIngredient = 1; - internal const int iMaxInventorySizePerRecipe = 24; - internal const double dFuzziness = 0.2; - internal const double dIngredientRatio = 0.45; - internal const float fFragmentSpawnChanceMin = 0.3f; - internal const float fFragmentSpawnChanceMax = 0.6f; + private static readonly Dictionary s_defaults = new Dictionary() + { + // Key, Default value, Minimum value, Maximum value. + { "iRandomiserMode", new[] { 0, 0, 1 } }, + { "bUseFish", new[] { true, true, true } }, + { "bUseEggs", new[] { false, false, false } }, + { "bUseSeeds", new[] { true, true, true } }, + { "bRandomiseDataboxes", new[] { true, true, true } }, + { "bRandomiseFragments", new[] { true, true, true } }, + { "bRandomiseNumFragments", new[] { true, true, true } }, + { "bRandomiseRecipes", new[] { true, true, true } }, + { "bVanillaUpgradeChains", new[] { false, false, false } }, + { "bDoBaseTheming", new[] { false, false, false } }, + { "iEquipmentAsIngredients", new[] { 1, 0, 2 } }, + { "iToolsAsIngredients", new[] { 1, 0, 2 } }, + { "iUpgradesAsIngredients", new[] { 1, 0, 2 } }, + { "iMaxAmountPerIngredient", new[] { 5, 1, 10 } }, + { "iMaxIngredientsPerRecipe", new[] { 7, 1, 10 } }, + { "iMaxBiomesPerFragment", new[] { 5, 3, 10 } }, + { "iMaxFragmentsToUnlock", new[] { 5, 1, 30 } }, + + // Advanced settings start here. + { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, + { "iDepthSearchTime", new[] { 15, 0, 45 } }, + { "iMaxBasicOutpostSize", new[] { 24, 4, 48 } }, + { "iMaxEggsAsSingleIngredient", new[] { 1, 1, 10 } }, + { "iMaxInventorySizePerRecipe", new[] { 24, 4, 100 } }, + { "dFuzziness", new[] { 0.2, 0.0, 1.0 } }, + { "dIngredientRatio", new[] { 0.45, 0.0, 1.0 } }, + { "fFragmentSpawnChanceMin", new[] { 0.3f, 0.01f, 10.0f } }, + { "fFragmentSpawnChanceMax", new[] { 0.6f, 0.01f, 10.0f } }, + }; + + internal static bool Contains(string key) + { + return s_defaults.ContainsKey(key); + } + + internal static object GetDefault(string key) + { + return s_defaults[key][0]; + } + + internal static object GetMax(string key) + { + return s_defaults[key][2]; + } + + internal static object GetMin(string key) + { + return s_defaults[key][1]; + } } -} +} \ No newline at end of file From 76f6dae5e6466e0d24a598f8d589e074c85b3d23 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:11:24 +0200 Subject: [PATCH 19/37] Close #31. Implement random scan rewards. --- SubnauticaRandomiser/EntitySerializer.cs | 26 ++- SubnauticaRandomiser/FragmentPatcher.cs | 160 ------------------ SubnauticaRandomiser/Logic/FragmentLogic.cs | 16 ++ .../Logic/Recipes/Materials.cs | 12 ++ .../Patches/FragmentPatcher.cs | 121 +++++++++++++ SubnauticaRandomiser/RandomiserConfig.cs | 13 +- .../SubnauticaRandomiser.csproj | 2 +- 7 files changed, 184 insertions(+), 166 deletions(-) delete mode 100644 SubnauticaRandomiser/FragmentPatcher.cs create mode 100644 SubnauticaRandomiser/Patches/FragmentPatcher.cs diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index c4b79cf..6456c62 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -27,10 +27,16 @@ namespace SubnauticaRandomiser [Serializable] public class EntitySerializer { - public Dictionary RecipeDict = new Dictionary(); - public Dictionary> SpawnDataDict = new Dictionary>(); + // All databoxes and their new locations. public Dictionary Databoxes = new Dictionary(); + // The options to choose from for spawning materials when scanning a fragment which is already unlocked. + public Dictionary FragmentMaterialYield; + // The number of scans required to unlock the fragment item. public Dictionary NumFragmentsToUnlock = new Dictionary(); + // All modified recipes. + public Dictionary RecipeDict = new Dictionary(); + // All modified fragment spawn rates. + public Dictionary> SpawnDataDict = new Dictionary>(); public bool isDataboxRandomised = false; public const int SaveVersion = InitMod.s_expectedSaveVersion; @@ -63,6 +69,22 @@ public static EntitySerializer FromBase64String(string base64String) } } + /// + /// Try to add an entry to the duplicate fragment scan material dictionary. + /// + /// The TechType to spawn. + /// The weighting for the spawn rate. + /// True if successful, false if the key already exists in the dictionary. + public bool AddDuplicateFragmentMaterial(TechType type, float weight) + { + FragmentMaterialYield ??= new Dictionary(); + if (FragmentMaterialYield.ContainsKey(type)) + return false; + + FragmentMaterialYield.Add(type, weight); + return true; + } + /// /// Try to add an entry to the FragmentUnlockNumber dictionary. /// diff --git a/SubnauticaRandomiser/FragmentPatcher.cs b/SubnauticaRandomiser/FragmentPatcher.cs deleted file mode 100644 index 31de82b..0000000 --- a/SubnauticaRandomiser/FragmentPatcher.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using HarmonyLib; -using SMLHelper.V2.Handlers; -using static LootDistributionData; - -namespace SubnauticaRandomiser -{ - //[HarmonyPatch] - public class FragmentPatcher - { - private static List _rawMaterials = new List { - TechType.Titanium, - TechType.Copper, - TechType.Silver, - TechType.Gold, - TechType.ScrapMetal, - TechType.Peeper - }; - - public FragmentPatcher() - { - } - - internal static void EditLootDistribution() - { - // This spawns a bunch of laser cutters, but fragments do not seem affected - LogHandler.Debug("1, " + CraftData.GetClassIdForTechType(TechType.LaserCutter)); - //LootDistributionHandler.EditLootDistributionData(CraftData.GetClassIdForTechType(TechType.WorkbenchFragment), BiomeType.SafeShallows_Grass, (float)0.6, 20); - LogHandler.Debug("2"); - LootDistributionHandler.EditLootDistributionData(CraftData.GetClassIdForTechType(TechType.LaserCutter), BiomeType.SafeShallows_ShellTunnel, (float)0.9, 20); - LogHandler.Debug("3"); - // Does not seem to work. Wrong classId? - // LootDistributionData.PrefabData p = new LootDistributionData.PrefabData(); - //List dist = new List(); - //BiomeData d = new BiomeData(); - //d.biome = BiomeType.SafeShallows_CaveFloor; - //d.count = 20; - //d.probability = (float)0.95; - //dist.Add(d); - - //SrcData src = new SrcData(); - - //UWE.WorldEntityInfo info = new UWE.WorldEntityInfo(); - //UWE.WorldEntityDatabase.TryGetInfo(CraftData.GetClassIdForTechType(TechType.LaserCutterFragment), out info); - //LootDistributionHandler.AddLootDistributionData(CraftData.GetClassIdForTechType(TechType.WorkbenchFragment), dist, info,); - - - // Look at dnSpy, TechFragment class. Seems like there's one overarching - // class with an easy to change techType field? Intercept a function - // and overwrite that before the prefab gets loaded? - // TechFragment.GetRandom() seems like a good target? - - // BiomeType enum inexplicably has fragments in it? - - // *** HOW THINGS SEEM TO WORK *** - /* When the game first loads a cell, it decides to populate it with - * things, or not. This is when the loot distributor comes into play. - * Most fragments have like a 4-10% spawn chance in very specific biomes, - * like the sandy parts of the grassy plateaus. Afterwards, unless a - * mod is used to change that, the game no longer spawns anything within - * that cell. This is how the world can slowly become devoid of life - * and resources as you keep hoovering up everything. - * - * Many fragments can spawn in many different biomes, and have different - * distributions for that. However, those are mostly the random fragments - * somewhere on the seafloor. - * In addition, there's also biomes that are specifically named after - * the fragment they support. This seems to be the very specific locations - * where a specific fragment will ALWAYS spawn, such as within wrecks. - * - * Setting the distribution of fragments in their vanilla locations to 0 - * works, and is an effective way to prevent unwanted spawns. However, - * this has to be done for every single specific fragment prefab (of which - * e.g. the moonpool has six). - * - * The CustomizeYourSpawns mod has exposed all the spawn and biome IDs - * in handy json format in its mod folder. Now how to convert those prefabs - * to class ids which the function actually takes? No clue. But that mod - * can do it. - * - * As an aside, this same spawning system could also be used to mess with - * the spawns of fish. Ghost leviathan in the shallows? Suddenly possible. - * - */ - } - - // This method patches a few lines into PDAScanner.Scan() to make that - // massive thing do what we want. Instead of hard-coding two titanium - // on scanning a duplicate fragment, the game will instead call YieldMaterial() - // in this class here. - //[HarmonyTranspiler] - //[HarmonyPatch(typeof(PDAScanner), nameof(PDAScanner.Scan))] - public static IEnumerable Transpiler(IEnumerable codeInstructions) - { - LogHandler.Debug("Starting transpiler for duplicate scan results."); - - List instructions = new List(codeInstructions); - - string errorMsgBeforeMethodCall = "ScannerRedundantScanned"; - int methodArgsIndex = 0; - - // Before the crucial call that adds two titanium on duplicate scan, - // the game actually logs an error message. We abuse this fact to - // easily find the code lines we need. - for (int i = 0; i < instructions.Count; i++) - { - if (instructions[i].Is(OpCodes.Ldstr, errorMsgBeforeMethodCall)) - { - // Found the debug error message just before the method we need to alter. - // The part we're here for is somewhere in the next few lines. - for (int j = 1; j < 5; j++) - { - // The crucial line is a four-argument call to CraftData.AddToInventory(). - // The first of those arguments is TechType Titanium. - if (instructions[i+j].Is(OpCodes.Ldc_I4_S, 16)) - { - // Found the instruction pushing TechType 16 (Titanium) onto the stack. - methodArgsIndex = i + j; - LogHandler.Debug("Found arg0 Titanium at index " + methodArgsIndex); - - // The original method takes four arguments, but the replacement needs - // only one. Replace the first argument with the scan target (conveniently - // stored in local variable 0) and the others with NOP to preserve continuity. - instructions[methodArgsIndex].opcode = OpCodes.Ldloc_0; - instructions[methodArgsIndex + 1].opcode = OpCodes.Nop; - instructions[methodArgsIndex + 2].opcode = OpCodes.Nop; - instructions[methodArgsIndex + 3].opcode = OpCodes.Nop; - instructions[methodArgsIndex + 4].operand = typeof(FragmentPatcher).GetMethod("YieldMaterial", new Type[] { typeof(TechType) }); - - LogHandler.Debug("Successfully altered CodeInstructions."); - break; - } - } - break; - } - } - - if (methodArgsIndex == 0) - LogHandler.Error("Failed to find argument index while trying to transpile fragment scan rewards!"); - - return instructions.AsEnumerable(); - } - - public static void YieldMaterial(TechType target) - { - LogHandler.Debug("Replacing duplicate fragment scan yield of target " + target.AsString()); - - // TODO - //CraftData.AddToInventory(TechType.None, 2, false, true); - Random r = new Random(); - - TechType type = _rawMaterials[r.Next(_rawMaterials.Count)]; - CraftData.AddToInventory(type, 2, false, true); - } - } -} - diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 3517517..4305b2c 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -222,6 +222,22 @@ private void ChangeNumFragmentsToUnlock(LogicEntity entity) LogHandler.Debug(" New number of fragments required: " + numFragments); _masterDict.AddFragmentUnlockNum(entity.TechType, numFragments); } + + /// + /// Set up the dictionary of possible rewards for scanning an already unlocked fragment. + /// + internal void CreateDuplicateScanYieldDict() + { + _masterDict.FragmentMaterialYield = new Dictionary(); + var materials = _logic._materials.GetAllRawMaterials(50); + + foreach (LogicEntity entity in materials) + { + // Two random calls will tend to produce less extreme and more evenly distributed values. + double weight = _random.NextDouble() + _random.NextDouble(); + _masterDict.AddDuplicateFragmentMaterial(entity.TechType, (float)weight); + } + } /// /// Assemble a dictionary of all relevant prefabs with their unique classId identifier. diff --git a/SubnauticaRandomiser/Logic/Recipes/Materials.cs b/SubnauticaRandomiser/Logic/Recipes/Materials.cs index 674b7cf..ebf75d9 100644 --- a/SubnauticaRandomiser/Logic/Recipes/Materials.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Materials.cs @@ -171,5 +171,17 @@ internal List GetAllFragments() return fragments; } + + /// + /// Get all entities that are considered raw materials and accessible by the given depth. + /// + /// The maximum depth at which the raw materials must be available. + internal List GetAllRawMaterials(int maxDepth = 2000) + { + var rawMaterials = _allMaterials.FindAll(x => + x.Category.Equals(ETechTypeCategory.RawMaterials) && x.AccessibleDepth <= maxDepth); + + return rawMaterials; + } } } diff --git a/SubnauticaRandomiser/Patches/FragmentPatcher.cs b/SubnauticaRandomiser/Patches/FragmentPatcher.cs new file mode 100644 index 0000000..c9cb70b --- /dev/null +++ b/SubnauticaRandomiser/Patches/FragmentPatcher.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using HarmonyLib; + +namespace SubnauticaRandomiser.Patches +{ + [HarmonyPatch] + public class FragmentPatcher + { + /// + /// This method patches a few lines into PDAScanner.Scan() to intercept the game's normal operations. + /// Instead of hard-coding two titanium on scanning a duplicate fragment, the game will instead call + /// YieldMaterial() in this class here. + /// + /// The IL code of the function. + /// The transpiled, modified code. + [HarmonyTranspiler] + [HarmonyPatch(typeof(PDAScanner), nameof(PDAScanner.Scan))] + public static IEnumerable Transpiler(IEnumerable codeInstructions) + { + LogHandler.Debug("Starting transpiler for duplicate scan results."); + + List instructions = new List(codeInstructions); + + string errorMsgBeforeMethodCall = "ScannerRedundantScanned"; + int methodArgsIndex = 0; + + // Before the crucial call that adds two titanium on duplicate scan, + // the game actually logs an error message. We abuse this fact to + // easily find the code lines we need. + for (int i = 0; i < instructions.Count; i++) + { + if (!instructions[i].Is(OpCodes.Ldstr, errorMsgBeforeMethodCall)) + continue; + + // Found the debug error message just before the method we need to alter. + // The part we're here for is somewhere in the next few lines. + for (int j = 1; j < 5; j++) + { + // The crucial line is a four-argument call to CraftData.AddToInventory(). + // The first of those arguments is TechType Titanium. + if (instructions[i+j].Is(OpCodes.Ldc_I4_S, 16)) + { + // Found the instruction pushing TechType 16 (Titanium) onto the stack. + methodArgsIndex = i + j; + LogHandler.Debug("Found arg0 Titanium at index " + methodArgsIndex); + + // The original method takes four arguments, but the replacement needs + // only one. Replace the first argument with the scan target (conveniently + // stored in local variable 0) and the others with NOP to preserve continuity. + instructions[methodArgsIndex].opcode = OpCodes.Ldloc_0; + instructions[methodArgsIndex + 1].opcode = OpCodes.Nop; + instructions[methodArgsIndex + 2].opcode = OpCodes.Nop; + instructions[methodArgsIndex + 3].opcode = OpCodes.Nop; + instructions[methodArgsIndex + 4].operand + = typeof(FragmentPatcher).GetMethod("YieldMaterial", new[] { typeof(TechType) }); + + LogHandler.Debug("Successfully altered CodeInstructions."); + break; + } + } + break; + } + + if (methodArgsIndex == 0) + LogHandler.Error("Failed to find argument index while trying to transpile fragment scan rewards!"); + + return instructions.AsEnumerable(); + } + + /// + /// Add a random material to the player's inventory upon scanning an already known fragment. + /// + /// The fragment being scanned. + public static void YieldMaterial(TechType target) + { + // If the options for yields were not randomised, just go with the game's default behaviour. + if (!(InitMod.s_masterDict?.FragmentMaterialYield?.Count > 0)) + { + CraftData.AddToInventory(TechType.Titanium, 2, false, true); + return; + } + + Random rand = new Random(); + TechType type = GetRandomMaterial(rand); + int number = rand.Next(1, InitMod.s_config?.iMaxDuplicateScanYield + 1 ?? 4); + LogHandler.Debug("Replacing duplicate fragment scan yield of target " + target.AsString() + " with " + + type.AsString()); + CraftData.AddToInventory(type, number, false, true); + } + + /// + /// Choose a random weighted material for duplicate scan rewards. + /// + /// An instance of Random. + /// The TechType of the chosen material, or Titanium if an error occurred. + private static TechType GetRandomMaterial(Random rand) + { + if (!(InitMod.s_masterDict?.FragmentMaterialYield?.Count > 0)) + return TechType.Titanium; + + double sumOfWeights = InitMod.s_masterDict.FragmentMaterialYield.Sum(x => x.Value); + double choice = sumOfWeights * rand.NextDouble(); + + // Add up the weights of the material options until the value of 'choice' is exceeded, and choose that one. + double sum = 0.0; + foreach (var kv in InitMod.s_masterDict.FragmentMaterialYield) + { + sum += kv.Value; + if (sum >= choice) + return kv.Key; + } + + LogHandler.Warn("Failed to choose random material for duplicate fragment scan."); + return TechType.Titanium; + } + } +} + diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index 288df3d..ece845b 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -98,11 +98,12 @@ public void NewRandomOldSeed() } public string ADVANCED_SETTINGS_BELOW_THIS_POINT = "ADVANCED_SETTINGS_BELOW_THIS_POINT"; - public int iMinFragmentsToUnlock = (int)ConfigDefaults.GetDefault("iMinFragmentsToUnlock"); public int iDepthSearchTime = (int)ConfigDefaults.GetDefault("iDepthSearchTime"); public int iMaxBasicOutpostSize = (int)ConfigDefaults.GetDefault("iMaxBasicOutpostSize"); - public int iMaxEggsAsSingleIngredient = (int)ConfigDefaults.GetDefault("iMaxEgssAsSingleIngredient"); + public int iMaxDuplicateScanYield = (int)ConfigDefaults.GetDefault("iMaxDuplicateScanYield"); + public int iMaxEggsAsSingleIngredient = (int)ConfigDefaults.GetDefault("iMaxEggsAsSingleIngredient"); public int iMaxInventorySizePerRecipe = (int)ConfigDefaults.GetDefault("iMaxInventorySizePerRecipe"); + public int iMinFragmentsToUnlock = (int)ConfigDefaults.GetDefault("iMinFragmentsToUnlock"); public double dFuzziness = (double)ConfigDefaults.GetDefault("dFuzziness"); public double dIngredientRatio = (double)ConfigDefaults.GetDefault("dIngredientRatio"); public float fFragmentSpawnChanceMin = (float)ConfigDefaults.GetDefault("fFragmentSpawnChanceMin"); @@ -180,11 +181,12 @@ internal static class ConfigDefaults { "iMaxFragmentsToUnlock", new[] { 5, 1, 30 } }, // Advanced settings start here. - { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, { "iDepthSearchTime", new[] { 15, 0, 45 } }, { "iMaxBasicOutpostSize", new[] { 24, 4, 48 } }, + { "iMaxDuplicateScanYield", new[] { 3, 1, 10 } }, { "iMaxEggsAsSingleIngredient", new[] { 1, 1, 10 } }, { "iMaxInventorySizePerRecipe", new[] { 24, 4, 100 } }, + { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, { "dFuzziness", new[] { 0.2, 0.0, 1.0 } }, { "dIngredientRatio", new[] { 0.45, 0.0, 1.0 } }, { "fFragmentSpawnChanceMin", new[] { 0.3f, 0.01f, 10.0f } }, @@ -198,6 +200,11 @@ internal static bool Contains(string key) internal static object GetDefault(string key) { + if (!s_defaults.ContainsKey(key)) + { + LogHandler.Warn("Tried to get invalid key from config default dictionary: " + key); + return null; + } return s_defaults[key][0]; } diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index ae4b5f4..3931982 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -41,6 +41,7 @@ cp $(SolutionDir)recipeInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomi cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser + @@ -67,7 +68,6 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis - From d83f560239a4837b00cdb8164488a5f0b87a7af3 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:39:00 +0200 Subject: [PATCH 20/37] Fix masterDict not being updated in InitMod. --- SubnauticaRandomiser/InitMod.cs | 7 ++++--- SubnauticaRandomiser/Logic/CoreLogic.cs | 12 ++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 34c4510..37daeed 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -82,7 +82,7 @@ public static void Initialise() /// internal static void Randomise() { - s_masterDict = new EntitySerializer(); + s_masterDict = null; s_config.SanitiseConfigValues(); s_config.iSaveVersion = s_expectedSaveVersion; var csvReader = new CSVReader(); @@ -121,8 +121,9 @@ internal static void Randomise() random = new Random(s_config.iSeed); // Randomise! - CoreLogic logic = new CoreLogic(random, s_masterDict, s_config, materials, biomes, databoxes); - logic.Randomise(); + CoreLogic logic = new CoreLogic(random, s_config, materials, biomes, databoxes); + s_masterDict = logic.Randomise(); + ApplyAllChanges(); LogHandler.Info("Randomisation successful!"); SaveGameStateToDisk(); diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 7cab0fd..2a0e8bd 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -24,12 +24,12 @@ public class CoreLogic private readonly FragmentLogic _fragmentLogic; private readonly RecipeLogic _recipeLogic; - public CoreLogic(System.Random random, EntitySerializer masterDict, RandomiserConfig config, + public CoreLogic(System.Random random, RandomiserConfig config, List allMaterials, List biomes = null, List databoxes = null) { _config = config; _databoxes = databoxes; - _masterDict = masterDict; + _masterDict = new EntitySerializer(); _materials = new Materials(allMaterials); _random = random; _spoilerLog = new SpoilerLog(config); @@ -61,6 +61,8 @@ private void Setup(List notRandomised) FragmentLogic.Init(); // Queue up all fragments to be randomised. notRandomised.AddRange(_materials.GetAllFragments()); + // Randomise duplicate scan rewards. + _fragmentLogic.CreateDuplicateScanYieldDict(); } if (_recipeLogic != null) @@ -79,9 +81,10 @@ private void Setup(List notRandomised) /// /// Start the randomisation process. /// + /// A serialisation instance containing all changes made. /// Raised to prevent infinite loops if the core loop takes too long to find /// a valid solution. - internal void Randomise() + internal EntitySerializer Randomise() { LogHandler.Info("Randomising using logic-based system..."); @@ -138,8 +141,9 @@ internal void Randomise() } _spoilerLog.WriteLog(); - InitMod.ApplyAllChanges(); LogHandler.Info("Finished randomising within " + circuitbreaker + " cycles!"); + + return _masterDict; } /// From 19f570f2902db41399528c3169fe061619993885 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Wed, 10 Aug 2022 12:43:42 +0200 Subject: [PATCH 21/37] Reduce default number of duplicate scan rewards. --- SubnauticaRandomiser/RandomiserConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index ece845b..d66cac6 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -183,7 +183,7 @@ internal static class ConfigDefaults // Advanced settings start here. { "iDepthSearchTime", new[] { 15, 0, 45 } }, { "iMaxBasicOutpostSize", new[] { 24, 4, 48 } }, - { "iMaxDuplicateScanYield", new[] { 3, 1, 10 } }, + { "iMaxDuplicateScanYield", new[] { 2, 1, 10 } }, { "iMaxEggsAsSingleIngredient", new[] { 1, 1, 10 } }, { "iMaxInventorySizePerRecipe", new[] { 24, 4, 100 } }, { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, From e09f3b4b58ea51804e4dac24841ac7f7e4ee6331 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:09:44 +0200 Subject: [PATCH 22/37] Decouple fragment randomisation from other fragment options. --- SubnauticaRandomiser/InitMod.cs | 2 +- SubnauticaRandomiser/Logic/CoreLogic.cs | 21 ++++++++++----- SubnauticaRandomiser/Logic/FragmentLogic.cs | 29 +++++++++++++++------ SubnauticaRandomiser/RandomiserConfig.cs | 4 +++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 37daeed..e69be26 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -143,7 +143,7 @@ internal static void ApplyAllChanges() RecipeLogic.ApplyMasterDict(s_masterDict); // Load fragment changes. - if (s_masterDict.SpawnDataDict?.Count > 0) + if (s_masterDict.SpawnDataDict?.Count > 0 || s_masterDict.NumFragmentsToUnlock?.Count > 0) { FragmentLogic.ApplyMasterDict(s_masterDict); LogHandler.Info("Loaded fragment state."); diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 2a0e8bd..32123df 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -36,7 +36,7 @@ public CoreLogic(System.Random random, RandomiserConfig config, if (_config.bRandomiseDataboxes) _databoxLogic = new DataboxLogic(this); - if (_config.bRandomiseFragments) + if (_config.bRandomiseFragments || _config.bRandomiseNumFragments || _config.bRandomiseDuplicateScans) _fragmentLogic = new FragmentLogic(this, biomes); if (_config.bRandomiseRecipes) _recipeLogic = new RecipeLogic(this); @@ -57,12 +57,21 @@ private void Setup(List notRandomised) if (_fragmentLogic != null) { - // Initialise the fragment cache and remove vanilla spawns. - FragmentLogic.Init(); - // Queue up all fragments to be randomised. - notRandomised.AddRange(_materials.GetAllFragments()); + if (_config.bRandomiseFragments) + { + // Initialise the fragment cache and remove vanilla spawns. + FragmentLogic.Init(); + // Queue up all fragments to be randomised. + notRandomised.AddRange(_materials.GetAllFragments()); + } + + // Randomise the number of fragment scans required per blueprint. + if (_config.bRandomiseNumFragments) + _fragmentLogic.RandomiseNumFragments(_materials.GetAllFragments()); + // Randomise duplicate scan rewards. - _fragmentLogic.CreateDuplicateScanYieldDict(); + if (_config.bRandomiseDuplicateScans) + _fragmentLogic.CreateDuplicateScanYieldDict(); } if (_recipeLogic != null) diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 4305b2c..14906b1 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -124,13 +124,23 @@ internal List RandomiseFragment(LogicEntity entity, int depth) LogHandler.Debug(" Adding fragment to biome: " + biomeType.AsString() + ", " + spawnRate); } - - // Change the number of fragments required to unlock the blueprint. - ChangeNumFragmentsToUnlock(entity); + ApplyRandomisedFragment(entity, spawnList); return spawnList; } + /// + /// Change the number of scans required to unlock the blueprint for all fragments. + /// + /// The list of fragments to change scan numbers for. + internal void RandomiseNumFragments(List fragments) + { + foreach (LogicEntity entity in fragments) + { + ChangeNumFragmentsToUnlock(entity); + } + } + /// /// Go through all the BiomeData in the game and reset any fragment spawn rates to 0.0f, effectively "deleting" /// them from the game until the randomiser has decided on a new distribution. @@ -330,13 +340,16 @@ private float[] SplitFragmentSpawnRate(float spawnRate, int parts) /// internal static void ApplyMasterDict(EntitySerializer masterDict) { - Init(); - - foreach (TechType key in masterDict.SpawnDataDict.Keys) + if (masterDict.SpawnDataDict?.Count > 0) { - foreach (SpawnData spawnData in masterDict.SpawnDataDict[key]) + Init(); + + foreach (TechType key in masterDict.SpawnDataDict.Keys) { - LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); + foreach (SpawnData spawnData in masterDict.SpawnDataDict[key]) + { + LootDistributionHandler.EditLootDistributionData(spawnData.ClassId, spawnData.GetBaseBiomeData()); + } } } diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index d66cac6..cae4831 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -37,6 +37,9 @@ public class RandomiserConfig : ConfigFile [Toggle("Randomise number of fragments needed?")] public bool bRandomiseNumFragments = (bool)ConfigDefaults.GetDefault("bRandomiseNumFragments"); + [Toggle("Randomise duplicate scan rewards?")] + public bool bRandomiseDuplicateScans = (bool)ConfigDefaults.GetDefault("bRandomiseDuplicateScans"); + [Toggle("Randomise recipes?")] public bool bRandomiseRecipes = (bool)ConfigDefaults.GetDefault("bRandomiseRecipes"); @@ -169,6 +172,7 @@ internal static class ConfigDefaults { "bRandomiseDataboxes", new[] { true, true, true } }, { "bRandomiseFragments", new[] { true, true, true } }, { "bRandomiseNumFragments", new[] { true, true, true } }, + { "bRandomiseDuplicateScans", new[] { true, true ,true } }, { "bRandomiseRecipes", new[] { true, true, true } }, { "bVanillaUpgradeChains", new[] { false, false, false } }, { "bDoBaseTheming", new[] { false, false, false } }, From 9165872fcb276592e9ef9d05c99ce9cdb1c251af Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Thu, 11 Aug 2022 05:55:17 +0200 Subject: [PATCH 23/37] Switch to manual harmony patching. --- SubnauticaRandomiser/EntitySerializer.cs | 6 +-- SubnauticaRandomiser/InitMod.cs | 31 ++++++++++--- SubnauticaRandomiser/Logic/DataboxLogic.cs | 2 +- .../{ => Patches}/DataboxPatcher.cs | 43 +++++++++---------- .../Patches/FragmentPatcher.cs | 2 +- .../RandomiserObjects/SpoilerLog.cs | 2 +- .../SubnauticaRandomiser.csproj | 2 +- 7 files changed, 51 insertions(+), 37 deletions(-) rename SubnauticaRandomiser/{ => Patches}/DataboxPatcher.cs (85%) diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index 6456c62..625290b 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -28,7 +28,7 @@ namespace SubnauticaRandomiser public class EntitySerializer { // All databoxes and their new locations. - public Dictionary Databoxes = new Dictionary(); + public Dictionary Databoxes; // The options to choose from for spawning materials when scanning a fragment which is already unlocked. public Dictionary FragmentMaterialYield; // The number of scans required to unlock the fragment item. @@ -37,8 +37,8 @@ public class EntitySerializer public Dictionary RecipeDict = new Dictionary(); // All modified fragment spawn rates. public Dictionary> SpawnDataDict = new Dictionary>(); - - public bool isDataboxRandomised = false; + + public bool NeedsHarmony = false; public const int SaveVersion = InitMod.s_expectedSaveVersion; /// diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index e69be26..b381864 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -7,6 +7,7 @@ using SMLHelper.V2.Handlers; using SubnauticaRandomiser.Logic; using SubnauticaRandomiser.Logic.Recipes; +using SubnauticaRandomiser.Patches; using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser @@ -70,8 +71,6 @@ public static void Initialise() LogHandler.Warn("Failed to load game state from disk: dictionary empty."); Randomise(); - if (s_masterDict?.isDataboxRandomised == true) - EnableHarmonyPatching(); } LogHandler.Info("Finished loading."); @@ -149,8 +148,8 @@ internal static void ApplyAllChanges() LogHandler.Info("Loaded fragment state."); } - // Load databox changes. - if (s_masterDict.isDataboxRandomised) + // Load any changes that rely on harmony patches. + if (s_masterDict.NeedsHarmony) EnableHarmonyPatching(); } @@ -223,14 +222,32 @@ internal static string GetSubnauticaRandomiserDirectory() } /// - /// Enables all necessary harmony patches based on the randomisation state in s_masterDict. + /// Enables all necessary harmony patches based on the randomisation state in the serialiser. + /// Must use manual patching since PatchAll() will not respect any config settings. /// private static void EnableHarmonyPatching() { + Harmony harmony = new Harmony("SubnauticaRandomiser"); + + // Swapping databoxes. if (s_masterDict?.Databoxes?.Count > 0) { - Harmony harmony = new Harmony("SubnauticaRandomiser"); - harmony.PatchAll(); + var original = AccessTools.Method(typeof(DataboxSpawner), nameof(DataboxSpawner.Start)); + var prefix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnSpawn)); + harmony.Patch(original, new HarmonyMethod(prefix)); + + original = AccessTools.Method(typeof(ProtobufSerializer), + nameof(ProtobufSerializer.DeserializeIntoGameObject)); + var postfix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnLoad)); + harmony.Patch(original, postfix: new HarmonyMethod(postfix)); + } + + // Changing duplicate scan rewards. + if (s_masterDict?.FragmentMaterialYield?.Count > 0) + { + var original = AccessTools.Method(typeof(PDAScanner), nameof(PDAScanner.Scan)); + var transpiler = AccessTools.Method(typeof(FragmentPatcher), nameof(FragmentPatcher.Transpiler)); + harmony.Patch(original, transpiler: new HarmonyMethod(transpiler)); } } } diff --git a/SubnauticaRandomiser/Logic/DataboxLogic.cs b/SubnauticaRandomiser/Logic/DataboxLogic.cs index 6cb502a..ddd824d 100644 --- a/SubnauticaRandomiser/Logic/DataboxLogic.cs +++ b/SubnauticaRandomiser/Logic/DataboxLogic.cs @@ -46,7 +46,7 @@ internal List RandomiseDataboxes() + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); toBeRandomised.RemoveAt(next); } - _masterDict.isDataboxRandomised = true; + _masterDict.NeedsHarmony = true; return randomDataboxes; } diff --git a/SubnauticaRandomiser/DataboxPatcher.cs b/SubnauticaRandomiser/Patches/DataboxPatcher.cs similarity index 85% rename from SubnauticaRandomiser/DataboxPatcher.cs rename to SubnauticaRandomiser/Patches/DataboxPatcher.cs index e2e24b9..49c6086 100644 --- a/SubnauticaRandomiser/DataboxPatcher.cs +++ b/SubnauticaRandomiser/Patches/DataboxPatcher.cs @@ -3,13 +3,14 @@ using SubnauticaRandomiser.RandomiserObjects; using UnityEngine; -namespace SubnauticaRandomiser +namespace SubnauticaRandomiser.Patches { - [HarmonyPatch(typeof(DataboxSpawner), nameof(DataboxSpawner.Start))] + // [HarmonyPatch] internal class DataboxPatcher { [HarmonyPrefix] + [HarmonyPatch(typeof(DataboxSpawner), nameof(DataboxSpawner.Start))] internal static bool PatchDataboxOnSpawn(ref DataboxSpawner __instance) { Dictionary boxDict = InitMod.s_masterDict.Databoxes; @@ -23,6 +24,23 @@ internal static bool PatchDataboxOnSpawn(ref DataboxSpawner __instance) return true; } + + /// + /// Intercept loading any GameObject from disk, and swap the blueprint if the GameObject happens to be a + /// databox. Very much suboptimal since this function gets called every single time something spawns in game. + /// + [HarmonyPostfix] + [HarmonyPatch(typeof(ProtobufSerializer), nameof(ProtobufSerializer.DeserializeIntoGameObject))] + internal static void PatchDataboxOnLoad(ref ProtobufSerializer __instance, UniqueIdentifier uid) + { + BlueprintHandTarget blueprint = uid.gameObject.GetComponent(); + + if (blueprint == null) + return; + + LogHandler.Debug("[OnLoad] Found blueprint " + blueprint.unlockTechType.AsString()); + ReplaceDatabox(InitMod.s_masterDict.Databoxes, uid.transform.position, blueprint); + } internal static void ReplaceDatabox(Dictionary boxDict, Vector3 position, BlueprintHandTarget blueprint) { @@ -39,25 +57,4 @@ internal static void ReplaceDatabox(Dictionary boxDi } } } - - - [HarmonyPatch(typeof(ProtobufSerializer), nameof(ProtobufSerializer.DeserializeIntoGameObject))] - internal class DataboxSavePatcher - { - /// - /// Intercept loading any GameObject from disk, and swap the blueprint if the GameObject happens to be a - /// databox. Very much suboptimal since this function gets called every single time something spawns in game. - /// - [HarmonyPostfix] - internal static void PatchDataboxOnLoad(ref ProtobufSerializer __instance, UniqueIdentifier uid) - { - BlueprintHandTarget blueprint = uid.gameObject.GetComponent(); - - if (blueprint == null) - return; - - LogHandler.Debug("[OnLoad] Found blueprint " + blueprint.unlockTechType.AsString()); - DataboxPatcher.ReplaceDatabox(InitMod.s_masterDict.Databoxes, uid.transform.position, blueprint); - } - } } diff --git a/SubnauticaRandomiser/Patches/FragmentPatcher.cs b/SubnauticaRandomiser/Patches/FragmentPatcher.cs index c9cb70b..0507fa2 100644 --- a/SubnauticaRandomiser/Patches/FragmentPatcher.cs +++ b/SubnauticaRandomiser/Patches/FragmentPatcher.cs @@ -6,7 +6,7 @@ namespace SubnauticaRandomiser.Patches { - [HarmonyPatch] + // [HarmonyPatch] public class FragmentPatcher { /// diff --git a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs index 758c69f..1c19b33 100644 --- a/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs +++ b/SubnauticaRandomiser/RandomiserObjects/SpoilerLog.cs @@ -129,7 +129,7 @@ private string[] PrepareAdvancedSettings() /// The prepared log entries. private string[] PrepareDataboxes() { - if (!InitMod.s_masterDict.isDataboxRandomised) + if (InitMod.s_masterDict.Databoxes is null) return new [] { "Not randomised, all in vanilla locations." }; List preparedDataboxes = new List(); diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 3931982..02a3bc1 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -41,6 +41,7 @@ cp $(SolutionDir)recipeInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomi cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser + @@ -58,7 +59,6 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis - From ddf075b3142cfe5ebb4e2bf0c3d0dc1a080db181 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Thu, 11 Aug 2022 06:50:03 +0200 Subject: [PATCH 24/37] Fix #32. Corridors return correct materials. --- SubnauticaRandomiser/InitMod.cs | 11 +++++-- .../Patches/DeconstructionFix.cs | 32 +++++++++++++++++++ .../SubnauticaRandomiser.csproj | 1 + 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 SubnauticaRandomiser/Patches/DeconstructionFix.cs diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index b381864..1943c58 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -229,11 +229,16 @@ private static void EnableHarmonyPatching() { Harmony harmony = new Harmony("SubnauticaRandomiser"); + // Make corridors return the correct building materials. + var original = AccessTools.Method(typeof(BaseDeconstructable), nameof(BaseDeconstructable.Deconstruct)); + var prefix = AccessTools.Method(typeof(DeconstructionFix), nameof(DeconstructionFix.FixCorridors)); + harmony.Patch(original, prefix: new HarmonyMethod(prefix)); + // Swapping databoxes. if (s_masterDict?.Databoxes?.Count > 0) { - var original = AccessTools.Method(typeof(DataboxSpawner), nameof(DataboxSpawner.Start)); - var prefix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnSpawn)); + original = AccessTools.Method(typeof(DataboxSpawner), nameof(DataboxSpawner.Start)); + prefix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnSpawn)); harmony.Patch(original, new HarmonyMethod(prefix)); original = AccessTools.Method(typeof(ProtobufSerializer), @@ -245,7 +250,7 @@ private static void EnableHarmonyPatching() // Changing duplicate scan rewards. if (s_masterDict?.FragmentMaterialYield?.Count > 0) { - var original = AccessTools.Method(typeof(PDAScanner), nameof(PDAScanner.Scan)); + original = AccessTools.Method(typeof(PDAScanner), nameof(PDAScanner.Scan)); var transpiler = AccessTools.Method(typeof(FragmentPatcher), nameof(FragmentPatcher.Transpiler)); harmony.Patch(original, transpiler: new HarmonyMethod(transpiler)); } diff --git a/SubnauticaRandomiser/Patches/DeconstructionFix.cs b/SubnauticaRandomiser/Patches/DeconstructionFix.cs new file mode 100644 index 0000000..6d5bec4 --- /dev/null +++ b/SubnauticaRandomiser/Patches/DeconstructionFix.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using HarmonyLib; + +namespace SubnauticaRandomiser.Patches +{ + // [HarmonyPatch] + internal static class DeconstructionFix + { + private static readonly Dictionary _corridors = new Dictionary() + { + { "BaseCorridorIShape(Clone)", TechType.BaseCorridorI }, + { "BaseCorridorLShape(Clone)", TechType.BaseCorridorL }, + { "BaseCorridorTShape(Clone)", TechType.BaseCorridorT }, + { "BaseCorridorXShape(Clone)", TechType.BaseCorridorX }, + { "BaseCorridorIShapeGlass(Clone)", TechType.BaseCorridorGlassI }, + { "BaseCorridorLShapeGlass(Clone)", TechType.BaseCorridorGlassL } + }; + + /// + /// Shaped corridors are falsely associated with their straight counterparts. Because their recipes can differ + /// wildly, the difference can be crushing. This updates the recipe to what it should be on deconstruction. + /// + /// The base part that is being deconstructed. + [HarmonyPrefix] + [HarmonyPatch(typeof(BaseDeconstructable), nameof(BaseDeconstructable.Deconstruct))] + internal static void FixCorridors(ref BaseDeconstructable __instance) + { + if (_corridors.ContainsKey(__instance.name)) + __instance.recipe = _corridors[__instance.name]; + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 02a3bc1..54bca42 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -42,6 +42,7 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis + From 56875777d8efab7f9d0c385836e8ee14603485d2 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Thu, 11 Aug 2022 06:53:57 +0200 Subject: [PATCH 25/37] Always enable Harmony. --- SubnauticaRandomiser/EntitySerializer.cs | 1 - SubnauticaRandomiser/InitMod.cs | 3 +-- SubnauticaRandomiser/Logic/DataboxLogic.cs | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index 625290b..aad2402 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -38,7 +38,6 @@ public class EntitySerializer // All modified fragment spawn rates. public Dictionary> SpawnDataDict = new Dictionary>(); - public bool NeedsHarmony = false; public const int SaveVersion = InitMod.s_expectedSaveVersion; /// diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 1943c58..ba71707 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -149,8 +149,7 @@ internal static void ApplyAllChanges() } // Load any changes that rely on harmony patches. - if (s_masterDict.NeedsHarmony) - EnableHarmonyPatching(); + EnableHarmonyPatching(); } /// diff --git a/SubnauticaRandomiser/Logic/DataboxLogic.cs b/SubnauticaRandomiser/Logic/DataboxLogic.cs index ddd824d..ff3dbc8 100644 --- a/SubnauticaRandomiser/Logic/DataboxLogic.cs +++ b/SubnauticaRandomiser/Logic/DataboxLogic.cs @@ -46,7 +46,6 @@ internal List RandomiseDataboxes() + replacementBox.TechType.AsString() + " now contains " + originalBox.TechType.AsString()); toBeRandomised.RemoveAt(next); } - _masterDict.NeedsHarmony = true; return randomDataboxes; } From 113bdb5fd55fe0a72721985ab0b51a448566c0f0 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:12:05 +0200 Subject: [PATCH 26/37] Implement #27. Randomise starting location. --- SubnauticaRandomiser.sln | 1 + SubnauticaRandomiser/CSVReader.cs | 85 +++++++++++++++++++ SubnauticaRandomiser/EntitySerializer.cs | 1 + SubnauticaRandomiser/InitMod.cs | 83 ++++++++++++------ .../Logic/AlternateStartLogic.cs | 49 +++++++++++ SubnauticaRandomiser/Logic/CoreLogic.cs | 11 ++- .../Patches/AlternateStart.cs | 30 +++++++ .../RandomiserObjects/RandomiserVector.cs | 5 ++ .../SubnauticaRandomiser.csproj | 2 + alternateStarts.csv | 16 ++++ 10 files changed, 254 insertions(+), 29 deletions(-) create mode 100644 SubnauticaRandomiser/Logic/AlternateStartLogic.cs create mode 100644 SubnauticaRandomiser/Patches/AlternateStart.cs create mode 100644 alternateStarts.csv diff --git a/SubnauticaRandomiser.sln b/SubnauticaRandomiser.sln index 0380cc8..fa1ec3c 100644 --- a/SubnauticaRandomiser.sln +++ b/SubnauticaRandomiser.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution wreckInformation.csv = wreckInformation.csv ReadMe-Documentation.txt = ReadMe-Documentation.txt biomeSlots.csv = biomeSlots.csv + alternateStarts.csv = alternateStarts.csv EndProjectSection EndProject Global diff --git a/SubnauticaRandomiser/CSVReader.cs b/SubnauticaRandomiser/CSVReader.cs index 18d7e3d..5d3ecec 100644 --- a/SubnauticaRandomiser/CSVReader.cs +++ b/SubnauticaRandomiser/CSVReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Security.Cryptography; using JetBrains.Annotations; using SubnauticaRandomiser.RandomiserObjects; @@ -10,6 +11,7 @@ namespace SubnauticaRandomiser { internal class CSVReader { + internal Dictionary> _csvAlternateStarts; internal List _csvBiomeList; internal List _csvDataboxList; internal List _csvRecipeList; @@ -26,6 +28,89 @@ internal CSVReader() _csvRecipeList = new List(); } + /// + /// Attempt to parse a csv file containing information on alternate starts. + /// + /// The .csv file to parse. + /// The parsed Dictionary if successful, or null otherwise. + internal Dictionary> ParseAlternateStartFile(string fileName) + { + // First, try to find and grab the file containing recipe information. + string[] csvLines; + string path = Path.Combine(InitMod.s_modDirectory, fileName); + LogHandler.Debug("Looking for alternate start CSV as " + path); + + try + { + csvLines = File.ReadAllLines(path); + } + catch (Exception ex) + { + LogHandler.MainMenuMessage("Failed to read alternate start CSV!"); + LogHandler.Error(ex.Message); + return null; + } + + _csvAlternateStarts = new Dictionary>(); + + int lineCounter = 0; + foreach (string line in csvLines) + { + lineCounter++; + if (line.StartsWith("Biome", StringComparison.InvariantCulture)) + { + // This is the header line. Skip. + continue; + } + + // ParseRecipeFileLine fails upwards, so this ensures all errors are caught in one central location. + try + { + ParseAlternateStartLine(line); + } + catch (Exception ex) + { + LogHandler.Error("Failed to parse information from alternate start CSV on line "+lineCounter); + LogHandler.Error(ex.Message); + } + } + + return _csvAlternateStarts; + } + + /// + /// Attempt to parse one content line of the alternate starts csv. + /// + /// The line to parse. + private void ParseAlternateStartLine(string line) + { + string[] cells = line.Split(','); + if (cells.Length < 2) + throw new FormatException("Unexpected number of columns: " + cells.Length); + + EBiomeType biome = StringToEBiomeType(cells[0]); + List starts = new List(); + foreach (string cell in cells.Skip(1)) + { + if (String.IsNullOrEmpty(cell)) + continue; + + string[] rawCoords = cell.Split('/'); + if (rawCoords.Length != 4) + throw new FormatException("Invalid number of coordinates: " + rawCoords.Length); + + float[] parsedCoords = new float[4]; + for (int i = 0; i < 4; i++) + { + parsedCoords[i] = float.Parse(rawCoords[i]); + } + starts.Add(parsedCoords); + } + + LogHandler.Debug("Registering alternate starts for biome " + biome); + _csvAlternateStarts.Add(biome, starts); + } + /// /// Attempt to parse the given file into a list of entities representing recipes. /// diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index aad2402..24d6147 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -27,6 +27,7 @@ namespace SubnauticaRandomiser [Serializable] public class EntitySerializer { + public RandomiserVector StartPoint; // All databoxes and their new locations. public Dictionary Databoxes; // The options to choose from for spawning materials when scanning a fragment which is already unlocked. diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index ba71707..2d9b614 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -17,6 +17,7 @@ public static class InitMod { internal static string s_modDirectory; internal static RandomiserConfig s_config; + internal const string s_alternateStartFile = "alternateStarts.csv"; internal const string s_biomeFile = "biomeSlots.csv"; internal const string s_recipeFile = "recipeInformation.csv"; internal const string s_wreckageFile = "wreckInformation.csv"; @@ -84,31 +85,9 @@ internal static void Randomise() s_masterDict = null; s_config.SanitiseConfigValues(); s_config.iSaveVersion = s_expectedSaveVersion; - var csvReader = new CSVReader(); - - // Attempt to read and parse the CSV with all biome information. - var biomes = csvReader.ParseBiomeFile(s_biomeFile); - if (biomes is null) - { - LogHandler.Fatal("Failed to extract biome information from CSV, aborting."); - throw new ParsingException("Failed to extract biome information: null"); - } - - // Attempt to read and parse the CSV with all recipe information. - var materials = csvReader.ParseRecipeFile(s_recipeFile); - if (materials is null) - { - LogHandler.Fatal("Failed to extract recipe information from CSV, aborting."); - throw new ParsingException("Failed to extract recipe information: null"); - } - // Attempt to read and parse the CSV with wreckages and databox info. - var databoxes = csvReader.ParseWreckageFile(s_wreckageFile); - if (databoxes is null || databoxes.Count == 0) - { - LogHandler.Error("Failed to extract databox information from CSV."); - throw new ParsingException("Failed to extract databox information: null"); - } + // Parse all the necessary input files. + var (alternateStarts, biomes, databoxes, materials) = ParseInputFiles(); // Create a new seed if the current one is just a default Random random; @@ -120,7 +99,7 @@ internal static void Randomise() random = new Random(s_config.iSeed); // Randomise! - CoreLogic logic = new CoreLogic(random, s_config, materials, biomes, databoxes); + CoreLogic logic = new CoreLogic(random, s_config, materials, alternateStarts, biomes, databoxes); s_masterDict = logic.Randomise(); ApplyAllChanges(); LogHandler.Info("Randomisation successful!"); @@ -171,6 +150,51 @@ private static bool CheckSaveCompatibility() return false; } + /// + /// Parse all CSV files needed for randomisation. + /// + /// The parsed objects. + /// Raised if a file could not be parsed. + private static (Dictionary> starts, List biomes, List + databoxes, List materials) ParseInputFiles() + { + var csvReader = new CSVReader(); + + // Attempt to read and parse the CSV with all alternate starts. + var alternateStarts = csvReader.ParseAlternateStartFile(s_alternateStartFile); + if (alternateStarts is null) + { + LogHandler.Error("Failed to extract alternate start information from CSV."); + throw new ParsingException("Failed to extract alternate start information: null."); + } + + // Attempt to read and parse the CSV with all biome information. + var biomes = csvReader.ParseBiomeFile(s_biomeFile); + if (biomes is null) + { + LogHandler.Error("Failed to extract biome information from CSV."); + throw new ParsingException("Failed to extract biome information: null"); + } + + // Attempt to read and parse the CSV with all recipe information. + var materials = csvReader.ParseRecipeFile(s_recipeFile); + if (materials is null) + { + LogHandler.Error("Failed to extract recipe information from CSV."); + throw new ParsingException("Failed to extract recipe information: null"); + } + + // Attempt to read and parse the CSV with wreckages and databox info. + var databoxes = csvReader.ParseWreckageFile(s_wreckageFile); + if (databoxes is null || databoxes.Count == 0) + { + LogHandler.Error("Failed to extract databox information from CSV."); + throw new ParsingException("Failed to extract databox information: null"); + } + + return (alternateStarts, biomes, databoxes, materials); + } + /// /// Serialise the current randomisation state to disk. /// @@ -228,8 +252,13 @@ private static void EnableHarmonyPatching() { Harmony harmony = new Harmony("SubnauticaRandomiser"); + // Alternate starting location. + var original = AccessTools.Method(typeof(RandomStart), nameof(RandomStart.GetRandomStartPoint)); + var postfix = AccessTools.Method(typeof(AlternateStart), nameof(AlternateStart.OverrideStart)); + harmony.Patch(original, postfix: new HarmonyMethod(postfix)); + // Make corridors return the correct building materials. - var original = AccessTools.Method(typeof(BaseDeconstructable), nameof(BaseDeconstructable.Deconstruct)); + original = AccessTools.Method(typeof(BaseDeconstructable), nameof(BaseDeconstructable.Deconstruct)); var prefix = AccessTools.Method(typeof(DeconstructionFix), nameof(DeconstructionFix.FixCorridors)); harmony.Patch(original, prefix: new HarmonyMethod(prefix)); @@ -242,7 +271,7 @@ private static void EnableHarmonyPatching() original = AccessTools.Method(typeof(ProtobufSerializer), nameof(ProtobufSerializer.DeserializeIntoGameObject)); - var postfix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnLoad)); + postfix = AccessTools.Method(typeof(DataboxPatcher), nameof(DataboxPatcher.PatchDataboxOnLoad)); harmony.Patch(original, postfix: new HarmonyMethod(postfix)); } diff --git a/SubnauticaRandomiser/Logic/AlternateStartLogic.cs b/SubnauticaRandomiser/Logic/AlternateStartLogic.cs new file mode 100644 index 0000000..a2eddc8 --- /dev/null +++ b/SubnauticaRandomiser/Logic/AlternateStartLogic.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using SubnauticaRandomiser.RandomiserObjects; +using UnityEngine; +using Random = System.Random; + +namespace SubnauticaRandomiser.Logic +{ + internal class AlternateStartLogic + { + private readonly Dictionary> _alternateStarts; + private readonly CoreLogic _logic; + + private EntitySerializer _masterDict => _logic._masterDict; + private Random _random => _logic._random; + + internal AlternateStartLogic(CoreLogic logic, Dictionary> alternateStarts) + { + _logic = logic; + _alternateStarts = alternateStarts; + } + + internal void Randomise() + { + _masterDict.StartPoint = GetRandomStart(); + } + + private RandomiserVector GetRandomStart() + { + EBiomeType biome = (EBiomeType)15; // TODO: Replace this with a config value. + // TODO: Choose a random biome if the config demands it. + + if (!_alternateStarts.ContainsKey(biome)) + { + LogHandler.Error("No information found on chosen starting biome " + biome); + return new RandomiserVector(0, 0, 0); + } + + // Choose one of the possible spawning boxes within the biome. + int boxIdx = _random.Next(0, _alternateStarts[biome].Count); + float[] box = _alternateStarts[biome][boxIdx]; + // Choose the specific spawn point within the box. + int x = _random.Next((int)box[0], (int)box[2] + 1); + int z = _random.Next((int)box[3], (int)box[1] + 1); + + LogHandler.Debug("Chosen new lifepod spawnpoint at x:" + x + " y:0" + " z:" + z); + return new RandomiserVector(x, 0, z); + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 32123df..236c4f7 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -20,12 +20,14 @@ public class CoreLogic internal readonly SpoilerLog _spoilerLog; internal readonly ProgressionTree _tree; + private readonly AlternateStartLogic _altStartLogic; private readonly DataboxLogic _databoxLogic; private readonly FragmentLogic _fragmentLogic; private readonly RecipeLogic _recipeLogic; - public CoreLogic(System.Random random, RandomiserConfig config, - List allMaterials, List biomes = null, List databoxes = null) + public CoreLogic(System.Random random, RandomiserConfig config, List allMaterials, + Dictionary> alternateStarts, List biomes = null, + List databoxes = null) { _config = config; _databoxes = databoxes; @@ -34,6 +36,8 @@ public CoreLogic(System.Random random, RandomiserConfig config, _random = random; _spoilerLog = new SpoilerLog(config); + // TODO Config + _altStartLogic = new AlternateStartLogic(this, alternateStarts); if (_config.bRandomiseDataboxes) _databoxLogic = new DataboxLogic(this); if (_config.bRandomiseFragments || _config.bRandomiseNumFragments || _config.bRandomiseDuplicateScans) @@ -48,6 +52,9 @@ public CoreLogic(System.Random random, RandomiserConfig config, /// private void Setup(List notRandomised) { + if (_altStartLogic != null) + _altStartLogic.Randomise(); + if (_databoxLogic != null) { // Just randomise those flat out for now, instead of including them in the core loop. diff --git a/SubnauticaRandomiser/Patches/AlternateStart.cs b/SubnauticaRandomiser/Patches/AlternateStart.cs new file mode 100644 index 0000000..7f2979b --- /dev/null +++ b/SubnauticaRandomiser/Patches/AlternateStart.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using HarmonyLib; +using SubnauticaRandomiser.RandomiserObjects; +using UnityEngine; + +namespace SubnauticaRandomiser.Patches +{ + // [HarmonyPatch] + internal static class AlternateStart + { + /// + /// Override the spawn location of the lifepod at the start of the game. + /// + /// The spawnpoint chosen by the game. + [HarmonyPostfix] + [HarmonyPatch(typeof(RandomStart), nameof(RandomStart.GetRandomStartPoint))] + internal static void OverrideStart(ref Vector3 __result) + { + if (__result.y > 50f) + // User is likely using Lifepod Unleashed, skip randomising in that case. + return; + if (InitMod.s_masterDict?.StartPoint is null) + // Has not been randomised, don't do anything. + return; + + LogHandler.Debug("Replacing lifepod spawnpoint with " + InitMod.s_masterDict.StartPoint); + __result = InitMod.s_masterDict.StartPoint.ToUnityVector(); + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs b/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs index 7e9d9a4..55facf3 100644 --- a/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs +++ b/SubnauticaRandomiser/RandomiserObjects/RandomiserVector.cs @@ -43,5 +43,10 @@ public override string ToString() { return x + ", " + y + ", " + z; } + + public Vector3 ToUnityVector() + { + return new Vector3(x, y, z); + } } } diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 54bca42..c61140a 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -41,6 +41,7 @@ cp $(SolutionDir)recipeInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomi cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser + @@ -52,6 +53,7 @@ cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomis + diff --git a/alternateStarts.csv b/alternateStarts.csv new file mode 100644 index 0000000..4f7d853 --- /dev/null +++ b/alternateStarts.csv @@ -0,0 +1,16 @@ +Biome,Boundaries,,, +BloodKelp,-850/1600/-350/1200,-1050/-480/-910/-600,, +CragField,-100/-1200/500/-1500,,, +CrashZone,250/-650/700/-1100,700/-900/1200/-1300,1100/-650/1400/-1000,1350/550/1550/350 +Dunes,-1600/300/-1100/0,-1600/900/-1300/300,-1200/800/-1100/700 +FloatingIsland,-712/-1072/-708/-1076 +GrandReef,-1400/-1100/-900/-1500,-900/-1300/-300/-1600,, +GrassyPlateaus,-400/600/-300/500,-800/0/-500/200,250/350/350/150,0/-600/100/-700 +Kelp,-550/320/-250/280,100/700/400/600,-300/-500/-200/-600,280/-380/320/-420 +KooshZone,900/800/1400/600,,, +Mountains,600/1500/1300/1100,,, +MushroomForest,-1000/700/-700/500,600/600/700/300,, +SeaTreaderPath,-1500/-500/-1200/-800,,, +SparseReef,-800/-500/-600/-850,,, +UnderwaterIslands,-250/1150/0/800,,, +None,-1540/-1000/1500/-1040,1580/-580/1620/-620,-1410/1410/-1390/1390, \ No newline at end of file From 18057c5b6f26a128aaef3a2020c370f818138557 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:25:03 +0200 Subject: [PATCH 27/37] Declutter mod folder. --- .../alternateStarts.csv | 0 biomeSlots.csv => DataFiles/biomeSlots.csv | 0 .../recipeInformation.csv | 0 .../wreckInformation.csv | 0 SubnauticaRandomiser.sln | 8 ++++---- SubnauticaRandomiser/CSVReader.cs | 19 +++++++++++++++---- .../SubnauticaRandomiser.csproj | 5 ++--- 7 files changed, 21 insertions(+), 11 deletions(-) rename alternateStarts.csv => DataFiles/alternateStarts.csv (100%) rename biomeSlots.csv => DataFiles/biomeSlots.csv (100%) rename recipeInformation.csv => DataFiles/recipeInformation.csv (100%) rename wreckInformation.csv => DataFiles/wreckInformation.csv (100%) diff --git a/alternateStarts.csv b/DataFiles/alternateStarts.csv similarity index 100% rename from alternateStarts.csv rename to DataFiles/alternateStarts.csv diff --git a/biomeSlots.csv b/DataFiles/biomeSlots.csv similarity index 100% rename from biomeSlots.csv rename to DataFiles/biomeSlots.csv diff --git a/recipeInformation.csv b/DataFiles/recipeInformation.csv similarity index 100% rename from recipeInformation.csv rename to DataFiles/recipeInformation.csv diff --git a/wreckInformation.csv b/DataFiles/wreckInformation.csv similarity index 100% rename from wreckInformation.csv rename to DataFiles/wreckInformation.csv diff --git a/SubnauticaRandomiser.sln b/SubnauticaRandomiser.sln index fa1ec3c..5c483a6 100644 --- a/SubnauticaRandomiser.sln +++ b/SubnauticaRandomiser.sln @@ -6,12 +6,12 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{14E05614-1903-437D-AD80-25E6EE173917}" ProjectSection(SolutionItems) = preProject mod.json = mod.json - recipeInformation.csv = recipeInformation.csv DevNotes.txt = DevNotes.txt - wreckInformation.csv = wreckInformation.csv ReadMe-Documentation.txt = ReadMe-Documentation.txt - biomeSlots.csv = biomeSlots.csv - alternateStarts.csv = alternateStarts.csv + DataFiles\biomeSlots.csv = DataFiles\biomeSlots.csv + DataFiles\alternateStarts.csv = DataFiles\alternateStarts.csv + DataFiles\recipeInformation.csv = DataFiles\recipeInformation.csv + DataFiles\wreckInformation.csv = DataFiles\wreckInformation.csv EndProjectSection EndProject Global diff --git a/SubnauticaRandomiser/CSVReader.cs b/SubnauticaRandomiser/CSVReader.cs index 5d3ecec..0126bcd 100644 --- a/SubnauticaRandomiser/CSVReader.cs +++ b/SubnauticaRandomiser/CSVReader.cs @@ -37,7 +37,7 @@ internal Dictionary> ParseAlternateStartFile(string fi { // First, try to find and grab the file containing recipe information. string[] csvLines; - string path = Path.Combine(InitMod.s_modDirectory, fileName); + string path = GetDataPath(fileName); LogHandler.Debug("Looking for alternate start CSV as " + path); try @@ -121,7 +121,7 @@ internal List ParseRecipeFile(string fileName) { // First, try to find and grab the file containing recipe information. string[] csvLines; - string path = Path.Combine(InitMod.s_modDirectory, fileName); + string path = GetDataPath(fileName); LogHandler.Debug("Looking for recipe CSV as " + path); try @@ -298,7 +298,7 @@ internal List ParseBiomeFile(string fileName) { // Try and grab the file containing biome information. string[] csvLines; - string path = Path.Combine(InitMod.s_modDirectory, fileName); + string path = GetDataPath(fileName); LogHandler.Debug("Looking for biome CSV as " + path); try @@ -415,7 +415,7 @@ private Biome ParseBiomeFileLine(string line) internal List ParseWreckageFile(string fileName) { string[] csvLines; - string path = Path.Combine(InitMod.s_modDirectory, fileName); + string path = GetDataPath(fileName); LogHandler.Debug("Looking for wreckage CSV as " + path); try @@ -547,6 +547,17 @@ internal static string CalculateMD5(string path) return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } + /// + /// Get the absolute path to a file in the mod's DataFiles folder. + /// + /// The file. + /// The absolute path. + private static string GetDataPath(string fileName) + { + string dataFolder = Path.Combine(InitMod.s_modDirectory, "DataFiles"); + return Path.Combine(dataFolder, fileName); + } + /// /// Turn multiple strings into their TechType equivalents. /// diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index c61140a..0af243d 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -34,11 +34,10 @@ mkdir $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser cp $(OutDir)SubnauticaRandomiser.dll $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser -cp $(SolutionDir)biomeSlots.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser cp $(SolutionDir)mod.json $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser cp $(SolutionDir)ReadMe-Documentation.txt $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser -cp $(SolutionDir)recipeInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser -cp $(SolutionDir)wreckInformation.csv $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser +mkdir $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser/DataFiles +cp $(SolutionDir)DataFiles/* $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser/DataFiles From 8347743568b1ddd106cc0a61ac45d45e636e8d2b Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:25:47 +0200 Subject: [PATCH 28/37] Close #27. Implement menu options for spawn points. --- .../Logic/AlternateStartLogic.cs | 36 ++++++++++++++++--- SubnauticaRandomiser/RandomiserConfig.cs | 6 ++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/SubnauticaRandomiser/Logic/AlternateStartLogic.cs b/SubnauticaRandomiser/Logic/AlternateStartLogic.cs index a2eddc8..54de84c 100644 --- a/SubnauticaRandomiser/Logic/AlternateStartLogic.cs +++ b/SubnauticaRandomiser/Logic/AlternateStartLogic.cs @@ -1,6 +1,7 @@ +using System; using System.Collections.Generic; +using System.Linq; using SubnauticaRandomiser.RandomiserObjects; -using UnityEngine; using Random = System.Random; namespace SubnauticaRandomiser.Logic @@ -10,6 +11,7 @@ internal class AlternateStartLogic private readonly Dictionary> _alternateStarts; private readonly CoreLogic _logic; + private RandomiserConfig _config => _logic._config; private EntitySerializer _masterDict => _logic._masterDict; private Random _random => _logic._random; @@ -24,11 +26,37 @@ internal void Randomise() _masterDict.StartPoint = GetRandomStart(); } - private RandomiserVector GetRandomStart() + /// + /// Convert the config value to a usable biome. + /// + /// The biome. + private EBiomeType GetBiome() { - EBiomeType biome = (EBiomeType)15; // TODO: Replace this with a config value. - // TODO: Choose a random biome if the config demands it. + switch (_config.sSpawnPoint) + { + case "Random": + return _logic.GetRandom(_alternateStarts.Keys.ToList()); + case "BulbZone": + return EBiomeType.KooshZone; + case "Floating Island": + return EBiomeType.FloatingIsland; + case "Void": + return EBiomeType.None; + } + return (EBiomeType)Enum.Parse(typeof(EBiomeType), _config.sSpawnPoint); + } + + /// + /// Find a suitable random spawn point for the lifepod. + /// + /// The new spawn point. + private RandomiserVector GetRandomStart() + { + if (_config.sSpawnPoint.StartsWith("Vanilla")) + return null; + + EBiomeType biome = GetBiome(); if (!_alternateStarts.ContainsKey(biome)) { LogHandler.Error("No information found on chosen starting biome " + biome); diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index cae4831..ee8fa15 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using SMLHelper.V2.Json; using SMLHelper.V2.Options.Attributes; +using SubnauticaRandomiser.RandomiserObjects; namespace SubnauticaRandomiser { @@ -19,6 +20,11 @@ public class RandomiserConfig : ConfigFile [Choice("Mode", "Balanced", "Chaotic")] public int iRandomiserMode = (int)ConfigDefaults.GetDefault("iRandomiserMode"); + [Choice("Spawnpoint", "Vanilla", "Random", "BloodKelp", "BulbZone", "CragField", "CrashZone", + "Dunes", "Floating Island", "GrandReef", "GrassyPlateaus", "Kelp", "Mountains", "MushroomForest", + "SeaTreaderPath", "SparseReef", "UnderwaterIslands", "Void")] + public string sSpawnPoint = "Vanilla"; + [Toggle("Use fish in logic?")] public bool bUseFish = (bool)ConfigDefaults.GetDefault("bUseFish"); From 6077fbb5f2071cd77ccc098d88cc0f5b27932663 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:33:52 +0200 Subject: [PATCH 29/37] Properly respect config options. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 236c4f7..75ba0f6 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -36,8 +36,8 @@ public CoreLogic(System.Random random, RandomiserConfig config, List private void Setup(List notRandomised) { - if (_altStartLogic != null) - _altStartLogic.Randomise(); - + _altStartLogic?.Randomise(); + if (_databoxLogic != null) { // Just randomise those flat out for now, instead of including them in the core loop. @@ -132,7 +131,6 @@ internal EntitySerializer Randomise() // Choose a logic appropriate to the entity. if (nextEntity.IsFragment) { - // TODO implement proper depth restrictions and config options. if (_config.bRandomiseFragments && _fragmentLogic != null) _fragmentLogic.RandomiseFragment(nextEntity, currentDepth); From acc472bbb38f9dd6f479e582232f166d44b385c5 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:45:21 +0200 Subject: [PATCH 30/37] General style cleanup. --- SubnauticaRandomiser/CSVReader.cs | 10 --- SubnauticaRandomiser/ConfigDefaults.cs | 72 +++++++++++++++++++ SubnauticaRandomiser/EntitySerializer.cs | 1 - SubnauticaRandomiser/LogHandler.cs | 1 - SubnauticaRandomiser/Logic/CoreLogic.cs | 2 +- .../Logic/Recipes/ModeBalanced.cs | 6 +- .../Patches/DataboxPatcher.cs | 1 - .../Patches/FragmentPatcher.cs | 3 +- SubnauticaRandomiser/RandomiserConfig.cs | 66 ----------------- .../RandomiserObjects/ParsingException.cs | 14 ++++ .../SubnauticaRandomiser.csproj | 2 + 11 files changed, 93 insertions(+), 85 deletions(-) create mode 100644 SubnauticaRandomiser/ConfigDefaults.cs create mode 100644 SubnauticaRandomiser/RandomiserObjects/ParsingException.cs diff --git a/SubnauticaRandomiser/CSVReader.cs b/SubnauticaRandomiser/CSVReader.cs index 0126bcd..e4a42ee 100644 --- a/SubnauticaRandomiser/CSVReader.cs +++ b/SubnauticaRandomiser/CSVReader.cs @@ -699,14 +699,4 @@ private static int StringToInt(string input, string column) return output; } } - - /// - /// The exception that is thrown when an input file cannot be parsed properly into the expected objects. - /// - public class ParsingException : Exception - { - public ParsingException() {} - - public ParsingException(string message) : base(message) {} - } } diff --git a/SubnauticaRandomiser/ConfigDefaults.cs b/SubnauticaRandomiser/ConfigDefaults.cs new file mode 100644 index 0000000..7b7aee9 --- /dev/null +++ b/SubnauticaRandomiser/ConfigDefaults.cs @@ -0,0 +1,72 @@ +using System.Collections; +using System.Collections.Generic; + +namespace SubnauticaRandomiser +{ + /// + /// A class that contains all the default values for most of the config options. Also provides minimum and maximum + /// bounds for numeric options. + /// + internal static class ConfigDefaults + { + private static readonly Dictionary s_defaults = new Dictionary() + { + // Key, Default value, Minimum value, Maximum value. + { "iRandomiserMode", new[] { 0, 0, 1 } }, + { "bUseFish", new[] { true, true, true } }, + { "bUseEggs", new[] { false, false, false } }, + { "bUseSeeds", new[] { true, true, true } }, + { "bRandomiseDataboxes", new[] { true, true, true } }, + { "bRandomiseFragments", new[] { true, true, true } }, + { "bRandomiseNumFragments", new[] { true, true, true } }, + { "bRandomiseDuplicateScans", new[] { true, true ,true } }, + { "bRandomiseRecipes", new[] { true, true, true } }, + { "bVanillaUpgradeChains", new[] { false, false, false } }, + { "bDoBaseTheming", new[] { false, false, false } }, + { "iEquipmentAsIngredients", new[] { 1, 0, 2 } }, + { "iToolsAsIngredients", new[] { 1, 0, 2 } }, + { "iUpgradesAsIngredients", new[] { 1, 0, 2 } }, + { "iMaxAmountPerIngredient", new[] { 5, 1, 10 } }, + { "iMaxIngredientsPerRecipe", new[] { 7, 1, 10 } }, + { "iMaxBiomesPerFragment", new[] { 5, 3, 10 } }, + { "iMaxFragmentsToUnlock", new[] { 5, 1, 30 } }, + + // Advanced settings start here. + { "iDepthSearchTime", new[] { 15, 0, 45 } }, + { "iMaxBasicOutpostSize", new[] { 24, 4, 48 } }, + { "iMaxDuplicateScanYield", new[] { 2, 1, 10 } }, + { "iMaxEggsAsSingleIngredient", new[] { 1, 1, 10 } }, + { "iMaxInventorySizePerRecipe", new[] { 24, 4, 100 } }, + { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, + { "dFuzziness", new[] { 0.2, 0.0, 1.0 } }, + { "dIngredientRatio", new[] { 0.45, 0.0, 1.0 } }, + { "fFragmentSpawnChanceMin", new[] { 0.3f, 0.01f, 10.0f } }, + { "fFragmentSpawnChanceMax", new[] { 0.6f, 0.01f, 10.0f } }, + }; + + internal static bool Contains(string key) + { + return s_defaults.ContainsKey(key); + } + + internal static object GetDefault(string key) + { + if (!s_defaults.ContainsKey(key)) + { + LogHandler.Warn("Tried to get invalid key from config default dictionary: " + key); + return null; + } + return s_defaults[key][0]; + } + + internal static object GetMax(string key) + { + return s_defaults[key][2]; + } + + internal static object GetMin(string key) + { + return s_defaults[key][1]; + } + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/EntitySerializer.cs b/SubnauticaRandomiser/EntitySerializer.cs index 24d6147..1b6fa84 100644 --- a/SubnauticaRandomiser/EntitySerializer.cs +++ b/SubnauticaRandomiser/EntitySerializer.cs @@ -6,7 +6,6 @@ namespace SubnauticaRandomiser { - /// /// This class does three things. /// diff --git a/SubnauticaRandomiser/LogHandler.cs b/SubnauticaRandomiser/LogHandler.cs index e5fa156..ec5b392 100644 --- a/SubnauticaRandomiser/LogHandler.cs +++ b/SubnauticaRandomiser/LogHandler.cs @@ -6,7 +6,6 @@ namespace SubnauticaRandomiser /// Also includes main menu messages for relaying information to the user directly. public static class LogHandler { - internal static void Info(string message) { Logger.Log(Logger.Level.Info, message); diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 75ba0f6..850ace5 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -259,7 +259,7 @@ private int CalculateSoloDepth(int vehicleDepth, int soloDepthRaw) // Below 100 meters, air is consumed three times as fast. // Below 200 meters, it is consumed five times as fast. - return (int)(soloDepths[0] + soloDepths[1] / 3 + soloDepths[2] / 5); + return (int)(soloDepths[0] + (soloDepths[1] / 3) + (soloDepths[2] / 5)); } /// diff --git a/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs b/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs index 4eb1078..74d3dbc 100644 --- a/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs +++ b/SubnauticaRandomiser/Logic/Recipes/ModeBalanced.cs @@ -154,7 +154,7 @@ private LogicEntity ChoosePrimaryIngredient(LogicEntity entity, double targetVal /// A positive integer. private int FindMaximum(LogicEntity ingredient, double targetValue, double currentValue) { - int max = (int)((targetValue + targetValue * _config.dFuzziness / 2) - currentValue) / ingredient.Value; + int max = (int)((targetValue + ((targetValue * _config.dFuzziness) / 2)) - currentValue) / ingredient.Value; max = max > 0 ? max : 1; max = max > _config.iMaxAmountPerIngredient ? _config.iMaxAmountPerIngredient : max; @@ -193,8 +193,8 @@ private LogicEntity ReplaceWithSimilarValue(LogicEntity undesirable) { // Add all items of the same category with value +- range% betterOptions.AddRange(_reachableMaterials.FindAll(x => x.Category.Equals(undesirable.Category) - && x.Value < undesirable.Value + undesirable.Value * range - && x.Value > undesirable.Value - undesirable.Value * range + && x.Value < undesirable.Value + (undesirable.Value * range) + && x.Value > undesirable.Value - (undesirable.Value * range) )); range += 0.2; } diff --git a/SubnauticaRandomiser/Patches/DataboxPatcher.cs b/SubnauticaRandomiser/Patches/DataboxPatcher.cs index 49c6086..bff3d49 100644 --- a/SubnauticaRandomiser/Patches/DataboxPatcher.cs +++ b/SubnauticaRandomiser/Patches/DataboxPatcher.cs @@ -8,7 +8,6 @@ namespace SubnauticaRandomiser.Patches // [HarmonyPatch] internal class DataboxPatcher { - [HarmonyPrefix] [HarmonyPatch(typeof(DataboxSpawner), nameof(DataboxSpawner.Start))] internal static bool PatchDataboxOnSpawn(ref DataboxSpawner __instance) diff --git a/SubnauticaRandomiser/Patches/FragmentPatcher.cs b/SubnauticaRandomiser/Patches/FragmentPatcher.cs index 0507fa2..7dbde1b 100644 --- a/SubnauticaRandomiser/Patches/FragmentPatcher.cs +++ b/SubnauticaRandomiser/Patches/FragmentPatcher.cs @@ -117,5 +117,4 @@ private static TechType GetRandomMaterial(Random rand) return TechType.Titanium; } } -} - +} \ No newline at end of file diff --git a/SubnauticaRandomiser/RandomiserConfig.cs b/SubnauticaRandomiser/RandomiserConfig.cs index ee8fa15..f237711 100644 --- a/SubnauticaRandomiser/RandomiserConfig.cs +++ b/SubnauticaRandomiser/RandomiserConfig.cs @@ -1,6 +1,4 @@ using System; -using System.Collections; -using System.Collections.Generic; using SMLHelper.V2.Json; using SMLHelper.V2.Options.Attributes; using SubnauticaRandomiser.RandomiserObjects; @@ -164,68 +162,4 @@ private bool EnsureButtonTime() return false; } } - - /// Mostly used so that the spoiler log can tell which settings to include. - internal static class ConfigDefaults - { - private static readonly Dictionary s_defaults = new Dictionary() - { - // Key, Default value, Minimum value, Maximum value. - { "iRandomiserMode", new[] { 0, 0, 1 } }, - { "bUseFish", new[] { true, true, true } }, - { "bUseEggs", new[] { false, false, false } }, - { "bUseSeeds", new[] { true, true, true } }, - { "bRandomiseDataboxes", new[] { true, true, true } }, - { "bRandomiseFragments", new[] { true, true, true } }, - { "bRandomiseNumFragments", new[] { true, true, true } }, - { "bRandomiseDuplicateScans", new[] { true, true ,true } }, - { "bRandomiseRecipes", new[] { true, true, true } }, - { "bVanillaUpgradeChains", new[] { false, false, false } }, - { "bDoBaseTheming", new[] { false, false, false } }, - { "iEquipmentAsIngredients", new[] { 1, 0, 2 } }, - { "iToolsAsIngredients", new[] { 1, 0, 2 } }, - { "iUpgradesAsIngredients", new[] { 1, 0, 2 } }, - { "iMaxAmountPerIngredient", new[] { 5, 1, 10 } }, - { "iMaxIngredientsPerRecipe", new[] { 7, 1, 10 } }, - { "iMaxBiomesPerFragment", new[] { 5, 3, 10 } }, - { "iMaxFragmentsToUnlock", new[] { 5, 1, 30 } }, - - // Advanced settings start here. - { "iDepthSearchTime", new[] { 15, 0, 45 } }, - { "iMaxBasicOutpostSize", new[] { 24, 4, 48 } }, - { "iMaxDuplicateScanYield", new[] { 2, 1, 10 } }, - { "iMaxEggsAsSingleIngredient", new[] { 1, 1, 10 } }, - { "iMaxInventorySizePerRecipe", new[] { 24, 4, 100 } }, - { "iMinFragmentsToUnlock", new[] { 2, 1, 30 } }, - { "dFuzziness", new[] { 0.2, 0.0, 1.0 } }, - { "dIngredientRatio", new[] { 0.45, 0.0, 1.0 } }, - { "fFragmentSpawnChanceMin", new[] { 0.3f, 0.01f, 10.0f } }, - { "fFragmentSpawnChanceMax", new[] { 0.6f, 0.01f, 10.0f } }, - }; - - internal static bool Contains(string key) - { - return s_defaults.ContainsKey(key); - } - - internal static object GetDefault(string key) - { - if (!s_defaults.ContainsKey(key)) - { - LogHandler.Warn("Tried to get invalid key from config default dictionary: " + key); - return null; - } - return s_defaults[key][0]; - } - - internal static object GetMax(string key) - { - return s_defaults[key][2]; - } - - internal static object GetMin(string key) - { - return s_defaults[key][1]; - } - } } \ No newline at end of file diff --git a/SubnauticaRandomiser/RandomiserObjects/ParsingException.cs b/SubnauticaRandomiser/RandomiserObjects/ParsingException.cs new file mode 100644 index 0000000..ef6964f --- /dev/null +++ b/SubnauticaRandomiser/RandomiserObjects/ParsingException.cs @@ -0,0 +1,14 @@ +using System; + +namespace SubnauticaRandomiser.RandomiserObjects +{ + /// + /// The exception that is thrown when an input file cannot be parsed properly into the expected objects. + /// + public class ParsingException : Exception + { + public ParsingException() {} + + public ParsingException(string message) : base(message) {} + } +} \ No newline at end of file diff --git a/SubnauticaRandomiser/SubnauticaRandomiser.csproj b/SubnauticaRandomiser/SubnauticaRandomiser.csproj index 0af243d..7ff7db1 100644 --- a/SubnauticaRandomiser/SubnauticaRandomiser.csproj +++ b/SubnauticaRandomiser/SubnauticaRandomiser.csproj @@ -40,6 +40,7 @@ mkdir $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser/DataFiles cp $(SolutionDir)DataFiles/* $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser/DataFiles + @@ -57,6 +58,7 @@ cp $(SolutionDir)DataFiles/* $(SUBNAUTICA_DIR)/QMods/SubnauticaRandomiser/DataFi + From 53d1adc722a41478aec4b24da8a664bcc9823364 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:50:28 +0200 Subject: [PATCH 31/37] Update notes. --- DevNotes.txt | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/DevNotes.txt b/DevNotes.txt index 2237482..5f484e3 100644 --- a/DevNotes.txt +++ b/DevNotes.txt @@ -1,17 +1,9 @@ - Thoughts and notes on features and issues. - Randomise more things. - Fish spawn points. - Fish size and aggression. - Raw material spawn points. - -- Expand on fragments. - - Make it possible to randomise the amount of fragments - needed for each blueprint. - - Is it possible to swap fragments around? - - Change what resources you get upon scanning duplicate fragments - - Maybe you'd get one or two resources needed to build what you scanned? - Add a casual mode. - Casual mode would respect vanilla upgrade chains and be more lenient with its @@ -21,8 +13,7 @@ - Add a fiendish mode. - Many more recipes are locked behind databoxes scattered throughout the ocean. - Databoxes can be many more places, including the cave systems in many biomes. - - Raw materials are no longer found in their original biomes. Look into using - LootDistributionHandler for this? + - Raw materials are no longer found in their original biomes. - Add support for glitches. - Several glitches in the game can let you get items or blueprints much earlier. @@ -30,6 +21,8 @@ - Since this would make the logic run on a timer, better not implement this. - Large wrecks can be made to despawn with the building tool, skipping the need for e.g. a laser cutter. + - Table coral can be acquired with a Crashfish, or bumping into it with a vehicle. + No knife required! - Mod support. - Even if autodetecting mods and their items seems impossible with how the @@ -63,10 +56,6 @@ Thoughts from a playthrough: - What about land seeds? If those get added, indoor growbeds are a necessity. - Add Aurora interior to logic. What parts need repair tool, laser cutter, cannon? - -Thoughts from users: -- Add console commands to reroll a specific recipe only. - Other: - Sea treaders are a potential anti softlock mechanism. You could change their poop and shale into something useful on the fly, or make shale always contain From 4087f5d9c5fcebd0053af41a7df70620add194b4 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 14:54:40 +0200 Subject: [PATCH 32/37] Version bump to v0.8.0 --- SubnauticaRandomiser/InitMod.cs | 13 ++++++++----- mod.json | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/SubnauticaRandomiser/InitMod.cs b/SubnauticaRandomiser/InitMod.cs index 2d9b614..dd1274b 100644 --- a/SubnauticaRandomiser/InitMod.cs +++ b/SubnauticaRandomiser/InitMod.cs @@ -22,11 +22,14 @@ public static class InitMod internal const string s_recipeFile = "recipeInformation.csv"; internal const string s_wreckageFile = "wreckInformation.csv"; internal const string s_expectedRecipeMD5 = "4ab1b7a019037f76c0d508f1c2aee5f8"; - internal const int s_expectedSaveVersion = 3; - - internal static readonly Dictionary s_versionDict = new Dictionary { [1] = "v0.5.1", - [2] = "v0.6.1", - [3] = "v0.7.0"}; + internal const int s_expectedSaveVersion = 4; + internal static readonly Dictionary s_versionDict = new Dictionary + { + [1] = "v0.5.1", + [2] = "v0.6.1", + [3] = "v0.7.0", + [4] = "v0.8.0" + }; // The master list of everything that is modified by the mod. internal static EntitySerializer s_masterDict; diff --git a/mod.json b/mod.json index 84a8c10..d61e859 100644 --- a/mod.json +++ b/mod.json @@ -1,8 +1,8 @@ { "Id": "SubnauticaRandomiser", "DisplayName": "SubnauticaRandomiser", - "Author": "Raqzas", - "Version": "0.7.0", + "Author": "tinyhoot", + "Version": "0.8.0", "Dependencies": [ "SMLHelper" ], "Enable": true, "Game": "Subnautica", From 10c46f3f9ac2c319da03da365e104526934b3d68 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:20:40 +0200 Subject: [PATCH 33/37] Update README.md --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c9529b1..34b63fe 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ # Subnautica Randomiser -A Subnautica Mod that randomises recipes for replayability, originally based on [the work of stephenengland](https://github.com/stephenengland/SubnauticaRandomizer). +A Subnautica Mod that randomises many aspects of the game for replayability, originally based on [the first Randomizer](https://github.com/stephenengland/SubnauticaRandomizer). This can make the game considerably more difficult, and the varying availability of ingredients may also make it harder to collect enough of what you need. Since ingredients can vary so wildly, use of a blueprint tracker mod like [this one on nexus](https://www.nexusmods.com/subnautica/mods/22) is recommended. -The randomisation persists between play sessions and save games. If you decide to stop playing for the day, everything will remain randomised as it was. In case you need it, there is a mod menu option for randomising everything from scratch again. +The randomisation persists between play sessions and save games. If you decide to stop playing for the day, everything will remain randomised as it was. + +The mod will randomise using recommended settings on first startup. You can either start playing immediately, or customise your experience in the mod options menu. Note that, should you choose to re-randomise from the mod options menu, you must **restart your game** for all changes to properly take effect. #### This mod randomises: -* All recipes for basic and advanced materials -* All recipes for tools, equipment, vehicles and upgrades -* Most ingredients required for base building - * Decorative pieces like chairs or beds are unaffected -* The blueprints found in databoxes +* Recipes for most craftable things in the game, excluding decorative base pieces. +* Blueprints found in databoxes * Fragment spawn rates and locations +* Lifepod spawn location ## Features -- ✔️ Randomise most items in the game -- ✔️ Include fish, eggs and seeds in recipes (customisable in in-game menu) -- ✔️ Randomise blueprints from databoxes -- ✔️ Randomise fragments -- ✔️ No softlocks -- ✔️ Upgrades can be made independent from their basic variants, so you might acquire e.g. a Seamoth Depth Module 3 long before you ever manage to get Module 1 or 2 -- ✔️ Most things you can make may also show up as an ingredient in other recipes. Do you really need that laser cutter, or do you craft it into Polyaniline? +- ✔️ No softlocks +- ✔️ Detailed mod options menu +- ✔️ (Probably) Don't spawn in the void +- ✔️ Randomise rewards from scanning a fragment you already know +- ✔️ Include fish, eggs and seeds in recipes +- ✔️ Out-of-order upgrades, so you might acquire e.g. a Seamoth Depth Module 3 long before you ever manage to get Module 1 or 2 +- ✔️ Long recipe chains. Do you really need that laser cutter, or do you craft it into Polyaniline for a Rebreather? - ✔️ Items are balanced on an underlying value logic - If you prefer pure chaos, simply turn on Chaotic mode! -- ✔️ Easily share your seed with friends +- ✔️ Share your seed with friends ## How to Use 1. Install [QModManager](https://www.nexusmods.com/subnautica/mods/201) @@ -36,9 +36,10 @@ The randomisation persists between play sessions and save games. If you decide t 4. Enjoy! ## How to Build -* Install QModManager and SMLHelper * git clone +* Add a SUBNAUTICA_DIR variable to your PATH pointing to your install directory of Subnautica +* Install QModManager and SMLHelper * In Visual Studio, update the project's assembly references to point to the correct locations on your computer. * For more information, see [QMod Wiki](https://github.com/SubnauticaModding/QModManager/wiki/Libraries) - * In addition, you'll likely need a publicised version of Subnautica's `Assembly-CSharp.dll`. I used [the BepinEx plugin](https://github.com/MrPurple6411/Bepinex-Tools/releases/tag/1.0.1-Publicizer) for that. + * In addition, you'll need a publicised version of Subnautica's `Assembly-CSharp.dll`. Start the game once using [the BepinEx plugin](https://github.com/MrPurple6411/Bepinex-Tools/releases/) for this. * Building in the Release configuration should leave you with a `SubnauticaRandomiser.dll` in `SubnauticaRandomiser/bin/Release/` From 9fb098ec6f4bf6c1d352b9ec858488643631e17b Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:36:19 +0200 Subject: [PATCH 34/37] Update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34b63fe..79521fa 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # Subnautica Randomiser -A Subnautica Mod that randomises many aspects of the game for replayability, originally based on [the first Randomizer](https://github.com/stephenengland/SubnauticaRandomizer). -This can make the game considerably more difficult, and the varying availability of ingredients may also make it harder to collect enough of what you need. Since ingredients can vary so wildly, use of a blueprint tracker mod like [this one on nexus](https://www.nexusmods.com/subnautica/mods/22) is recommended. +![GitHub release (latest by date)](https://img.shields.io/github/v/release/tinyhoot/SubnauticaRandomiser) +![GitHub](https://img.shields.io/github/license/tinyhoot/SubnauticaRandomiser) +[![CodeFactor](https://www.codefactor.io/repository/github/tinyhoot/subnauticarandomiser/badge/dev)](https://www.codefactor.io/repository/github/tinyhoot/subnauticarandomiser/overview/master) +[![wakatime](https://wakatime.com/badge/user/d7c60741-27ca-486e-a1d0-e23b93d91114/project/acdd8cb2-0e10-422a-990f-d717dc24ae45.svg)](https://wakatime.com/badge/user/d7c60741-27ca-486e-a1d0-e23b93d91114/project/acdd8cb2-0e10-422a-990f-d717dc24ae45) + +A Subnautica Mod that randomises many aspects of the game for replayability, originally based on [the first Subnautica randomizer](https://github.com/stephenengland/SubnauticaRandomizer). + +This mod can make the game considerably more difficult, and the varying availability of fragments and ingredients may also make it harder to collect enough of what you need. Since ingredients can vary so wildly, use of a blueprint tracker mod like [this one on nexus](https://www.nexusmods.com/subnautica/mods/22) is recommended. The randomisation persists between play sessions and save games. If you decide to stop playing for the day, everything will remain randomised as it was. -The mod will randomise using recommended settings on first startup. You can either start playing immediately, or customise your experience in the mod options menu. Note that, should you choose to re-randomise from the mod options menu, you must **restart your game** for all changes to properly take effect. +On first startup, the mod will randomise using recommended settings. You can either start playing immediately, or customise your experience in the mod options menu. Note that, should you choose to re-randomise from the mod options menu, you must **restart your game** for all changes to properly take effect. #### This mod randomises: * Recipes for most craftable things in the game, excluding decorative base pieces. From 3d5353f3d192bcab0380d653bc16c84139a382ab Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 16:07:36 +0200 Subject: [PATCH 35/37] Update ReadMe-Documentation.txt --- ReadMe-Documentation.txt | 72 +++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/ReadMe-Documentation.txt b/ReadMe-Documentation.txt index 4c26509..924a936 100644 --- a/ReadMe-Documentation.txt +++ b/ReadMe-Documentation.txt @@ -57,6 +57,13 @@ is the recommended way to play the randomiser. Should you wish for a completely off-the-rails experience, Chaotic has you covered. Chaotic will not softlock you, but it provides very little protection from the ups and downs of random chance. +| Spawnpoint | Vanilla / Random / / Void | +--Default: Vanilla +Change where the lifepod spawns. Random simply chooses one among all available options. +You can constrain the general area of where the lifepod will end up by choosing one +of the given surface biomes. It is also possible to spawn in the little lake of the +floating island by choosing Floating Island. + | Use fish in logic | Yes / No | --Default: Yes Enabling this setting will include all fish you can grab in the wild in the logic, @@ -82,6 +89,23 @@ not add any recipes to the boxes that are not already contained in them in vanil and the boxes themselves will still be found in the same locations. However, the blueprints you get from them will no longer be in the same boxes as they used to be. +| Randomise fragment locations | Yes / No | +--Default: Yes +When enabled, all fragments will be distributed throughout the game. Logic is in place +to prevent any softlocks from not being able to access a specific fragment. However, +note that this will not (yet) put fragments into biomes where the vanilla game didn't +spawn any. + +| Randomise number of fragments needed | Yes / No | +--Default: Yes +When enabled, all fragments will require a random number of scans to unlock. Ties into +the setting "Max number of fragments needed" below. + +| Randomise duplicate scan rewards | Yes / No | +--Default: Yes +Scanning a fragment you've already unlocked will yield a random raw material instead +of the normal two titanium. + | Respect vanilla upgrade chains | Yes / No | --Default: No By default, the randomiser breaks the sequential upgrade chains present in the vanilla @@ -118,16 +142,23 @@ the same recipe. Thus, setting this to 5 will never allow e.g. a knife to be mad from 6 titanium and 3 copper. The titanium would be capped at 5, and other ingredients would be used in the 6th piece's stead. Reducing this number leads to "flatter" recipes which require more diverse ingredients, -but only a few of them each. This setting is very strongly tied with the one below. +but only a few of them each. This setting is very strongly tied to the one below. | Max ingredients per recipe | 1 - 10 | -This number determines how many different ingredient types a recipe can hold. Assuming +This number determines how many different ingredient types a recipe can hold. Assume you set this setting to 3, and you're trying to craft a knife. The recipe you get is 2 copper, 4 gold and 1 stalker tooth. The setting then prevents the randomiser from adding e.g. 2 titanium to the list of necessary materials, which would bring the total of diverse types up to 4. However, it does NOT influence how many of each of these ingredients can be required at once. -This setting is very strongly tied with the one above. +This setting is very strongly tied to the one above. + +| Max biomes to spawn each fragment in | 3 - 10 | +This number regulates within how many biomes any given fragment can show up in. Note +that these biomes do NOT refer to biomes in player terms, but rather in game terms, +which treats e.g. walls, ceilings, caves, wreckages, rooms behind locked doors inside +wreckages, etc. each as a different biome. Setting this too low will make it difficult +to find enough fragments. | Randomise with new seed | | Randomise with same seed | @@ -155,18 +186,17 @@ the randomiser will stop at a depth where you can remain for this many seconds before having to return for air without dying. This number is capped at 45 seconds since that is your maximum at the beginning of the game, without any tanks. -| iMaxAmountPerIngredient | 1 - 20 | ---Default: 5 -Most ingredients in a recipe may show up as multiples. For example, a Seamoth might -require you to collect four Titanium, five Gold and three Holefish. This setting -controls the maximum amount any single ingredient of a recipe can require. - | iMaxBasicOutpostSize | 4 - 48 | --Default: 24 The absolute essentials to establish a small scanning outpost all taken together will not require ingredients which exceed this much space in your inventory. This affects I-corridors, hatches, scanner rooms, windows, solar panels, and beacons. +| iMaxDuplicateScanYield | 1 - 10 | +--Default: 2 +The maximum number of items you will be given upon scanning a fragment that is +already known. Setting this number too high will quickly clutter your inventory. + | iMaxEggsAsSingleIngredient | 1 - 10 | --Default: 1 This setting does the same as the one above, but specialised for eggs. Because @@ -187,7 +217,7 @@ it at your own risk. | dFuzziness | 0.0 - 1.0 | --Default: 0.2 -Every item in the game is assigned a value before randomising. This setting controls +Every recipe in the game is assigned a value before randomising. This setting controls how closely the randomiser tries to stick to that value before it declares a recipe done. The setting represents a percentage. Assume that Titanium Ingots had a value of 100. @@ -206,10 +236,22 @@ With the default value, this means that the randomiser will first try to find an ingredient with 35%-55% of the total value before moving on to entirely random ones. Set to 0.0 to disable this behaviour. +| fFragmentSpawnChanceMin | 0.01f - 10.0f | +--Default: 0.3f +This setting, tethered with the one below, provides a global modifier for the +randomiser to decide how likely a fragment spawn should be within a biome. The value +it ultimately decides on is multiplied with the vanilla average fragment spawn rate +within that biome. Small adjustments can have large effects, particularly if combined +with the maximum number of fragments allowed to spawn in a single biome. + +| fFragmentSpawnChanceMax | 0.01f - 10.0f | +--Default: 0.6f +See above. + | sBase64Seed | This extremely long string represents your savegame. All recipes, databoxes, everything that this mod changes is saved here. You should never change this -manually, unless you're using it share seeds with someone else. In that case, +manually, unless you're using it to share seeds with someone else. In that case, copying someone else's base seed to your own file allows you to skip synchronising your settings with them and pressing the randomise button. The game will simply load their game state next time you boot it up. @@ -337,6 +379,12 @@ of information. These are the categories the randomiser will accept: - Note that fish bred from eggs are also listed in this category. - Additionally, this category requires an alien containment and a multipurpose room to be unlocked in logic. +- Fragments + - Provides a base TechType for the randomiser to latch on to while looking through + prefabs in the spawn registry. + - Removing an entry here will cause it to no longer be randomised in a new location. + - Adding an entry will likely do nothing, as the randomiser will probably fail to + find any associated custom prefabs. [3.3] Depth @@ -401,4 +449,4 @@ In addition, this column gives the randomiser a rough idea of how deep you need to go to be able to grab fragments or databoxes of something. Any item which included fragments or databoxes in the previous column should provide an unlock depth here. The randomiser will leave this item alone until its unlock depth is -considered reachable. \ No newline at end of file +considered reachable. From 05a9ea1ab36f5bb0331f267b7f83c4cb39f2873b Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 16:16:35 +0200 Subject: [PATCH 36/37] Do not yield materials on scan that are too balance-breaking. --- SubnauticaRandomiser/Logic/FragmentLogic.cs | 2 ++ SubnauticaRandomiser/Logic/Recipes/Materials.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/SubnauticaRandomiser/Logic/FragmentLogic.cs b/SubnauticaRandomiser/Logic/FragmentLogic.cs index 14906b1..94d50f6 100644 --- a/SubnauticaRandomiser/Logic/FragmentLogic.cs +++ b/SubnauticaRandomiser/Logic/FragmentLogic.cs @@ -240,6 +240,8 @@ internal void CreateDuplicateScanYieldDict() { _masterDict.FragmentMaterialYield = new Dictionary(); var materials = _logic._materials.GetAllRawMaterials(50); + // Gaining seeds from fragments is not great for balance. Remove that. + materials.Remove(_logic._materials.Find(TechType.CreepvineSeedCluster)); foreach (LogicEntity entity in materials) { diff --git a/SubnauticaRandomiser/Logic/Recipes/Materials.cs b/SubnauticaRandomiser/Logic/Recipes/Materials.cs index ebf75d9..6238f4f 100644 --- a/SubnauticaRandomiser/Logic/Recipes/Materials.cs +++ b/SubnauticaRandomiser/Logic/Recipes/Materials.cs @@ -173,13 +173,14 @@ internal List GetAllFragments() } /// - /// Get all entities that are considered raw materials and accessible by the given depth. + /// Get all entities that are considered raw materials without prerequisites and accessible by the given depth. /// /// The maximum depth at which the raw materials must be available. internal List GetAllRawMaterials(int maxDepth = 2000) { var rawMaterials = _allMaterials.FindAll(x => - x.Category.Equals(ETechTypeCategory.RawMaterials) && x.AccessibleDepth <= maxDepth); + x.Category.Equals(ETechTypeCategory.RawMaterials) && x.AccessibleDepth <= maxDepth + && !x.HasPrerequisites); return rawMaterials; } From faf03f0fa751c6d9cda0067011c143624a146884 Mon Sep 17 00:00:00 2001 From: tinyhoot <78366332+tinyhoot@users.noreply.github.com> Date: Fri, 12 Aug 2022 17:37:45 +0200 Subject: [PATCH 37/37] Add fragments to spoiler log. --- SubnauticaRandomiser/Logic/CoreLogic.cs | 4 +- .../RandomiserObjects/SpoilerLog.cs | 99 ++++++++++++------- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/SubnauticaRandomiser/Logic/CoreLogic.cs b/SubnauticaRandomiser/Logic/CoreLogic.cs index 850ace5..f6aa4c9 100644 --- a/SubnauticaRandomiser/Logic/CoreLogic.cs +++ b/SubnauticaRandomiser/Logic/CoreLogic.cs @@ -34,7 +34,7 @@ public CoreLogic(System.Random random, RandomiserConfig config, List> _progression = new List>(); private List _basicOptions; @@ -20,10 +21,12 @@ public class SpoilerLog private string[] _contentBasics; private string[] _contentAdvanced; private string[] _contentDataboxes; + private string[] _contentFragments; - internal SpoilerLog(RandomiserConfig config) + internal SpoilerLog(RandomiserConfig config, EntitySerializer serializer) { _config = config; + _serializer = serializer; } /// @@ -44,7 +47,7 @@ private void PrepareStrings() "iMaxIngredientsPerRecipe", "iMaxAmountPerIngredient", "bMaxBiomesPerFragments" }; - _contentHeader = new [] + _contentHeader = new[] { "*************************************************", "***** SUBNAUTICA RANDOMISER SPOILER LOG *****", @@ -52,16 +55,20 @@ private void PrepareStrings() "", "Generated on " + DateTime.Now + " with " + InitMod.s_versionDict[InitMod.s_expectedSaveVersion] }; - _contentBasics = new [] + _contentBasics = new[] { "", "", "///// Basic Information /////", "Seed: " + _config.iSeed, "Mode: " + _config.iRandomiserMode, + "Spawnpoint: " + _config.sSpawnPoint, "Fish, Eggs, Seeds: " + _config.bUseFish + ", " + _config.bUseEggs + ", " + _config.bUseSeeds, "Random Databoxes: " + _config.bRandomiseDataboxes, "Random Fragments: " + _config.bRandomiseFragments, + "Random Fragment numbers: " + _config.bRandomiseNumFragments + ", " + _config.iMaxFragmentsToUnlock, + "Random Duplicate Scan Rewards: " + _config.bRandomiseDuplicateScans, + "Random Recipes: " + _config.bRandomiseRecipes, "Vanilla Upgrade Chains: " + _config.bVanillaUpgradeChains, "Base Theming: " + _config.bDoBaseTheming, "Equipment, Tools, Upgrades: " + _config.iEquipmentAsIngredients + ", " + _config.iToolsAsIngredients + ", " + _config.iUpgradesAsIngredients, @@ -69,18 +76,24 @@ private void PrepareStrings() "Max Biomes per Fragment: " + _config.iMaxBiomesPerFragment, "" }; - _contentAdvanced = new [] + _contentAdvanced = new[] { "", "", "///// Depth Progression Path /////" }; - _contentDataboxes = new [] + _contentDataboxes = new[] { "", "", "///// Databox Locations /////" }; + _contentFragments = new[] + { + "", + "", + "///// Fragment Locations /////" + }; } /// @@ -90,32 +103,24 @@ private void PrepareStrings() private string[] PrepareAdvancedSettings() { List preparedAdvSettings = new List(); - FieldInfo[] defaultFieldInfoArray = typeof(ConfigDefaults).GetFields(BindingFlags.NonPublic | BindingFlags.Static); FieldInfo[] fieldInfoArray = typeof(RandomiserConfig).GetFields(BindingFlags.Public | BindingFlags.Instance); - - LogHandler.Debug("Number of fields in default, instance: " + defaultFieldInfoArray.Length + ", " + fieldInfoArray.Length); - - foreach (FieldInfo defaultField in defaultFieldInfoArray) + + foreach (FieldInfo field in fieldInfoArray) { // Check whether this field is an advanced config option or not. - if (_basicOptions.Contains(defaultField.Name)) + if (_basicOptions.Contains(field.Name)) continue; - - foreach (FieldInfo field in fieldInfoArray) - { - if (!field.Name.Equals(defaultField.Name)) - continue; - - var value = field.GetValue(_config); - - // If the value of a config field does not correspond to its default value, the user must have - // modified it. Add it to the list in that case. - if (!value.Equals(defaultField.GetValue(null))) - preparedAdvSettings.Add(field.Name + ": " + value); - - break; - } + if (!ConfigDefaults.Contains(field.Name)) + continue; + + var userValue = field.GetValue(_config); + var defaultValue = ConfigDefaults.GetDefault(field.Name); + // If the value of a config field does not correspond to its default value, the user must have + // modified it. Add it to the list in that case. + if (!userValue.Equals(defaultValue)) + preparedAdvSettings.Add(field.Name + ": " + userValue); } + LogHandler.Debug("Added anomalies: " + preparedAdvSettings.Count); if (preparedAdvSettings.Count == 0) preparedAdvSettings.Add("No advanced settings were modified."); @@ -129,20 +134,45 @@ private string[] PrepareAdvancedSettings() /// The prepared log entries. private string[] PrepareDataboxes() { - if (InitMod.s_masterDict.Databoxes is null) + if (_serializer.Databoxes is null) return new [] { "Not randomised, all in vanilla locations." }; List preparedDataboxes = new List(); - - foreach (KeyValuePair entry in InitMod.s_masterDict.Databoxes) + foreach (KeyValuePair entry in _serializer.Databoxes) { preparedDataboxes.Add(entry.Value.AsString() + " can be found at " + entry.Key); } - preparedDataboxes.Sort(); return preparedDataboxes.ToArray(); } + + /// + /// Grab the randomise fragments from masterDict, and sort them alphabetically. + /// + /// The prepared log entries. + private string[] PrepareFragments() + { + if (_serializer.SpawnDataDict is null || _serializer.SpawnDataDict.Count == 0) + return new[] { "Not randomised, all in vanilla locations." }; + + List preparedFragments = new List(); + // Iterate through every TechType representing each fragment. + foreach (var kv in _serializer.SpawnDataDict) + { + string line = kv.Key.AsString() + ": "; + foreach (var spawnData in kv.Value) + { + // Fragments are split up into their respective prefabs, but those all have the same spawn biomes + // and can be neglected. Just take the first prefab's biome spawns directly. + line += spawnData.BiomeDataList[0].Biome.AsString() + ", "; + } + preparedFragments.Add(line); + } + preparedFragments.Sort(); + + return preparedFragments.ToArray(); + } /// /// Compare the MD5 of the recipe CSV and try to see if it's still the same. @@ -226,14 +256,17 @@ internal async Task WriteLog() lines.AddRange(_contentHeader); lines.Add(PrepareMD5()); - + lines.AddRange(_contentBasics); lines.AddRange(PrepareAdvancedSettings()); lines.AddRange(_contentAdvanced); - + lines.AddRange(PrepareProgressionPath()); lines.AddRange(_contentDataboxes); lines.AddRange(PrepareDataboxes()); + + lines.AddRange(_contentFragments); + lines.AddRange(PrepareFragments()); using (StreamWriter file = new StreamWriter(Path.Combine(InitMod.s_modDirectory, _FileName))) {