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