diff --git a/AUTHORS.md b/AUTHORS.md index 4d2450b3..fc3478c7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,15 +3,15 @@ We thank all of the following individuals for their contributions to Nautilus. T | Name | Picture | Commits | Name | Picture | Commits | Name | Picture | Commits | | ---- | --------------- | ------ | ---- | --------------- | ------ | ---- | --------------- | ------ | -| [MrPurple6411](https://github.com/MrPurple6411) | | 433 | [PrimeSonic](https://github.com/PrimeSonic) | | 312 | [Metious](https://github.com/Metious) | | 297 | +| [MrPurple6411](https://github.com/MrPurple6411) | | 451 | [PrimeSonic](https://github.com/PrimeSonic) | | 312 | [Metious](https://github.com/Metious) | | 309 | | [toebeann](https://github.com/toebeann) | | 228 | [ahk1221](https://github.com/ahk1221) | | 212 | [Alexejhero](https://github.com/Alexejhero) | | 207 | -| [LeeTwentyThree](https://github.com/LeeTwentyThree) | | 122 | [zorgesho](https://github.com/zorgesho) | | 42 | [JKohlman](https://github.com/JKohlman) | | 37 | +| [LeeTwentyThree](https://github.com/LeeTwentyThree) | | 126 | [zorgesho](https://github.com/zorgesho) | | 42 | [JKohlman](https://github.com/JKohlman) | | 37 | | [K07H](https://github.com/K07H) | | 22 | [Vlad-00003](https://github.com/Vlad-00003) | | 11 | [EckoTheBat](https://github.com/EckoTheBat) | | 11 | -| [github-actions[bot]](https://github.com/apps/github-actions) | | 6 | [Cattlesquat](https://github.com/Cattlesquat) | | 5 | [DingoDjango](https://github.com/DingoDjango) | | 4 | +| [github-actions[bot]](https://github.com/apps/github-actions) | | 8 | [Cattlesquat](https://github.com/Cattlesquat) | | 5 | [DingoDjango](https://github.com/DingoDjango) | | 4 | | [celvro](https://github.com/celvro) | | 4 | [NeisesMike](https://github.com/NeisesMike) | | 3 | [VELD-Dev](https://github.com/VELD-Dev) | | 3 | | [vlyon](https://github.com/vlyon) | | 3 | [EldritchCarMaker](https://github.com/EldritchCarMaker) | | 3 | [jonahnm](https://github.com/jonahnm) | | 3 | -| [brett-taylor](https://github.com/brett-taylor) | | 2 | [RamuneNeptune](https://github.com/RamuneNeptune) | | 2 | [DeadMor0z](https://github.com/DeadMor0z) | | 1 | -| [SamuramongeDev](https://github.com/SamuramongeDev) | | 1 | +| [brett-taylor](https://github.com/brett-taylor) | | 2 | [RamuneNeptune](https://github.com/RamuneNeptune) | | 2 | [tinyhoot](https://github.com/tinyhoot) | | 2 | +| [DeadMor0z](https://github.com/DeadMor0z) | | 1 | [SamuramongeDev](https://github.com/SamuramongeDev) | | 1 | If you notice a problem with this file, feel free to report an issue in the repository. diff --git a/CLA/Signatures.json b/CLA/Signatures.json index e27f4706..b5d0b204 100644 --- a/CLA/Signatures.json +++ b/CLA/Signatures.json @@ -55,6 +55,14 @@ "created_at": "2023-09-25T14:00:10Z", "repoId": 123711758, "pullRequestNo": 468 + }, + { + "name": "Govorunb", + "id": 3830522, + "comment_id": 1825332180, + "created_at": "2023-11-24T08:52:03Z", + "repoId": 123711758, + "pullRequestNo": 505 } ] } \ No newline at end of file diff --git a/Example mod/Example mod.csproj b/Example mod/Example mod.csproj index fa1791b4..5b4e2d93 100644 --- a/Example mod/Example mod.csproj +++ b/Example mod/Example mod.csproj @@ -1,47 +1,48 @@ - - - - net472 - false - false - Nautilus.Examples - Nautilus.Example - true - true - AnyCPU - latest - SN.STABLE;BZ.STABLE - AnyCPU - Copyright © 2023 - false - false - - - - bin\SN.STABLE\ - SUBNAUTICA;SUBNAUTICA_STABLE - - - bin\BZ.STABLE\ - BELOWZERO;BELOWZERO_STABLE - - - - False - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + net472 + false + false + Nautilus.Examples + Nautilus.Example + $(VersionPrefix).$(SuffixNumber) + true + true + AnyCPU + latest + SN.STABLE;BZ.STABLE + AnyCPU + Copyright © 2023 + false + false + + + bin\SN.STABLE\ + SUBNAUTICA;SUBNAUTICA_STABLE + + + bin\BZ.STABLE\ + BELOWZERO;BELOWZERO_STABLE + - + + False + - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/Nautilus.sln b/Nautilus.sln index 1a386e0f..d8f313ae 100644 --- a/Nautilus.sln +++ b/Nautilus.sln @@ -13,10 +13,12 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5152F028-6D03-43C0-88C5-986313017D4F}" ProjectSection(SolutionItems) = preProject common.props = common.props + Directory.Build.props = Directory.Build.props + PostBuild.targets = PostBuild.targets Version.targets = Version.targets EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UploadHelper", "UploadHelper\UploadHelper.csproj", "{1D6AED20-87CC-4756-8E6A-1E8A10E16EB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UploadHelper", "UploadHelper\UploadHelper.csproj", "{1D6AED20-87CC-4756-8E6A-1E8A10E16EB1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Nautilus/Assets/Gadgets/ScanningGadget.cs b/Nautilus/Assets/Gadgets/ScanningGadget.cs index a84dec1e..367a66ad 100644 --- a/Nautilus/Assets/Gadgets/ScanningGadget.cs +++ b/Nautilus/Assets/Gadgets/ScanningGadget.cs @@ -318,7 +318,7 @@ public ScanningGadget WithAnalysisTech( /// A reference to this instance after the operation has completed. public ScanningGadget WithAnalysisTech( Sprite popupSprite, - List storyGoalsToTrigger = null, + List storyGoalsToTrigger, FMODAsset unlockSound = null, string unlockMessage = null ) diff --git a/Nautilus/Assets/ModPrefabCache.cs b/Nautilus/Assets/ModPrefabCache.cs index ef74e5a8..933c546d 100644 --- a/Nautilus/Assets/ModPrefabCache.cs +++ b/Nautilus/Assets/ModPrefabCache.cs @@ -111,7 +111,7 @@ public void EnterPrefabIntoCache(GameObject prefab) if(!Entries.ContainsKey(prefabIdentifier.classId)) { Entries.Add(prefabIdentifier.classId, prefab); - InternalLogger.Debug($"ModPrefabCache: adding prefab {prefab}"); + InternalLogger.Debug($"ModPrefabCache: added prefab {prefab}"); } else // this should never happen { diff --git a/Nautilus/Assets/ModPrefabRequest.cs b/Nautilus/Assets/ModPrefabRequest.cs index dc5e67ed..c3154ca5 100644 --- a/Nautilus/Assets/ModPrefabRequest.cs +++ b/Nautilus/Assets/ModPrefabRequest.cs @@ -7,12 +7,14 @@ namespace Nautilus.Assets; // request for getting ModPrefab asynchronously -internal class ModPrefabRequest: IPrefabRequest, IEnumerator +internal class ModPrefabRequest: IPrefabRequest { + internal bool Done { get; private set; } + private readonly PrefabInfo prefabInfo; - - private int state = 0; + private CoroutineTask task; + private TaskResult taskResult; public ModPrefabRequest(PrefabInfo prefabInfo) @@ -50,13 +52,22 @@ public object Current public bool TryGetPrefab(out GameObject result) { result = taskResult.Get(); + if (!Done) + { + Done = result; + } return result != null; } public bool MoveNext() { Init(); - return state++ == 0; + if (task == null) + { + return false; + } + + return !TryGetPrefab(out _); } public void Reset() {} diff --git a/Nautilus/Commands/ConsoleCommand.cs b/Nautilus/Commands/ConsoleCommand.cs index 6955feb7..66c24ce2 100644 --- a/Nautilus/Commands/ConsoleCommand.cs +++ b/Nautilus/Commands/ConsoleCommand.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using HarmonyLib; +using Nautilus.Extensions; namespace Nautilus.Commands; @@ -24,7 +25,11 @@ internal class ConsoleCommand /// /// The parameters for the command. /// - public IEnumerable Parameters { get; } + public IReadOnlyList Parameters { get; } + /// + /// The minimum number of parameters required to invoke the command. + /// + public int RequiredParameterCount { get; } /// /// The types of the parameters. @@ -53,8 +58,9 @@ public ConsoleCommand(string trigger, MethodInfo targetMethod, bool isDelegate = IsDelegate = isDelegate; Instance = instance; ModName = DeclaringType.Assembly.GetName().Name; - Parameters = targetMethod.GetParameters().Select(param => new Parameter(param)); + Parameters = targetMethod.GetParameters().Select(param => new Parameter(param)).ToList(); ParameterTypes = Parameters.Select(param => param.ParameterType).ToArray(); + RequiredParameterCount = Parameters.Count(param => !param.IsOptional); } /// @@ -66,73 +72,96 @@ public bool HasValidInvoke() return IsDelegate || Instance != null || IsMethodStatic; } - /// - /// Determines whether the target methods parameters are valid. - /// - /// - public bool HasValidParameterTypes() - { - foreach (Parameter parameter in Parameters) - { - if (!parameter.IsValidParameterType) - { - return false; - } - } - - return true; - } - /// /// Returns a list of all invalid parameters. /// /// public IEnumerable GetInvalidParameters() { - return Parameters.Where(param => !param.IsValidParameterType); + return Parameters.Where(p => p.ValidState != Parameter.ValidationError.Valid); } /// /// Attempts to parse input parameters into appropriate types as defined in the target method. /// - /// The parameters as input by the user. + /// The parameters as input by the user. /// The parameters that have been successfully parsed. - /// Whether or not all parameters were succesfully parsed. - public bool TryParseParameters(IEnumerable inputParameters, out object[] parsedParameters) + /// + /// A tuple containing: + /// + /// The number of input items consumed. + /// The number of command parameters that were successfully parsed. + /// + /// + public (int consumed, int parsed) TryParseParameters(IReadOnlyList input, out object[] parsedParameters) { parsedParameters = null; // Detect incorrect number of parameters (allow for optional) - if (Parameters.Count() < inputParameters.Count() || - Parameters.Where(param => !param.IsOptional).Count() > inputParameters.Count()) + int paramCount = Parameters.Count; + int inputCount = input.Count; + int paramsArrayLength = Math.Max(0, input.Count - (paramCount - 1)); + + if (inputCount < RequiredParameterCount) { - return false; + return default; } - parsedParameters = new object[Parameters.Count()]; - for (int i = 0; i < Parameters.Count(); i++) + parsedParameters = new object[paramCount]; + for (int i = 0; i < paramCount; i++) { - Parameter parameter = Parameters.ElementAt(i); - - if (i >= inputParameters.Count()) // It's an optional parameter that wasn't passed by the user - { - parsedParameters[i] = Type.Missing; - continue; - } + Type paramType = Parameters[i].ParameterType; + parsedParameters[i] = paramType.TryUnwrapArrayType(out Type elementType) + ? Array.CreateInstance(elementType, paramsArrayLength) + : DBNull.Value; + } - string input = inputParameters.ElementAt(i); + int consumed = 0; + int parsed = 0; + while (consumed < inputCount) + { + if (parsed >= paramCount) break; + + Parameter parameter = Parameters[parsed]; + string inputItem = input[consumed]; + object parsedItem; try { - parsedParameters[i] = parameter.Parse(input); + parsedItem = parameter.Parse(inputItem); } catch (Exception) { - return false; // couldn't parse, wasn't a valid conversion + return (consumed, parsed); + } + consumed++; + + if (parameter.ParameterType.IsArray) + { + Array parsedArr = (Array)parsedParameters[parsed]; + parsedArr.SetValue(parsedItem, consumed - parsed - 1); + if (consumed >= inputCount) + { + parsed++; + } + } + else + { + parsedParameters[parsed] = parsedItem; + parsed++; } } - return true; + // Optional parameters that weren't passed by the user + // at this point all required parameters should've been parsed + for (int i = parsed; i < paramCount; i++) + { + if (parsedParameters[i] == DBNull.Value) + parsedParameters[i] = Type.Missing; + parsed++; + } + + return (consumed, parsed); } /// diff --git a/Nautilus/Commands/Parameter.cs b/Nautilus/Commands/Parameter.cs index c24ee012..f9aa3f3c 100644 --- a/Nautilus/Commands/Parameter.cs +++ b/Nautilus/Commands/Parameter.cs @@ -1,15 +1,21 @@ - using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Reflection; +using Nautilus.Extensions; namespace Nautilus.Commands; internal struct Parameter { - private static Dictionary> TypeConverters = new() + [Flags] + public enum ValidationError + { + Valid = 0, + UnsupportedType = 1, + ArrayNotParams = 2, + } + private static Dictionary> _typeConverters = new() { [typeof(string)] = (s) => s, [typeof(bool)] = (s) => bool.Parse(s), @@ -18,23 +24,47 @@ internal struct Parameter [typeof(double)] = (s) => double.Parse(s, CultureInfo.InvariantCulture.NumberFormat) }; - public static IEnumerable SupportedTypes => TypeConverters.Keys; + public static IEnumerable SupportedTypes => _typeConverters.Keys; public Type ParameterType { get; } + public Type UnderlyingValueType { get; } public bool IsOptional { get; } public string Name { get; } - public bool IsValidParameterType { get; } + public ValidationError ValidState { get; } public Parameter(ParameterInfo parameter) { ParameterType = parameter.ParameterType; - IsOptional = parameter.IsOptional; + UnderlyingValueType = ParameterType.GetUnderlyingType(); + IsOptional = parameter.IsOptional || ParameterType.IsArray; Name = parameter.Name; - IsValidParameterType = SupportedTypes.Contains(ParameterType); + ValidState = ValidateParameter(parameter); + } + + private readonly ValidationError ValidateParameter(ParameterInfo paramInfo) + { + ValidationError valid = ValidationError.Valid; + // arrays MUST be a "params T[]" parameter + // this enforces them being last *and* only having one + if (ParameterType.IsArray && !paramInfo.IsDefined(typeof(ParamArrayAttribute), false)) + valid |= ValidationError.ArrayNotParams; + if (!_typeConverters.ContainsKey(UnderlyingValueType)) + valid |= ValidationError.UnsupportedType; + + return valid; } public object Parse(string input) { - return TypeConverters[ParameterType](input); + Type paramType = ParameterType; + if (paramType.TryUnwrapArrayType(out Type elementType)) + paramType = elementType; + if (paramType.TryUnwrapNullableType(out _)) + { + if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase)) + return null; + } + + return _typeConverters[UnderlyingValueType](input); } } \ No newline at end of file diff --git a/Nautilus/Extensions/GeneralExtensions.cs b/Nautilus/Extensions/GeneralExtensions.cs index b85bfe9f..819f52d6 100644 --- a/Nautilus/Extensions/GeneralExtensions.cs +++ b/Nautilus/Extensions/GeneralExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Text; using UnityEngine; namespace Nautilus.Extensions; @@ -43,4 +45,18 @@ public static void AddHint(this ErrorMessage @this, string message) else if (msg.timeEnd <= Time.time + @this.timeFadeOut) msg.timeEnd += @this.timeFadeOut + @this.timeInvisible; } + + /// + /// Concatenates string representations of the provided , + /// using the specified between them. + /// + /// Type of value that will be converted to . + /// The . + /// The to insert between each pair of values. + /// Values to concatenate into the . + /// The provided . + public static StringBuilder AppendJoin(this StringBuilder builder, string separator, IEnumerable values) + { + return builder.Append(string.Join(separator, values)); + } } \ No newline at end of file diff --git a/Nautilus/Extensions/TypeExtensions.cs b/Nautilus/Extensions/TypeExtensions.cs new file mode 100644 index 00000000..87536ae3 --- /dev/null +++ b/Nautilus/Extensions/TypeExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; + +namespace Nautilus.Extensions; + +internal static class TypeExtensions +{ + private static readonly List _builtinTypeAliases = new() + { + "void", + null, // all other types + "DBNull", + "bool", + "char", + "sbyte", + "byte", + "short", + "ushort", + "int", + "uint", + "long", + "ulong", + "float", + "double", + "decimal", + null, // DateTime? + null, // ??? + "string" + }; + + /// + /// Format the given 's name into a more developer-friendly form. + /// + /// + /// + public static string GetFriendlyName(this Type type) + { + if (type.TryUnwrapArrayType(out Type elementType)) + return GetFriendlyName(elementType) + "[]"; + + if (type.TryUnwrapNullableType(out Type valueType)) + return GetFriendlyName(valueType) + "?"; + + // TODO: format tuples as well + + if (type.IsConstructedGenericType) + return type.Name[..type.Name.LastIndexOf('`')] + + $"<{type.GenericTypeArguments.Select(GetFriendlyName).Join()}>"; + + return _builtinTypeAliases[(int) Type.GetTypeCode(type)] ?? type.Name; + } + + /// + /// "Unwraps" the inner from an array and/or nullable type. + /// + /// + /// + /// The inner type - for example, from a string[], or from bool?.
+ /// If the isn't wrapped, it is returned as-is. + ///
+ public static Type GetUnderlyingType(this Type type) + { + if (type.TryUnwrapArrayType(out Type elementType)) + type = elementType; + if (type.TryUnwrapNullableType(out Type valueType)) + type = valueType; + return type; + } + + public static bool TryUnwrapArrayType(this Type type, out Type elementType) + { + // GetElementType checks if it's an array, pointer, or reference + elementType = type.GetElementType(); + return type.IsArray // restrict to arrays only + && elementType != null; + } + + public static bool TryUnwrapNullableType(this Type type, out Type valueType) + { + valueType = Nullable.GetUnderlyingType(type); + return valueType != null; + } +} diff --git a/Nautilus/Handlers/LanguageHandler.cs b/Nautilus/Handlers/LanguageHandler.cs index 2f8fc85d..ef75ff58 100644 --- a/Nautilus/Handlers/LanguageHandler.cs +++ b/Nautilus/Handlers/LanguageHandler.cs @@ -45,7 +45,22 @@ public static void RegisterLocalizationFolder(string languageFolderName = "Local foreach (var file in Directory.GetFiles(path)) { - var content = JsonConvert.DeserializeObject>(File.ReadAllText(file)); + if (Path.GetExtension(file) != ".json") + { + continue; + } + + // I hate this + Dictionary content = null; + try + { + content = JsonConvert.DeserializeObject>(File.ReadAllText(file)); + } + catch (Exception e) + { + InternalLogger.Error($"Exception caught while trying to deserialize localization file at path: '{file}'. Exception: {e}"); + } + if (content is null) { InternalLogger.Warn($"Localization file '{file}' is empty, skipping registration."); diff --git a/Nautilus/Handlers/LootDistributionHandler.cs b/Nautilus/Handlers/LootDistributionHandler.cs index 59087292..00f180b7 100644 --- a/Nautilus/Handlers/LootDistributionHandler.cs +++ b/Nautilus/Handlers/LootDistributionHandler.cs @@ -95,6 +95,8 @@ public static void AddLootDistributionData(string classId, LootDistributionData. /// The dictating how the prefab should spawn in the world. public static void AddLootDistributionData(string classId, params LootDistributionData.BiomeData[] biomeDistribution) { + CraftData.PreparePrefabIDCache(); + if (!PrefabDatabase.TryGetPrefabFilename(classId, out var filename)) { InternalLogger.Error($"Could not find prefab file path for class ID '{classId}'. Cancelling loot distribution addition."); @@ -112,6 +114,8 @@ public static void AddLootDistributionData(string classId, params LootDistributi /// The dictating how the prefab should spawn in the world. public static void AddLootDistributionData(string classId, WorldEntityInfo info, params LootDistributionData.BiomeData[] biomeDistribution) { + CraftData.PreparePrefabIDCache(); + if (!PrefabDatabase.TryGetPrefabFilename(classId, out var filename)) { InternalLogger.Error($"Could not find prefab file path for class ID '{classId}'. Cancelling loot distribution addition."); diff --git a/Nautilus/Nautilus.csproj b/Nautilus/Nautilus.csproj index 8123602d..ae1db35c 100644 --- a/Nautilus/Nautilus.csproj +++ b/Nautilus/Nautilus.csproj @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ Nautilus Nautilus com.snmodding.nautilus - $(VersionPrefix) + $(VersionPrefix).$(SuffixNumber) 11 true true diff --git a/Nautilus/Patchers/ConsoleCommandsPatcher.cs b/Nautilus/Patchers/ConsoleCommandsPatcher.cs index 197488d9..12a08dd5 100644 --- a/Nautilus/Patchers/ConsoleCommandsPatcher.cs +++ b/Nautilus/Patchers/ConsoleCommandsPatcher.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using BepInEx.Logging; using HarmonyLib; using Nautilus.Commands; +using Nautilus.Extensions; using Nautilus.Utility; using UnityEngine; @@ -13,7 +15,7 @@ namespace Nautilus.Patchers; internal static class ConsoleCommandsPatcher { - private static Dictionary ConsoleCommands = new(); + private static Dictionary ConsoleCommands = new(StringComparer.OrdinalIgnoreCase); private static Color CommandColor = new(1, 1, 0); private static Color ParameterTypeColor = new(0, 1, 1); @@ -63,17 +65,41 @@ public static void AddCustomCommand(string command, MethodInfo targetMethod, boo return; } - // if any of the parameter types of the method are unsupported, print an error and don't add it - if (!consoleCommand.HasValidParameterTypes()) + // if any of the parameters of the method aren't valid, print an error and don't add it + if (consoleCommand.GetInvalidParameters().Any()) { - string error = $"Could not register custom command {GetColoredString(consoleCommand)} for mod " + - $"{GetColoredString(consoleCommand.ModName, ModOriginColor)}\n" + - "The following parameters have unsupported types:\n" + - consoleCommand.GetInvalidParameters().Select(param => GetColoredString(param)).Join(delimiter: "\n") + - "Supported parameter types:\n" + - Parameter.SupportedTypes.Select(type => type.Name).Join(); + List parametersWithUnsupportedTypes = new(); + List nonParamsArrayParameters = new(); + foreach (var parameter in consoleCommand.GetInvalidParameters()) + { + Parameter.ValidationError state = parameter.ValidState; + if (state.HasFlag(Parameter.ValidationError.UnsupportedType)) + parametersWithUnsupportedTypes.Add(parameter); + if (state.HasFlag(Parameter.ValidationError.ArrayNotParams)) + nonParamsArrayParameters.Add(parameter); + } + + StringBuilder error = new StringBuilder( + $"Could not register custom command {GetColoredString(consoleCommand)} for mod " + + $"{GetColoredString(consoleCommand.ModName, ModOriginColor)}\n" + ); - LogAndAnnounce(error, LogLevel.Error); + if (parametersWithUnsupportedTypes.Count > 0) + { + error.AppendLine("The following parameters have unsupported types:"); + error.AppendJoin("\n", parametersWithUnsupportedTypes.Select(GetColoredString)); + error.AppendLine("\nSupported parameter types:"); + error.AppendJoin(",", Parameter.SupportedTypes.Select(type => type.GetFriendlyName())); + } + + if (nonParamsArrayParameters.Count > 0) + { + error.AppendLine("Array parameters must be marked as 'params'."); + error.AppendLine("Incorrect parameters:"); + error.AppendJoin(",", nonParamsArrayParameters.Select(GetColoredString)); + } + + LogAndAnnounce(error.ToString(), LogLevel.Error); return; } @@ -148,7 +174,7 @@ private static bool HandleCommand(string input) input = input.Trim(); string[] components = input.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - string trigger = components[0].ToLowerInvariant(); + string trigger = components[0]; if (!ConsoleCommands.TryGetValue(trigger, out ConsoleCommand command)) { @@ -156,30 +182,39 @@ private static bool HandleCommand(string input) return false; } - IEnumerable parameters = components.Skip(1); + List inputParameters = components.Skip(1).ToList(); + + (int consumed, int parsed) = command.TryParseParameters(inputParameters, out object[] parsedParameters); // If the parameters couldn't be parsed by the command, print a user and developer-friendly error message both // on-screen and in the log. - if (!command.TryParseParameters(parameters, out object[] parsedParameters)) + bool consumedAll = consumed >= inputParameters.Count; + bool parsedAll = parsed == command.Parameters.Count; + if (!consumedAll || !parsedAll) { - if (parsedParameters != null) + if (!parsedAll) { - // Find the first invalid parameter - string invalidParameter = null; - string parameterTypeName = null; - for (int i = 0; i < parsedParameters.Length; i++) + if (parsedParameters != null) { - if (parsedParameters[i] == null) - { - invalidParameter = parameters.ElementAt(i); - parameterTypeName = command.ParameterTypes[i].Name; - break; - } - } + // Get the first invalid parameter + string invalidInput = inputParameters[consumed]; + + var invalidParameter = command.Parameters[parsed]; + string parameterTypeName = invalidParameter.UnderlyingValueType.GetFriendlyName(); - // Print a message about why it is invalid - string error = $"{GetColoredString(invalidParameter, ParameterInputColor)} is not a valid " + - $"{GetColoredString(parameterTypeName, ParameterTypeColor)}!"; + // Print a message about why it is invalid + string error = $"Parameter {GetColoredString(invalidParameter.Name, ParameterOptionalColor)} could not be parsed:\n" + + $"{GetColoredString(invalidInput, ParameterInputColor)} is not a valid " + + $"{GetColoredString(parameterTypeName, ParameterTypeColor)}!"; + + LogAndAnnounce(error, LogLevel.Error); + } + } + else if (!consumedAll) + { + string error = "Received too many parameters!\n" + + $"expected {GetColoredString(command.Parameters.Count.ToString(), ParameterTypeColor)} " + + $"but got {GetColoredString(inputParameters.Count.ToString(), ParameterInputColor)}"; LogAndAnnounce(error, LogLevel.Error); } @@ -187,14 +222,14 @@ private static bool HandleCommand(string input) // Print a message about what parameters the command expects string parameterInfoString = $"{GetColoredString(command.Trigger, CommandColor)} " + "expects the following parameters\n" + - command.Parameters.Select(param => GetColoredString(param)).Join(delimiter: "\n"); + command.Parameters.Select(GetColoredString).Join(delimiter: "\n"); LogAndAnnounce(parameterInfoString, LogLevel.Error); // Print a message detailing all received parameters. - if (parameters.Any()) + if (inputParameters.Any()) { - InternalLogger.Announce($"Received parameters: {parameters.Join()}", LogLevel.Error, true); + InternalLogger.Announce($"Received parameters: {inputParameters.Join()}", LogLevel.Error, true); } return true; // We've handled the command insofar as we've handled and reported the user error to them. @@ -232,7 +267,7 @@ private static string GetColoredString(ConsoleCommand command) private static string GetColoredString(Parameter parameter) { - return $"{parameter.Name}: {GetColoredString(parameter.ParameterType.Name, ParameterTypeColor)}" + + return $"{parameter.Name}: {GetColoredString(parameter.ParameterType.GetFriendlyName(), ParameterTypeColor)}" + (parameter.IsOptional ? $" {GetColoredString("(optional)", ParameterOptionalColor)}" : string.Empty); } diff --git a/Nautilus/Patchers/PrefabDatabasePatcher.cs b/Nautilus/Patchers/PrefabDatabasePatcher.cs index 8ea415bc..172f2251 100644 --- a/Nautilus/Patchers/PrefabDatabasePatcher.cs +++ b/Nautilus/Patchers/PrefabDatabasePatcher.cs @@ -80,7 +80,14 @@ private static IPrefabRequest GetModPrefabAsync(string classId) } if(ModPrefabCache.Requests.TryGetValue(prefabInfo.ClassID, out var request)) + { + if (request.Done && !request.TryGetPrefab(out _)) + { + return new ModPrefabRequest(prefabInfo); + } + return request; + } return new ModPrefabRequest(prefabInfo); } diff --git a/PostBuild.targets b/PostBuild.targets index 6b3968e5..52552125 100644 --- a/PostBuild.targets +++ b/PostBuild.targets @@ -1,11 +1,11 @@ - + Nautilus $(OutDir)\plugins $([System.IO.Path]::Combine('$(PluginsDir)', '$(BuildPath)')) - $([System.IO.Path]::Combine('$(TargetDir)', 'Nautilus_$(ConfigurationName).zip')) + $([System.IO.Path]::Combine('$(TargetDir)', 'Nautilus_$(ConfigurationName)_$(BepInExPluginVersion).zip')) $([System.IO.Path]::Combine('$(OutDir)', 'TemporaryDir')) $([System.IO.Path]::Combine('$(TemporaryDir)', 'Build')) diff --git a/UploadHelper/Program.cs b/UploadHelper/Program.cs index e8022ae6..2ae9414a 100644 --- a/UploadHelper/Program.cs +++ b/UploadHelper/Program.cs @@ -8,10 +8,15 @@ internal static class Program private const string VersionPrefixStart = ""; private const string VersionPrefixEnd = ""; + private const string SuffixNumberStart = ""; + private const string SuffixNumberEnd = ""; private const string VersionSuffixStart = ""; private const string VersionSuffixEnd = ""; + private static string _versionPrefix; + private static int _versionSuffix; + private static Version _version; - private static string[] _uploadPageURLs = new string[] + private static readonly string[] _uploadPageURLs = new string[] { "https://github.com/SubnauticaModding/Nautilus/releases", "https://www.submodica.xyz/mods/sn1/250", @@ -22,171 +27,275 @@ internal static class Program public static void Main(string[] args) { + Console.Clear(); // essential variables - _nautilusDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..", ".."); - - // greeting - Console.WriteLine("Welcome to the Upload Helper for Nautilus. This program should become obsolete as soon as we set up a proper build deployment system.\n"); + _nautilusDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var solutionPath = ""; + while (_nautilusDirectory != null) + { + var file = Path.Combine(_nautilusDirectory, "Nautilus.sln"); + if (File.Exists(file)) + { + solutionPath = file; + break; + } - Console.WriteLine("Press ENTER to begin..."); - Console.ReadLine(); + _nautilusDirectory = Directory.GetParent(_nautilusDirectory)?.FullName; + } - // get old version - Console.WriteLine("First of all, we need to determine the version string."); - var oldVersion = GetCurrentVersionString(); - Console.WriteLine($"I think the current version string is {oldVersion} (suffixes such as the pre-release number might not be shown here), but it's a good idea to double check.\n"); - Console.WriteLine("When you press enter, I will open https://www.submodica.xyz/mods/sn1/246 and https://github.com/SubnauticaModding/Nautilus/releases in your browser so you can find the version string that is currently uploaded to the internet."); - Console.WriteLine("If you don't want this, just type something before you hit enter."); - if (string.IsNullOrEmpty(Console.ReadLine())) + if (string.IsNullOrWhiteSpace(solutionPath)) { - Process.Start("explorer", "https://www.submodica.xyz/mods/sn1/250"); - Thread.Sleep(1000); - Process.Start("explorer", "https://github.com/SubnauticaModding/Nautilus/releases"); + Console.WriteLine($"Could not find the Nautilus solution in any parent directory."); + Console.ReadLine(); + return; } - // determine new version - - Console.WriteLine("Please write the NEW version number here (do NOT include the pre-release suffix): "); - var versionPrefix = Console.ReadLine(); - if (versionPrefix != null && versionPrefix.StartsWith("v")) + var nautilusProjectPath = Path.Combine(_nautilusDirectory, "Nautilus", "Nautilus.csproj"); + if (!File.Exists(nautilusProjectPath)) { - Console.WriteLine("Hm, why does it start with a V? Are you sure you meant to do that? If not, type R and we can retry that."); - var line = Console.ReadLine(); - if (line != null && line.ToLower() == "r") - { - Console.Write("Version number: "); - versionPrefix = Console.ReadLine(); - } + Console.WriteLine($"Could not find the Nautilus project at {nautilusProjectPath}"); + Console.ReadLine(); + return; } - Console.WriteLine("\nAnd now, if applicable, send the pre-release number (or leave empty): "); - string prereleaseNum = Console.ReadLine(); - string versionString = versionPrefix; + // greeting + Console.WriteLine("Welcome to the Upload Helper for Nautilus. \nThis program should become obsolete as soon as we set up a proper build deployment system.\n"); - if (!string.IsNullOrEmpty(prereleaseNum)) + if (!Ask("Do you want to begin? (y/n)")) { - versionString += "-pre." + prereleaseNum; - Console.WriteLine($"\nOh, you want to add \"pre.{prereleaseNum}\" to the version suffix? Sure, just remember we have to remove that when making the builds that we distribute."); - Console.WriteLine("If you are wondering why, that is because the BepInEx plugin."); + Console.Clear(); + Console.WriteLine("Alright, goodbye!"); + return; } - Console.WriteLine($"\nAlright, thanks! We’ll use {versionString} for this release."); - - SetCurrentVersionString(versionPrefix, "pre." + prereleaseNum); - - Console.WriteLine("\nThe Version.targets file was automatically updated. Remember that we have to fix that later."); - Console.WriteLine("\nNow, let's work on getting the NuGet package up and running."); + Console.Clear(); - Console.WriteLine("I should warn you now that you’ll need to log in to upload your update (for security reasons)." + - "\nPlease contact an administrator if you need help, otherwise we’ll continue from here."); - - WalkThroughNuGetSteps("SN.STABLE"); - - WalkThroughNuGetSteps("BZ.STABLE"); + // get old version + Console.WriteLine("First of all, we need to determine the version string."); - Console.WriteLine("\nRemember, these two versions you built are ONLY used for NUGET DEPENDENCIES. The version you have now should NOT be used in-game."); + Console.WriteLine("You can check https://github.com/SubnauticaModding/Nautilus/releases to find the version that is currently uploaded."); - Console.WriteLine("Alright, great, did everything work out? I’m having connection issues and I can’t see your responses, so I’ll assume that’s a yes."); + if (Ask("Do you want to open this in your browser? (y/n)")) + Process.Start("explorer", "https://github.com/SubnauticaModding/Nautilus/releases"); - SetCurrentVersionString(versionPrefix, null); + try + { + GetCurrentVersionString(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + Console.ReadLine(); + return; + } + var versionPrefix = _versionPrefix; + var suffixNumber = _versionSuffix; - Console.WriteLine("\nNow let’s work on getting this update pushed out to our users. First, we need to remove the prerelease tags from the assembly." + - "\nI’ve done that for you already. Now build for BOTH versions of the game."); + // determine new version + if (!Ask($"I think the current version string is {versionPrefix}.{suffixNumber}. Do you want to keep it? (y/n)")) + { + Console.WriteLine("Alright, we'll change it."); + Console.WriteLine("Please write the NEW 3 digit version number here (eg: 1.0.0) (do NOT include the pre-release suffix): "); + versionPrefix = RequestVersion(); + Version v; + while (!Version.TryParse(versionPrefix, out v) || v.Major < _version.Major || v.Minor < _version.Minor || v.Build < _version.Build) + { + Console.Clear(); + Console.WriteLine("That doesn't look like a valid version number or is less than the current version. Please try again."); + Console.WriteLine("Please write the NEW 3 digit version number here (eg: 1.0.0) (do NOT include the pre-release suffix): "); + versionPrefix = RequestVersion(); + } - Console.WriteLine("Press enter after you have ONCE AGAIN built the project for both SN.STABLE and BZ.STABLE."); + var isEqual = v.Major == _version.Major && v.Minor == _version.Minor && v.Build == _version.Build; + var response = isEqual || Ask("Do you want to add a pre-release suffix? (y/n)"); + while (response) + { + Console.WriteLine("What should the pre-release suffix be?"); + var prereleaseNum = Console.ReadLine(); + if (string.IsNullOrEmpty(prereleaseNum) || !int.TryParse(prereleaseNum, out suffixNumber)) + { + if (!Ask("That doesn't look like an intiger. Do you want to try again? (y/n)")) + break; + + Console.Clear(); + continue; + } + + if (isEqual && suffixNumber < _versionSuffix) + { + Console.Clear(); + Console.WriteLine("That's less than the current pre-release suffix. I'm not sure what you're trying to do, but I'm not going to let you do it."); + continue; + } + + if (suffixNumber < 0) + { + Console.Clear(); + Console.WriteLine("That's a negative number. I'm not sure what you're trying to do, but I'm not going to let you do it."); + continue; + } + + if (suffixNumber == 0) + { + Console.Clear(); + Console.WriteLine("That's a zero. I'm not sure what you're trying to do, but I'm not going to let you do it."); + if (!Ask("Do you want to try again? (y/n)")) + break; + continue; + } + + Console.Clear(); + if (Ask($"\nAdd \"{prereleaseNum}\" as the pre-release suffix? (y/n)")) + break; + Console.Clear(); + response = isEqual || Ask("Do you want to add a pre-release suffix? (y/n)"); + } + } - Console.ReadLine(); + Console.WriteLine($"\nAlright, thanks! We’ll use {versionPrefix}.{suffixNumber} for this release."); + SetCurrentVersionString(versionPrefix, suffixNumber); - Console.WriteLine("Press enter and I will open all the relevant pages where these mods should be uploaded in your browser."); + Console.WriteLine("\nNow, let's work on getting the NuGet packages up and running."); + Console.WriteLine("I should warn you now that you’ll need to log in to upload your update (for security reasons)." + + "\nPlease contact an administrator if you need help, otherwise we’ll continue from here."); - Console.ReadLine(); + RebuildNautilus(nautilusProjectPath); foreach (var url in _uploadPageURLs) { + if (!Ask($"Do you want to open {url} to upload? (y/n)")) + continue; + Console.WriteLine("Opening " + url + "..."); Process.Start("explorer", url); Thread.Sleep(500); + Console.Clear(); } + Console.Clear(); + Console.WriteLine("Congratulations, you're done!!!"); // END Console.ReadLine(); } - private static bool Ask(string prompt) + private static void RebuildNautilus(string projectPath) { - Console.WriteLine(prompt); - var l = Console.ReadLine(); - return !string.IsNullOrEmpty(l) && l.ToLower() == "y"; - } - - private static string VersionTargetsPath => Path.Combine(_nautilusDirectory, "Version.targets"); + // Start a new process + var process = new Process() + { + StartInfo = new ProcessStartInfo() + { + FileName = "cmd.exe", + RedirectStandardInput = true, + RedirectStandardOutput = true, + UseShellExecute = false + } + }; - private static void RebuildAuthorsTable() - { - var proc = new Process(); - proc.StartInfo.FileName = Path.Combine(_nautilusDirectory, "AuthorsTableGenerator", "AuthorTableGenerator.exe"); - proc.Start(); - proc.WaitForExit(); - proc.Close(); - } + process.Start(); - private static void WalkThroughNuGetSteps(string branch) - { - Console.WriteLine($"\nIn your IDE, switch to the {branch} build configuration, build the project, and then press ENTER in this window when you have finished."); + // Define the configurations to build + var configurations = new string[] { "SN.STABLE", "BZ.STABLE" }; - Console.ReadLine(); + // Execute the build with nuget restore for each configuration. + foreach (var configuration in configurations) + process.StandardInput.WriteLine($"dotnet build \"{projectPath}\" /restore /p:Configuration={configuration}"); - Console.WriteLine("All built and ready? When you press ENTER I will open the folder containing the built files."); + process.StandardInput.WriteLine("exit"); - Console.ReadLine(); + Console.WriteLine(process.StandardOutput.ReadToEnd()); + process.WaitForExit(); + Console.WriteLine("Now you need to upload the.nupkg files."); - Process.Start("explorer", Path.Combine(_nautilusDirectory, "Nautilus", "bin", branch)); + if (Ask("Do you want to open https://www.nuget.org/packages/manage/upload in your browser? (y/n)")) + Process.Start("explorer", "https://www.nuget.org/packages/manage/upload"); - Console.WriteLine("Got it? Now you need to upload the correct .nupkg file at https://www.nuget.org/packages/manage/upload. Press ENTER to open that in your browser."); + if (Ask($"Do you want to open the output paths in explorer? (y/n)")) + foreach (var configuration in configurations) + Process.Start("explorer", Path.Combine(_nautilusDirectory, "Nautilus", "bin", configuration)); + Console.WriteLine("Press enter to continue."); Console.ReadLine(); + } + + private static string RequestVersion() + { + var versionPrefix = Console.ReadLine(); + if (!string.IsNullOrWhiteSpace(versionPrefix) && versionPrefix.StartsWith("v")) + { + Console.WriteLine("Hm, why does it start with a V? Are you sure you meant to do that? If not, type R and we can retry that."); + var line = Console.ReadLine(); + if (line != null && line.ToLower() == "r") + { + Console.Write("Version number: "); + versionPrefix = Console.ReadLine(); + } + } + else if (string.IsNullOrWhiteSpace(versionPrefix)) + { + Console.WriteLine("You didn't write anything. I'll assume you want to keep the current version number."); + versionPrefix = _versionPrefix; + } + + return versionPrefix; + } - Process.Start("explorer", "https://www.nuget.org/packages/manage/upload"); + private static bool Ask(string prompt) + { + Console.WriteLine(prompt); + var l = Console.ReadLine(); + while (string.IsNullOrWhiteSpace(l) || (l.ToLower() != "y" && l.ToLower() != "n")) + { + Console.WriteLine("Please answer with y or n."); + Console.WriteLine(prompt); + l = Console.ReadLine(); + } + return l.ToLower() == "y"; } - private static string GetCurrentVersionString() + private static string VersionTargetsPath => Path.Combine(_nautilusDirectory, "Version.targets"); + + private static void GetCurrentVersionString() { var text = File.ReadAllText(VersionTargetsPath); var prefixStartIndex = text.IndexOf(VersionPrefixStart) + VersionPrefixStart.Length; var prefixLength = text.IndexOf(VersionPrefixEnd) - prefixStartIndex; - string prefix = text.Substring(prefixStartIndex, prefixLength); + var prefix = text.Substring(prefixStartIndex, prefixLength); string suffix = null; - if (text.Contains(VersionSuffixStart)) + if (text.Contains(SuffixNumberStart)) { - var suffixStartIndex = text.IndexOf(VersionSuffixStart) + VersionSuffixStart.Length; - var suffixLength = text.IndexOf(VersionSuffixEnd) - suffixStartIndex; + var suffixStartIndex = text.IndexOf(SuffixNumberStart) + SuffixNumberStart.Length; + var suffixLength = text.IndexOf(SuffixNumberEnd) - suffixStartIndex; suffix = text.Substring(suffixStartIndex, suffixLength); } - if (string.IsNullOrEmpty(suffix)) return prefix; + if (string.IsNullOrWhiteSpace(prefix) || !Version.TryParse(prefix, out _version)) + throw new Exception("The VersionPrefix in Version.targets is not a valid version number. I'm not sure what you're trying to do, but I'm not going to let you do it."); - else return prefix + "-" + suffix; + _versionPrefix = prefix; + if (string.IsNullOrWhiteSpace(suffix) || !int.TryParse(suffix, out _versionSuffix)) + throw new Exception("The SuffixNumber in Version.targets is not a number. I'm not sure what you're trying to do, but I'm not going to let you do it."); } - public static void SetCurrentVersionString(string prefix, string suffix = null) + public static void SetCurrentVersionString(string prefix, int suffix) { var text = File.ReadAllText(VersionTargetsPath); - var split = text.Split(new string[] { VersionSuffixStart, VersionSuffixEnd, VersionPrefixStart, VersionPrefixEnd }, StringSplitOptions.None); + var split = text.Split(new string[] { VersionSuffixStart, VersionSuffixEnd, SuffixNumberStart, SuffixNumberEnd, VersionPrefixStart, VersionPrefixEnd }, StringSplitOptions.None); - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); sb.AppendLine(split[0].TrimEnd()); sb.AppendLine(" " + VersionPrefixStart + prefix + VersionPrefixEnd); - if (!string.IsNullOrEmpty(suffix)) - { - sb.AppendLine(" " + VersionSuffixStart + suffix + VersionSuffixEnd); - } - sb.Append(" " + split[split.Length - 1].TrimStart()); + sb.AppendLine(" " + SuffixNumberStart + suffix + SuffixNumberEnd); + if (suffix > 0) + sb.AppendLine(" " + VersionSuffixStart + "pre.$(SuffixNumber)" + VersionSuffixEnd); + + sb.Append(" " + split[^1].TrimStart()); var final = sb.ToString(); diff --git a/Version.targets b/Version.targets index 9e43a2da..69107f7f 100644 --- a/Version.targets +++ b/Version.targets @@ -3,5 +3,7 @@ 1.0.0 + 23 + pre.$(SuffixNumber) \ No newline at end of file