instructions)
26 | {
27 | var m = AccessTools.Field(typeof(Logger), nameof(Logger.IsUnityApplication));
28 | var count = 0;
29 | foreach (var i in instructions)
30 | {
31 | // replace second bool HBS.Logging.Logger::IsUnityApplication call
32 | // avoids logging twice
33 | if (i.opcode == OpCodes.Ldsfld && m.Equals(i.operand) && count++ == 1)
34 | {
35 | i.opcode = OpCodes.Ldc_I4_0;
36 | i.operand = null;
37 | }
38 | yield return i;
39 | }
40 | }
41 |
42 | [HarmonyPriority(Priority.High)]
43 | public static bool Prefix(Logger.LogImpl __instance, string ___name, LogLevel level, object message, Exception exception, IStackTrace location)
44 | {
45 | try
46 | {
47 | // TODO __instance.IsEnabledFor is slow, speedup requires to keep track of all loggers and/or children
48 | // NullableLogger can skip this check
49 | if (__instance.IsEnabledFor(level))
50 | {
51 | LoggingFeature.LogAtLevel(
52 | ___name,
53 | level,
54 | message,
55 | exception,
56 | location
57 | );
58 | }
59 | return false;
60 | }
61 | catch (Exception e)
62 | {
63 | Log.Main.Error?.Log("Couldn't rewrite LogAtLevel call", e);
64 | }
65 | return true;
66 | }
67 | }
--------------------------------------------------------------------------------
/ModTek/Features/Logging/Patches/LogImpl_set_Level_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using HBS.Logging;
3 | using ModTek.Common.Utils;
4 |
5 | namespace ModTek.Features.Logging.Patches;
6 |
7 | [HarmonyPatch(typeof(Logger.LogImpl), nameof(Logger.LogImpl.Level), MethodType.Setter)]
8 | internal static class LogImpl_set_Level_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled;
13 | }
14 |
15 | [HarmonyPriority(Priority.High)]
16 | public static void Prefix(LogLevel? ___level, out LogLevel? __state)
17 | {
18 | __state = ___level;
19 | }
20 |
21 | [HarmonyPriority(Priority.Low)]
22 | public static void Postfix(string ___name, LogLevel? ___level, LogLevel? __state)
23 | {
24 | var log = Log.Main.Trace;
25 | if (log == null)
26 | {
27 | return;
28 | }
29 |
30 | if (___level == __state)
31 | {
32 | return;
33 | }
34 |
35 | var debugText = ModTek.Config.Logging.DebugLogLevelSetters
36 | ? "\n" + DebugUtils.GetStackTraceWithoutPatch()
37 | : "";
38 |
39 | var oldLevel = __state?.Let(l => LogLevelExtension.LogToString(l) + $"({(int)__state})") ?? "null";
40 | var newLevel = ___level?.Let(l => LogLevelExtension.LogToString(l) + $"({(int)___level})") ?? "null";
41 | log.Log(
42 | $"Log Level of logger name={___name} changed from level={oldLevel} to level={newLevel}{debugText}"
43 | );
44 | }
45 |
46 | private static R Let(this P s, Func
func) where P: notnull
47 | {
48 | return func(s);
49 | }
50 | }
--------------------------------------------------------------------------------
/ModTek/Features/Logging/Patches/Logger_CaptureUnityLogs_Patch.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using Logger = HBS.Logging.Logger;
3 |
4 | namespace ModTek.Features.Logging.Patches;
5 |
6 | [HarmonyPatch(typeof(Logger), nameof(Logger.CaptureUnityLogs))]
7 | internal static class Logger_CaptureUnityLogs_Patch
8 | {
9 | public static bool Prepare()
10 | {
11 | return ModTek.Enabled;
12 | }
13 |
14 | public static bool Prefix()
15 | {
16 | Application.logMessageReceived -= Logger.HandleUnityLog;
17 | return false;
18 | }
19 | }
--------------------------------------------------------------------------------
/ModTek/Features/Logging/Patches/SqlMapper_ExecuteImpl_Patch.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Dapper;
3 | using Newtonsoft.Json;
4 |
5 | namespace ModTek.Features.Logging.Patches;
6 |
7 | [HarmonyPatch(typeof(SqlMapper), "ExecuteImpl")]
8 | internal static class SqlMapper_ExecuteImpl_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled && ModTek.Config.Logging.LogSqlQueryExecutionsFromDapper;
13 | }
14 |
15 | public static void Prefix(ref CommandDefinition command)
16 | {
17 | var debug = Log.Debugger.Debug;
18 | if (debug == null)
19 | {
20 | return;
21 | }
22 |
23 | var parameters = command.Parameters;
24 | if (parameters == null)
25 | {
26 | return;
27 | }
28 | var serializedParameters = JsonConvert.SerializeObject(command.Parameters, Formatting.Indented);
29 |
30 | debug.Log(
31 | $"""
32 | SQL query being executed via Dapper
33 | CommandText: {command.CommandText}
34 | Parameters: {serializedParameters}
35 | Stacktrace: {new StackTrace(1)}
36 | """
37 | );
38 | }
39 | }
--------------------------------------------------------------------------------
/ModTek/Features/Logging/Patches/SqliteCommand_CommandText_Setter_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using Mono.Data.Sqlite;
4 |
5 | namespace ModTek.Features.Logging.Patches;
6 |
7 | [HarmonyPatch(typeof(SqliteCommand), nameof(SqliteCommand.CommandText), MethodType.Setter)]
8 | internal static class SqliteCommand_CommandText_Setter_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled && ModTek.Config.Logging.LogSqlQueryInitializations;
13 | }
14 |
15 | public static void Postfix(SqliteCommand __instance)
16 | {
17 | var cmd = __instance;
18 | Log.Debugger.Debug?.Log("A SQL query was initialized: " + cmd?.CommandText + Environment.NewLine + new StackTrace(1));
19 | }
20 | }
--------------------------------------------------------------------------------
/ModTek/Features/Logging/Patches/Thread_StartInternal_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Threading;
4 |
5 | namespace ModTek.Features.Logging.Patches;
6 |
7 | [HarmonyPatch(typeof(Thread), "StartInternal")]
8 | internal static class Thread_StartInternal_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled && ModTek.Config.Logging.LogThreadStarts;
13 | }
14 |
15 | public static void Postfix(Thread __instance)
16 | {
17 | Log.Debugger.Debug?.Log("A thread was started with ThreadId=" + __instance.ManagedThreadId + Environment.NewLine + new StackTrace(4));
18 | }
19 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/BTConstants.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using BattleTech;
5 | using ModTek.Features.CustomResources;
6 |
7 | namespace ModTek.Features.Manifest;
8 |
9 | internal static class BTConstants
10 | {
11 | internal static readonly string[] PREDEFINED_TYPES =
12 | Enum.GetNames(typeof(BattleTechResourceType))
13 | .Concat(Enum.GetNames(typeof(InternalCustomResourceType)))
14 | .Concat(Enum.GetNames(typeof(CustomType)))
15 | .ToArray();
16 |
17 | internal static readonly BattleTechResourceType[] VanillaMDDBTypes =
18 | {
19 | BattleTechResourceType.ContractOverride,
20 | BattleTechResourceType.LanceDef,
21 | BattleTechResourceType.PilotDef,
22 | BattleTechResourceType.SimGameEventDef,
23 | BattleTechResourceType.MechDef,
24 | BattleTechResourceType.WeaponDef,
25 | BattleTechResourceType.TurretDef,
26 | BattleTechResourceType.VehicleDef,
27 | BattleTechResourceType.UpgradeDef
28 | };
29 |
30 | internal static readonly HashSet MDDBTypes =
31 | VanillaMDDBTypes.Select(x => x.ToString())
32 | .Union(new[]
33 | {
34 | InternalCustomResourceType.EncounterLayer.ToString()
35 | })
36 | .ToHashSet();
37 |
38 | internal static bool BTResourceType(string Type, out BattleTechResourceType type)
39 | {
40 | return Enum.TryParse(Type, out type);
41 | }
42 |
43 | internal static bool ICResourceType(string Type, out InternalCustomResourceType type)
44 | {
45 | return Enum.TryParse(Type, out type);
46 | }
47 |
48 | internal static bool CType(string Type, out CustomType type)
49 | {
50 | return Enum.TryParse(Type, out type);
51 | }
52 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/BTRL/VersionManifestEntryComparer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using BattleTech;
3 |
4 | namespace ModTek.Features.Manifest.BTRL;
5 |
6 | internal class VersionManifestEntryComparer : IEqualityComparer
7 | {
8 | public bool Equals(VersionManifestEntry x, VersionManifestEntry y)
9 | {
10 | if (x == null || y == null)
11 | {
12 | return false;
13 | }
14 | return x.Type == y.Type && x.Id == y.Id;
15 | }
16 |
17 | public int GetHashCode(VersionManifestEntry obj)
18 | {
19 | unchecked
20 | {
21 | return ((obj.Type != null ? obj.Type.GetHashCode() : 0) * 397) ^ (obj.Id != null ? obj.Id.GetHashCode() : 0);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/CacheKey.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BattleTech;
3 | using Newtonsoft.Json;
4 |
5 | namespace ModTek.Features.Manifest;
6 |
7 | public class CacheKey : IEquatable
8 | {
9 | [JsonProperty(Required = Required.Always)]
10 | internal readonly string Type;
11 | [JsonProperty(Required = Required.Always)]
12 | internal readonly string Id;
13 |
14 | [JsonConstructor]
15 | private CacheKey()
16 | {
17 | }
18 |
19 | internal CacheKey(string type, string id)
20 | {
21 | Type = type;
22 | Id = id;
23 | }
24 |
25 | internal CacheKey(VersionManifestEntry entry)
26 | {
27 | Type = entry.Type;
28 | Id = entry.Id;
29 | }
30 |
31 | internal CacheKey(ModEntry entry)
32 | {
33 | Type = entry.Type;
34 | Id = entry.Id;
35 | }
36 |
37 | public override string ToString()
38 | {
39 | return $"{Id} ({Type})";
40 | }
41 |
42 | // GENERATED CODE BELOW
43 |
44 | public bool Equals(CacheKey other)
45 | {
46 | if (ReferenceEquals(null, other))
47 | {
48 | return false;
49 | }
50 |
51 | if (ReferenceEquals(this, other))
52 | {
53 | return true;
54 | }
55 |
56 | return Type == other.Type && Id == other.Id;
57 | }
58 |
59 | public override bool Equals(object obj)
60 | {
61 | if (ReferenceEquals(null, obj))
62 | {
63 | return false;
64 | }
65 |
66 | if (ReferenceEquals(this, obj))
67 | {
68 | return true;
69 | }
70 |
71 | if (obj.GetType() != GetType())
72 | {
73 | return false;
74 | }
75 |
76 | return Equals((CacheKey) obj);
77 | }
78 |
79 | public override int GetHashCode()
80 | {
81 | unchecked
82 | {
83 | return ((Type != null ? Type.GetHashCode() : 0) * 397) ^ (Id != null ? Id.GetHashCode() : 0);
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/CustomType.cs:
--------------------------------------------------------------------------------
1 | namespace ModTek.Features.Manifest;
2 |
3 | internal enum CustomType
4 | {
5 | AdvancedJSONMerge,
6 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/MDD/VersionManifestEntryExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using BattleTech;
5 |
6 | namespace ModTek.Features.Manifest.MDD;
7 |
8 | internal static class VersionManifestEntryExtensions
9 | {
10 | // this is to determine if something is already in the unmodified MDDB
11 | // a more proper way would be to query the default manifest and the temporarily loaded DLC in BetterBTRL
12 | internal static bool IsVanillaOrDlc(this VersionManifestEntry entry)
13 | {
14 | return entry.IsMemoryAsset || entry.IsResourcesAsset || entry.IsStreamingAssetData() || entry.IsContentPackAssetBundle();
15 | }
16 |
17 | private static bool IsStreamingAssetData(this VersionManifestEntry entry)
18 | {
19 | return entry.IsFileAsset && (entry.GetRawPath()?.StartsWith("data/") ?? false);
20 | }
21 |
22 | private static bool IsContentPackAssetBundle(this VersionManifestEntry entry)
23 | {
24 | return entry.IsAssetBundled && HBSContentNames.Contains(entry.AssetBundleName);
25 | }
26 |
27 | // possibly not complete
28 | private static readonly string[] HBSContentNames =
29 | {
30 | "shadowhawkdlc",
31 | "flashpoint",
32 | "urbanwarfare",
33 | "heavymetal"
34 | };
35 |
36 | internal static string ToShortString(this VersionManifestEntry entry)
37 | {
38 | return $"{entry.Id} ({entry.Type})";
39 | }
40 |
41 | // lazily call GetLastWriteTimeUtc, as a proper UpdatedOn is only required when merging or indexing
42 | internal static readonly DateTime UpdatedOnLazyTracking = DateTime.MinValue;
43 | internal static DateTime GetUpdatedOnForTracking(this VersionManifestEntry entry)
44 | {
45 | var value = entry.UpdatedOn;
46 | if (value == UpdatedOnLazyTracking)
47 | {
48 | value = File.GetLastWriteTimeUtc(entry.FilePath);
49 | entry.Update(VersionManifestUtilities.DateTimeToString(value), "1");
50 | }
51 | return value;
52 | }
53 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/MTContentPackManager.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using BattleTech;
4 | using BattleTech.Data;
5 | using ModTek.Features.Manifest.BTRL;
6 | using UnityEngine;
7 |
8 | namespace ModTek.Features.Manifest;
9 |
10 | internal class MTContentPackManager
11 | {
12 | private readonly Dictionary loadedBundles = new();
13 |
14 | internal void LoadAllContentPacks()
15 | {
16 | var entries = BetterBTRL.Instance.AllEntriesOfResource(BattleTechResourceType.ContentPackDef);
17 | foreach (var entry in entries)
18 | {
19 | var def = new ContentPackDef();
20 | var json = File.ReadAllText(entry.FilePath);
21 | def.FromJSON(json);
22 | Load(def.Name);
23 | }
24 | }
25 |
26 | private void Load(string name)
27 | {
28 | if (loadedBundles.ContainsKey(name))
29 | {
30 | return;
31 | }
32 | var entry = BetterBTRL.Instance.EntryByID(name, BattleTechResourceType.AssetBundle);
33 | if (entry == null)
34 | {
35 | Log.Main.Info?.Log($"Can't find bundle {name} for loading");
36 | return;
37 | }
38 | var bundle = AssetBundle.LoadFromFile(entry.FilePath);
39 | loadedBundles.Add(name, bundle);
40 | var manifestCsv = bundle.LoadAsset(name);
41 | var addendum = new VersionManifestAddendum(AddendumNameFromBundleName(name));
42 | using (var stringReader = new StringReader(manifestCsv.text))
43 | using (var csvReader = new CSVReader(stringReader))
44 | addendum.LoadFromCSV(csvReader);
45 | BetterBTRL.Instance.ApplyAddendum(addendum);
46 | }
47 |
48 | internal void UnloadAll()
49 | {
50 | foreach (var kv in loadedBundles)
51 | {
52 | var name = kv.Key;
53 | var bundle = kv.Value;
54 | BetterBTRL.Instance.RemoveAddendum(AddendumNameFromBundleName(name));
55 | bundle.Unload(true);
56 | }
57 | loadedBundles.Clear();
58 | }
59 |
60 | internal string GetText(string bundleName, string resourceId)
61 | {
62 | if (!loadedBundles.TryGetValue(bundleName, out var bundle))
63 | {
64 | Log.Main.Info?.Log($"Could not find bundle {bundleName}.");
65 | return null;
66 | }
67 |
68 | var asset = bundle.LoadAsset(resourceId);
69 | if (asset == null)
70 | {
71 | Log.Main.Info?.Log($"Could not find resource {resourceId} in bundle {bundleName}.");
72 | return null;
73 | }
74 | return asset.text;
75 | }
76 |
77 | private string AddendumNameFromBundleName(string name)
78 | {
79 | return name + "Manifest";
80 | }
81 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Mods/ModState.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using System.IO;
3 | using Newtonsoft.Json;
4 |
5 | namespace ModTek.Features.Manifest.Mods;
6 |
7 | internal class ModState
8 | {
9 | [DefaultValue(true)]
10 | public bool Enabled { get; set; } = true;
11 |
12 | public static ModState CreateFromPath(string path)
13 | {
14 | var modState = JsonConvert.DeserializeObject(File.ReadAllText(path));
15 | return modState;
16 | }
17 |
18 | public void SaveToPath(string path)
19 | {
20 | File.WriteAllText(path, JsonConvert.SerializeObject(this));
21 | }
22 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/AssetBundleManager_AssetBundleNameToFileURL_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BattleTech.Assetbundles;
3 |
4 | namespace ModTek.Features.Manifest.Patches;
5 |
6 | [HarmonyPatch(typeof(AssetBundleManager), "AssetBundleNameToFileURL")]
7 | internal static class AssetBundleManager_AssetBundleNameToFileURL_Patch
8 | {
9 | public static bool Prepare()
10 | {
11 | return ModTek.Enabled;
12 | }
13 |
14 | public static bool Prefix(string assetBundleName, ref string __result)
15 | {
16 | try
17 | {
18 | var filePath = AssetBundleManager_AssetBundleNameToFilepath_Patch.AssetBundleNameToFilepath(assetBundleName);
19 | __result = $"file://{filePath}";
20 | return false;
21 | }
22 | catch (Exception e)
23 | {
24 | Log.Main.Info?.Log("Error running prefix", e);
25 | }
26 | return true;
27 | }
28 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/AssetBundleManager_AssetBundleNameToFilepath_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using BattleTech;
4 | using BattleTech.Assetbundles;
5 | using ModTek.Features.Manifest.BTRL;
6 | using ModTek.Misc;
7 |
8 | namespace ModTek.Features.Manifest.Patches;
9 |
10 | [HarmonyPatch(typeof(AssetBundleManager), "AssetBundleNameToFilepath")]
11 | internal static class AssetBundleManager_AssetBundleNameToFilepath_Patch
12 | {
13 | public static bool Prepare()
14 | {
15 | return ModTek.Enabled;
16 | }
17 |
18 | public static bool Prefix(string assetBundleName, ref string __result)
19 | {
20 | try
21 | {
22 | var filePath = AssetBundleNameToFilepath(assetBundleName);
23 | __result = filePath;
24 | return false;
25 | }
26 | catch (Exception e)
27 | {
28 | Log.Main.Info?.Log("Error running prefix", e);
29 | }
30 | return true;
31 | }
32 |
33 | internal static string AssetBundleNameToFilepath(string assetBundleName)
34 | {
35 | var entry = BetterBTRL.Instance.EntryByID(assetBundleName, BattleTechResourceType.AssetBundle);
36 | if (entry == null)
37 | {
38 | return Path.Combine(FilePaths.AssetBundlesDirectory, assetBundleName);
39 | }
40 | return Path.Combine(FilePaths.StreamingAssetsDirectory, entry.FilePath);
41 | }
42 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/ContentPackIndex_IsResourceOwned_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BattleTech.Data;
3 | using ModTek.Features.Manifest.BTRL;
4 |
5 | namespace ModTek.Features.Manifest.Patches;
6 |
7 | [HarmonyPatch(typeof(ContentPackIndex), nameof(ContentPackIndex.IsResourceOwned))]
8 | internal static class ContentPackIndex_IsResourceOwned_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled;
13 | }
14 |
15 | public static bool Prefix(ContentPackIndex __instance, string resourceId, ref bool __result)
16 | {
17 | try
18 | {
19 | __result = BetterBTRL.Instance.PackIndex.IsResourceOwned(resourceId);
20 | }
21 | catch (Exception e)
22 | {
23 | Log.Main.Info?.Log("Error running prefix", e);
24 | }
25 | return false;
26 | }
27 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/ContentPackIndex_PatchMDD_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using BattleTech.Data;
4 | using ModTek.Features.Manifest.BTRL;
5 |
6 | namespace ModTek.Features.Manifest.Patches;
7 |
8 | [HarmonyPatch(typeof(ContentPackIndex), "PatchMDD")]
9 | internal static class ContentPackIndex_PatchMDD_Patch
10 | {
11 | public static bool Prepare()
12 | {
13 | return ModTek.Enabled;
14 | }
15 |
16 | public static void Prefix(out bool ___rebuildMDDOnLoadComplete, out Stopwatch __state)
17 | {
18 | __state = new Stopwatch();
19 | __state.Start();
20 |
21 | ___rebuildMDDOnLoadComplete = true; //rebuilding is less work than having to track changes
22 | }
23 |
24 | public static void Postfix(ContentPackIndex __instance, Stopwatch __state)
25 | {
26 | try
27 | {
28 | BetterBTRL.Instance.PackIndex.PatchMDD(__instance);
29 | }
30 | catch (Exception e)
31 | {
32 | Log.Main.Error?.Log("Error running postfix", e);
33 | }
34 |
35 | Log.Main.Debug?.LogIfSlow(__state, "ContentPackIndex.PatchMDD");
36 | }
37 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/ContentPackIndex_TryFinalizeDataLoad_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using BattleTech.Data;
4 | using ModTek.Features.Manifest.BTRL;
5 |
6 | namespace ModTek.Features.Manifest.Patches;
7 |
8 | [HarmonyPatch(typeof(ContentPackIndex), "TryFinalizeDataLoad")]
9 | internal static class ContentPackIndex_TryFinalizeDataLoad_Patch
10 | {
11 | public static bool Prepare()
12 | {
13 | return ModTek.Enabled;
14 | }
15 |
16 | [HarmonyPriority(Priority.High)]
17 | public static void Prefix(ContentPackIndex __instance, Dictionary ___resourceMap)
18 | {
19 | try
20 | {
21 | BetterBTRL.Instance.PackIndex.TryFinalizeDataLoad(__instance, ___resourceMap);
22 | }
23 | catch (Exception e)
24 | {
25 | Log.Main.Info?.Log("Error running prefix", e);
26 | }
27 | }
28 |
29 | [HarmonyPriority(Priority.Low)]
30 | public static void Postfix(ContentPackIndex __instance)
31 | {
32 | try
33 | {
34 | if (__instance.AllContentPacksLoaded())
35 | {
36 | BetterBTRL.Instance.ContentPackManifestsLoaded();
37 | }
38 | }
39 | catch (Exception e)
40 | {
41 | Log.Main.Info?.Log("Error running prefix", e);
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/DataManager_ProcessPrewarmRequests_Patch.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using BattleTech.Data;
4 |
5 | namespace ModTek.Features.Manifest.Patches;
6 |
7 | [HarmonyPatch(typeof(DataManager), nameof(DataManager.ProcessPrewarmRequests))]
8 | internal static class DataManager_ProcessPrewarmRequests_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled && ModTek.Config.DelayPrewarmToMainMenu;
13 | }
14 |
15 | public static List GetAndClearPrewarmRequests()
16 | {
17 | var copy = PrewarmRequests.ToList();
18 | PrewarmRequests.Clear();
19 | return copy;
20 | }
21 | private static readonly List PrewarmRequests = new();
22 | public static bool Prefix(IEnumerable toPrewarm)
23 | {
24 | if (toPrewarm != null)
25 | {
26 | PrewarmRequests.AddRange(toPrewarm);
27 | }
28 | return false;
29 | }
30 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/LoadRequest_PopPendingRequest_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BattleTech.Data;
3 | using UnityEngine;
4 |
5 | namespace ModTek.Features.Manifest.Patches;
6 |
7 | [HarmonyPatch(typeof(LoadRequest), "PopPendingRequest")]
8 | internal static class LoadRequest_PopPendingRequest_Patch
9 | {
10 | public static bool Prepare()
11 | {
12 | return ModTek.Enabled;
13 | }
14 |
15 | private static float lastNull;
16 | // by returning "null" we allow the UI to render again (if only one LoadRequest is active)
17 | // fixes the issue that vanilla happily loads and loads and loads on main thread
18 | // which gets worse with modded content and hooks
19 | public static bool Prefix(LoadRequest __instance, ref DataManager.FileLoadRequest __result)
20 | {
21 | try
22 | {
23 | var deltaInSecondsMax = ModTek.Config.DataManagerUnfreezeDelta;
24 | var deltaInSecondsCurrent = Time.realtimeSinceStartup - lastNull;
25 |
26 | if (deltaInSecondsCurrent >= deltaInSecondsMax) {
27 | // logging just takes space and time
28 | // Logging.Info?.Log($"LoadRequest unfreeze delta {deltaInSecondsCurrent:0.##}/{deltaInSecondsMax:0.##}");
29 | __result = null;
30 | return false;
31 | }
32 | }
33 | catch (Exception e)
34 | {
35 | Log.Main.Info?.Log("Error running prefix", e);
36 | }
37 |
38 | return true;
39 | }
40 |
41 | public static void Postfix(ref DataManager.FileLoadRequest __result)
42 | {
43 | if (__result == null)
44 | {
45 | lastNull = Time.realtimeSinceStartup;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/MetadataDatabase_MDD_DB_PATH_Patch.cs:
--------------------------------------------------------------------------------
1 | using BattleTech.Data;
2 | using ModTek.Misc;
3 |
4 | namespace ModTek.Features.Manifest.Patches;
5 |
6 | ///
7 | /// Patch the MDDB path to direct to the one in the .modtek path
8 | ///
9 | [HarmonyPatch(typeof(MetadataDatabase))]
10 | [HarmonyPatch("MDD_DB_PATH", MethodType.Getter)]
11 | internal static class MetadataDatabase_MDD_DB_PATH_Patch
12 | {
13 | public static bool Prepare()
14 | {
15 | return ModTek.Enabled;
16 | }
17 |
18 | public static bool Prefix(ref string __result)
19 | {
20 | if (string.IsNullOrEmpty(FilePaths.ModMDDBPath))
21 | {
22 | return true;
23 | }
24 |
25 | __result = FilePaths.ModMDDBPath;
26 | return false;
27 | }
28 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/MetadataDatabase_MDD_TMP_PATH_Patch.cs:
--------------------------------------------------------------------------------
1 | using BattleTech.Data;
2 | using ModTek.Misc;
3 |
4 | // ReSharper disable InconsistentNaming
5 | // ReSharper disable UnusedMember.Global
6 |
7 | namespace ModTek.Features.Manifest.Patches;
8 |
9 | ///
10 | /// Patch the MDDB tmp path to direct to the one in the .modtek path
11 | ///
12 | [HarmonyPatch(typeof(MetadataDatabase))]
13 | [HarmonyPatch("MDD_TMP_PATH", MethodType.Getter)]
14 | internal static class MetadataDatabase_MDD_TMP_PATH_Patch
15 | {
16 | public static bool Prepare()
17 | {
18 | return ModTek.Enabled;
19 | }
20 |
21 | public static bool Prefix(ref string __result)
22 | {
23 | if (string.IsNullOrEmpty(FilePaths.ModMDDBPath))
24 | {
25 | return true;
26 | }
27 |
28 | __result = FilePaths.ModMDDBPath + ".tmp";
29 | return false;
30 | }
31 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Patches/SimGame_MDDExtensions_UpdateContract_Patch.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using BattleTech.Data;
3 |
4 | // ReSharper disable InconsistentNaming
5 | // ReSharper disable UnusedMember.Global
6 |
7 | namespace ModTek.Features.Manifest.Patches;
8 |
9 | ///
10 | /// Patch the UpdateContract MDD to fix it so that the fileID instead of the path that is passed to it
11 | /// If this wasn't done, all mod contracts would be incorrectly added to the DB
12 | ///
13 | [HarmonyPatch(typeof(SimGame_MDDExtensions), nameof(SimGame_MDDExtensions.UpdateContract))]
14 | internal static class SimGame_MDDExtensions_UpdateContract_Patch
15 | {
16 | public static bool Prepare()
17 | {
18 | return ModTek.Enabled;
19 | }
20 |
21 | public static void Prefix(ref string fileID)
22 | {
23 | if (Path.IsPathRooted(fileID))
24 | {
25 | fileID = Path.GetFileNameWithoutExtension(fileID);
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Public/DataAddendumEntry.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | // ReSharper disable once CheckNamespace
4 | namespace ModTek;
5 |
6 | [JsonObject]
7 | public class DataAddendumEntry
8 | {
9 | [JsonProperty("name")]
10 | public string Name { get; set; }
11 |
12 | [JsonProperty("path")]
13 | public string Path { get; set; }
14 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Public/ModTek.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using ModTek.Features.Manifest.Mods;
3 |
4 | // ReSharper disable once CheckNamespace
5 | // ReSharper disable once UnusedMember.Global
6 | namespace ModTek;
7 |
8 | public static partial class ModTek
9 | {
10 | public static readonly Dictionary allModDefs = ModDefsDatabase.allModDefs;
11 | }
--------------------------------------------------------------------------------
/ModTek/Features/Manifest/Public/PreloaderAPI.cs:
--------------------------------------------------------------------------------
1 | using ModTek.Features.Manifest;
2 |
3 | // ReSharper disable once CheckNamespace
4 | // ReSharper disable once UnusedMember.Global
5 | namespace ModTek;
6 |
7 | // this API is experimental
8 | // this class is trying to get compatibility with CustomPrewarm
9 | // TODO remove functions not needed after CustomPrewarm compatibility is established
10 | public static class PreloaderAPI
11 | {
12 | public static bool IsPreloading => ModsManifestPreloader.HasPreloader;
13 | public static int PreloadChecksFinishedCounter => ModsManifestPreloader.finishedChecksAndPreloadsCounter;
14 | public static bool FirstPreloadFinished => PreloadChecksFinishedCounter > 0 && !IsPreloading;
15 | }
--------------------------------------------------------------------------------
/ModTek/Features/Profiler/Patches/JSONSerializationUtility_RehydrateObjectFromDictionary_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using HBS;
4 | using HBS.Util;
5 | using ModTek.Util.Stopwatch;
6 |
7 | namespace ModTek.Features.Profiler.Patches;
8 |
9 | [HarmonyPatch(
10 | typeof(JSONSerializationUtility),
11 | nameof(JSONSerializationUtility.RehydrateObjectFromDictionary),
12 | typeof(object),
13 | typeof(Dictionary),
14 | typeof(string),
15 | typeof(Stopwatch),
16 | typeof(Stopwatch),
17 | typeof(JSONSerializationUtility.RehydrationFilteringMode),
18 | typeof(Func[])
19 | )]
20 | internal static class JSONSerializationUtility_RehydrateObjectFromDictionary_Patch
21 | {
22 | public static bool Prepare()
23 | {
24 | return ModTek.Enabled && ModTek.Config.ProfilerEnabled;
25 | }
26 |
27 | private static readonly MTStopwatchWithCallback s_stopwatch = new(stats =>
28 | {
29 | var id = "JSONSerializationUtility.RehydrateObjectFromDictionary";
30 | Log.Main.Trace?.Log($"{id} was called {stats.Count} times, taking a total of {stats.TotalTime} with an average of {stats.AverageNanoseconds}ns.");
31 | }
32 | );
33 |
34 | [HarmonyPriority(Priority.First)]
35 | public static void Prefix(string classStructure, ref long __state)
36 | {
37 | if (string.IsNullOrEmpty(classStructure))
38 | {
39 | __state = MTStopwatch.GetTimestamp();
40 | }
41 | }
42 |
43 | [HarmonyPriority(Priority.Last)]
44 | public static void Postfix(string classStructure, ref long __state)
45 | {
46 | if (__state > 0)
47 | {
48 | s_stopwatch.EndMeasurement(__state);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/ModTek/Features/Profiler/Patches/JSON_ToObject_Patch.cs:
--------------------------------------------------------------------------------
1 | using fastJSON;
2 | using ModTek.Util.Stopwatch;
3 |
4 | namespace ModTek.Features.Profiler.Patches;
5 |
6 | [HarmonyPatch(typeof(JSON), nameof(JSON.ToObject), typeof(string), typeof(bool))]
7 | internal static class JSON_ToObject_Patch
8 | {
9 | public static bool Prepare()
10 | {
11 | return ModTek.Enabled && ModTek.Config.ProfilerEnabled;
12 | }
13 |
14 | private static readonly MTStopwatchWithCallback s_stopwatch = new(stats =>
15 | {
16 | Log.Main.Trace?.Log(
17 | $"JSON.ToObject called {stats.Count} times, taking a total of {stats.TotalTime} with an average of {stats.AverageNanoseconds}ns."
18 | );
19 | }
20 | );
21 |
22 | [HarmonyPriority(Priority.First)]
23 | public static void Prefix(ref long __state)
24 | {
25 | __state = MTStopwatch.GetTimestamp();
26 | }
27 |
28 | [HarmonyPriority(Priority.Last)]
29 | public static void Postfix(ref long __state)
30 | {
31 | s_stopwatch.EndMeasurement(__state);
32 | }
33 | }
--------------------------------------------------------------------------------
/ModTek/Features/Profiler/Patches/UIManager_LateUpdate_Patch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BattleTech.UI;
3 |
4 | namespace ModTek.Features.Profiler.Patches;
5 |
6 | [HarmonyPatch(typeof(UIManager), nameof(UIManager.LateUpdate))]
7 | internal static class UIManager_LateUpdate_Patch
8 | {
9 | public static bool Prepare()
10 | {
11 | return ModTek.Enabled && ModTek.Config.ProfilerEnabled;
12 | }
13 |
14 | public static void Postfix(LoadingCurtain __instance)
15 | {
16 | try
17 | {
18 | ProfilerStats.LogIfChanged();
19 | }
20 | catch (Exception e)
21 | {
22 | Log.Main.Error?.Log("Failed running postfix", e);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/ModTek/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using HarmonyLib;
--------------------------------------------------------------------------------
/ModTek/Log.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using ModTek.Public;
3 |
4 | namespace ModTek;
5 |
6 | internal static class Log
7 | {
8 | internal static readonly NullableLogger Main = NullableLogger.GetLogger(typeof(Log).Assembly.GetName().Name, NullableLogger.TraceLogLevel);
9 | internal static readonly NullableLogger Profiler = NullableLogger.GetLogger(nameof(Profiler), NullableLogger.TraceLogLevel);
10 | internal static readonly NullableLogger Debugger = NullableLogger.GetLogger(nameof(Debugger), NullableLogger.TraceLogLevel);
11 | internal static readonly NullableLogger AppDomain = NullableLogger.GetLogger(nameof(AppDomain), NullableLogger.TraceLogLevel);
12 | internal static readonly NullableLogger HarmonyX = NullableLogger.GetLogger(nameof(HarmonyX), NullableLogger.TraceLogLevel);
13 |
14 | internal static void LogIf(this NullableLogger.ILevel @this, bool condition, string message)
15 | {
16 | if (condition)
17 | {
18 | @this.Log(message);
19 | }
20 | }
21 |
22 | internal static void LogIfSlow(this NullableLogger.ILevel @this, Stopwatch sw, string id = null, long threshold = 1000, bool resetIfLogged = true)
23 | {
24 | if (sw.ElapsedMilliseconds < threshold)
25 | {
26 | return;
27 | }
28 |
29 | id = id ?? "Method " + GetFullMethodName();
30 | @this.Log($"{id} took {sw.Elapsed}");
31 |
32 | if (resetIfLogged)
33 | {
34 | sw.Reset();
35 | }
36 | }
37 |
38 | private static string GetFullMethodName()
39 | {
40 | var frame = new StackTrace().GetFrame(2);
41 | var method = frame.GetMethod();
42 | var methodName = method.Name;
43 | var className = method.ReflectedType?.FullName;
44 | var fullMethodName = className + "." + methodName;
45 | return fullMethodName;
46 | }
47 | }
--------------------------------------------------------------------------------
/ModTek/Misc/FilePaths.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using ModTek.Common.Globals;
3 | using UnityEngine;
4 |
5 | namespace ModTek.Misc;
6 |
7 | internal static class FilePaths
8 | {
9 | internal const string MOD_JSON_NAME = "mod.json";
10 | private const string MDD_FILE_NAME = "MetadataDatabase.db";
11 |
12 | // Common paths
13 | internal static string ModsDirectory => Paths.ModsDirectory;
14 | internal static string ModTekDirectory => Paths.ModTekDirectory;
15 | internal static string TempModTekDirectory => Paths.DotModTekDirectory;
16 |
17 | // ModTek paths
18 | internal static readonly string StreamingAssetsDirectory = Application.streamingAssetsPath;
19 | internal static readonly string MDDBPath = Path.Combine(StreamingAssetsDirectory, "MDD", MDD_FILE_NAME);
20 | internal static readonly string AssetBundlesDirectory = Path.Combine(StreamingAssetsDirectory, "data", "assetbundles");
21 |
22 | internal static readonly string MergeCacheDirectory = Path.Combine(TempModTekDirectory, "Cache");
23 | internal static readonly string MDDBCacheDirectory = Path.Combine(TempModTekDirectory, "Database");
24 | internal static readonly string ModMDDBPath = Path.Combine(MDDBCacheDirectory, MDD_FILE_NAME);
25 | internal static readonly string LoadOrderPath = Path.Combine(TempModTekDirectory, "load_order.json");
26 | internal static readonly string ModTekModJsonPath = Path.Combine(ModTekDirectory, MOD_JSON_NAME);
27 | internal static readonly string LogPath = Path.Combine(TempModTekDirectory, "ModTek.log");
28 | }
--------------------------------------------------------------------------------
/ModTek/Misc/ModTekCacheStorage.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Reflection;
4 | using HBS.Util;
5 | using Newtonsoft.Json;
6 |
7 | namespace ModTek.Misc;
8 |
9 | internal static class ModTekCacheStorage
10 | {
11 | internal static void CSVWriteTo
12 | (
13 | string path,
14 | IEnumerable enumerable,
15 | BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
16 | )
17 | {
18 | void process(CSVWriter csvWriter)
19 | {
20 | CSVTemplatingUtility.WriteFieldNamesToRow(csvWriter, flags);
21 | csvWriter.AdvanceRow();
22 | foreach (var entry in enumerable)
23 | {
24 | CSVSerializationUtility.WriteFieldValuesToRow(entry, csvWriter, flags);
25 | csvWriter.AdvanceRow();
26 | }
27 | }
28 |
29 | using var c = new CSVWriter(path);
30 | process(c);
31 | }
32 |
33 | internal static void WriteTo(object obj, string path)
34 | {
35 | using var f = new FileStream(path, FileMode.Create);
36 | using var s = new StreamWriter(f);
37 | using var j = new JsonTextWriter(s);
38 | var ser = new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.Indented };
39 | ser.Serialize(j, obj);
40 | j.Flush();
41 | }
42 |
43 | internal static T ReadFrom(string path)
44 | {
45 | using var f = new FileStream(path, FileMode.Open);
46 | using var s = new StreamReader(f);
47 | using var j = new JsonTextReader(s);
48 | var ser = new JsonSerializer();
49 | return ser.Deserialize(j);
50 | }
51 | }
--------------------------------------------------------------------------------
/ModTek/Patches/ActivateAfterInit.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 |
3 | // ReSharper disable InconsistentNaming
4 | // ReSharper disable UnusedMember.Global
5 |
6 | namespace ModTek.Patches;
7 |
8 | ///
9 | /// Disable activateAfterInit from functioning for the Start() on the "Main" game object which activates the BattleTechGame object
10 | /// This stops the main game object from loading immediately -- so work can be done beforehand
11 | ///
12 | [HarmonyPatch(typeof(ActivateAfterInit), "Start")]
13 | internal static class ActivateAfterInit_Start_Patch
14 | {
15 | public static bool Prepare()
16 | {
17 | return ModTek.Enabled;
18 | }
19 |
20 | public static bool Prefix(ActivateAfterInit __instance, ActivateAfterInit.ActivateAfter ___activateAfter, GameObject[] ___activationSet)
21 | {
22 | //Log("ActivateAfterInit.Start activateAfter:" + ___activateAfter);
23 | //foreach(GameObject gameObject in ___activationSet)
24 | //{
25 | //Log("\t"+ gameObject.name);
26 | //}
27 | if (ActivateAfterInit.ActivateAfter.Start.Equals(__instance.activateAfter))
28 | {
29 | var gameObjects = __instance.activationSet;
30 | foreach (var gameObject in gameObjects)
31 | {
32 | if ("SplashLauncher".Equals(gameObject.name))
33 | {
34 | // Don't activate through this call!
35 | return false;
36 | }
37 | }
38 | }
39 |
40 | return true;
41 | // Call the method
42 | //return true;
43 | }
44 | }
--------------------------------------------------------------------------------
/ModTek/Patches/VersionInfo.cs:
--------------------------------------------------------------------------------
1 |
2 |
3 | // ReSharper disable InconsistentNaming
4 | // ReSharper disable UnusedMember.Global
5 |
6 | namespace ModTek.Patches;
7 |
8 | ///
9 | /// Patch the GetReleaseVersion method to tack on the ModTek version to the game version in the main menu
10 | ///
11 | [HarmonyPatch(typeof(VersionInfo), "GetReleaseVersion")]
12 | internal static class VersionInfo_GetReleaseVersion_Patch
13 | {
14 | public static bool Prepare()
15 | {
16 | return ModTek.Enabled;
17 | }
18 |
19 | public static void Postfix(ref string __result)
20 | {
21 | var old = __result;
22 | __result = old + $"\nw/ ModTek v{GitVersionInformation.FullSemVer}";
23 | }
24 | }
--------------------------------------------------------------------------------
/ModTek/Public/BTRL.cs:
--------------------------------------------------------------------------------
1 | using BattleTech;
2 | using ModTek.Features.Manifest.BTRL;
3 |
4 | namespace ModTek.Public;
5 |
6 | public static class BTRL
7 | {
8 | public static string[] AllTypes()
9 | {
10 | return BetterBTRL.Instance.AllTypes();
11 | }
12 |
13 | public static VersionManifestEntry[] AllEntriesOfType(string type, bool filterByOwnership = false)
14 | {
15 | return BetterBTRL.Instance.AllEntriesOfType(type, filterByOwnership);
16 | }
17 |
18 | public static VersionManifestEntry EntryByIDAndType(string id, string type, bool filterByOwnership = false)
19 | {
20 | return BetterBTRL.Instance.EntryByIDAndType(id, type, filterByOwnership);
21 | }
22 | }
--------------------------------------------------------------------------------
/ModTek/Public/CustomResources.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using ModTek.Features.CustomResources;
3 |
4 | namespace ModTek.Public;
5 |
6 | public static class CustomResources
7 | {
8 | public static IReadOnlyCollection GetTypes()
9 | {
10 | return CustomResourcesFeature.GetTypes();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ModTek/Public/FilePaths.cs:
--------------------------------------------------------------------------------
1 | namespace ModTek.Public;
2 |
3 | public static class FilePaths
4 | {
5 | public static string TempModTekDirectory => Misc.FilePaths.TempModTekDirectory;
6 | }
--------------------------------------------------------------------------------
/ModTek/Util/FastRandom.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Numerics;
3 | using System.Runtime.CompilerServices;
4 |
5 | namespace ModTek.Util;
6 |
7 | // .NET 9 XoshiroImpl
8 | internal sealed class FastRandom
9 | {
10 | private ulong _s0, _s1, _s2, _s3;
11 |
12 | internal FastRandom()
13 | {
14 | var rnd = new Random();
15 | do
16 | {
17 | _s0 = ((ulong)rnd.Next() << 32) | (uint)rnd.Next();
18 | _s1 = ((ulong)rnd.Next() << 32) | (uint)rnd.Next();
19 | _s2 = ((ulong)rnd.Next() << 32) | (uint)rnd.Next();
20 | _s3 = ((ulong)rnd.Next() << 32) | (uint)rnd.Next();
21 | } while ((_s0 | _s1 | _s2 | _s3) == 0); // at least one value must be non-zero
22 | }
23 |
24 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
25 | internal ulong NextUInt64()
26 | {
27 | ulong s0 = _s0, s1 = _s1, s2 = _s2, s3 = _s3;
28 |
29 | var result = BitOperations.RotateLeft(s1 * 5, 7) * 9;
30 | var t = s1 << 17;
31 |
32 | s2 ^= s0;
33 | s3 ^= s1;
34 | s1 ^= s2;
35 | s0 ^= s3;
36 |
37 | s2 ^= t;
38 | s3 = BitOperations.RotateLeft(s3, 45);
39 |
40 | _s0 = s0;
41 | _s1 = s1;
42 | _s2 = s2;
43 | _s3 = s3;
44 |
45 | return result;
46 | }
47 | }
--------------------------------------------------------------------------------
/ModTek/Util/HBSJsonUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text.RegularExpressions;
4 | using HBS.Util;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace ModTek.Util;
8 |
9 | internal static class HBSJsonUtils
10 | {
11 | internal static JObject ParseGameJSONFile(string path, bool log = false)
12 | {
13 | var content = File.ReadAllText(path);
14 | return ParseGameJSON(content, log);
15 | }
16 |
17 | internal static JObject ParseGameJSON(string content, bool log = false)
18 | {
19 | Log.Main.Info?.LogIf(log,"content: " + content);
20 |
21 | try
22 | {
23 | return JObject.Parse(content);
24 | }
25 | catch (Exception)
26 | {
27 | // ignored
28 | }
29 |
30 | var commentsStripped = JSONSerializationUtility.StripHBSCommentsFromJSON(content);
31 | Log.Main.Info?.LogIf(log, "commentsStripped: " + commentsStripped);
32 |
33 | var commasAdded = FixHBSJsonCommas(commentsStripped);
34 | Log.Main.Info?.LogIf(log,"commasAdded: " + commasAdded);
35 |
36 | return JObject.Parse(commasAdded);
37 | }
38 |
39 | private static readonly Regex s_fixMissingCommasInJson = new(
40 | """(\]|\}|"|[A-Za-z0-9])\s*\n\s*(\[|\{|")""",
41 | RegexOptions.Singleline|RegexOptions.Compiled
42 | );
43 | private static string FixHBSJsonCommas(string json)
44 | {
45 | // add missing commas, this only fixes if there is a newline
46 | return s_fixMissingCommasInJson.Replace(json, "$1,\n$2");
47 | }
48 | }
--------------------------------------------------------------------------------
/ModTek/Util/RunOnlyOnceHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace ModTek.Util;
5 |
6 | internal class RunOnlyOnceHandler
7 | {
8 | private int hasExecuted;
9 | internal void Run(Action callback) {
10 | if (Interlocked.CompareExchange(ref hasExecuted, 1, 0) == 1)
11 | {
12 | return;
13 | }
14 | callback();
15 | }
16 | }
--------------------------------------------------------------------------------
/ModTek/Util/Stopwatch/MTStopwatchStats.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ModTek.Util.Stopwatch;
4 |
5 | internal readonly struct MTStopwatchStats
6 | {
7 | private readonly MTStopwatch _sw;
8 | internal readonly long Count;
9 | internal readonly long Ticks;
10 | internal TimeSpan TotalTime => MTStopwatch.TimeSpanFromTicks(Ticks);
11 | internal long AverageNanoseconds => Count <= 0 ? 0 : (long)(MTStopwatch.TicksToNsMultiplier * Ticks / Count);
12 |
13 | internal MTStopwatchStats(MTStopwatch sw, long count, long ticks)
14 | {
15 | _sw = sw;
16 | Count = count;
17 | Ticks = ticks - (long)(sw._overheadInMeasurement * count);
18 | }
19 |
20 | public override string ToString()
21 | {
22 | var verb = "measured";
23 | var suffix = "";
24 | if (_sw is MTStopwatchWithSampling sws)
25 | {
26 | verb = "estimated at";
27 | var overheadWithoutSampling = sws.OverheadPerMeasurementWithoutSampling * Count;
28 | var saved = overheadWithoutSampling - sws.OverheadPerMeasurementWithSampling * Count;
29 | var savedPercent = (byte)(saved / (Ticks + overheadWithoutSampling) * 100);
30 | suffix = $", sampling interval of {sws._samplingInterval} reduced measurement overhead by {savedPercent}%";
31 | }
32 | return $"{verb} {Count} times, taking a total of {TotalTime} with an average of {AverageNanoseconds}ns{suffix}";
33 | }
34 | }
--------------------------------------------------------------------------------
/ModTek/Util/Stopwatch/MTStopwatchWithCallback.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.CompilerServices;
3 | using System.Threading;
4 |
5 | namespace ModTek.Util.Stopwatch;
6 |
7 | internal sealed class MTStopwatchWithCallback : MTStopwatch
8 | {
9 | internal MTStopwatchWithCallback(Action callback)
10 | {
11 | this._callback = callback;
12 | }
13 | private readonly Action _callback;
14 |
15 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
16 | protected override void AddMeasurement(long elapsedTicks, long delta)
17 | {
18 | var count = Interlocked.Add(ref _count, delta);
19 | var ticks = Interlocked.Add(ref _ticks, elapsedTicks);
20 | if ((count & CallbackFastModuloMask) == 0)
21 | {
22 | _callback.Invoke(new MTStopwatchStats(this, count, ticks));
23 | }
24 | }
25 | private const long CallbackEveryMeasurement = 1 << 14; // every 16k, FastModulo requires base 2
26 | private const long CallbackFastModuloMask = CallbackEveryMeasurement - 1;
27 | }
--------------------------------------------------------------------------------
/ModTek/Util/Stopwatch/MTStopwatchWithSampling.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | namespace ModTek.Util.Stopwatch;
4 |
5 | // Stopwatch.GetTimestamp takes about 16-30ns, probably due to "extern" overhead
6 | // fast random is much faster, runs unrolled and therefore in parallel on the CPU
7 | internal sealed class MTStopwatchWithSampling : MTStopwatch
8 | {
9 | internal MTStopwatchWithSampling(uint samplingInterval)
10 | {
11 | _samplingInterval = samplingInterval;
12 | _sampleIfRandomSmallerOrEqualsTo = ulong.MaxValue / samplingInterval;
13 | _overheadInMeasurement = _overheadInMeasurement / _samplingInterval + s_samplingCheckOverhead;
14 | }
15 | internal readonly uint _samplingInterval;
16 | private readonly ulong _sampleIfRandomSmallerOrEqualsTo;
17 | private readonly FastRandom _random = new();
18 |
19 | internal double OverheadPerMeasurementWithSampling => OverheadPerMeasurement/_samplingInterval + s_samplingCheckOverhead;
20 | internal double OverheadPerMeasurementWithoutSampling => OverheadPerMeasurement;
21 |
22 | private static readonly double s_samplingCheckOverhead;
23 | internal static readonly bool DontOptimize;
24 | static MTStopwatchWithSampling()
25 | {
26 | var ws = new MTStopwatchWithSampling(100);
27 | var dontOptimize = false;
28 | for (var r = 0; r < 100; r++)
29 | {
30 | var start = GetTimestamp();
31 | const int Count = 1000;
32 | for (var i = 0; i < Count; i++)
33 | {
34 | dontOptimize = ws.ShouldMeasure();
35 | }
36 | var end = GetTimestamp();
37 | s_samplingCheckOverhead = (end - start)/(double)Count - s_timestampOverhead;
38 | DontOptimize = dontOptimize;
39 | }
40 | }
41 |
42 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
43 | internal bool ShouldMeasure()
44 | {
45 | return _random.NextUInt64() <= _sampleIfRandomSmallerOrEqualsTo;
46 | }
47 |
48 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
49 | internal override void EndMeasurement(long start, long delta = 1)
50 | {
51 | if (ShouldMeasure())
52 | {
53 | AddMeasurement((GetTimestamp() - start) * _samplingInterval, delta * _samplingInterval);
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/ModTek/modtekassetbundle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BattletechModders/ModTek/96c94efd466d41ec651d3fbeef59f2ae8e42c0a5/ModTek/modtekassetbundle
--------------------------------------------------------------------------------
/ModTekInjector/ModTekInjector.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | All
12 | runtime
13 |
14 |
15 | False
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ModTekSimpleInjector/ModTekSimpleInjector.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | All
13 | runtime
14 |
15 |
16 | False
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ModTekSimpleInjector/XmlDeserializationTypes.cs:
--------------------------------------------------------------------------------
1 | using System.Xml.Serialization;
2 | using Mono.Cecil;
3 |
4 | namespace ModTekSimpleInjector;
5 |
6 | // XmlSerializer requires the following classes to be public
7 |
8 | [XmlRoot(ElementName = "ModTekSimpleInjector")]
9 | public class Additions
10 | {
11 | [XmlElement(ElementName = "AddField")]
12 | public AddField[] AddField = [];
13 | [XmlElement(ElementName = "AddEnumConstant")]
14 | public AddEnumConstant[] AddEnumConstant = [];
15 | }
16 |
17 | public abstract class AssemblyAddition
18 | {
19 | [XmlAttribute("InAssembly")]
20 | public string InAssembly;
21 | [XmlAttribute("Comment")]
22 | public string Comment;
23 |
24 | public override string ToString()
25 | {
26 | return $"{this.GetType().Name}:{InAssembly}:{Comment}";
27 | }
28 | }
29 |
30 | public abstract class MemberAddition : AssemblyAddition
31 | {
32 | [XmlAttribute("ToType")]
33 | public string ToType;
34 |
35 | public override string ToString()
36 | {
37 | return $"{base.ToString()}:{ToType}";
38 | }
39 | }
40 |
41 | [XmlType("AddField")]
42 | [XmlRoot(ElementName = "AddField")]
43 | public class AddField : MemberAddition
44 | {
45 | [XmlAttribute("Name")]
46 | public string Name;
47 | [XmlAttribute("OfType")]
48 | public string OfType;
49 | [XmlAttribute("Attributes")]
50 | public FieldAttributes Attributes = FieldAttributes.Private;
51 |
52 | public override string ToString()
53 | {
54 | return $"{base.ToString()}:{Name}:{OfType}:{Attributes}";
55 | }
56 | }
57 |
58 | [XmlType("AddEnumConstant")]
59 | [XmlRoot(ElementName = "AddEnumConstant")]
60 | public class AddEnumConstant : MemberAddition
61 | {
62 | [XmlAttribute("Name")]
63 | public string Name;
64 | [XmlAttribute("Value")]
65 | public int Value;
66 |
67 | public override string ToString()
68 | {
69 | return $"{base.ToString()}:{Name}:{Value}";
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ModTek
2 |
3 | ModTek is a mod-loader for [HBS's BattleTech PC game](https://harebrained-schemes.com/battletech/). It allows modders to create self-contained mods that do not over-write game files. ModTek is run at game startup and dynamically loads mods that conform to the [mod.json format](https://github.com/BattletechModders/ModTek/wiki/The-mod.json-format). Mod dependencies are resolved and load order enforced without needing to edit the `VersionManifest.csv`.
4 |
5 | Since BattleTech 1.7, HBS introduced their own mod-loader, which is based on an older ModTek version. It is missing many features of newer ModTek versions, including DLC support.
6 |
7 | # Topics
8 |
9 | - [Installation Instructions](INSTALL.md)
10 | - [Changelog](CHANGES.md)
11 | - [How to contribute to ModTek](doc/CONTRIBUTE.md).
12 |
13 | ## Modding
14 |
15 | - [A Brief Primer on Developing ModTek Mods](doc/PRIMER.md)
16 | - [The mod.json Format](doc/MOD_JSON_FORMAT.md)
17 |
18 | ### JSON Modding
19 |
20 | - [Writing ModTek JSON mods](doc/MOD_JSON.md)
21 | - [Advanced JSON Merging](doc/ADVANCED_JSON_MERGING.md)
22 | - [Manifest Manipulation](doc/MANIFEST.md)
23 | - [Dynamic Enums / DataAddendumEntries](doc/DATA_ADDENDUM_ENTRIES.md)
24 | - [Content Pack Assets](doc/CONTENT_PACK_ASSETS.md)
25 |
26 | ### DLL Modding
27 |
28 | - [Writing ModTek DLL mods](doc/MOD_DLL.md)
29 | - [Development Guide](doc/DEVELOPMENT_GUIDE.md)
30 | - [Preloader and Injectors](doc/PRELOADER.md)
31 | - [Logging with ModTek](doc/LOGGING.md)
32 | - [HarmonyX Support](doc/HARMONY12X.md)
33 |
34 | ### Custom Types
35 |
36 | - [DebugSettings](doc/CUSTOM_TYPE_DEBUGSETTINGS.md)
37 | - [SVGAssets](doc/CUSTOM_TYPE_SVGASSET.md)
38 | - [Custom Tags and Tagsets](doc/CUSTOM_TYPE_CUSTOMTAGS.md)
39 | - [SoundBanks](doc/CUSTOM_TYPE_SOUNDBANKS.md)
40 | - Custom Video - TBD
41 | - Assembly - TBD
42 |
--------------------------------------------------------------------------------
/doc/CONTENT_PACK_ASSETS.md:
--------------------------------------------------------------------------------
1 | # ContentPackAssets
2 |
3 | a.k.a. DLC content support
4 |
5 | If you want to merge changes to base game content, you can use `StreamingAssets`:
6 | > mods/{MOD}/StreamingAssets/{ID}.{FILE_EXTENSION}
7 |
8 | Example:
9 | > mods/YourMod/StreamingAssets/chassisdef_atlas_AS7-D-HT.json
10 |
11 | DLC content works the same way, just add the file to the StreamingAssets folder.
12 |
13 | Example:
14 | > mods/YourMod/StreamingAssets/chassisdef_annihilator_ANH-JH.json
15 |
16 | Variables:
17 | - {MOD}: The name of your mod.
18 | - {ID}: The identifier of the resource, is the same as the filename without the file extension.
19 | - {FILE_EXTENSION}: .json .csv and .txt are valid values.
20 |
21 | Visit [design-data](https://github.com/caardappel-hbs/bt-dlc-designdata) to find extracted DLC jsons.
22 |
23 | ## Additional content
24 |
25 | To add new resources to the game if a DLC loaded, one can use the `RequiredContentPack` field in a manifest entry.
26 |
27 | Example:
28 | ```json
29 | {
30 | "Name": "MyModForHeavyMetal",
31 | "Enabled": true,
32 | "Manifest": [
33 | { "Type": "MechDef", "Path": "MyMechDefs", "RequiredContentPack": "heavymetal" }
34 | ]
35 | }
36 | ```
37 |
38 | In the example, the MechDefs found at the path are only available in the game when the DLC `heavymetal` is available.
39 |
40 | Possible content pack names:
41 | - flashpoint
42 | - shadowhawkdlc
43 | - urbanwarfare
44 | - heavymetal
45 |
46 | Note that `RequiredContentPack` is only supported for newly added resources.
47 | Merging, appending or replacing resources won't allow to change `RequiredContentPack` of existing resources.
48 | Custom resources do not support `RequiredContentPack`.
49 |
50 | ## Implementional details for DLL modders
51 |
52 | As with original DLC content, ModTek adds additional DLC dependent resources to the BattleTechResourceLocator and indexes them into the MDDB.
53 |
54 | When querying the `BattleTechResourceLocator`, make sure to supply the `filterByOwnership` flag with a value of `true`.
55 | Otherwise use the method `IsResourceOwned` from `ContentPackIndex` to check if a resource id is owned and should be processed.
56 |
57 | Note that ownership of DLC content can change during the game, e.g. if someone logs in or out of a Paradox account from the main menu.
58 |
--------------------------------------------------------------------------------
/doc/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # Developing ModTek
2 |
3 | ModTek itself is based on the best practices found in the [Development Guide](DEVELOPMENT_GUIDE.md).
4 |
5 | The ModTekPreloader is the a special piece of software, as it downloads and integrates UnitDoorstop into the release build.
6 | It also cannot use any functionality found in BattleTech itself, as it bootstrap the game before it is being loaded.
7 |
8 | Just use standard GitHub practices in creating a fork and creating a merge request to get your additions into ModTek.
9 |
--------------------------------------------------------------------------------
/doc/CUSTOM_TYPE_CUSTOMTAGS.md:
--------------------------------------------------------------------------------
1 | # Custom Tags and TagSets
2 |
3 | ModTek 0.7.8 and above supports adding and update Tags and TagSets in the MetadataDatabase. HBS BT uses Tags for many different purposes, such as the pilot attribute descriptors, contract validation, and many more. You can add your own custom tags or update existing by adding the `CustomTag` and `CustomTagSet` type to your Manifest element:
4 |
5 | ```json
6 | "Manifest": [
7 | { "Type": "CustomTag", "Path": "tags/" },
8 | { "Type": "CustomTagSet", "Path": "tagSets/" }
9 | ],
10 | ```
11 |
12 | > **Warning**
13 | > There is currently no way to delete CustomTags or CustomTagSets. These are modified in the ModTek MDDB copy (located in `BATTLETECH/Mods/.modtek/Database/`) and should not alter the base MDDB located in the `BATTLETECH/Data` directory.
14 |
15 | Each CustomTag needs to be defined in a .json file with the following structure:
16 | ```json
17 | {
18 | "Name" : "TAG_NAME_HERE",
19 | "Important" : false,
20 | "PlayerVisible" : true,
21 | "FriendlyName" : "FRIENDLY_NAME",
22 | "Description" : "DESCRIPTION TEXT"
23 | }
24 | ```
25 |
26 | You can overwrite HBS defined tags with your own values by using the name tag-name. If multiple mods write to the same tag, the *last* mod to write the tag wins.
27 |
28 | Each CustomTagSet needs to be defined in a .json file with the following structure:
29 |
30 | ```json
31 | {
32 | "ID" : "TAGSET_ID",
33 | "TypeID" : SEE_BELOW,
34 | "Tags" : [ "TAG_1" , "TAG_2", "TAG_3" ]
35 | }
36 | ```
37 |
38 | As with CustomTags, you can update HBS defined TagSets by using the same ID for your CustomTag. Any existing tags will be removed, and replaced with the tags you defined instead. You **must** include all tags you want in the updated TagSet in your CustomTag definition.
39 |
40 | Note that the TypeID is an enumeration type, that unfortunately was never upgraded to a data-driven enum. Thus you are limited to the following enum types, as defined in the `Assembly-CSharp.dll`. You **must not** use a TypeID that's not defined below, or you will experience erratic behaviors. If the internal logic cannot cast your specified value to the defined enum it will likely fail without printing any error message.
41 |
42 | | TagSetType | TypeID |
43 | | ---------- | ------ |
44 | | UNDEFINED | 1 |
45 | | Map | 2 |
46 | | Encounter | 3 |
47 | | Contract | 4 |
48 | | LanceDef | 5 |
49 | | UnitDef | 6 |
50 | | PilotDef | 7 |
51 | | RequirementDefRequirement | 8 |
52 | | RequirementDefExclusion | 9 |
53 | | BiomeRequiredMood | 10 |
54 | | Mood | 11 |
55 | | EventDefRequired | 12 |
56 | | EventDefExcluded | 13 |
57 | | EventDefOptionRequired | 14 |
58 | | EventDefOptionExcluded | 15 |
59 | | EventDefAdded | 16 |
60 | | EventDefRemoved | 17 |
61 | | UnitDef_RequiredToSpawnCompany | 18 |
62 |
--------------------------------------------------------------------------------
/doc/CUSTOM_TYPE_DEBUGSETTINGS.md:
--------------------------------------------------------------------------------
1 | # Custom Debug Settings
2 |
3 | HBS defined various configuration options in the `BATTLETECH/BattleTech_Data/StreamingAssets/data/debug/settings.json` file. Internal logger levels are defined here, as are some configuration settings only intended to be used by developers. Mods can modify these values using the `DebugSettings` custom type. The contents of any such files will be merged into the HBS provided settings.json.
4 |
5 | This custom type is last-in, last-out. The last mod to change a setting will win.
6 |
--------------------------------------------------------------------------------
/doc/CUSTOM_TYPE_SOUNDBANKS.md:
--------------------------------------------------------------------------------
1 | # Custom Sounds
2 |
3 | since 0.7.6.7 ModTek supports loading Wwise sound banks definitions
4 | example:
5 | ```
6 | {
7 | "name": "Screams",
8 | "filename": "Screams.bnk",
9 | "type": "Combat",
10 | "volumeRTPCIds":[2081458675],
11 | "volumeShift": 0,
12 | "events":{
13 | "scream01":147496415
14 | }
15 | }
16 | ```
17 | **name** - is unique name of sound bank
18 | **filename** - is name of file containing real audio content. Battletech is using Wwise 2016.2
19 | **type** - type of sound bank.
20 | Available types:
21 | * **Combat** - banks of this type always loading at combat start and unloading at combat end. Should be used for sounds played on battlefield.
22 | * **Default** - banks of this type loading at game start. Moistly used for UI
23 | * **Voice** - banks of this type contains pilot's voices
24 |
25 | **events** - map of events exported in bank. Needed for events can be referenced from code via WwiseManager.PostEvent which takes this name as parameter
26 | **volumeRTPCIds** - list of RTPC ids controlling loudness of samples. Combat sound banks controlled by effect settings volume slider, Voice by voice
27 |
--------------------------------------------------------------------------------
/doc/CUSTOM_TYPE_SVGASSET.md:
--------------------------------------------------------------------------------
1 | # Custom SVG Assets
2 |
3 | Custom SVG assets can be added through the `SVGAsset` custom type. Any files at these paths will be added to the HBS DataManager and can be referenced through a loadrequest on the datamanager.
4 |
5 | Simply define the path to your SVGs:
6 |
7 | ```json
8 | { "Type": "SVGAsset", "Path": "icons/" },
9 | ```
10 |
11 | then in your DLL mod read them from the DataManager:
12 |
13 | ```csharp
14 |
15 | # Load the file
16 | DataManager dm = UnityGameInstance.BattleTechGame.DataManager;
17 | LoadRequest loadRequest = dm.CreateLoadRequest();
18 | loadRequest.AddLoadRequest(BattleTechResourceType.SVGAsset, "icon_foo", null);
19 | loadRequest.ProcessRequests();
20 |
21 | ...
22 |
23 | # Read it
24 | SVGAsset icon = DataManager.GetObjectOfType("icon_foo", BattleTechResourceType.SVGAsset);
25 |
26 | ```
27 |
--------------------------------------------------------------------------------
/doc/DATA_ADDENDUM_ENTRIES.md:
--------------------------------------------------------------------------------
1 | # Dynamic Enums
2 | dynamic enums handled outside manifest array. By DataAddendumEntries
3 | name - name of type. Supporting types BattleTech.FactionEnumeration, BattleTech.WeaponCategoryEnumeration, BattleTech.AmmoCategoryEnumeration, BattleTech.ContractTypeEnumeration
4 | path - path to file relative to mod root folder. Examples for content at `BattleTech_Data/StreamingAssets/data/enums/` .
5 |
6 | example:
7 | ```
8 | "DataAddendumEntries":[
9 | {
10 | "name": "BattleTech.FactionEnumeration",
11 | "path": "Faction.json"
12 | },
13 | {
14 | "name": "BattleTech.WeaponCategoryEnumeration",
15 | "path": "WeaponCategory.json"
16 | }
17 | ]
18 | ```
19 |
--------------------------------------------------------------------------------
/doc/MANIFEST.md:
--------------------------------------------------------------------------------
1 | # Manifest Manipulation
2 |
3 | | | Untyped Merges | Merges | Replacements | Deletions | MDDB Update |
4 | |-------------------------|----------------|---------|--------------|-----------|-------------|
5 | | Custom Streaming Assets | x | x | x | - | - |
6 | | StreamingAssets | x | x | x | - | x |
7 | | Content Packs | x | x | x | - | x |
8 | | New Mod Content | - | x | x | - (1) | x |
9 | | Custom Resources | - | - (1) | x | - (1) | - |
10 |
11 | 1) Support was dropped in ModTek v2, mostly to support content packs.
12 |
13 | Resources:
14 | - Custom Streaming Assets: DebugSettings and GameTip found in the StreamingAssets folder but not in `VersionManifest.csv`.
15 |
16 | - StreamingAssets: Vanilla content found in BTRL.
17 |
18 | - Content Packs: DLC content provided by HBS. Content pack ids:
19 | shadowhawkdlc, flashpoint, urbanwarfare, heavymetal
20 |
21 | - New Mod Content: Content that is not from HBS. but still a BattleTechResourceType.
22 |
23 | - Custom Resources: Content that is not of type BattleTechResourceType,
24 | useful to provide completely new or not yet exposed data.
25 | Examples are SoundBank(Def) and Video in ModTek.
26 | DLL mods can introduce processing of new resource types that other mods can then provide content for.
27 |
28 | Resource Modification Types:
29 | - Untyped Merges: All merges reference an id and type,
30 | if the type of a resource can be auto-detected,
31 | since there is only one id, auto-merging can happen.
32 | All base game files that are mergable only have one type per id.
33 |
34 | - Merges: Resources with of vanilla type (BattleTechResourceType)
35 | and file type csv, txt and json can be merged.
36 | JSON merges are done with Newtonsoft JSON.NET.
37 | Txt and CSV merges simply append any text to the existing file.
38 |
39 | - Replacements: Instead of merging, one can specify a type,
40 | id and filepath to overwrite a file completely.
41 | This is recommended for large mod packs to make sure there are not several
42 | merges interfering which each other differently based on load order.
43 | For smaller mods, you don't want to overwrite changes made by other mods,
44 | there is a good chance using merges instead of replacements is better in that case.
45 |
46 | - Deletions: Not supported at all (anymore) since base types that would be in MDDB can't be removed anyway yet.
47 | Bigger mod packs disable instead of removing content, so deletion doesn't make much sense to keep around.
48 |
49 | - MDDB Update: MDDB is used by the game to index and then find certain types of data:
50 | ContractOverride, LanceDef, PilotDef, SimGameEventDef, MechDef, WeaponDef, TurretDef, VehicleDef, UpgradeDef.
51 |
--------------------------------------------------------------------------------
/doc/PLATFORM_FILE_TREES.md:
--------------------------------------------------------------------------------
1 |
2 | # Windows
3 |
4 | | Platform | Default Location |
5 | | -------- | ---------------- |
6 | | Steam | `C:\Program Files (x86)\Steam\steamapps\common\BATTLETECH` |
7 |
8 | File Tree
9 | ```
10 | ./BattleTech.exe
11 | ./BattleTech_Data/Managed/0Harmony.dll
12 | ./BattleTech_Data/Plugins/sqlite3.dll
13 | ./BattleTech_Data/StreamingAssets/data/VersionManifest.csv
14 | ./BattleTech_Data/StreamingAssets/data/jumpjets/Gear_JumpJet_Generic_Assault.json
15 | ./BattleTech_Data/StreamingAssets/editors/sqlite3.dll
16 | ./Mods/ModTek/AssembliesOverride/0Harmony.dll
17 | ```
18 |
19 | # Linux
20 |
21 | | Platform | Default Location |
22 | | -------- | ---------------- |
23 | | Steam | `~/.steam/steam/SteamApps/common/` |
24 |
25 | # macOS
26 |
27 | | Platform | Default Location |
28 | | -------- | ---------------- |
29 | | Steam | `~/Library/Application Support/Steam/steamapps/common/BATTLETECH` |
30 |
31 | File Tree
32 | ```
33 | ./BattleTech.app/Contents/MacOS/BattleTech
34 | ./BattleTech.app/Contents/Resources/Data/Managed/0Harmony.dll
35 | ./BattleTech.app/Contents/Resources/Data/Plugins/sqlite3.dll
36 | ./BattleTech.app/Contents/Resources/Data/StreamingAssets/data/VersionManifest.csv
37 | ./BattleTech.app/Contents/Resources/Data/StreamingAssets/data/jumpjets/Gear_JumpJet_Generic_Assault.json
38 | ./BattleTech.app/Contents/Resources/Data/StreamingAssets/editors/sqlite3.dll
39 | ./BattleTech.app/Contents/Resources/Mods/ModTek/AssembliesOverride/0Harmony.dll
40 | ```
41 |
--------------------------------------------------------------------------------
/doc/PRELOADER.md:
--------------------------------------------------------------------------------
1 | # Preloader and Injectors
2 |
3 | > **Note**
4 | > TODO finalize
5 |
6 | - Preloader uses UnityDoorstop to run itself, UnityDoorstop uses native libraries to load so early that the game didn't even load any .NET assemblies.
7 | That way injectors now can modify the games .NET assemblies as part of the loading process, instead of the user having to do it.
8 | - Runs injectors, e.g. ModTekInjector injects ModTek.
9 | - UnityDoorstop also makes it easy to override or add assemblies. Just put them into `Mods/ModTek/AssembliesOverride`.
10 | - UnityDoorstop also sets env variables to find the managed directory.
11 | - Load order of assemblies
12 | - `BATTLETECH/Mods/.modtek/AssembliesInjected/` (directory updated during Preloader run, each assembly in here was already force loaded during the run)
13 | - `BATTLETECH/Mods/ModTek/AssembliesOverride/` (doorstop settings to allow updating libs from the managed folder)
14 | - `BATTLETECH/Mods/ModTek/` (doorstop adds this since it loads Preloader from here)
15 | - `BATTLETECH/BattleTech_Data/Managed` (or the equivalent directory based on platform, should never be modified)
16 |
17 | ## Injectors
18 |
19 | > **Note**
20 | > TODO finalize
21 |
22 | - See [ModTekInjector](https://github.com/BattletechModders/ModTek/blob/master/ModTekInjector/ModTekInjector.csproj)
23 | or [RogueTechPerfFixesInjector](https://github.com/BattletechModders/RogueTechPerfFixes/blob/master/RogueTechPerfFixesInjector/RogueTechPerfFixesInjector.csproj)
24 | on how an injector works.
25 | - Only assemblies resolved and modified as a AssemblyDefinition will be loaded into the game, make sure to only use the resolver interface in the Inject method.
26 | - Injectors are loaded and run in the order of their names from `Mods/ModTek/Injectors/`.
27 | - The preloader searches for a class named `Injector` with a `public static void Inject` method, that then will be called with a parameter of type `Mono.Cecil.IAssemblyResolver`.
28 | > ```
29 | > internal static class Injector
30 | > {
31 | > public static void Inject(Mono.Cecil.IAssemblyResolver resolver)
32 | > {
33 | > var game = resolver.Resolve(new AssemblyNameReference("Assembly-CSharp", null));
34 | > }
35 | > }
36 | > ```
37 | - Injectors run in their own AppDomain. All directly loaded dlls (via Assembly.Load or due to reference in Assembly) during the injection phase will be lost.
38 | - Console output is redirected into `Mods/.modtek/ModTekPreloader.log`, use `Console.WriteLine` or `Console.Error.WriteLine` instead of writing a logger.
39 | - Modified assemblies (that were resolved earlier via the resolver) are then written to and loaded from `Mods/.modtek/AssembliesInjected/`.
40 | - Injections are cached unless the inputs changed, that includes injector assemblies themselves and any files in `Mods/ModTek/Injectors/`.
41 | Add configuration files for injectors in that folder, so any time a user changes an injector setting, the cache gets invalidated.
42 |
--------------------------------------------------------------------------------
/doc/QUICKSTART.md:
--------------------------------------------------------------------------------
1 | See [README](../README.md) for installation instructions.
--------------------------------------------------------------------------------
/examples/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 |
--------------------------------------------------------------------------------
/examples/ModTemplate/Main.cs:
--------------------------------------------------------------------------------
1 | using HarmonyLib;
2 | using HBS.Logging;
3 |
4 | namespace ModTemplate;
5 |
6 | public static class Main
7 | {
8 | private static readonly ILog s_log = Logger.GetLogger(nameof(ModTemplate));
9 | public static void Start()
10 | {
11 | s_log.Log("Starting");
12 |
13 | // apply all patches that are in classes annotated with [HarmonyPatch]
14 | Harmony.CreateAndPatchAll(typeof(Main).Assembly);
15 |
16 | // run a specific patch found in a class which wasn't annotated with HarmonyPatch and therefore wasn't applied earlier
17 | Harmony.CreateAndPatchAll(typeof(Main));
18 |
19 | s_log.Log("Started");
20 | }
21 |
22 | [HarmonyPatch(typeof(VersionInfo), nameof(VersionInfo.GetReleaseVersion))]
23 | [HarmonyPostfix]
24 | [HarmonyAfter("io.github.mpstark.ModTek")]
25 | static void GetReleaseVersion(ref string __result)
26 | {
27 | var old = __result;
28 | __result = old + "\nMy Mod Template shows up!";
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/ModTemplate/Patches/VersionInfo_GetReleaseVersion_Patch.cs:
--------------------------------------------------------------------------------
1 | using HarmonyLib;
2 |
3 | namespace ModTemplate.Patches;
4 |
5 | [HarmonyPatch(typeof(VersionInfo), nameof(VersionInfo.GetReleaseVersion))]
6 | static class VersionInfo_GetReleaseVersion_Patch
7 | {
8 | [HarmonyPostfix]
9 | [HarmonyAfter("io.github.mpstark.ModTek")]
10 | static void Postfix(ref string __result)
11 | {
12 | var old = __result;
13 | __result = old + "\nMTWHX(1)";
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/ModTemplate/mod.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "ModTemplate",
3 | "DLL": "ModTemplate.dll",
4 | "DLLEntryPoint": "ModTemplate.Main.Start"
5 | }
--------------------------------------------------------------------------------