diff --git a/Assembly-CSharp/Assembly-CSharp.csproj b/Assembly-CSharp/Assembly-CSharp.csproj
index 337c16c9..7330320f 100644
--- a/Assembly-CSharp/Assembly-CSharp.csproj
+++ b/Assembly-CSharp/Assembly-CSharp.csproj
@@ -20,6 +20,8 @@
+
+
@@ -45,11 +47,16 @@
-
+
-
-
+
+
+
+
+
+
+
@@ -72,10 +79,9 @@
-
+
+
-
-
@@ -148,7 +154,7 @@
..\override\mscorlib.dll
-
+
../Vanilla/netstandard.dll
@@ -200,5 +206,11 @@
../Vanilla/UnityEngine.UIModule.dll
+
+ ../Vanilla/TeamCherry.Localization.dll
+
+
+ ../Vanilla/TeamCherry.TK2D.dll
+
diff --git a/Assembly-CSharp/Menu/MenuUtils.cs b/Assembly-CSharp/Menu/MenuUtils.cs
index af0a1de8..0bd47d9a 100644
--- a/Assembly-CSharp/Menu/MenuUtils.cs
+++ b/Assembly-CSharp/Menu/MenuUtils.cs
@@ -8,7 +8,7 @@
using UnityEngine;
using UnityEngine.UI;
using Patch = Modding.Patches;
-using Lang = Language.Language;
+using Lang = TeamCherry.Localization.Language;
namespace Modding.Menu
diff --git a/Assembly-CSharp/Mod.cs b/Assembly-CSharp/Mod.cs
index 3e476940..717bc889 100644
--- a/Assembly-CSharp/Mod.cs
+++ b/Assembly-CSharp/Mod.cs
@@ -188,7 +188,7 @@ public virtual void Initialize() { }
/// change the text of the button to jump to this mod's menu.
///
///
- public virtual string GetMenuButtonText() => $"{GetName()} {Language.Language.Get("MAIN_OPTIONS", "MainMenu")}";
+ public virtual string GetMenuButtonText() => $"{GetName()} {TeamCherry.Localization.Language.Get("MAIN_OPTIONS", "MainMenu")}";
private void HookSaveMethods()
{
diff --git a/Assembly-CSharp/ModHooks.cs b/Assembly-CSharp/ModHooks.cs
index 1ba7815f..0e0ad49b 100644
--- a/Assembly-CSharp/ModHooks.cs
+++ b/Assembly-CSharp/ModHooks.cs
@@ -7,6 +7,7 @@
using JetBrains.Annotations;
using Modding.Patches;
using MonoMod;
+using MonoMod.RuntimeDetour;
using Newtonsoft.Json;
using UnityEngine;
using System.Linq;
@@ -219,20 +220,36 @@ internal static void LogConsole(string message, LogLevel level)
/// N/A
public static event LanguageGetProxy LanguageGetHook;
+ private static Hook _languageGetHook;
+
+ internal static void InitLanguageHook()
+ {
+ var method = typeof(TeamCherry.Localization.Language).GetMethod(
+ "Get",
+ System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static,
+ null,
+ new[] { typeof(string), typeof(string) },
+ null
+ );
+ if (method == null) return;
+ _languageGetHook = new Hook(method,
+ new Func, string, string, string>(
+ (orig, key, sheet) => LanguageGet(key, sheet, orig)
+ ));
+ }
+
///
/// Called whenever localization specific strings are requested
///
/// N/A
- internal static string LanguageGet(string key, string sheet)
+ internal static string LanguageGet(string key, string sheet, Func orig)
{
- string res = Patches.Language.GetInternal(key, sheet);
+ string res = orig(key, sheet);
if (LanguageGetHook == null)
return res;
- Delegate[] invocationList = LanguageGetHook.GetInvocationList();
-
- foreach (LanguageGetProxy toInvoke in invocationList)
+ foreach (LanguageGetProxy toInvoke in LanguageGetHook.GetInvocationList())
{
try
{
diff --git a/Assembly-CSharp/ModListMenu.cs b/Assembly-CSharp/ModListMenu.cs
index fec0f388..0a7b6082 100644
--- a/Assembly-CSharp/ModListMenu.cs
+++ b/Assembly-CSharp/ModListMenu.cs
@@ -6,7 +6,7 @@
using UnityEngine.UI;
using static Modding.ModLoader;
using Patch = Modding.Patches;
-using Lang = Language.Language;
+using Lang = TeamCherry.Localization.Language;
namespace Modding
{
diff --git a/Assembly-CSharp/ModLoader.cs b/Assembly-CSharp/ModLoader.cs
index 6a0a19e6..aaec24b0 100644
--- a/Assembly-CSharp/ModLoader.cs
+++ b/Assembly-CSharp/ModLoader.cs
@@ -84,6 +84,7 @@ public static IEnumerator LoadModsInit(GameObject coroutineHolder)
}
Logger.APILogger.Log("Starting mod loading");
+ ModHooks.InitLanguageHook();
string managed_path = SystemInfo.operatingSystemFamily switch
{
diff --git a/Assembly-CSharp/Patches/GameManager.cs b/Assembly-CSharp/Patches/GameManager.cs
index 9510c2d5..9a2b9d94 100644
--- a/Assembly-CSharp/Patches/GameManager.cs
+++ b/Assembly-CSharp/Patches/GameManager.cs
@@ -2,7 +2,6 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
-using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using MonoMod;
using Newtonsoft.Json;
@@ -196,28 +195,6 @@ public void SaveGame(int saveSlot, Action callback)
text = JsonUtility.ToJson(obj);
}
- bool flag = this.gameConfig.useSaveEncryption && !Platform.Current.IsFileSystemProtected;
-
- if (flag)
- {
- string graph = Encryption.Encrypt(text);
- BinaryFormatter binaryFormatter = new BinaryFormatter();
- MemoryStream memoryStream = new MemoryStream();
- binaryFormatter.Serialize(memoryStream, graph);
- byte[] binary = memoryStream.ToArray();
- memoryStream.Close();
- Platform.Current.WriteSaveSlot
- (
- saveSlot,
- binary,
- delegate (bool didSave)
- {
- this.HideSaveIcon();
- callback(didSave);
- }
- );
- }
- else
{
Platform.Current.WriteSaveSlot
(
@@ -289,27 +266,18 @@ public void SetupSceneRefs(bool refreshTilemapInfo)
#region LoadGame
- [MonoModReplace]
+ public extern void orig_LoadGame(int saveSlot, Action callback);
+
public void LoadGame(int saveSlot, Action callback)
{
if (!Platform.IsSaveSlotIndexValid(saveSlot))
{
- Debug.LogErrorFormat
- (
- "Cannot load from invalid save slot index {0}",
- new object[]
- {
- saveSlot
- }
- );
- if (callback != null)
- {
- CoreLoop.InvokeNext(delegate { callback(false); });
- }
-
+ Debug.LogErrorFormat("Cannot load from invalid save slot index {0}", new object[] { saveSlot });
+ if (callback != null) CoreLoop.InvokeNext(delegate { callback(false); });
return;
}
+ // Load modded save data from our separate file
try
{
var path = ModdedSavePath(saveSlot);
@@ -346,199 +314,23 @@ public void LoadGame(int saveSlot, Action callback)
}
ModHooks.OnLoadLocalSettings(this.moddedData);
- Platform.Current.ReadSaveSlot
- (
- saveSlot,
- delegate (byte[] fileBytes)
+ // Let vanilla handle Platform file reading (format, decryption, etc.)
+ orig_LoadGame(saveSlot, (success) =>
+ {
+ if (success)
{
- bool obj;
- try
- {
- bool flag = this.gameConfig.useSaveEncryption && !Platform.Current.IsFileSystemProtected;
- string json;
- if (flag)
- {
- BinaryFormatter binaryFormatter = new BinaryFormatter();
- MemoryStream serializationStream = new MemoryStream(fileBytes);
- string encryptedString = (string)binaryFormatter.Deserialize(serializationStream);
- json = Encryption.Decrypt(encryptedString);
- }
- else
- {
- json = Encoding.UTF8.GetString(fileBytes);
- }
-
- SaveGameData saveGameData;
-
- try
- {
- saveGameData = JsonConvert.DeserializeObject(json, new JsonSerializerSettings()
- {
- ContractResolver = ShouldSerializeContractResolver.Instance,
- TypeNameHandling = TypeNameHandling.Auto,
- ObjectCreationHandling = ObjectCreationHandling.Replace,
- Converters = JsonConverterTypes.ConverterTypes
- });
- }
- catch (Exception e)
- {
- Logger.APILogger.LogError("Failed to read save using Json.NET (GameManager::LoadGame), falling back.");
- Logger.APILogger.LogError(e);
-
- saveGameData = JsonUtility.FromJson(json);
- }
-
- global::PlayerData instance = saveGameData.playerData;
- SceneData instance2 = saveGameData.sceneData;
- global::PlayerData.instance = instance;
- this.playerData = instance;
- SceneData.instance = instance2;
- ModHooks.OnAfterSaveGameLoad(saveGameData);
- this.sceneData = instance2;
- this.profileID = saveSlot;
- this.inputHandler.RefreshPlayerData();
- ModHooks.OnSavegameLoad(saveSlot);
- obj = true;
- }
- catch (Exception ex)
- {
- Debug.LogFormat
- (
- "Error loading save file for slot {0}: {1}",
- new object[]
- {
- saveSlot,
- ex
- }
- );
- obj = false;
- }
-
- if (callback != null)
- {
- callback(obj);
- }
+ var saveGameData = new SaveGameData(this.playerData, this.sceneData);
+ ModHooks.OnAfterSaveGameLoad(saveGameData);
+ ModHooks.OnSavegameLoad(saveSlot);
}
- );
+ callback?.Invoke(success);
+ });
}
#endregion
#region GetSaveStatsForSlot
-
- [MonoModReplace]
- public void GetSaveStatsForSlot(int saveSlot, Action callback)
- {
- if (!Platform.IsSaveSlotIndexValid(saveSlot))
- {
- Debug.LogErrorFormat
- (
- "Cannot get save stats for invalid slot {0}",
- new object[]
- {
- saveSlot
- }
- );
- if (callback != null)
- {
- CoreLoop.InvokeNext(delegate { callback(null); });
- }
-
- return;
- }
-
- Platform.Current.ReadSaveSlot
- (
- saveSlot,
- delegate (byte[] fileBytes)
- {
- if (fileBytes == null)
- {
- if (callback != null)
- {
- CoreLoop.InvokeNext(delegate { callback(null); });
- }
-
- return;
- }
-
- try
- {
- bool flag = this.gameConfig.useSaveEncryption && !Platform.Current.IsFileSystemProtected;
- string json;
- if (flag)
- {
- BinaryFormatter binaryFormatter = new BinaryFormatter();
- MemoryStream serializationStream = new MemoryStream(fileBytes);
- string encryptedString = (string)binaryFormatter.Deserialize(serializationStream);
- json = Encryption.Decrypt(encryptedString);
- }
- else
- {
- json = Encoding.UTF8.GetString(fileBytes);
- }
-
- SaveGameData saveGameData;
- try
- {
- saveGameData = JsonConvert.DeserializeObject(json, new JsonSerializerSettings()
- {
- ContractResolver = ShouldSerializeContractResolver.Instance,
- TypeNameHandling = TypeNameHandling.Auto,
- ObjectCreationHandling = ObjectCreationHandling.Replace,
- Converters = JsonConverterTypes.ConverterTypes
- });
- }
- catch (Exception)
- {
- // Not a huge deal, this happens on saves with mod data which haven't been converted yet.
- Logger.APILogger.LogWarn($"Failed to get save stats for slot {saveSlot} using Json.NET, falling back");
-
- saveGameData = JsonUtility.FromJson(json);
- }
-
- global::PlayerData playerData = saveGameData.playerData;
- SaveStats saveStats = new SaveStats
- (
- playerData.GetInt(nameof(PlayerData.maxHealthBase)),
- playerData.GetInt(nameof(PlayerData.geo)),
- playerData.GetVariable(nameof(PlayerData.mapZone)),
- playerData.GetFloat(nameof(PlayerData.playTime)),
- playerData.GetInt(nameof(PlayerData.MPReserveMax)),
- playerData.GetInt(nameof(PlayerData.permadeathMode)),
- playerData.GetBool(nameof(PlayerData.bossRushMode)),
- playerData.GetFloat(nameof(PlayerData.completionPercentage)),
- playerData.GetBool(nameof(PlayerData.unlockedCompletionRate))
- );
- if (callback != null)
- {
- CoreLoop.InvokeNext(delegate { callback(saveStats); });
- }
- }
- catch (Exception ex)
- {
- Debug.LogError
- (
- string.Concat
- (
- new object[]
- {
- "Error while loading save file for slot ",
- saveSlot,
- " Exception: ",
- ex
- }
- )
- );
- if (callback != null)
- {
- CoreLoop.InvokeNext(delegate { callback(null); });
- }
- }
- }
- );
- }
-
+ // No mod hooks needed here; vanilla implementation handles Platform file reading correctly.
#endregion
#region LoadSceneAdditive
diff --git a/Assembly-CSharp/Patches/HeroController.cs b/Assembly-CSharp/Patches/HeroController.cs
index f0430b60..7cd8e070 100644
--- a/Assembly-CSharp/Patches/HeroController.cs
+++ b/Assembly-CSharp/Patches/HeroController.cs
@@ -638,7 +638,6 @@ public void TakeDamage(GameObject go, CollisionSide damageSide, int damageAmount
if (this.cState.wallSliding)
{
this.cState.wallSliding = false;
- this.wallSlideVibrationPlayer.Stop();
}
if (this.cState.touchingWall)
@@ -654,13 +653,13 @@ public void TakeDamage(GameObject go, CollisionSide damageSide, int damageAmount
if (this.cState.bouncing)
{
this.CancelBounce();
- this.rb2d.velocity = new Vector2(this.rb2d.velocity.x, 0f);
+ this.rb2d.linearVelocity = new Vector2(this.rb2d.linearVelocity.x, 0f);
}
if (this.cState.shroomBouncing)
{
this.CancelBounce();
- this.rb2d.velocity = new Vector2(this.rb2d.velocity.x, 0f);
+ this.rb2d.linearVelocity = new Vector2(this.rb2d.linearVelocity.x, 0f);
}
if (!flag)
@@ -879,7 +878,7 @@ private void Dash()
Vector2 vector = OrigDashVector();
vector = ModHooks.DashVelocityChange(vector);
- rb2d.velocity = vector;
+ rb2d.linearVelocity = vector;
dash_timer += Time.deltaTime;
}
diff --git a/Assembly-CSharp/Patches/Language.cs b/Assembly-CSharp/Patches/Language.cs
index 5fbd34c2..aa54b045 100644
--- a/Assembly-CSharp/Patches/Language.cs
+++ b/Assembly-CSharp/Patches/Language.cs
@@ -1,37 +1,30 @@
-using System.Collections.Generic;
-using MonoMod;
-using UnityEngine;
-
// ReSharper disable All
-#pragma warning disable 1591, CS0649
+#pragma warning disable 1591
namespace Modding.Patches
{
- [MonoModPatch("global::Language.Language")]
- public static class Language
+ // Language class moved to TeamCherry.Localization.dll in Unity 6.
+ // Hooking is now done via MonoMod.RuntimeDetour in ModHooks.InitLanguageHook().
+ internal static class Language
{
- [MonoModIgnore]
- private static Dictionary> currentEntrySheets;
-
- public static string GetInternal(string key, string sheetTitle)
+ internal static string GetInternal(string key, string sheetTitle)
{
- if (currentEntrySheets == null || !currentEntrySheets.ContainsKey(sheetTitle))
- {
- Debug.LogError($"The sheet with title \"{sheetTitle}\" does not exist!");
- return string.Empty;
- }
-
- if (currentEntrySheets[sheetTitle].ContainsKey(key))
- {
- return currentEntrySheets[sheetTitle][key];
- }
-
- return "#!#" + key + "#!#";
+ return TeamCherry.Localization.Language.Get(key, sheetTitle);
}
+ }
+}
+// Backward-compatibility shim: mods compiled against Unity 5 API reference
+// [Assembly-CSharp]Language.Language. We inject this stub so those mods
+// don't get TypeLoadException at runtime.
+namespace Language
+{
+ public static class Language
+ {
public static string Get(string key, string sheetTitle)
- {
- return ModHooks.LanguageGet(key, sheetTitle);
- }
+ => TeamCherry.Localization.Language.Get(key, sheetTitle);
+
+ public static bool Has(string key, string sheetTitle)
+ => TeamCherry.Localization.Language.Has(key, sheetTitle);
}
-}
\ No newline at end of file
+}
diff --git a/Assembly-CSharp/Patches/MenuButtonList.cs b/Assembly-CSharp/Patches/MenuButtonList.cs
index 57b084e7..57c125f1 100644
--- a/Assembly-CSharp/Patches/MenuButtonList.cs
+++ b/Assembly-CSharp/Patches/MenuButtonList.cs
@@ -65,7 +65,7 @@ public void ClearSelectables()
public void RecalculateNavigation()
{
- menuButtonLists.Remove(this);
+ menuButtonLists?.Remove(this);
Start();
}
diff --git a/Assembly-CSharp/Patches/SuppressPreloadException/GameCameras.cs b/Assembly-CSharp/Patches/SuppressPreloadException/GameCameras.cs
index 5956293c..ac835a8e 100644
--- a/Assembly-CSharp/Patches/SuppressPreloadException/GameCameras.cs
+++ b/Assembly-CSharp/Patches/SuppressPreloadException/GameCameras.cs
@@ -18,7 +18,7 @@ public static GameCameras instance
{
if (GameCameras._instance == null)
{
- GameCameras._instance = UnityEngine.Object.FindObjectOfType();
+ GameCameras._instance = UnityEngine.Object.FindFirstObjectByType();
if (GameCameras._instance == null)
{
Debug.LogError("Couldn't find GameCameras, make sure one exists in the scene.");
diff --git a/Assembly-CSharp/Patches/UIManager.cs b/Assembly-CSharp/Patches/UIManager.cs
index 0de510dd..75980c91 100644
--- a/Assembly-CSharp/Patches/UIManager.cs
+++ b/Assembly-CSharp/Patches/UIManager.cs
@@ -25,7 +25,7 @@ public static UIManager get_instance()
{
if (UIManager._instance == null)
{
- UIManager._instance = UnityEngine.Object.FindObjectOfType();
+ UIManager._instance = UnityEngine.Object.FindFirstObjectByType();
if (UIManager._instance == null)
{
diff --git a/PrePatcher/Program.cs b/PrePatcher/Program.cs
index e9180f40..256c86da 100644
--- a/PrePatcher/Program.cs
+++ b/PrePatcher/Program.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -14,6 +15,18 @@ internal class Program
{
private static void Main(string[] args)
{
+ // --mmhook mode: add type forwarders from one MMHOOK assembly into another
+ if (args.Length >= 1 && args[0] == "--mmhook")
+ {
+ if (args.Length < 3)
+ {
+ Console.WriteLine("Usage: PrePatcher.exe --mmhook ");
+ return;
+ }
+ PatchMMHook(args[1], args[2]);
+ return;
+ }
+
if (args.Length < 2)
{
Console.WriteLine("Usage: PrePatcher.exe ");
@@ -134,6 +147,41 @@ private static void Main(string[] args)
}
}
+ // Add type forwarders for types that moved from Assembly-CSharp to TeamCherry.*.dll in Unity 6.
+ // This covers TeamCherry.TK2D (tk2d sprite system) and TeamCherry.Cinematics (video player).
+ // This allows mods compiled against the old API to load without TypeLoadException.
+ string baseDir = Path.GetDirectoryName(Path.GetFullPath(args[0]));
+ string[] teamCherryDlls = { "TeamCherry.TK2D.dll", "TeamCherry.Cinematics.dll" };
+ foreach (string dllName in teamCherryDlls)
+ {
+ string dllPath = Path.Combine(baseDir, dllName);
+ if (!File.Exists(dllPath))
+ {
+ Console.WriteLine($"{dllName} not found, skipping type forwarders");
+ continue;
+ }
+ using ModuleDefinition tcModule = ModuleDefinition.ReadModule(dllPath);
+ var tcRef = new AssemblyNameReference(
+ tcModule.Assembly.Name.Name,
+ tcModule.Assembly.Name.Version
+ );
+ module.AssemblyReferences.Add(tcRef);
+
+ int forwardersAdded = 0;
+ foreach (TypeDefinition type in tcModule.Types)
+ {
+ if (!type.IsPublic) continue;
+ if (module.GetType(type.Namespace, type.Name) != null) continue;
+ if (module.ExportedTypes.Any(e => e.Namespace == type.Namespace && e.Name == type.Name)) continue;
+
+ var exported = new ExportedType(type.Namespace, type.Name, module, tcRef);
+ exported.Attributes = Mono.Cecil.TypeAttributes.Forwarder;
+ module.ExportedTypes.Add(exported);
+ forwardersAdded++;
+ }
+ Console.WriteLine($"Added {forwardersAdded} type forwarders to {dllName}");
+ }
+
module.Write(args[1]);
Console.WriteLine("Changed " + changes + " get/set calls");
@@ -266,6 +314,71 @@ ILProcessor il
instr.Operand = ldstr.Operand;
}
+ // Adds type forwarders into targetPath for all public types in sourcePath that don't already exist in target.
+ // Used to forward On.tk2dSprite etc. from MMHOOK_Assembly-CSharp → MMHOOK_TeamCherry.TK2D.
+ private static void PatchMMHook(string targetPath, string sourcePath)
+ {
+ try
+ {
+ string targetFull = Path.GetFullPath(targetPath);
+ string sourceFull = Path.GetFullPath(sourcePath);
+
+ if (!File.Exists(sourceFull))
+ {
+ Console.WriteLine($"Source MMHOOK not found: {sourceFull}, skipping.");
+ return;
+ }
+ if (!File.Exists(targetFull))
+ {
+ Console.WriteLine($"Target MMHOOK not found: {targetFull}, skipping.");
+ return;
+ }
+
+ var resolver = new DefaultAssemblyResolver();
+ resolver.AddSearchDirectory(Path.GetDirectoryName(targetFull));
+ var readerParams = new ReaderParameters { AssemblyResolver = resolver };
+
+ string tempPath = targetFull + ".tmp";
+ int added = 0;
+
+ // Scope the using blocks so files are released before we rename
+ using (ModuleDefinition target = ModuleDefinition.ReadModule(targetFull, readerParams))
+ using (ModuleDefinition source = ModuleDefinition.ReadModule(sourceFull, readerParams))
+ {
+ var sourceRef = new AssemblyNameReference(
+ source.Assembly.Name.Name,
+ source.Assembly.Name.Version
+ );
+ if (!target.AssemblyReferences.Any(r => r.Name == sourceRef.Name))
+ target.AssemblyReferences.Add(sourceRef);
+
+ foreach (TypeDefinition type in source.Types)
+ {
+ if (!type.IsPublic) continue;
+ if (target.GetType(type.Namespace, type.Name) != null) continue;
+ if (target.ExportedTypes.Any(e => e.Namespace == type.Namespace && e.Name == type.Name)) continue;
+
+ var exported = new ExportedType(type.Namespace, type.Name, target, sourceRef);
+ exported.Attributes = Mono.Cecil.TypeAttributes.Forwarder;
+ target.ExportedTypes.Add(exported);
+ added++;
+ }
+
+ target.Write(tempPath);
+ }
+
+ // Both modules are now closed; safely replace the target
+ File.Delete(targetFull);
+ File.Move(tempPath, targetFull);
+ Console.WriteLine($"Added {added} type forwarders from {Path.GetFileName(sourceFull)} into {Path.GetFileName(targetFull)}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"PatchMMHook failed: {ex}");
+ Environment.Exit(1);
+ }
+ }
+
private static MethodDefinition GenerateSwappedMethod(TypeDefinition methodParent, MethodReference oldMethod)
{
MethodDefinition swapped = new