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
+34
View File
@@ -0,0 +1,34 @@
# Build output
bin/
obj/
# Visual Studio files
*.user
*.suo
*.userosscache
*.sln.docstates
# NuGet packages
*.nupkg
*.snupkg
packages/
*.nuspec
# Logs
*.log
# Debug/Release folders
Debug/
Release/
# VS Code settings
.vscode/
.vs/
# Other
*.DS_Store
Thumbs.db
/blackjack_graph
/Assembly-CSharp
/Unity_Tools
/GameAssemblyReference
+52
View File
@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
</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>
</PropertyGroup>
<!-- BepInEx 6 core + Unity interop references -->
<ItemGroup>
<Reference Include="BepInEx.Core">
<HintPath>$(BepInExDir)\BepInEx.Core.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="BepInEx.Unity.IL2CPP">
<HintPath>$(BepInExDir)\BepInEx.Unity.IL2CPP.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(BepInExDir)\0Harmony.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2CppInterop.Runtime">
<HintPath>$(BepInExDir)\Il2CppInterop.Runtime.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(InteropDir)\UnityEngine.CoreModule.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>$(InteropDir)\Assembly-CSharp.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2Cppmscorlib">
<HintPath>$(InteropDir)\Il2Cppmscorlib.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Il2CppSystem.Core">
<HintPath>$(InteropDir)\Il2CppSystem.Core.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
+216
View File
@@ -0,0 +1,216 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AutoBlackjack;
/// <summary>
/// Manages JSON database for tracking blackjack statistics.
/// Tracks: bet amount, date/time, chip balance, and round outcome.
///
/// Logger doesnt work
/// </summary>
public class BlackjackStatsJsonDatabase : IDisposable
{
private readonly string _jsonPath;
private List<BlackjackRoundRecord> _records;
private bool _disposed;
public BlackjackStatsJsonDatabase(string jsonPath)
{
_jsonPath = jsonPath;
Initialize();
}
private void Initialize()
{
try
{
// Ensure directory exists
string directory = Path.GetDirectoryName(_jsonPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Load existing records or create new list
if (File.Exists(_jsonPath))
{
string jsonContent = File.ReadAllText(_jsonPath);
_records = JsonSerializer.Deserialize<List<BlackjackRoundRecord>>(jsonContent)
?? new List<BlackjackRoundRecord>();
//Log.LogInfo($"Loaded {_records.Count} records from JSON database");
}
else
{
_records = new List<BlackjackRoundRecord>();
SaveToFile();
//Log.LogInfo($"Created new JSON database at: {_jsonPath}");
}
}
catch (Exception ex)
{
//Log.Error($"Failed to initialize JSON database: {ex}");
_records = new List<BlackjackRoundRecord>();
}
}
/// <summary>
/// Log a blackjack round with statistics.
/// </summary>
public void LogRound(long betAmount, long chipBalance, string outcome = null,
int playerTotal = 0, int dealerUpcard = 0, string action = null, long profit = 0)
{
try
{
var record = new BlackjackRoundRecord
{
Id = _records.Count > 0 ? _records[^1].Id + 1 : 1,
Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
BetAmount = betAmount,
ChipBalance = chipBalance,
Outcome = outcome ?? "",
PlayerTotal = playerTotal,
DealerUpcard = dealerUpcard,
Action = action ?? "",
Profit = profit
};
_records.Add(record);
SaveToFile();
}
catch (Exception ex)
{
//Log.Error($"Failed to log round to JSON database: {ex}");
}
}
/// <summary>
/// Save records to JSON file.
/// </summary>
private void SaveToFile()
{
try
{
var options = new JsonSerializerOptions
{
WriteIndented = true
};
string jsonContent = JsonSerializer.Serialize(_records, options);
File.WriteAllText(_jsonPath, jsonContent);
}
catch (Exception ex)
{
//Log.Error($"Failed to save JSON database: {ex}");
}
}
/// <summary>
/// Get total number of rounds played.
/// </summary>
public long GetTotalRounds()
{
return _records?.Count ?? 0;
}
/// <summary>
/// Get total profit/loss across all rounds.
/// </summary>
public long GetTotalProfit()
{
try
{
if (_records == null || _records.Count == 0)
return 0;
long total = 0;
foreach (var record in _records)
{
total += record.Profit;
}
return total;
}
catch (Exception ex)
{
//Log.Error($"Failed to get total profit: {ex}");
return 0;
}
}
/// <summary>
/// Get statistics summary.
/// </summary>
public void LogStatsSummary()
{
try
{
long totalRounds = GetTotalRounds();
long totalProfit = GetTotalProfit();
//Log.Info("=== Blackjack Stats Summary (JSON) ===");
//Log.Info($"Total Rounds: {totalRounds}");
//Log.Info($"Total Profit: {totalProfit:+#;-#;0} chips");
//if (totalRounds > 0)
//Log.Info($"Average Profit per Round: {(double)totalProfit / totalRounds:F2} chips");
//Log.Info("=======================================");
}
catch (Exception ex)
{
//Log.Error($"Failed to log stats summary: {ex}");
}
}
public void Dispose()
{
if (_disposed) return;
try
{
SaveToFile();
}
catch (Exception ex)
{
//Log.Error($"Error disposing JSON database: {ex}");
}
_disposed = true;
}
}
/// <summary>
/// Represents a single blackjack round record.
/// </summary>
public class BlackjackRoundRecord
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("date")]
public string Date { get; set; }
[JsonPropertyName("betAmount")]
public long BetAmount { get; set; }
[JsonPropertyName("chipBalance")]
public long ChipBalance { get; set; }
[JsonPropertyName("outcome")]
public string Outcome { get; set; }
[JsonPropertyName("playerTotal")]
public int PlayerTotal { get; set; }
[JsonPropertyName("dealerUpcard")]
public int DealerUpcard { get; set; }
[JsonPropertyName("action")]
public string Action { get; set; }
[JsonPropertyName("profit")]
public long Profit { get; set; }
}
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
Strategy:
Bet sizing (Hi-Lo count-based):
True Count ≤ 1: minimum bet
True Count 2: 2× minimum
True Count 3: 4× minimum
True Count 4: 6× minimum
True Count ≥ 5: maximum bet
Card Counting (Hi-Lo):
Cards 2-6: +1
Cards 7-9: 0
Cards 10, J, Q, K, A: -1
True Count = Running Count / Estimated Decks Remaining
Assumes 6-deck shoe. Running count persists across rounds.
All visible cards (all players + dealer) are scanned at decision time.
Dealer reveal is also counted (Hook 4) for next-round accuracy.
Illustrious 18 Deviations (applied when True Count thresholds met):
16 vs 10: STAND when TC ≥ 0 (basic says HIT)
16 vs 9: STAND when TC ≥ 5 (basic says HIT)
15 vs 10: STAND when TC ≥ 4 (basic says HIT)
12 vs 3: STAND when TC ≥ 2 (basic says HIT)
12 vs 2: STAND when TC ≥ 3 (basic says HIT)
10 vs 10: DOUBLE when TC ≥ 4 (basic says HIT)
10 vs A: DOUBLE when TC ≥ 4 (basic says HIT)
9 vs 2: DOUBLE when TC ≥ 1 (basic says HIT)
9 vs 7: DOUBLE when TC ≥ 3 (basic says HIT)
Soft totals: A soft total is any hand that has an Ace as one of the first two cards, the ace counts as 11 to start.
Soft 20 (A,9) always stands.
Soft 19 (A,8) doubles against dealer 6, otherwise stand.
Soft 18 (A,7) doubles against dealer 2 through 6, and hits against 9 through Ace, otherwise stand.
Soft 17 (A,6) doubles against dealer 3 through 6, otherwise hit.
Soft 16 (A,5) doubles against dealer 4 through 6, otherwise hit.
Soft 15 (A,4) doubles against dealer 4 through 6, otherwise hit.
Soft 14 (A,3) doubles against dealer 5 through 6, otherwise hit.
Soft 13 (A,2) doubles against dealer 5 through 6, otherwise hit.
Hard totals: A hard total is any hand that does not start with an ace in it, or it has been dealt an ace that can only be counted as 1 instead of 11.
17 and up always stands.
16 stands against dealer 2 through 6, otherwise hit.
15 stands against dealer 2 through 6, otherwise hit.
14 stands against dealer 2 through 6, otherwise hit.
13 stands against dealer 2 through 6, otherwise hit.
12 stands against dealer 4 through 6, otherwise hit.
11 always doubles.
10 doubles against dealer 2 through 9 otherwise hit.
9 doubles against dealer 3 through 6 otherwise hit.
8 always hits.
Splits:
Aces : always split
10 : always stand
9 : split vs dealer 2-9 except 7, else stand
8 : always split
7 : split vs dealer 2-7, else hit
6 : split vs dealer 2-6, else hit
5 : double vs dealer 2-9, else hit
4 : split vs dealer 5-6, else hit
1-3 : split vs dealer 2-7, else hit
+16
View File
@@ -0,0 +1,16 @@
`YoudaGames.MonopolyPoker.MissionCollection.MissionClaimHandler` has a a method `MissionClaimHandler.ClaimMission(int, int)`
`YoudaGames.MonopolyPoker.MissionCollection.MissionGroupType` has Enum mission types
YoudaGames.MonopolyPoker.MissionCollection.MissionClaimResponseData:
- MissionClaimResponseData.message string
- MissionClaimResponseData.Message string
- MissionClaimResponseData.missionId int
- MissionClaimResponseData.missionId int
- MissionClaimResponseData.rewards Il2CppSystem.Collections.Generic.List<MissionReward>
- MissionClaimResponseData.Rewards Il2CppSystem.Collections.Generic.List<MissionReward>
- MissionClaimResponseData.success bool
- MissionClaimResponseData.Success bool
YoudaGames.MonopolyPoker.MissionCollection.MissionClaimHandler
+13
View File
@@ -0,0 +1,13 @@
Task list placeholders:
- [ ] Split mission automation into separate files in a project subfolder
- [ ] Add mission discovery service placeholder
- [ ] Add mission claim executor placeholder
- [ ] Add mission claim cache placeholder
- [ ] Add mission eligibility rules placeholder
- [ ] Add free daily currency handler cleanup placeholder
- [ ] Review reflection helper reuse across handlers
Notes:
- Replace placeholders with concrete implementation tasks.
- Keep tasks short and actionable.
@@ -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;
}
}
+80
View File
@@ -0,0 +1,80 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11430.68
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoBlackjack", "AutoBlackjack\AutoBlackjack.csproj", "{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C723F55D-2FC3-4D37-BFA0-A17FA0E0A0BF}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonopolyPokerHelper", "MonopolyPokerHelper\MonopolyPokerHelper.csproj", "{3175547E-8811-4E55-9249-FBB13297CC7E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp\Assembly-CSharp.csproj", "{DAF50183-FF42-4926-ADB9-772EFA521BC4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logdumps", "Logdumps", "{1E0CFDFA-7799-4D92-9C33-6871F67EB219}"
ProjectSection(SolutionItems) = preProject
..\BlackjackTypeDump.log = ..\BlackjackTypeDump.log
Boardwalkpopup.log = Boardwalkpopup.log
ClaimMissionInformation.txt = ClaimMissionInformation.txt
MonopolyPokerHelper_2026_03_22.log = MonopolyPokerHelper_2026_03_22.log
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.ActiveCfg = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.Build.0 = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.ActiveCfg = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.Build.0 = Debug|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.ActiveCfg = Release|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.Build.0 = Release|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.ActiveCfg = Release|Any CPU
{B1C2D3E4-5F6A-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.Build.0 = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|x64.ActiveCfg = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|x64.Build.0 = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|x86.ActiveCfg = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Debug|x86.Build.0 = Debug|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|Any CPU.Build.0 = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|x64.ActiveCfg = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|x64.Build.0 = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|x86.ActiveCfg = Release|Any CPU
{3175547E-8811-4E55-9249-FBB13297CC7E}.Release|x86.Build.0 = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Debug|x64.ActiveCfg = Debug|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Debug|x64.Build.0 = Debug|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Debug|x86.ActiveCfg = Debug|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Debug|x86.Build.0 = Debug|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|Any CPU.Build.0 = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|x64.ActiveCfg = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|x64.Build.0 = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|x86.ActiveCfg = Release|Any CPU
{DAF50183-FF42-4926-ADB9-772EFA521BC4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1E0CFDFA-7799-4D92-9C33-6871F67EB219} = {C723F55D-2FC3-4D37-BFA0-A17FA0E0A0BF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7F85BEED-1562-48D3-AF3C-97E6D84501D3}
EndGlobalSection
EndGlobal
+306
View File
@@ -0,0 +1,306 @@
# Monopoly Poker Mod Helper
This repository contains helper code, notes, and logs for modding `Monopoly Poker`.
## Overview
`Monopoly Poker` is an Unity IL2CPP game on Steam, so mods should be built for `IL2CPP`. BepInEx IL2CPP is used in this project.
This repo is focused on reverse engineering game behavior, finding useful types through reflection, and automating helper actions.
## Requirements
- Visual Studio
- `.NET 6`
- `BepInEx IL2CPP`
- `dnSpy` (for inspecting game assemblies and objects)
## Repository Structure
### `MonopolyPokerHelper/`
Main mod/helper project code.
Example files:
- `ClaimMissionHandler.cs` - discovers and claims missions through reflection
### Assembly-CSharp
This is a dnSpy dump of the game's main assembly, used for reference and exploration. It is not meant to be built or modified directly.
## What This Project Is For
- Uses reflection to inspect game objects and types
- Automates helper actions inside the game
- Stores notes and logs for reverse engineering work
## Notes
- The dash character `—` may display incorrectly in the BepInEx console.
- `dnSpy` may need a fix for loading certain assemblies.
- You can use `BepInEx.ConfigurationManager` if you want an in-game configuration UI.
## Setup
1. Open the solution in Visual Studio.
2. Ensure the project targets `.NET 6` and references the game assembly as needed.
3. Build the helper project.
4. Deploy the output into the BepInEx plugin folder.
## Useful References
- BepInEx IL2CPP: https://github.com/BepInEx/BepInEx
- Configuration Manager: https://github.com/BepInEx/BepInEx.ConfigurationManager/releases/tag/v18.4.1
- [UnityExplorer](https://github.com/sinai-dev/UnityExplorer) (for browsing game assets and types during runtime)
- **BepInEx Docs**: <https://docs.bepinex.dev/>
- **Harmony Docs**: <https://harmony.pardeike.net/>
Note UnityExplorer is archived, need to use a fork.
## Known Issues
### dnSpy Import Project
When trying to load a disassembled project that was then reassembled with dnSpy into Visual Studio you'll encounter the error:
`A numeric comparison was attempted on "$(_TargetFrameworkVersionWithoutV)" that evaluates to "" instead of a number, in condition "('$(TargetFrameworkIdentifier)' != '.NETCoreApp'`
This is because the project file generated by dnSpy is adding a v to <TargetFrameworkVersion> and as well as not including _TargetFrameworkVersionWithoutV. To fix this:
1. Open the .csproj file in a text editor.
2. Near the top you'll see these two lines:
```xml
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier>
```
Modify the first line (remove the "v"), and insert the second line, so it looks like this:
```xml
<TargetFrameworkVersion>5.0</TargetFrameworkVersion>
<_TargetFrameworkVersionWithoutV>5.0</_TargetFrameworkVersionWithoutV>
<TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier>
```
Load the project as you were, and it should work.
Mileage may vary with different versions of Visual Studio and .NET SDK, but this is the general fix for the issue.
[Source](https://www.clearhat.org/post/how-fix-numeric-comparison-was-attempted-_targetframeworkversionwithoutv-evaluates-instead-number-condition-targetframeworkidentifier-netcoreapp)
### Encoding issues in BepInEx Console
"—" always shows as ΓòÉ on BepInEx Console. Use the regular hyphen "-" instead.
## How to Extend This README
Add new sections when needed:
- setup and build instructions
- file-by-file descriptions
- known issues
- update history
## Git Stash Reminder
to git stash staged changes, you can use the following command:
```
git stash push --staged -m "message"
```
## Modding Guide
### Tech Stack
#### What is BepInEx?
**BepInEx** (Bepis Injector Extendable) is a plugin/mod loader for Unity games. It:
- Injects itself into the game when it launches
- Loads your plugin DLLs automatically from `BepInEx/plugins/`
- Provides logging, configuration, and modding APIs
- Handles IL2CPP games (compiled to native C++ code, harder to mod than Mono)
#### What is Harmony?
**Harmony** is a runtime patching library that lets you modify game methods without changing the original game files:
- **Prefix**: Run your code BEFORE the original method (can block the original)
- **Postfix**: Run your code AFTER the original method
- **Transpiler**: Modify the method's IL code directly (advanced)
You patch methods by their type and method name - no compile-time references needed!
---
## 📋 Common Patterns
### Pattern: Periodic Timer
```csharp
private static float _timer;
private const float INTERVAL = 10f;
private static void UpdatePostfix()
{
_timer += UnityEngine.Time.deltaTime;
if (_timer < INTERVAL) return;
_timer = 0f;
// Do something every 10 seconds
}
```
### Pattern: Singleton Access
```csharp
// Most game managers use the Singleton pattern
var instance = ManagerType.GetProperty("Instance").GetValue(null);
```
### Pattern: Safe Reflection
```csharp
try
{
var method = type.GetMethod("MethodName");
if (method == null)
{
Log.LogWarning("Method not found!");
return;
}
method.Invoke(instance, args);
}
catch (Exception ex)
{
Log.LogError($"Reflection error: {ex.Message}");
}
```
### Pattern: Patch All Overloads
```csharp
var methods = targetType.GetMethods(BindingFlags.Public | BindingFlags.Instance);
foreach (var method in methods)
{
if (method.Name == "TargetMethod")
{
_harmony.Patch(method, prefix: myPrefix);
}
}
```
---
## ⚠️ Common Pitfalls
### 1. **IL2CPP Array Indexing**
IL2CPP arrays need special handling:
```csharp
// DON'T: arr[0]
// DO:
var getMethod = array.GetType().GetMethod("get_Item");
var value = getMethod.Invoke(array, new object[] { 0 });
```
### 2. **Timing Issues**
Game managers may not be loaded immediately:
```csharp
// Wait before trying to access managers
if (!_initialDelayDone)
{
_elapsed += deltaTime;
if (_elapsed < 30f) return; // wait 30 seconds
_initialDelayDone = true;
}
```
### 3. **Type Name Collisions**
Multiple assemblies may have classes with the same name:
```csharp
// Use full namespace
FindType("YoudaGames.MonopolyPoker.Popups.RollingOffersPopup");
```
### 4. **Harmony Patch Failures**
Methods might not exist or load later:
```csharp
try
{
_harmony.Patch(method, prefix);
}
catch (Exception ex)
{
Log.LogWarning($"Patch failed: {ex.Message}");
// Method may load later - try again or check name
}
```
---
## 🎉 Example Mod Ideas
Now that you understand the basics, try creating:
1. **Auto Claimer** - Automatically collect daily rewards (you have this!)
2. **Speed Hack** - Patch animation methods to speed up gameplay
3. **UI Remover** - Block annoying popups (you have this!)
4. **Unlimited Resources** - Modify currency values via reflection
5. **Auto Player** - Hook game actions and make decisions automatically
6. **Visual Tweaks** - Modify Unity UI elements at runtime
---
## 🐛 Debugging Tips
### View Logs
```
<Game Dir>/BepInEx/LogOutput.log
```
### Enable Debug Logging
In your plugin:
```csharp
Log.LogDebug("Debug message"); // Will show if debug level enabled
```
In `BepInEx/config/BepInEx.cfg`:
```ini
[Logging.Console]
LogLevels = Fatal, Error, Warning, Message, Info, Debug
```
## UnityExplorer Console Snippets
### Dumping GameObject Information
Replace the type with the one you need, e.g., `YoudaGames.MonopolyPoker.Popups.BoardwalkJackpotRollPopup`.
```
var instances = UnityEngine.Object.FindObjectsOfType<YoudaGames.MonopolyPoker.Popups.BoardwalkJackpotRollPopup>();
foreach (var popup in instances)
{
Console.WriteLine($"--- Begin for instance {popup} ---");
var type = popup.GetType();
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
{
Console.WriteLine($"{prop.Name}: {prop.GetValue(popup)}");
}
Console.WriteLine("--- End ---");
}
```
TODO: Perhaps seperate this README into a docs folder and link to it from the main README, to keep the main README concise and focused on project overview and setup.