Skip to content

Commit

Permalink
Merge branch 'master' into docs
Browse files Browse the repository at this point in the history
  • Loading branch information
LeeTwentyThree committed Jul 19, 2023
2 parents 0438aae + 07bee60 commit 4638009
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 6 deletions.
5 changes: 3 additions & 2 deletions Nautilus/Documentation/tutorials/vehicle-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ So, first, let's make our prefab info.
```csharp
var prefabInfo = PrefabInfo.WithTechType("SeamothDepthUpgrade", "Seamoth Depth Module MK.4", "Dive down to 1700 meters!!! Let's meet the Sea Dragon!")
.WithIcon(SpriteManager.Get(TechType.HullReinforcementModule3));
CustomPrefab prefab = new CustomPrefab(prefabInfo);
```


Then, we're making the Custom Prefab based on Reinforced Hull prefab.

```csharp
var prefab = new CloneTemplate(prefabInfo, TechType.HullReinforcementModule3);
var clone = new CloneTemplate(prefabInfo, TechType.HullReinforcementModule3);
```


Expand Down Expand Up @@ -142,4 +143,4 @@ prefab.SetUpgradeModule(EquipmentType.VehicleModule, QuickSlotType.Selectable)
```


And you can do a lot more.
And you can do a lot more.
1 change: 1 addition & 0 deletions Nautilus/Initializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ public class Initializer : BaseUnityPlugin
VehicleUpgradesPatcher.Patch(_harmony);
StoryGoalPatcher.Patch(_harmony);
PDAEncyclopediaTabPatcher.Patch(_harmony);
NewtonsoftJsonPatcher.Patch(_harmony);
}
}
152 changes: 152 additions & 0 deletions Nautilus/Patchers/NewtonsoftJsonPatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using HarmonyLib;
using Newtonsoft.Json.Utilities;
using System.Collections.Generic;
using Nautilus.Utility;
using System.Reflection.Emit;
using Nautilus.Handlers;
using System.Linq;
using System;
using System.Reflection;

namespace Nautilus.Patchers;

// Patches methods in the Newtonsoft.Json.Utilities.EnumUtils class to ensure that custom enums are handled properly, without error
internal static class NewtonsoftJsonPatcher
{
private static Dictionary<Type, CachedCacheManager> _cachedCacheManagers = new();

public static void Patch(Harmony harmony)
{
// Transpiler to skip the initialization of custom enum values:

harmony.Patch(AccessTools.Method(typeof(EnumUtils), nameof(EnumUtils.InitializeValuesAndNames)),
transpiler: new HarmonyMethod(AccessTools.Method(typeof(NewtonsoftJsonPatcher), nameof(NewtonsoftJsonPatcher.InitializeValuesAndNamesTranspiler))));

/* Postfix to allow custom enum values to be converted to strings:
* I had to do this because I was unable to filter for methods based on their nullable parameters.
* I can't just put typeof(string?) in the list of parameter types.
* This method is *good enough* and attempts to find an overload of TryToString with a boolean... let's just hope Newtonsoft.Json isn't updated!
*/
var toStringMethod = typeof(EnumUtils)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First((meth) => meth.Name == "TryToString" && !meth.GetParameters().Types().Contains(typeof(bool)));

harmony.Patch(toStringMethod, postfix: new HarmonyMethod(AccessTools.Method(typeof(NewtonsoftJsonPatcher), nameof(NewtonsoftJsonPatcher.EnumUtilsTryToStringPostfix))));

// Prefix that checks an enum has custom entries, and if so, attempts to parse a custom enum value:

harmony.Patch(AccessTools.Method(typeof(EnumUtils), nameof(EnumUtils.ParseEnum)),
prefix: new HarmonyMethod(AccessTools.Method(typeof(NewtonsoftJsonPatcher), nameof(NewtonsoftJsonPatcher.EnumUtilsParseEnumPrefix))));
}

// Skip initialization of custom enum values
private static IEnumerable<CodeInstruction> InitializeValuesAndNamesTranspiler(IEnumerable<CodeInstruction> instructions)
{
var found = false;
foreach (var instruction in instructions)
{
yield return instruction;
if (!found && instruction.opcode == OpCodes.Stloc_S && (instruction.operand is LocalBuilder builder && builder.LocalIndex == 6))
{
// load the text variable to the eval stack
yield return new CodeInstruction(OpCodes.Ldloc_S, (byte) 6);
// load the type local variable (type of enum) to the eval stack
yield return new CodeInstruction(OpCodes.Ldloc_0, (byte) 6);
// call the IsEnumValueModdedByString method
yield return Transpilers.EmitDelegate(IsEnumValueModdedByString);
// find the label at the bottom of the for loop
var stelem = instructions.Last((instr) => instr.opcode == OpCodes.Stelem_Ref);
var endOfForLoop = stelem.labels[0];
// insert a jump IF AND ONLY IF the IsEnumValueModded method returned true
yield return new CodeInstruction(OpCodes.Brtrue_S, endOfForLoop);
found = true;
}
}
InternalLogger.Log("NewtonsoftJsonPatcher.InitializeValuesAndNamesTranspiler succeeded: " + found);
}

// Returns true if the enum string value is custom
private static bool IsEnumValueModdedByString(string text, Type enumType)
{
UpdateCachedEnumCacheManagers(enumType);
return (bool) _cachedCacheManagers[enumType].ContainsStringKey.Invoke(_cachedCacheManagers[enumType].CacheManager, new object[] { text });
}

// Returns true if the enum object value is custom
private static bool IsEnumValueModdedByObject(object value, Type enumType)
{
UpdateCachedEnumCacheManagers(enumType);
return (bool) _cachedCacheManagers[enumType].ContainsEnumKey.Invoke(_cachedCacheManagers[enumType].CacheManager, new object[] { value });
}

// Postfix to EnumUtils.TryToString that checks for custom enum values in the case that the method failed to find a built-in enum value name
private static void EnumUtilsTryToStringPostfix(Type enumType, ref bool __result, object value, ref string name)
{
// Don't run if we already found a name
if (__result == true)
return;
// Don't run if this enum type isn't modded
if (!EnumTypeHasCustomValues(enumType))
return;
// Don't run if this enum value isn't custom
if (!IsEnumValueModdedByObject(value, enumType))
return;
name = (string) _cachedCacheManagers[enumType].ValueToName.Invoke(_cachedCacheManagers[enumType].CacheManager, new object[] { value });
__result = true;
}

// Prefix to EnumUtils.ParseEnum that exits early if the enum value is custom (this is needed in order to avoid annoying exceptions)
private static bool EnumUtilsParseEnumPrefix(ref object __result, Type enumType, string value)
{
if (TryParseCustomEnumValue(enumType, value, out var enumVal))
{
__result = enumVal;
return false;
}
return true;
}

// Attempts to convert 'name' to an enum value (val)
public static bool TryParseCustomEnumValue(Type enumType, string name, out int val)
{
val = 0;

if (!EnumTypeHasCustomValues(enumType))
return false;

if (EnumCacheProvider.CacheManagers.TryGetValue(enumType, out var enumCacheManager))
{
if (enumCacheManager.TryParse(name, out var enumValue))
{
val = (int) enumValue;
return true;
}
}
return false;
}

// Returns true if an enum has any custom values at all
private static bool EnumTypeHasCustomValues(Type enumType)
{
return EnumCacheProvider.TryGetManager(enumType, out _);
}

// If a cache manager of the given enum is not already cached, then cache it
private static void UpdateCachedEnumCacheManagers(Type enumType)
{
if (!_cachedCacheManagers.ContainsKey(enumType))
{
var enumBuilderType = typeof(EnumBuilder<>).MakeGenericType(enumType);
var cacheManager = enumBuilderType.GetProperty("CacheManager", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
var cacheManagerType = cacheManager.GetType();
_cachedCacheManagers.Add(enumType, new CachedCacheManager(
cacheManager,
AccessTools.Method(cacheManagerType, "ContainsStringKey"),
AccessTools.Method(cacheManagerType, "ContainsEnumKey"),
AccessTools.Method(cacheManagerType, "ValueToName")
));
}
}

private record CachedCacheManager(object CacheManager, MethodInfo ContainsStringKey, MethodInfo ContainsEnumKey, MethodInfo ValueToName);
}
22 changes: 21 additions & 1 deletion Nautilus/Utility/EnumCacheManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ public bool IsKnownKey(TEnum key)
return _mapEnumString.ContainsKey(key);
}

public bool IsKnownKey(string key)
{
return _mapStringEnum.ContainsKey(key);
}

public bool IsKnownKey(int key)
{
return _mapIntString.ContainsKey(key);
Expand Down Expand Up @@ -202,6 +207,14 @@ public bool TryParse(string value, out TEnum type)
return entriesFromRequests.TryGetValue(value, out type);
}

// This method is referenced by the NewtonsoftJsonPatcher.UpdateCachedEnumCacheManagers method through reflection - PLEASE UPDATE THAT METHOD IF RENAMING!
public string ValueToName(TEnum value)
{
if (entriesFromRequests.TryGetValue(value, out var name))
return name;
return null;
}

bool IEnumCache.TryParse(string value, out object type)
{
if (entriesFromRequests.TryGetValue(value, out TEnum enumValue))
Expand All @@ -228,7 +241,14 @@ bool IEnumCache.ContainsKey(object key)
return entriesFromRequests.IsKnownKey(ConvertToObject(Convert.ToInt32(key)));
}

public bool ContainsKey(TEnum key)
// This method is referenced by the NewtonsoftJsonPatcher.UpdateCachedEnumCacheManagers method through reflection - PLEASE UPDATE THAT METHOD IF RENAMING!
public bool ContainsEnumKey(TEnum key)
{
return entriesFromRequests.IsKnownKey(key);
}

// This method is referenced by the NewtonsoftJsonPatcher.UpdateCachedEnumCacheManagers method through reflection - PLEASE UPDATE THAT METHOD IF RENAMING!
public bool ContainsStringKey(string key)
{
return entriesFromRequests.IsKnownKey(key);
}
Expand Down
25 changes: 22 additions & 3 deletions Nautilus/Utility/ModMessages/ModMessageSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ namespace Nautilus.Utility.ModMessages;
/// </summary>
public static class ModMessageSystem
{
static ModMessageSystem()
{
SaveUtils.RegisterOnStartLoadingEvent(OnStartLoading);
}

private static bool _allowedToHoldGlobalMessages = true;

// address - inbox
private static Dictionary<string, ModInbox> _inboxes = new Dictionary<string, ModInbox>();

Expand All @@ -27,8 +34,9 @@ public static void Send(string recipient, string subject, params object[] conten
}

/// <summary>
/// Sends a global message to every <see cref="ModInbox"/> that exists, and even to ones that will exist in the future.
/// If a message is not read immediately by any inbox, it will be held until read.
/// <para>Sends a global message to every <see cref="ModInbox"/> that exists, and even to ones that will exist in the future.
/// If a message is not read immediately by any inbox, it will be held until read.</para>
/// <para>IMPORTANT: Global messages can NOT be held after patch time has completed (once you have left the main menu).</para>
/// </summary>
/// <param name="subject">The subject of the message. Determines the purpose of a message. In C# terms, this is analogous to the method name.</param>
/// <param name="contents">Any arbitrary data sent through the message. Optional. In C# terms, this is analogous to the method's parameters.</param>
Expand All @@ -39,7 +47,10 @@ public static void SendGlobal(string subject, params object[] contents)
{
globalMessage.TrySendMessageToInbox(inbox);
}
_globalMessages.Add(globalMessage);
if (_allowedToHoldGlobalMessages)
{
_globalMessages.Add(globalMessage);
}
}

/// <summary>
Expand Down Expand Up @@ -106,10 +117,18 @@ internal static void SendHeldMessagesToInbox(ModInbox inbox)
{
inbox.ReceiveMessage(message);
}
_heldMessages[inbox.Address].Clear();
}
foreach (var globalMessage in _globalMessages)
{
globalMessage.TrySendMessageToInbox(inbox);
}
}

// Once game time has started, stop holding global messages and remove any held global messages from patch time
private static void OnStartLoading()
{
_globalMessages.Clear();
_allowedToHoldGlobalMessages = false;
}
}

0 comments on commit 4638009

Please sign in to comment.