Decompiled source of AchievementPatch v1.0.0

Mods/AchievementPatch.dll

Decompiled a day ago
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<int, string> _playerStateNames = new Dictionary<int, string>();

	private static object _psInstance;

	private static readonly HashSet<int> _alreadyFired = new HashSet<int>();

	private static readonly HashSet<int> _completedStatues = new HashSet<int>();

	private static int _drinkingGameStreak = 0;

	private static bool _drinkingGameMissedThisRound = false;

	private static readonly Dictionary<int, string> _fishNames = new Dictionary<int, string>();

	private static readonly HashSet<int> _fishTypesSeen = new HashSet<int>();

	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<KeyValuePair<int, string>> list = _fishNames.Where((KeyValuePair<int, string> 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<string> 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;
		}
	}
}