Initial commit

This commit is contained in:
2026-09-19 14:39:03 +01:00
commit 3449f141fa
21 changed files with 3349 additions and 0 deletions
@@ -0,0 +1,51 @@
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper
{
//<summary>
// Currently only handles auto-clicking the bailout button when the bankruptcy popup appears
// but can be expanded in the future for doing it before the popup appears, or for other related features.
//</summary>
internal static class BankrupcyRefillHandler
{
internal static void Load()
{
var postfix = typeof(BankrupcyRefillHandler).GetMethod(
nameof(AutoClickBailout), BindingFlags.Static | BindingFlags.NonPublic)
?? throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} - BankrupcyRefillHandler.AutoClickBailout method not found.");
var harmonyPostfix = new HarmonyLib.HarmonyMethod(postfix);
// Hook into the popup showing up
Plugin.PatchMethod("YoudaGames.MonopolyPoker.Popups.BailoutPopup", "Show", harmonyPostfix, true);
LogInfo("Hooked into BailoutPopup.Show for auto-refill.");
}
private static void AutoClickBailout(object __instance)
{
LogInfo("BailoutPopup appeared! Automatically clicking bailout...");
try
{
// Find and invoke the BailoutClicked method on the given instance
var clickMethod = __instance.GetType().GetMethod("BailoutClicked", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (clickMethod != null)
{
clickMethod.Invoke(__instance, null);
LogInfo("Successfully auto-clicked the Bailout button.");
}
else
{
LogError("BailoutClicked method could not be found via reflection.");
}
}
catch (Exception ex)
{
LogError($"Failed to click bailout: {ex.Message}");
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
/// <summary>
/// A Handler that plays Blackjack
/// </summary>
namespace MonopolyPokerHelper
{
internal class BlackjackHandler : FeatureHandler<BlackjackHandler>
{
}
}
+107
View File
@@ -0,0 +1,107 @@
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper;
/// <summary>
/// BepInEx 6 IL2CPP plugin that automatically claims Boardwalk free dice rolls in Monopoly Poker.
///
/// - Boardwalk Roll — Dice roll whenever available, usually available every ~90 - 180 minutes
///
/// Works by hooking SocialLayerManager.Update() and running periodic checks
/// via reflection (no compile-time game assembly references needed).
/// </summary>
internal static class BoardwalkHandler
{
// Cooldown flags (avoid spamming the same action)
private static bool _boardwalkRolled;
private static float _boardwalkRetryTimer;
private const float BOARDWALK_RETRY_INTERVAL = 60f; // re-check boardwalk every 1 minute
// BoardwalkManager cached reflection handles
private static Type _boardwalkMgrType;
private static PropertyInfo _boardwalkInstance;
private static PropertyInfo _canRollDice;
private static MethodInfo _doDiceRoll;
public static bool IsResolved { get; private set; }
public static void ResolveReflection()
{
if (IsResolved) return;
try
{
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
_boardwalkMgrType = ReflectionHelper.FindType("BoardwalkManager");
if (_boardwalkMgrType != null)
{
_boardwalkInstance = _boardwalkMgrType.GetProperty("Instance", all);
_canRollDice = _boardwalkMgrType.GetProperty("CanRollDice", all);
_doDiceRoll = _boardwalkMgrType.GetMethod("DoDiceRoll", all);
LogDebug($"BoardwalkManager: found={_boardwalkMgrType != null} " +
$"instance={_boardwalkInstance != null} " +
$"canRoll={_canRollDice != null} " +
$"doRoll={_doDiceRoll != null}");
}
else
{
LogError("BoardwalkManager type not found!");
throw new TypeLoadException(
"BoardwalkHandler failed to resolve required type 'BoardwalkManager'.");
}
IsResolved = true;
LogInfo("Reflection resolution complete.");
}
catch (Exception ex)
{
LogError($"Failed to resolve BoardwalkManager reflection: {ex}");
}
}
public static void Update(float deltaTime)
{
if (!IsResolved) return;
_boardwalkRetryTimer += deltaTime;
if (_boardwalkRolled && _boardwalkRetryTimer < BOARDWALK_RETRY_INTERVAL)
return;
TryBoardwalkRoll();
}
private static void TryBoardwalkRoll()
{
try
{
var bwInstance = _boardwalkInstance?.GetValue(null);
if (bwInstance == null) { Log.LogError("BoardwalkManager instance is null"); return; }
var canRoll = _canRollDice?.GetValue(bwInstance);
if (canRoll == null || !Convert.ToBoolean(canRoll))
{
LogDebug("Boardwalk dice roll not available yet.");
return;
}
_doDiceRoll.Invoke(bwInstance, [false]);
_boardwalkRolled = true;
_boardwalkRetryTimer = 0f;
LogInfo(">>> Rolled Boardwalk dice!");
}
catch (Exception ex)
{
LogError($"Error with Boardwalk roll: {ex.Message}");
}
}
public static void Reset()
{
_boardwalkRolled = false;
_boardwalkRetryTimer = 0f;
}
}
+564
View File
@@ -0,0 +1,564 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper;
internal static class ClaimMissionHandler
{
// Main polling cadence for mission scanning.
private const float MISSION_CHECK_INTERVAL = 20f;
// Safety limits to avoid walking very large/cyclic object graphs.
private const int MAX_DISCOVERY_NODES = 2000;
private const int MAX_DISCOVERY_DEPTH = 8;
// After repeated update failures, pause before retrying.
private const int FAILURE_BACKOFF_THRESHOLD = 3;
private const float FAILURE_BACKOFF_SECONDS = 60f;
private static float _timer;
private static float _backoffRemaining;
public static bool IsResolved { get; private set; }
private static int _consecutiveFailures;
private static Type _missionClaimHandlerType;
private static Type _missionGroupType;
private static MethodInfo _claimMissionMethod;
private static object _missionClaimHandlerInstance;
// Keeps track of successfully claimed missions across ticks.
private static readonly HashSet<string> _claimedMissionKeys = [];
// Reflection lookup cache to reduce repeated GetProperty/GetField calls.
private static readonly Dictionary<(Type Type, string Name), MemberInfo> _memberCache = [];
// Negative cache for members not found on a given type.
private static readonly HashSet<(Type Type, string Name)> _missingMembers = [];
public static void ResolveReflection()
{
try
{
if (IsResolved) return;
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
_missionClaimHandlerType = ReflectionHelper.FindType("YoudaGames.MonopolyPoker.MissionCollection.MissionClaimHandler")
?? ReflectionHelper.FindType("MissionClaimHandler");
_missionGroupType = ReflectionHelper.FindType("YoudaGames.MonopolyPoker.MissionCollection.MissionGroupType")
?? ReflectionHelper.FindType("MissionGroupType");
if (_missionClaimHandlerType == null)
{
LogWarn("ClaimMissionHandler type not found.");
return;
}
_claimMissionMethod = _missionClaimHandlerType.GetMethod(
"ClaimMission",
all,
binder: null,
types: new[] { typeof(int), typeof(int) },
modifiers: null);
if (_claimMissionMethod == null)
{
LogWarn("ClaimMission(int,int) method not found.");
return;
}
_missionClaimHandlerInstance = ReflectionHelper.GetSingletonInstance(_missionClaimHandlerType);
if (!_claimMissionMethod.IsStatic && _missionClaimHandlerInstance == null)
{
LogWarn("MissionClaimHandler instance not found for non-static ClaimMission.");
return;
}
IsResolved = true;
LogInfo("ClaimMissionHandler reflection resolved.");
}
catch (Exception ex)
{
LogError($"Failed to resolve ClaimMissionHandler reflection: {ex.Message}");
}
}
public static void Update(float deltaTime)
{
if (!IsResolved) return;
// Cooldown after consecutive failures to avoid hammering reflection/invoke paths.
if (_backoffRemaining > 0f)
{
_backoffRemaining = Math.Max(0f, _backoffRemaining - deltaTime);
if (_backoffRemaining > 0f)
return;
LogInfo("ClaimMissionHandler retrying after backoff.");
}
_timer += deltaTime;
if (_timer < MISSION_CHECK_INTERVAL)
return;
_timer = 0f;
try
{
var instance = _claimMissionMethod.IsStatic
? null
: (_missionClaimHandlerInstance ?? ReflectionHelper.GetSingletonInstance(_missionClaimHandlerType));
if (!_claimMissionMethod.IsStatic && instance == null)
{
LogError("MissionClaimHandler instance is null.");
return;
}
_missionClaimHandlerInstance = instance;
var candidates = DiscoverClaimableMissions(instance);
// Avoid duplicate ClaimMission invokes for the same mission in the same scan.
var uniqueCandidates = new HashSet<(int Group, int Id)>();
foreach (var mission in candidates)
{
if (!uniqueCandidates.Add(mission))
continue;
var key = $"{mission.Group}:{mission.Id}";
if (_claimedMissionKeys.Contains(key))
continue;
object response;
try
{
response = _claimMissionMethod.Invoke(instance, new object[] { mission.Group, mission.Id });
}
catch (TargetInvocationException ex)
{
LogError($"ClaimMission invoke failed for mission {mission.Id} in group {mission.Group}: {ex.Message}");
continue;
}
catch (TargetException ex)
{
LogError($"ClaimMission target invalid for mission {mission.Id} in group {mission.Group}: {ex.Message}");
continue;
}
catch (TargetParameterCountException ex)
{
LogError($"ClaimMission signature mismatch for mission {mission.Id} in group {mission.Group}: {ex.Message}");
continue;
}
catch (MethodAccessException ex)
{
LogError($"ClaimMission access denied for mission {mission.Id} in group {mission.Group}: {ex.Message}");
continue;
}
if (WasClaimSuccessful(response))
{
_claimedMissionKeys.Add(key);
LogInfo($">>> Claimed mission {mission.Id} in group {mission.Group}");
}
}
_consecutiveFailures = 0;
}
catch (Exception ex)
{
_consecutiveFailures++;
LogWarn($"ClaimMissionHandler update error ({_consecutiveFailures}): {ex.Message}");
if (_consecutiveFailures >= FAILURE_BACKOFF_THRESHOLD)
{
_consecutiveFailures = 0;
_backoffRemaining = FAILURE_BACKOFF_SECONDS;
LogWarn($"ClaimMissionHandler entering backoff for {FAILURE_BACKOFF_SECONDS} seconds after repeated errors.");
}
}
}
private static List<(int Group, int Id)> DiscoverClaimableMissions(object root)
{
var results = new List<(int Group, int Id)>();
if (root == null)
return results;
// Breadth-first walk over reachable objects from the root mission handler.
var queue = new Queue<(object Node, int Depth)>();
var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
var processedNodes = 0;
queue.Enqueue((root, 0));
while (queue.Count > 0)
{
var (current, depth) = queue.Dequeue();
if (current == null || !visited.Add(current))
continue;
processedNodes++;
if (processedNodes > MAX_DISCOVERY_NODES)
{
LogWarn($"Claim mission discovery reached node limit ({MAX_DISCOVERY_NODES}).");
break;
}
if (TryExtractClaimableMission(current, out var mission))
{
results.Add(mission);
}
if (depth >= MAX_DISCOVERY_DEPTH)
continue;
foreach (var child in GetChildObjects(current))
{
if (child != null)
queue.Enqueue((child, depth + 1));
}
}
return results;
}
private static bool TryExtractClaimableMission(object candidate, out (int Group, int Id) mission)
{
mission = default;
if (!TryGetBool(candidate, new[] { "CanClaim", "canClaim", "Claimable", "claimable", "IsClaimable", "isClaimable" }, out var canClaim) || !canClaim)
return false;
if (!TryGetInt(candidate, new[] { "MissionId", "missionId", "Id", "id" }, out var missionId))
return false;
if (!TryGetMissionGroup(candidate, out var missionGroup))
return false;
mission = (missionGroup, missionId);
return true;
}
private static bool TryGetMissionGroup(object source, out int group)
{
group = 0;
if (TryGetInt(source, new[] { "MissionGroupType", "missionGroupType", "GroupType", "groupType" }, out group))
return true;
if (_missionGroupType != null)
{
if (TryGetEnumAsInt(source, "MissionGroupType", out group) ||
TryGetEnumAsInt(source, "missionGroupType", out group) ||
TryGetEnumAsInt(source, "GroupType", out group) ||
TryGetEnumAsInt(source, "groupType", out group))
{
return true;
}
}
return false;
}
private static bool TryGetEnumAsInt(object source, string name, out int value)
{
value = 0;
var memberValue = GetMemberValue(source, name);
if (memberValue == null)
return false;
var type = memberValue.GetType();
if (!type.IsEnum)
return false;
value = Convert.ToInt32(memberValue);
return true;
}
private static bool TryGetInt(object source, string[] names, out int value)
{
value = 0;
foreach (var name in names)
{
var memberValue = GetMemberValue(source, name);
if (memberValue == null)
continue;
try
{
value = Convert.ToInt32(memberValue);
return true;
}
catch (InvalidCastException ex)
{
LogError($"Failed int conversion for member '{name}': {ex.Message}");
}
catch (FormatException ex)
{
LogError($"Failed int conversion for member '{name}': {ex.Message}");
}
catch (OverflowException ex)
{
LogError($"Failed int conversion for member '{name}': {ex.Message}");
}
catch (NotSupportedException ex)
{
LogError($"Failed int conversion for member '{name}': {ex.Message}");
}
}
return false;
}
private static bool TryGetBool(object source, string[] names, out bool value)
{
value = false;
foreach (var name in names)
{
var memberValue = GetMemberValue(source, name);
if (memberValue == null)
continue;
try
{
value = Convert.ToBoolean(memberValue);
return true;
}
catch (InvalidCastException ex)
{
LogError($"Failed bool conversion for member '{name}': {ex.Message}");
}
catch (FormatException ex)
{
LogError($"Failed bool conversion for member '{name}': {ex.Message}");
}
catch (NotSupportedException ex)
{
LogError($"Failed bool conversion for member '{name}': {ex.Message}");
}
}
return false;
}
private static object GetMemberValue(object source, string name)
{
ArgumentNullException.ThrowIfNull(source);
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
var type = source.GetType();
var key = (type, name);
MemberInfo member;
lock (_memberCache)
{
// Fast path: use cached member info when available.
if (_memberCache.TryGetValue(key, out member))
return GetMemberValueFromMember(member, source, name);
// Fast path: skip reflection when we've already confirmed the member is missing.
if (_missingMembers.Contains(key))
return null;
}
try
{
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
for (var current = type; current != null; current = current.BaseType)
{
var prop = current.GetProperty(name, all);
if (prop != null)
{
lock (_memberCache)
{
_memberCache[key] = prop;
}
return GetMemberValueFromMember(prop, source, name);
}
var field = current.GetField(name, all);
if (field != null)
{
lock (_memberCache)
{
_memberCache[key] = field;
}
return GetMemberValueFromMember(field, source, name);
}
}
lock (_memberCache)
{
_missingMembers.Add(key);
}
return null;
}
catch (AmbiguousMatchException ex)
{
LogError($"Member lookup for '{name}' on type '{type.FullName}' is ambiguous: {ex.Message}");
}
catch (NotSupportedException ex)
{
LogError($"Member lookup for '{name}' on type '{type.FullName}' is unsupported: {ex.Message}");
}
return null;
}
private static object GetMemberValueFromMember(MemberInfo member, object source, string name)
{
try
{
if (member is PropertyInfo property)
return property.GetValue(source);
if (member is FieldInfo field)
return field.GetValue(source);
}
catch (TargetInvocationException ex)
{
LogError($"Reading member '{name}' failed due to invocation error: {ex.Message}");
}
catch (MethodAccessException ex)
{
LogError($"Reading member '{name}' failed due to access restriction: {ex.Message}");
}
catch (FieldAccessException ex)
{
LogError($"Reading member '{name}' failed due to field access restriction: {ex.Message}");
}
catch (TargetException ex)
{
LogError($"Reading member '{name}' failed due to invalid target: {ex.Message}");
}
catch (NotSupportedException ex)
{
LogError($"Reading member '{name}' is unsupported: {ex.Message}");
}
return null;
}
private static IEnumerable<object> GetChildObjects(object source)
{
ArgumentNullException.ThrowIfNull(source);
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance;
var type = source.GetType();
foreach (var property in type.GetProperties(all))
{
if (property.GetIndexParameters().Length > 0)
continue;
object value;
try
{
value = property.GetValue(source);
}
catch (TargetInvocationException ex)
{
LogError($"Skipping child property '{property.Name}' due to invocation error: {ex.Message}");
continue;
}
catch (MethodAccessException ex)
{
LogError($"Skipping child property '{property.Name}' due to access restriction: {ex.Message}");
continue;
}
catch (NotSupportedException ex)
{
LogError($"Skipping child property '{property.Name}' due to unsupported operation: {ex.Message}");
continue;
}
foreach (var child in ExpandChildValue(value))
yield return child;
}
foreach (var field in type.GetFields(all))
{
object value;
try
{
value = field.GetValue(source);
}
catch (FieldAccessException ex)
{
LogError($"Skipping child field '{field.Name}' due to access restriction: {ex.Message}");
continue;
}
catch (NotSupportedException ex)
{
LogError($"Skipping child field '{field.Name}' due to unsupported operation: {ex.Message}");
continue;
}
catch (TargetException ex)
{
LogError($"Skipping child field '{field.Name}' due to invalid target: {ex.Message}");
continue;
}
foreach (var child in ExpandChildValue(value))
yield return child;
}
}
private static IEnumerable<object> ExpandChildValue(object value)
{
if (value == null)
yield break;
if (value is string)
yield break;
if (value is IEnumerable enumerable)
{
foreach (var item in enumerable)
{
if (item != null)
yield return item;
}
yield break;
}
if (value.GetType().IsValueType)
yield break;
yield return value;
}
private static bool WasClaimSuccessful(object response)
{
if (response == null)
return false;
if (TryGetBool(response, new[] { "Success", "success" }, out var success))
return success;
return false;
}
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
public static readonly ReferenceEqualityComparer Instance = new();
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace MonopolyPokerHelper
{
internal class DailyWheelHandler
{
// TODO: Implement DailyWheelHandler.
// It is claimable once a day. is a popup as well as a HUD element.
// Ideally auto-claim automatically and remove popup and UI elements related to it.
}
}
+83
View File
@@ -0,0 +1,83 @@
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
/// <summary>
/// Suppresses Distractions and Rolling Offers pop-ups.
/// </summary>
namespace MonopolyPokerHelper
{
internal static class DistractionHandler
{
internal static void Load()
{
// Method Distractions
var targetMethods = new (string TypeName, string MethodName)[]
{
// // Has daily free packs on login.
//("YoudaGames.MonopolyPoker.Popups.RollingOffersPopup", "Show"), // Rolling Offers Popup (the actual UI)
//("YoudaGames.MonopolyPoker.RollingOffers.MPRollingOffersManager", "OpenPopup"), //Rolling Offers Manager (triggers the popup)
//("YoudaGames.MonopolyPoker.HUD.RollingOffersHUDSideButton", "OnClick"), // HUD Button that opens the Rolling Offers Popup
// Vault related classes - blocking all methods to prevent any vault-related popups, events, or UI from functioning
("YoudaGames.MonopolyPoker.UI.HUD.Buttons.VaultHUDButton", "Show"),
// Game Boot popups
("YoudaGames.MonopolyPoker.Popups.BuyOneOrAllPopup", "Show"),
("YoudaGames.MonopolyPoker.Popups.DealPopup", "Show"),
("YoudaGames.MonopolyPoker.Popups.LimitedOffersPopup", "Show"),
};
var blockPatch = typeof(DistractionHandler).GetMethod(
nameof(BlockPrefix), BindingFlags.Static | BindingFlags.NonPublic)
?? throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} - DistractionHandler.BlockPrefix method not found.");
foreach (var (TypeName, MethodName) in targetMethods)
{
Plugin.PatchMethod(TypeName, MethodName, blockPatch);
LogInfo($"Blocked Type: {TypeName} Method: {MethodName}");
}
// GameObjects - for HUD elements that don't have a show method but are active by default
var gameObjects = new (string TypeName, string MethodName)[]
{
// BuyOneOrAll doesn't declare its own Awake, so we patch its base class
// ("YoudaGames.MonopolyPoker.UI.HUD.SideButtons.BuyOneOrAllHUDSideButton", "Awake"), Doesn't have an awake method, patching the base
("YoudaGames.MonopolyPoker.UI.HUD.SideButtons.BaseLeftHUDSideButton", "Awake"),
("YoudaGames.MonopolyPoker.UI.HUD.SideButtons.DealsHUDSideButton", "Awake"),
};
var disableObjectPatch = typeof(DistractionHandler).GetMethod(
nameof(DisableGameObjectPostfix), BindingFlags.Static | BindingFlags.NonPublic)
?? throw new Exception($"{MethodBase.GetCurrentMethod()?.Name} - DistractionHandler.DisableGameObjectPostfix method not found.");
var disablePatchHarmony = new HarmonyLib.HarmonyMethod(disableObjectPatch);
foreach (var (TypeName, MethodName) in gameObjects)
{
Plugin.PatchMethod(TypeName, MethodName, disablePatchHarmony, isPostfix: true);
LogInfo($"Blocked GameObject: {TypeName}");
}
}
/// <summary>
/// Harmony prefix that blocks execution of the original method.
/// Returning false = skip original method body entirely.
/// Harmony redirects to here.
/// </summary>
private static bool BlockPrefix() => false;
/// <summary>
/// Forces a MonoBehaviour's GameObject to instantly deactivate.
/// For HUD elements that don't have a show method but are active by default
/// </summary>
private static void DisableGameObjectPostfix(UnityEngine.Component __instance)
{
var name = __instance.GetType().Name;
if (name is "BuyOneOrAllHUDSideButton" or "DealsHUDSideButton")
{
__instance.gameObject.SetActive(false);
LogDebug($"Deactivated GameObject for: {name}");
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
using System;
namespace MonopolyPokerHelper
{
/// <summary>
/// Base class for handlers to share properties like IsEnabled and IsResolved.
/// By using a generic type parameter (CRTP), each handler gets its own independent static state.
/// </summary>
internal abstract class FeatureHandler<T> where T : FeatureHandler<T>
{
public static bool IsEnabled { get; set; } = true;
public static bool IsResolved { get; protected set; }
}
}
@@ -0,0 +1,131 @@
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper;
internal static class FreeDailyCurrencyHandler
{
// FreeBonusManager
private static Type _freeBonusMgrType;
private static PropertyInfo _freeBonusInstance;
private static PropertyInfo _canClaimFreeBonus;
private static MethodInfo _collectBonus;
public static bool IsResolved { get; private set; }
public static void ResolveReflection()
{
try
{
if (IsResolved)
{
LogInfo("FreeDailyCurrencyHandler reflection already resolved.");
return;
}
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
_freeBonusMgrType = ReflectionHelper.FindType("FreeBonusManager");
if (_freeBonusMgrType != null)
{
_freeBonusInstance = _freeBonusMgrType.GetProperty("Instance", all)
?? _freeBonusMgrType.BaseType?.GetProperty("Instance", all);
_canClaimFreeBonus = _freeBonusMgrType.GetProperty("CanClaimFreeBonus", all);
_collectBonus = _freeBonusMgrType.GetMethod("CollectBonus", all);
LogInfo($"FreeBonusManager: found={_freeBonusMgrType != null} " +
$"instance={_freeBonusInstance != null} " +
$"canClaim={_canClaimFreeBonus != null} " +
$"collect={_collectBonus != null}");
IsResolved = true;
}
else
{
LogError("FreeBonusManager type not found!");
throw new TypeLoadException("FreeDailyCurrencyHandler failed to resolve required type 'FreeBonusManager'.");
}
}
catch (Exception ex)
{
LogError($"Failed to resolve FreeBonusManager reflection: {ex}");
}
}
public static void Update()
{
if (!IsResolved) return;
TryCollectFreeBonus(0, "Chips");
TryCollectFreeBonus(1, "M-Money");
}
private static void TryCollectFreeBonus(int channel, string label)
{
try
{
if (_freeBonusMgrType == null || _freeBonusInstance == null)
{
throw new InvalidOperationException($"FreeBonusManager type or instance property not found, cannot claim free {label}.");
}
var instance = _freeBonusInstance.GetValue(null);
if (instance == null) { LogError($"FreeBonusManager instance is null"); return; }
var canClaim = _canClaimFreeBonus?.GetValue(instance);
if (canClaim == null) { LogError($"canClaimFreeBonus instance is null") ; return; }
bool canClaimChannel;
if (canClaim is bool[] boolArr)
{
if (channel >= boolArr.Length) return;
canClaimChannel = boolArr[channel];
}
else
{
var getMethod = canClaim.GetType().GetMethod("get_Item")
?? canClaim.GetType().GetMethod("Get");
if (getMethod != null)
{
var result = getMethod.Invoke(canClaim, new object[] { channel });
canClaimChannel = Convert.ToBoolean(result);
}
else
{
var indexer = canClaim.GetType().GetProperty("Item");
if (indexer != null)
{
canClaimChannel = Convert.ToBoolean(indexer.GetValue(canClaim, new object[] { channel }));
}
else
{
LogError($"Could not read CanClaimFreeBonus[{channel}]");
return;
}
}
}
if (!canClaimChannel)
{
LogDebug($"Free {label} not available (already claimed or on cooldown).");
return;
}
_collectBonus.Invoke(instance, new object[] { channel });
LogInfo($">>> Claimed free {label} (channel {channel})!");
}
catch (Exception ex)
{
LogError($"Error collecting free {label}: {ex.Message}");
}
}
public static void Reset()
{
//_chipsClaimed = false;
//_mMoneyClaimed = false;
}
}
+12
View File
@@ -0,0 +1,12 @@
using BepInEx.Logging;
namespace MonopolyPokerHelper;
internal static class LoggerHelper
{
internal static ManualLogSource Log { get; set; }
internal static void LogInfo(string message) => Log?.LogInfo(message);
internal static void LogWarn(string message) => Log?.LogWarning(message);
internal static void LogError(string message) => Log?.LogError(message);
internal static void LogDebug(string message) => Log?.LogDebug(message);
}
@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>MonopolyPokerHelper</AssemblyName>
<Product>MonopolyPokerHelper</Product>
<Version>1.0.0</Version>
<GameVersion>Unknown</GameVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<RestoreAdditionalProjectSources>
https://api.nuget.org/v3/index.json;
https://nuget.bepinex.dev/v3/index.json;
https://nuget.samboy.dev/v3/index.json
</RestoreAdditionalProjectSources>
<RootNamespace>MonopolyPokerHelper</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<BepInExDir>C:\Program Files (x86)\Steam\steamapps\common\Monopoly Poker\BepInEx\core</BepInExDir>
<InteropDir>C:\Program Files (x86)\Steam\steamapps\common\Monopoly Poker\BepInEx\interop</InteropDir>
<PluginDeployDir>$(BepInExDir)\..\plugins\MonopolyPokerHelper</PluginDeployDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.*" IncludeAssets="compile" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" />
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(InteropDir)\UnityEngine.CoreModule.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2Cppmscorlib">
<HintPath>$(InteropDir)\Il2Cppmscorlib.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2CppInterop.Runtime">
<HintPath>$(InteropDir)\Il2CppInterop.Runtime.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<Target Name="DeployPluginToGame" AfterTargets="Build">
<!-- Check if Monopoly Poker is running -->
<Exec Command="tasklist /FI &quot;IMAGENAME eq Monopoly Poker.exe&quot; | find /I &quot;Monopoly Poker.exe&quot;" IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="PokerProcessExitCode" />
</Exec>
<Message Importance="high" Text="Skipping plugin deployment: Monopoly Poker is running. Please close the game to deploy the plugin." Condition=" '$(PokerProcessExitCode)' == '0' " />
<MakeDir Directories="$(PluginDeployDir)" Condition=" '$(PokerProcessExitCode)' != '0' " />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PluginDeployDir)" SkipUnchangedFiles="true" OverwriteReadOnlyFiles="true" Condition=" '$(PokerProcessExitCode)' != '0' " />
<Copy SourceFiles="$(TargetDir)$(AssemblyName).pdb" DestinationFolder="$(PluginDeployDir)" SkipUnchangedFiles="true" Condition="Exists('$(TargetDir)$(AssemblyName).pdb') and '$(PokerProcessExitCode)' != '0'" />
<Message Importance="high" Text="Deployed $(AssemblyName).dll to $(PluginDeployDir)" Condition=" '$(PokerProcessExitCode)' != '0' " />
</Target>
</Project>
+219
View File
@@ -0,0 +1,219 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper
{
/// <summary>
/// Main BepInEx plugin entry point for Monopoly Poker helper features.
/// Sets up Harmony patches and periodically runs feature handlers.
/// </summary>
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BasePlugin
{
private static Harmony _harmony;
// Accumulated time since last handler run.
private static float _elapsed;
// How often to run handlers after startup, in seconds.
private const float CHECK_INTERVAL = 0.25f;
// Initial delay before any logic runs, in seconds.
private const float INITIAL_DELAY = 15f;
// Ensures the initial delay logic only runs once.
private static bool _initialDelayDone;
// Ensures reflection is resolved successfully before handlers execute.
public static bool IsResolved { get; private set; }
public static bool _pluginDisabled;
/// <summary>
/// BepInEx entry point. Sets up Harmony patches and logs plugin startup.
/// </summary>
public override void Load()
{
LoggerHelper.Log = base.Log;
_harmony = new Harmony(MyPluginInfo.PLUGIN_GUID);
// Redundant, BepInEx does this automatically
//LogInfo($"{MyPluginInfo.PLUGIN_NAME} v{MyPluginInfo.PLUGIN_VERSION} loading...");
var updatePatch = new HarmonyMethod(typeof(Plugin).GetMethod(
nameof(UpdatePostfix), BindingFlags.Static | BindingFlags.NonPublic));
PatchMethod("SocialLayerManager", "Update", updatePatch, isPostfix: true);
DistractionHandler.Load();
BankrupcyRefillHandler.Load();
LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
}
/// <summary>
/// Resolves all reflection-based handles used by handlers.
/// Must succeed before periodic updates start running.
/// </summary>
private static bool ResolveReflection()
{
if (IsResolved) return true;
LogInfo("Resolving reflection handles...");
BoardwalkHandler.ResolveReflection();
FreeDailyCurrencyHandler.ResolveReflection();
// ClaimMissionHandler.ResolveReflection();
IsResolved = BoardwalkHandler.IsResolved &&
FreeDailyCurrencyHandler.IsResolved
// ClaimMissionHandler.IsResolved
;
if (IsResolved) { LogInfo("Reflection handles for Monopoly Poker helper resolved successfully."); }
return IsResolved;
}
/// <summary>
/// Harmony postfix for SocialLayerManager.Update.
/// Throttles execution, enforces an initial delay,
/// ensures reflection is ready, then invokes feature handlers.
/// </summary>
private static void UpdatePostfix()
{
if (_pluginDisabled) return;
float dt = UnityEngine.Time.deltaTime;
if (!_initialDelayDone)
{
_elapsed += dt;
if (_elapsed < INITIAL_DELAY) return;
_initialDelayDone = true;
_elapsed = CHECK_INTERVAL;
LogInfo("Initial start delay complete");
}
if (!ResolveReflection())
{
DisablePlugin("Failed to resolve reflection handles");
return;
}
_elapsed += dt;
if (_elapsed < CHECK_INTERVAL) return;
_elapsed = 0f;
try
{
//LogInfo("Running periodic handlers...");
SafeInvoke(() => BoardwalkHandler.Update(dt), nameof(BoardwalkHandler.Update));
SafeInvoke(FreeDailyCurrencyHandler.Update, nameof(FreeDailyCurrencyHandler.Update));
// SafeInvoke(() => ClaimMissionHandler.Update(dt), nameof(ClaimMissionHandler.Update));
}
catch (Exception ex)
{
LogError($"UpdatePostfix error: {ex.Message}");
}
}
/// <summary>
/// Safely invokes a handler action and logs any exceptions with the given name.
/// Use when you want individual handler failures not to affect others.
/// </summary>
private static void SafeInvoke(Action action, string name)
{
try { action(); }
catch (Exception ex) { LogError($"Error invoking handler {name}: {ex}"); }
}
/// <summary>
/// Finds a target type and patches all methods with the given name
/// using the provided HarmonyMethod as prefix or postfix.
/// </summary>
/// <param name="typeName">Name of the target type to search for.</param>
/// <param name="methodName">Name of the methods to patch on that type.</param>
/// <param name="patch">Harmony patch method (prefix or postfix).</param>
/// <param name="isPostfix">True to apply as postfix, false for prefix.</param>
public static bool PatchMethod(
string typeName,
string methodName,
HarmonyMethod patch,
bool isPostfix = false,
Type[] parameterTypes = null)
{
Type targetType = ReflectionHelper.FindType(typeName);
if (targetType == null)
{
LogError($" Type not found: {typeName}");
return false;
}
int patched = 0;
if (parameterTypes != null)
{
var method = targetType.GetMethod(
methodName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly,
null,
parameterTypes,
null);
if (method == null)
{
LogError($"No method named '{methodName}' with specified parameters found on {typeName}");
return false;
}
if (isPostfix) _harmony.Patch(method, postfix: patch);
else _harmony.Patch(method, prefix: patch);
patched = 1;
}
else
{
var methods = targetType.GetMethods(
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly);
foreach (var method in methods)
{
if (method.Name != methodName) continue;
if (isPostfix) _harmony.Patch(method, postfix: patch);
else _harmony.Patch(method, prefix: patch);
patched++;
}
}
if (patched == 0)
{
LogError($"No methods named '{methodName}' found on {typeName}");
return false;
}
LogInfo($"Patched {patched} method(s): {typeName}.{methodName}");
return true;
}
public static void PatchMethod(string typeName, string methodName, MethodInfo patchMethod, bool isPostfix = false)
{
if (patchMethod == null)
{
LogWarn($"Patch method is null for {typeName}.{methodName}");
return;
}
PatchMethod(typeName, methodName, new HarmonyMethod(patchMethod), isPostfix);
}
private static void DisablePlugin(string reason)
{
if (_pluginDisabled) return;
_pluginDisabled = true;
LogError($"Disabling plugin: {reason}");
try
{
_harmony?.UnpatchSelf();
}
catch (Exception e)
{
LogError($"Error during plugin unpatching: {e}");
}
}
}
}
+170
View File
@@ -0,0 +1,170 @@
using System;
using System.Reflection;
using static MonopolyPokerHelper.LoggerHelper;
namespace MonopolyPokerHelper;
internal static class ReflectionHelper
{
// Common binding flags for reflection (shared across helpers)
// Unsure if this is a good idea as it's possible some helpers might want to use different flags
// TODO: Decide if we want to keep AllBindingFlags
// It might be more flexible to not have a shared constant if different helpers have different needs, but it also might be nice to have a common set of flags for consistency. For now, I'll keep it as a constant for convenience, but we can always change it later if needed.
public const BindingFlags AllBindingFlags =
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
private static readonly string[] SingletonPropertyNames = ["Instance", "instance"];
private static readonly string[] SingletonFieldNames = ["Instance", "instance", "_instance", "s_instance"];
/// <summary>
/// Searches all loaded assemblies for a type with the specified name and returns the corresponding Type object
/// if found.
/// </summary>
/// <remarks>The search first attempts to find an exact match using the fully qualified name. If
/// no match is found, it searches for a type with a matching simple name. Only assemblies currently loaded in
/// the application domain are searched.</remarks>
/// <param name="name">The fully qualified or simple name of the type to locate. This value is case-sensitive.</param>
/// <returns>A Type object representing the type with the specified name, or null if the type cannot be found in any
/// loaded assembly.</returns>
public static Type FindType(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
// Try exact match first
foreach (var asm in assemblies)
{
var t = asm.GetType(name);
if (t != null)
{
LogDebug(name + ": exact match found in " + asm.FullName);
return t;
}
}
// Try partial match (unqualified name)
foreach (var asm in assemblies)
{
try
{
foreach (var t in asm.GetTypes())
{
if (t.Name == name || t.FullName == name)
return t;
}
}
catch (ReflectionTypeLoadException ex)
{
LogDebug($"{name}: partial type load failure in {asm.FullName}: {ex.Message}");
foreach (var t in ex.Types)
{
if (t == null)
continue;
if (t.Name == name || t.FullName == name)
{
LogDebug(name + ": partial match found in " + asm.FullName);
return t;
}
}
}
catch (NotSupportedException ex)
{
LogDebug($"{name}: unsupported reflection operation for {asm.FullName}: {ex.Message}");
}
}
LogError(name + ": type not found in any loaded assembly!");
return null;
}
/// <summary>
/// Retrieves the singleton instance of the specified type, if available.
/// </summary>
/// <remarks>This method searches for a static property or field commonly named 'Instance',
/// 'instance', '_instance', or 's_instance' on the specified type or its base types. If no such member is found
/// or the value is null, the method returns null.</remarks>
/// <param name="type">The type to search for a singleton instance.</param>
/// <returns>The singleton instance of the specified type if found; otherwise, null.</returns>
public static object GetSingletonInstance(Type type)
{
ArgumentNullException.ThrowIfNull(type);
const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static;
foreach (var propertyName in SingletonPropertyNames)
{
try
{
for (var current = type; current != null; current = current.BaseType)
{
var prop = current.GetProperty(propertyName, all);
if (prop == null || prop.GetMethod == null || !prop.GetMethod.IsStatic)
continue;
var value = prop.GetValue(null);
if (value == null)
continue;
LogDebug($"Found singleton instance via property '{propertyName}' on type '{type.FullName}'");
return value;
}
}
catch (AmbiguousMatchException ex)
{
LogDebug($"Skipping property '{propertyName}' on type '{type.FullName}' due to ambiguous match: {ex.Message}");
}
catch (TargetInvocationException ex)
{
LogDebug($"Skipping property '{propertyName}' on type '{type.FullName}' due to invocation error: {ex.Message}");
}
catch (MethodAccessException ex)
{
LogDebug($"Skipping property '{propertyName}' on type '{type.FullName}' due to access restriction: {ex.Message}");
}
catch (NotSupportedException ex)
{
LogDebug($"Skipping property '{propertyName}' on type '{type.FullName}' due to unsupported reflection operation: {ex.Message}");
}
}
foreach (var fieldName in SingletonFieldNames)
{
try
{
for (var current = type; current != null; current = current.BaseType)
{
var field = current.GetField(fieldName, all);
if (field == null || !field.IsStatic)
continue;
var value = field.GetValue(null);
if (value == null)
continue;
LogDebug($"Found singleton instance via field '{fieldName}' on type '{type.FullName}'");
return value;
}
}
catch (AmbiguousMatchException ex)
{
LogDebug($"Skipping field '{fieldName}' on type '{type.FullName}' due to ambiguous match: {ex.Message}");
}
catch (FieldAccessException ex)
{
LogDebug($"Skipping field '{fieldName}' on type '{type.FullName}' due to access restriction: {ex.Message}");
}
catch (NotSupportedException ex)
{
LogDebug($"Skipping field '{fieldName}' on type '{type.FullName}' due to unsupported reflection operation: {ex.Message}");
}
}
LogError($"Failed to find singleton instance for type '{type.FullName}'");
return null;
}
}