Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotreload save #1635

Merged
merged 10 commits into from
Oct 3, 2022
45 changes: 44 additions & 1 deletion TLM/TLM/Lifecycle/AssetDataExtension.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace TrafficManager.Lifecycle {
using CSUtil.Commons;
using ICities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using TrafficManager.State;
using TrafficManager.State.Asset;
using static TrafficManager.Util.Shortcuts;
Expand Down Expand Up @@ -34,7 +36,7 @@ public static void OnAssetLoadedImpl(string name, object asset, Dictionary<strin
}
}

public void OnAssetSavedImpl(string name, object asset, out Dictionary<string, byte[]> userData) {
public static void OnAssetSavedImpl(string name, object asset, out Dictionary<string, byte[]> userData) {
Log.Info($"AssetDataExtension.OnAssetSavedImpl({name}, {asset}, userData) called");
userData = null;
if (asset is BuildingInfo prefab) {
Expand All @@ -49,5 +51,46 @@ public void OnAssetSavedImpl(string name, object asset, out Dictionary<string, b
}
}
}

[Conditional("DEBUG")]
public static void HotReload() {
var assets2UserData = Type.GetType("LoadOrderMod.LOMAssetDataExtension, LoadOrderMod", throwOnError: false)
?.GetField("Assets2UserData")
?.GetValue(null)
as Dictionary<PrefabInfo, Dictionary<string, byte[]>>;

if (assets2UserData == null) {
Log.Warning("Could not hot reload assets because LoadOrderMod was not found");
return;
}

var editPrefabInfo = ToolsModifierControl.toolController.m_editPrefabInfo;
foreach (var asset2UserData in assets2UserData) {
var asset = asset2UserData.Key;
var userData = asset2UserData.Value;
if (asset) {
if (editPrefabInfo) {
// asset editor work around
asset = FindLoadedCounterPart<NetInfo>(asset);
}
OnAssetLoadedImpl(asset.name, asset, userData);
}
}
}

/// <summary>
/// OnLoad() calls IntializePrefab() which can create duplicates. Therefore we should match by name.
/// </summary>
private static PrefabInfo FindLoadedCounterPart<T>(PrefabInfo source)
where T : PrefabInfo {
int n = PrefabCollection<T>.LoadedCount();
for (uint i = 0; i < n; ++i) {
T prefab = PrefabCollection<T>.GetLoaded(i);
if (prefab?.name == source.name) {
return prefab;
}
}
return source;
}
}
}
34 changes: 30 additions & 4 deletions TLM/TLM/Lifecycle/Patcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static void Install() {
Harmony.DEBUG = false; // set to true to get harmony debug info.
#endif
AssertCitiesHarmonyInstalled();
fail = !PatchAll(API.Harmony.HARMONY_ID, forbidden: typeof(CustomPathFindPatchAttribute));
fail = !PatchAll(API.Harmony.HARMONY_ID, typeof(CustomPathFindPatchAttribute), typeof(PreloadPatchAttribute));

if (fail) {
Log.Info("patcher failed");
Expand All @@ -60,7 +60,7 @@ public static void InstallPathFinding() {
Harmony.DEBUG = false; // set to true to get harmony debug info.
#endif
AssertCitiesHarmonyInstalled();
fail = !PatchAll(API.Harmony.HARMONY_ID_PATHFINDING , required: typeof(CustomPathFindPatchAttribute));;
fail = !PatchAll(API.Harmony.HARMONY_ID_PATHFINDING, required: typeof(CustomPathFindPatchAttribute));

if (fail) {
Log.Info("TMPE Path-finding patcher failed");
Expand All @@ -74,13 +74,37 @@ public static void InstallPathFinding() {
}
}

public static void InstallPreload() {

bool fail = false;
#if DEBUG
Harmony.DEBUG = false; // set to true to get harmony debug info.
#endif
AssertCitiesHarmonyInstalled();

// reinstall:
Uninstall(API.Harmony.HARMONY_ID_PRELOAD);
fail = !PatchAll(API.Harmony.HARMONY_ID_PRELOAD, required: typeof(PreloadPatchAttribute));

if (fail) {
Log.Info("TMPE patcher failed at preload");
Prompt.Error(
"TM:PE failed to patch at preload",
"Traffic Manager: President Edition failed to load necessary patches. You can " +
"continue playing but it's NOT recommended. Traffic Manager will " +
"not work as expected.");
} else {
Log.Info("preload patches installed successfully");
}
}

/// <summary>
/// applies all attribute driven harmony patches.
/// continues on error.
/// </summary>
/// <returns>false if exception happens, true otherwise</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool PatchAll(string harmonyId, Type required = null, Type forbidden = null) {
private static bool PatchAll(string harmonyId, Type required = null, params Type[] forbiddens) {
try {
bool success = true;
var harmony = new Harmony(harmonyId);
Expand All @@ -89,8 +113,10 @@ private static bool PatchAll(string harmonyId, Type required = null, Type forbid
try {
if (required is not null && !type.IsDefined(required, true))
continue;
if (forbidden is not null && type.IsDefined(forbidden, true))
bool isForbidden = forbiddens?.Any(forbidden => type.IsDefined(forbidden, true)) ?? false;
if (isForbidden)
continue;

var methods = harmony.CreateClassProcessor(type).Patch();
if (methods != null && methods.Any()) {
var strMethods = methods.Select(_method => _method.Name).ToArray();
Expand Down
7 changes: 5 additions & 2 deletions TLM/TLM/Lifecycle/TMPELifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ private static void CompatibilityCheck() {
}

internal void Preload() {
Log.Info("Preloading Managers");
Patcher.InstallPreload();
Asset2Data = new Dictionary<BuildingInfo, AssetData>();
Log.Info("Preloading Managers");
CustomPathManager.Initialize();
RegisteredManagers.Clear();
RegisterCustomManagers();
Expand Down Expand Up @@ -224,6 +225,7 @@ void Awake() {
InGameHotReload = InGameOrEditor();
if (InGameHotReload) {
Preload();
AssetDataExtension.HotReload();
SerializableDataExtension.Load();
Load();
}
Expand All @@ -243,6 +245,7 @@ void Awake() {
void OnDestroy() {
try {
Log.Info("TMPELifecycle.OnDestroy()");
API.Implementations.Reset();
LoadingManager.instance.m_introLoaded -= CompatibilityCheck;
LocaleManager.eventLocaleChanged -= Translation.HandleGameLocaleChange;
LoadingManager.instance.m_levelPreLoaded -= Preload;
Expand All @@ -252,6 +255,7 @@ void OnDestroy() {
//Hot Unload
Unload();
}
Patcher.Uninstall(API.Harmony.HARMONY_ID_PRELOAD);
Instance = null;
} catch (Exception ex) {
ex.LogException(true);
Expand All @@ -276,7 +280,6 @@ internal static void StartMod() {
}

internal static void EndMod() {
API.Implementations.Reset();
DestroyImmediate(Instance?.gameObject);
}

Expand Down
55 changes: 55 additions & 0 deletions TLM/TLM/Patch/HotReload/ReadTypeMetadataPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace TrafficManager.Patch.HotReload {
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using System.Text.RegularExpressions;
using TrafficManager.Util;
using TrafficManager.Lifecycle;

[HarmonyPatch]
[PreloadPatch]
/// <summary>
/// Problem: object graph and type converter use type from different assembly versions
/// (one gets type from first assembly while the other uses last assembly) which creates a conflict.
/// Solution: Here we make sure both get type from last assembly by removing assembly version from type string.
/// </summary>
public static class ReadTypeMetadataPatch {
private delegate Type GetType(string typeName, bool throwOnError);
private static string assemblyName_ = typeof(ReadTypeMetadataPatch).Assembly.GetName().Name;

private static bool Prepare() => TMPELifecycle.Instance.InGameHotReload; // only apply when hot-reloading.

private static MethodBase TargetMethod() {
var t = Type.GetType("System.Runtime.Serialization.Formatters.Binary.ObjectReader");
return AccessTools.DeclaredMethod(t, "ReadTypeMetadata");
}

/// <summary>
/// searches for call to GetType(typeString, true) and removes version data from type string.
/// </summary>
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) {
MethodInfo mType_GetType = TranspilerUtil.DeclaredMethod<GetType>(typeof(Type), nameof(GetType));
MethodInfo mReplaceAssemblyVersion = AccessTools.DeclaredMethod(typeof(ReadTypeMetadataPatch), nameof(ReplaceAssemblyVersion));

foreach (var code in instructions) {
if (code.Calls(mType_GetType)) {
yield return new CodeInstruction(OpCodes.Call, mReplaceAssemblyVersion);
yield return new CodeInstruction(OpCodes.Ldc_I4_1); // load true again
}
yield return code;
}
}

private static string ReplaceAssemblyVersion(string s, bool throwOnError) => ReplaceAssemblyVersionImpl(s);

private static string ReplaceAssemblyVersionImpl(string s) {
string num = "\\d+"; // matches ###
string d = "\\."; // matches .
string pattern = $"{assemblyName_}, Version={num}{d}{num}{d}{num}{d}{num}, Culture=neutral, PublicKeyToken=null";
var s2 = Regex.Replace(s, pattern, assemblyName_);
return s2;
}
}
}
4 changes: 4 additions & 0 deletions TLM/TLM/Patch/PreloadPatchAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace TrafficManager.Patch {
using System;
public class PreloadPatchAttribute : Attribute { }
}
2 changes: 2 additions & 0 deletions TLM/TLM/TLM.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
<Compile Include="Manager\Impl\ExtSegmentEndManager.cs" />
<Compile Include="Manager\Impl\ExtSegmentManager.cs" />
<Compile Include="Manager\Impl\GeometryNotifier.cs" />
<Compile Include="Patch\HotReload\ReadTypeMetadataPatch.cs" />
<Compile Include="Patch\PreloadPatchAttribute.cs" />
<Compile Include="Patch\NetManagerEvents.cs" />
<Compile Include="Manager\Impl\LaneConnection\ConnectionDataBase.cs" />
<Compile Include="Manager\Impl\LaneConnection\LaneConnectionData.cs" />
Expand Down
1 change: 1 addition & 0 deletions TLM/TMPE.API/Harmony.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ namespace TrafficManager.API {
public class Harmony {
public const string HARMONY_ID = "me.tmpe";
public const string HARMONY_ID_PATHFINDING = "me.tmpe.pathfinding";
public const string HARMONY_ID_PRELOAD = "me.tmpe.preload";
}
}