using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using AchievementPatch; using HarmonyLib; using MelonLoader; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Plugin), "Achievement Patch", "1.0.0", "you", null)] [assembly: MelonGame("The Sledding Corporation", "Sledding Game")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] [assembly: AssemblyCompany("AchievementPatch")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("AchievementPatch")] [assembly: AssemblyTitle("AchievementPatch")] [assembly: AssemblyVersion("1.0.0.0")] namespace AchievementPatch; public class Plugin : MelonMod { private const int ACH_DRIVE_BY = 5; private const int ACH_JUST_A_BUMP = 7; private const int ACH_TRAINING_ARC = 12; private const int ACH_FIREBALL = 15; private const int ACH_BARISTA = 16; private const int ACH_SLED_MASTER = 17; private const int ACH_DRINKING_GAME = 19; private const int ACH_FISHERMAN = 27; private const int ACH_PROFESSIONAL_DRINKER = 32; private const int ACH_DO_YOU_WANT_TO = 33; private const int STAT_SNOWMEN_MADE = 7; private const int STAT_MARSHMALLOWS_BURNED = 28; private const int STAT_LIQUID_CONSUMED = 31; private const int STAT_VENDING_MACHINES = 32; private const int STAT_DISTANCE_SLED = 37; private const float THRESHOLD_DISTANCE_SLED = 1000000f; private const float THRESHOLD_DRINKING_GAME = 1000f; private const float THRESHOLD_BARISTA = 1000f; private const int THRESHOLD_FIREBALL = 5; private const int THRESHOLD_FISHERMAN_UNIQUE = 20; private const int THRESHOLD_TRAINING_ARC_STATUES = 5; private const int THRESHOLD_PROFESSIONAL_DRINKER_STREAK = 100; private const float SWEEP_INTERVAL_SECONDS = 5f; private static float _sweepTimer = 0f; private static Type _tPlayerSavedStats; private static Type _tAchievementController; private static Type _tAchievementType; private static Type _tStatType; private static Type _tPlayerControl; private static Type _tStatueSetup; private static Type _tStatueUnlockSystem; private static Type _tDrinkingGame; private static Type _tPlayerState; private static MethodInfo _miUnlockAchievement; private static MethodInfo _miGetAcInstance; private static FieldInfo _fiAcInstance; private static MethodInfo _miGetPsInstance; private static FieldInfo _fiPsInstance; private static MethodInfo _miGetStatAsFloat; private static MethodInfo _miGetStatAsInt; private static PropertyInfo _piFishTypesCaughtSet; private static PropertyInfo _piLocalPlayer; private static MethodInfo _miGetPlayerState; private static readonly Dictionary _playerStateNames = new Dictionary(); private static object _psInstance; private static readonly HashSet _alreadyFired = new HashSet(); private static readonly HashSet _completedStatues = new HashSet(); private static int _drinkingGameStreak = 0; private static bool _drinkingGameMissedThisRound = false; private static readonly Dictionary _fishNames = new Dictionary(); private static readonly HashSet _fishTypesSeen = new HashSet(); private static int _totalFishCaught = 0; private static float _missingReportTimer = 0f; private const float MISSING_REPORT_INTERVAL_SECONDS = 60f; public override void OnInitializeMelon() { ((MelonBase)this).LoggerInstance.Msg("AchievementPatch v1.0.0 loading..."); _tPlayerSavedStats = AccessTools.TypeByName("PlayerSavedStats"); _tAchievementController = AccessTools.TypeByName("AchievementController"); _tAchievementType = AccessTools.TypeByName("AchievementType"); _tStatType = AccessTools.TypeByName("StatType"); _tPlayerControl = AccessTools.TypeByName("PlayerControl"); _tStatueSetup = AccessTools.TypeByName("StatueSetup"); _tStatueUnlockSystem = AccessTools.TypeByName("StatueUnlockSystem"); _tDrinkingGame = AccessTools.TypeByName("DrinkingGame"); _tPlayerState = AccessTools.TypeByName("PlayerState"); ((MelonBase)this).LoggerInstance.Msg("PlayerSavedStats: " + (_tPlayerSavedStats?.FullName ?? "NOT FOUND")); ((MelonBase)this).LoggerInstance.Msg("AchievementController:" + (_tAchievementController?.FullName ?? "NOT FOUND")); ((MelonBase)this).LoggerInstance.Msg("StatueSetup: " + (_tStatueSetup?.FullName ?? "NOT FOUND")); ((MelonBase)this).LoggerInstance.Msg("StatueUnlockSystem: " + (_tStatueUnlockSystem?.FullName ?? "NOT FOUND")); ((MelonBase)this).LoggerInstance.Msg("DrinkingGame: " + (_tDrinkingGame?.FullName ?? "NOT FOUND")); try { Type type = AccessTools.TypeByName("FishType"); if (type != null && type.IsEnum) { string[] names = Enum.GetNames(type); foreach (string value in names) { try { _fishNames[Convert.ToInt32(Enum.Parse(type, value))] = value; } catch { } } ((MelonBase)this).LoggerInstance.Msg($"FishType: {_fishNames.Count} values loaded"); } } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("FishType enum load: " + ex.Message); } if (_tPlayerSavedStats != null) { _miGetPsInstance = _tPlayerSavedStats.GetMethod("get_Instance", BindingFlags.Static | BindingFlags.Public); if (_miGetPsInstance == null) { _fiPsInstance = _tPlayerSavedStats.GetField("Instance", BindingFlags.Static | BindingFlags.Public) ?? _tPlayerSavedStats.GetField("instance", BindingFlags.Static | BindingFlags.Public) ?? _tPlayerSavedStats.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic); } _piFishTypesCaughtSet = _tPlayerSavedStats.GetProperty("_fishTypesCaughtSet", BindingFlags.Instance | BindingFlags.Public); _miGetStatAsFloat = AccessTools.Method(_tPlayerSavedStats, "GetStatAsFloat", (Type[])null, (Type[])null); _miGetStatAsInt = AccessTools.Method(_tPlayerSavedStats, "GetStatAsInt", (Type[])null, (Type[])null); } if (_tAchievementController != null) { _miUnlockAchievement = AccessTools.Method(_tAchievementController, "UnlockAchievement", (Type[])null, (Type[])null); _miGetAcInstance = _tAchievementController.GetMethod("get_Instance", BindingFlags.Static | BindingFlags.Public); if (_miGetAcInstance == null) { _fiAcInstance = _tAchievementController.GetField("Instance", BindingFlags.Static | BindingFlags.Public) ?? _tAchievementController.GetField("instance", BindingFlags.Static | BindingFlags.Public) ?? _tAchievementController.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic); } } if (_tPlayerControl != null) { _piLocalPlayer = _tPlayerControl.GetProperty("LocalPlayerInstance", BindingFlags.Static | BindingFlags.Public) ?? _tPlayerControl.GetProperty("LocalPlayer", BindingFlags.Static | BindingFlags.Public) ?? _tPlayerControl.GetProperty("Local", BindingFlags.Static | BindingFlags.Public); _miGetPlayerState = AccessTools.Method(_tPlayerControl, "GetPlayerState", new Type[0], (Type[])null) ?? AccessTools.Method(_tPlayerControl, "GetPlayerState", (Type[])null, (Type[])null); } if (_tPlayerState != null && _tPlayerState.IsEnum) { string[] names = Enum.GetNames(_tPlayerState); foreach (string value2 in names) { try { _playerStateNames[Convert.ToInt32(Enum.Parse(_tPlayerState, value2))] = value2; } catch { } } } Harmony harmonyInstance = ((MelonBase)this).HarmonyInstance; PatchOptional(harmonyInstance, _tPlayerSavedStats, "Initialise", "PostfixCaptureInstance"); PatchOptional(harmonyInstance, _tPlayerSavedStats, "SetValues", "PostfixCaptureInstance"); PatchOptional(harmonyInstance, _tPlayerSavedStats, "AddFishingStats", "PostfixAddFishingStats"); PatchOptional(harmonyInstance, _tPlayerSavedStats, "AddFishToInventory", "PostfixAddFishingStats"); PatchOptional(harmonyInstance, _tPlayerSavedStats, "AddToStatCounter", "PostfixAddToStatCounter"); PatchOptional(harmonyInstance, AccessTools.TypeByName("Snowball"), "LocalPlayerHitOtherPlayer", "PostfixSnowballHit"); PatchOptional(harmonyInstance, AccessTools.TypeByName("Sled"), "RpcLogic___Target_NotifySledOwnerThatAPlayerWasHit___328543758", "PostfixSledBump"); PatchOptional(harmonyInstance, _tStatueSetup, "InteractableEvent_UnlockItem", "PostfixStatueUnlock"); PatchOptional(harmonyInstance, _tDrinkingGame, "Local_StartActivity", "PostfixDrinkingStart"); PatchOptional(harmonyInstance, _tDrinkingGame, "Local_SetHits", "PostfixDrinkingHit"); PatchOptional(harmonyInstance, _tDrinkingGame, "Local_SetMisses", "PostfixDrinkingMiss"); ((MelonBase)this).LoggerInstance.Msg("AchievementPatch loaded — silent watch begins. Sweep every 5s."); } private void PatchOptional(Harmony h, Type t, string method, string postfixName) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown if (t == null) { ((MelonBase)this).LoggerInstance.Warning("Type for " + method + " is null, skipping"); return; } MethodInfo methodInfo = AccessTools.Method(t, method, (Type[])null, (Type[])null); if (methodInfo == null) { ((MelonBase)this).LoggerInstance.Warning(t.Name + "." + method + " not found, skipping"); return; } MethodInfo method2 = typeof(Plugin).GetMethod(postfixName, BindingFlags.Static | BindingFlags.NonPublic); h.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(method2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ((MelonBase)this).LoggerInstance.Msg("Patched " + t.Name + "." + method); } private static object ResolveSingleton(MethodInfo miGetter, FieldInfo fiStatic, Type type) { try { if (miGetter != null) { return miGetter.Invoke(null, null); } if (fiStatic != null) { return fiStatic.GetValue(null); } if (type != null) { MethodInfo method = typeof(Object).GetMethod("FindObjectOfType", new Type[1] { typeof(Type) }); if (method != null) { return method.Invoke(null, new object[1] { type }); } } } catch (Exception ex) { MelonLogger.Warning("ResolveSingleton(" + type?.Name + "): " + ex.Message); } return null; } private static void UnlockAchievement(int achId, string display) { try { if (!_alreadyFired.Add(achId)) { return; } if (_miUnlockAchievement == null || _tAchievementType == null) { MelonLogger.Warning($"[Unlock] {display} (id {achId}): UnlockAchievement reflection missing"); return; } object obj = ResolveSingleton(_miGetAcInstance, _fiAcInstance, _tAchievementController); if (obj == null) { MelonLogger.Warning("[Unlock] " + display + ": AchievementController instance not found"); return; } object obj2 = Enum.ToObject(_tAchievementType, achId); _miUnlockAchievement.Invoke(obj, new object[1] { obj2 }); MelonLogger.Msg($"[Unlock] {display} (id {achId}) — fired via AchievementController.UnlockAchievement"); } catch (Exception ex) { MelonLogger.Warning($"UnlockAchievement({achId}): {ex.Message}"); } } private static void CaptureInstance(object inst) { if (inst != null && _psInstance == null) { _psInstance = inst; MelonLogger.Msg("[Capture] PlayerSavedStats instance cached"); } } private static void PostfixCaptureInstance(object __instance) { CaptureInstance(__instance); } private static void PostfixAddFishingStats(object __instance, object fishData) { CaptureInstance(__instance); try { int? fishTypeId = GetFishTypeId(fishData); if (fishTypeId.HasValue) { bool flag = _fishTypesSeen.Add(fishTypeId.Value); _totalFishCaught++; MelonLogger.Msg($"[Fish] caught {FishName(fishTypeId.Value)} (id {fishTypeId.Value}) {(flag ? "NEW" : "dup")}. Unique {_fishTypesSeen.Count}/20, total {_totalFishCaught}"); } } catch (Exception ex) { MelonLogger.Warning("AddFishingStats postfix: " + ex.Message); } CheckFisherman(__instance); } private static int? GetFishTypeId(object fishData) { if (fishData == null) { return null; } try { Type type = fishData.GetType(); PropertyInfo propertyInfo = type.GetProperty("fishType") ?? type.GetProperty("FishType"); if (propertyInfo != null) { object value = propertyInfo.GetValue(fishData); if (value != null) { return Convert.ToInt32(value); } } FieldInfo fieldInfo = type.GetField("fishType") ?? type.GetField("FishType") ?? type.GetField("_fishType", BindingFlags.Instance | BindingFlags.NonPublic); if (fieldInfo != null) { object value2 = fieldInfo.GetValue(fishData); if (value2 != null) { return Convert.ToInt32(value2); } } } catch { } return null; } private static string FishName(int id) { if (!_fishNames.TryGetValue(id, out var value)) { return $"Type{id}"; } return value; } private static void LogMissingFish() { try { List> list = _fishNames.Where((KeyValuePair kv) => kv.Key > 0).ToList(); int value = _fishTypesSeen.Count((int id) => id > 0); if (list.Count == 0) { MelonLogger.Msg($"[Missing] ({value}/20, {_totalFishCaught} total catches) — fish names unavailable"); return; } List list2 = (from kv in list where !_fishTypesSeen.Contains(kv.Key) orderby kv.Key select kv.Value).ToList(); if (list2.Count == 0) { MelonLogger.Msg($"[Missing] All 20 unique fish caught ({_totalFishCaught} total) — Fisherman should be unlocked."); } else { MelonLogger.Msg($"[Missing] ({value}/20, {_totalFishCaught} total catches) Still need: {string.Join(", ", list2)}"); } } catch (Exception ex) { MelonLogger.Warning("LogMissingFish: " + ex.Message); } } private static void PostfixAddToStatCounter(object __instance, object __0) { try { CaptureInstance(__instance); switch (Convert.ToInt32(__0)) { case 7: UnlockAchievement(33, "Do You Want To (build a snowman)"); break; case 28: if (TryGetStatAsInt(__instance, 28) >= 5) { UnlockAchievement(15, "Fireball (5 marshmallows)"); } break; } } catch (Exception ex) { MelonLogger.Warning("AddToStatCounter postfix: " + ex.Message); } } private static void PostfixSnowballHit(object __instance, int hitPlayerId) { try { if (_piLocalPlayer == null || _miGetPlayerState == null) { return; } object value = _piLocalPlayer.GetValue(null); if (value != null) { int num = Convert.ToInt32(_miGetPlayerState.Invoke(value, null)); string value2; string a = (_playerStateNames.TryGetValue(num, out value2) ? value2 : "?"); if ((_playerStateNames.Count > 0) ? string.Equals(a, "Sled", StringComparison.OrdinalIgnoreCase) : (num == 2)) { UnlockAchievement(5, "Drive By (snowball hit while sledding)"); } } } catch (Exception ex) { MelonLogger.Warning("DriveBy postfix: " + ex.Message); } } private static void PostfixSledBump() { UnlockAchievement(7, "Just a Bump (sled hit a player)"); } private static void PostfixStatueUnlock(object __instance) { try { int num; try { num = Convert.ToInt32(__instance.GetType().GetMethod("GetInstanceID").Invoke(__instance, null)); } catch { num = __instance?.GetHashCode() ?? 0; } if (_completedStatues.Add(num)) { MelonLogger.Msg($"[TrainingArc] statue completed (id {num}) — {_completedStatues.Count}/{5}"); if (_completedStatues.Count >= 5) { UnlockAchievement(12, $"Training Arc ({5} statues completed)"); } } } catch (Exception ex) { MelonLogger.Warning("StatueUnlock postfix: " + ex.Message); } } private static void PostfixDrinkingStart(object __instance, object __0) { _drinkingGameStreak = 0; _drinkingGameMissedThisRound = false; MelonLogger.Msg("[ProDrinker] new round started — streak reset"); } private static void PostfixDrinkingHit(object __instance, object __0) { try { _drinkingGameStreak = Convert.ToInt32(__0); if (!_drinkingGameMissedThisRound && _drinkingGameStreak >= 100) { UnlockAchievement(32, $"Professional Drinker ({100}-streak in DrinkingGame)"); } } catch { } } private static void PostfixDrinkingMiss(object __instance, object __0) { try { if (Convert.ToInt32(__0) > 0) { _drinkingGameMissedThisRound = true; } } catch { } } public override void OnUpdate() { _missingReportTimer += Time.deltaTime; if (_missingReportTimer >= 60f) { _missingReportTimer = 0f; LogMissingFish(); } _sweepTimer += Time.deltaTime; if (_sweepTimer < 5f) { return; } _sweepTimer = 0f; try { Sweep(); } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Sweep: " + ex.Message); } } private void Sweep() { object obj = _psInstance ?? ResolveSingleton(_miGetPsInstance, _fiPsInstance, _tPlayerSavedStats); if (obj != null) { CaptureInstance(obj); float num = TryGetStatAsFloat(obj, 37); if (num >= 1000000f) { UnlockAchievement(17, $"Sled Master ({num:F0} m sled)"); } float num2 = TryGetStatAsFloat(obj, 31); if (num2 >= 1000f) { UnlockAchievement(19, $"Drinking Game ({num2:F0} cocoa consumed)"); } float num3 = TryGetStatAsFloat(obj, 32); if (num3 >= 1000f) { UnlockAchievement(16, $"Barista ({num3:F0} cups made)"); } int num4 = TryGetStatAsInt(obj, 28); if (num4 >= 5) { UnlockAchievement(15, $"Fireball ({num4}/{5} marshmallows)"); } CheckFisherman(obj); } } private static void CheckFisherman(object stats) { try { if (_piFishTypesCaughtSet == null || !(_piFishTypesCaughtSet.GetValue(stats) is IEnumerable enumerable)) { return; } foreach (object item in enumerable) { try { _fishTypesSeen.Add(Convert.ToInt32(item)); } catch { } } int num = _fishTypesSeen.Count((int id) => id > 0); if (num >= 20) { UnlockAchievement(27, $"Fisherman ({num} unique fish caught)"); } } catch (Exception ex) { MelonLogger.Warning("CheckFisherman: " + ex.Message); } } private static float TryGetStatAsFloat(object stats, int statId) { try { if (_miGetStatAsFloat == null || _tStatType == null) { return 0f; } object obj = Enum.ToObject(_tStatType, statId); return Convert.ToSingle(_miGetStatAsFloat.Invoke(stats, new object[1] { obj })); } catch { return 0f; } } private static int TryGetStatAsInt(object stats, int statId) { try { if (_miGetStatAsInt == null || _tStatType == null) { return 0; } object obj = Enum.ToObject(_tStatType, statId); return Convert.ToInt32(_miGetStatAsInt.Invoke(stats, new object[1] { obj })); } catch { return 0; } } }