mirror of
https://github.com/AHA705/Monopoly-Poker-Helper.git
synced 2026-10-07 22:56:34 +01:00
Initial commit
This commit is contained in:
+34
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 "IMAGENAME eq Monopoly Poker.exe" | find /I "Monopoly Poker.exe"" 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>
|
||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user