mirror of
https://github.com/AHA705/Monopoly-Poker-Helper.git
synced 2026-10-07 22:56:34 +01:00
1138 lines
53 KiB
C#
1138 lines
53 KiB
C#
using BepInEx;
|
|
using BepInEx.Unity.IL2CPP;
|
|
using HarmonyLib;
|
|
using System;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using static Il2CppSystem.Reflection.RuntimePropertyInfo;
|
|
|
|
namespace AutoBlackjack;
|
|
|
|
/// <summary>
|
|
/// BepInEx 6 IL2CPP plugin that automatically plays Blackjack in Monopoly Poker.
|
|
///
|
|
/// BlackjackCardValue enum (confirmed from runtime dump):
|
|
/// Two=0, Three=1, Four=2, Five=3, Six=4, Seven=5,
|
|
/// Eight=6, Nine=7, Ten=8, Jack=9, Queen=10, King=11, Ace=12, Unknown=-1
|
|
///
|
|
/// Round flow (confirmed from BlackjackView method dump):
|
|
/// ShowBlackjackBetInterface -> auto-bet fires here
|
|
/// HandleOnInitRound -> initial deal: dealer upcard + player cards Hook 2
|
|
/// HandleOnNextDecision -> player decision Hook 3
|
|
/// HandleOnDealerCards -> dealer reveals full hand (AFTER player acts)
|
|
/// HandleOnEndRound -> results
|
|
///
|
|
/// Dealer upcard data path (confirmed from runtime dumps):
|
|
/// BlackjackRoundData.get_dealerHand() -> BlackjackHandData
|
|
/// .get_handValuesPerCard()[upcardIdx] -> List<int> (possible values for that card)
|
|
/// max(inner list) -> direct blackjack point value (2-11, where 11 = Ace)
|
|
///
|
|
/// Note: get_Value() on BlackjackHandData cards always returns 0.
|
|
/// Those are data-layer objects; actual values live in handValuesPerCard.
|
|
///
|
|
/// Hooks:
|
|
/// 1. BlackjackDecisionView.ShowBlackjackBetInterface -> auto-bet minimum
|
|
/// 2. BlackjackView.HandleOnInitRound -> store dealer upcard
|
|
/// 3. BlackjackView.HandleOnNextDecision -> auto-decide hit/stand
|
|
/// </summary>
|
|
[BepInPlugin(PluginGUID, PluginName, PluginVersion)]
|
|
public class Plugin : BasePlugin
|
|
{
|
|
public const string PluginGUID = "com.mod.autoblackjack";
|
|
public const string PluginName = "Auto Blackjack";
|
|
public const string PluginVersion = "1.0.0";
|
|
public const string GameVersion = "0.0"; // for future compatibility checks
|
|
|
|
public static bool DEBUG = true; // set to false to suppress diagnostic logs
|
|
public static bool START_BETTING = false;
|
|
|
|
private Harmony _harmony;
|
|
private static BepInEx.Logging.ManualLogSource _log;
|
|
private static bool _pluginEnabled = true; // Runtime enable/disable flag
|
|
private static BlackjackStatsJsonDatabase _statsDatabase; // JSON stats tracker
|
|
|
|
// ── Reflection handles: shared ──────────────────────────────
|
|
private static bool _sharedResolved;
|
|
private static MethodInfo _blackjackAction; // BlackjackEvents.BlackjackAction(String, Int32, Int64)
|
|
private static MethodInfo _getActionBet; // BlackjackUserDecision.get_ActionBet() [static]
|
|
private static MethodInfo _getActionHit; // BlackjackUserDecision.get_ActionHit() [static]
|
|
private static MethodInfo _getActionStand; // BlackjackUserDecision.get_ActionStand() [static]
|
|
private static MethodInfo _getActionDouble; // BlackjackUserDecision.get_ActionDouble() [static]
|
|
private static MethodInfo _getActionSplit; // BlackjackUserDecision.get_ActionSplit() [static]
|
|
|
|
// ── Reflection handles: bet hook ─────────────────────────────
|
|
private static bool _betResolved;
|
|
private static MethodInfo _getBets; // BlackjackAskBetData.get_bets()
|
|
private static MethodInfo _getDecViewEvents; // BlackjackDecisionView.get_m_BlackjackEvents()
|
|
private static MethodInfo _hideBetInterface; // BlackjackDecisionView.HideBlackjackBetInterface()
|
|
|
|
// ── Reflection handles: decision hook ───────────────────────
|
|
private static bool _decResolved;
|
|
// From BlackjackNextDecisionData
|
|
private static MethodInfo _getDecHandId; // get_handId() -> Int32
|
|
private static MethodInfo _getDecBetAmt; // get_handValue() -> Int64 (chip bet amount)
|
|
// From BlackjackView
|
|
private static MethodInfo _getBjViewEvents; // get_m_BlackjackEvents()
|
|
private static MethodInfo _getHandViews; // get_m_HandViews() -> List<BlackjackHandView>
|
|
private static MethodInfo _getDecInterface; // get_m_DecisionInterface() -> BlackjackDecisionView
|
|
// From BlackjackDecisionView
|
|
private static MethodInfo _hideDecision; // HideBlackjackDecision()
|
|
private static MethodInfo _getDecUIGameObject; // get_m_BlackjackDecisionInterface() -> GameObject
|
|
// From BlackjackHandView
|
|
private static MethodInfo _getHVHandId; // get_HandId() -> Int32
|
|
private static MethodInfo _getHVCards; // get_m_Cards() -> List<BlackjackCardData>
|
|
// From BlackjackCardData
|
|
private static MethodInfo _getCardValue; // get_Value() -> BlackjackCardValue enum
|
|
|
|
// ── Reflection handles: dealer upcard (init-round hook) ─────
|
|
private static bool _dealerResolved;
|
|
private static MethodInfo _getRoundDealerHand; // BlackjackRoundData.get_dealerHand() -> BlackjackHandData
|
|
private static MethodInfo _getHandValuesPerCard; // BlackjackHandData.get_handValuesPerCard() -> List<List<int>>
|
|
private static MethodInfo _getHandValues; // BlackjackHandData.get_handValues() -> List<int> (fallback)
|
|
|
|
// ── Reflection handles: end round hook ──────────────────────
|
|
private static bool _endRoundResolved;
|
|
private static MethodInfo _getEndRoundData; // BlackjackEndRoundData.get_roundResults() -> List<BlackjackRoundResult>
|
|
private static MethodInfo _getRoundResultHandId; // BlackjackRoundResult.get_handId() -> Int32
|
|
private static MethodInfo _getRoundResultOutcome; // BlackjackRoundResult.get_outcome() -> String or enum
|
|
private static MethodInfo _getRoundResultChips; // BlackjackRoundResult.get_chips() -> Int64
|
|
private static MethodInfo _getPlayerChips; // Try to find player chip balance getter
|
|
|
|
// ── Round state tracking ─────────────────────────────────────
|
|
// Direct blackjack point value (2-11 where 11=Ace), -1 = not yet received.
|
|
// Set in OnInitRound; read in OnHandleNextDecision.
|
|
private static int _dealerUpcardValue = -1;
|
|
private static long _roundStartChips = 0; // Chip balance at round start
|
|
private static long _roundBetAmount = 0; // Total bet amount for the round
|
|
private static int _roundPlayerTotal = 0; // Player hand total (first hand if split)
|
|
private static string _roundAction = ""; // Last action taken
|
|
// ── Session tracking ─────────────────────────────────────────
|
|
private static long _sessionNetProfit = 0; // Cumulative net profit for entire session
|
|
private static int _sessionRoundsPlayed = 0; // Total rounds played this session
|
|
|
|
public static int bet_index = 1;
|
|
public override void Load()
|
|
{
|
|
_harmony = new Harmony(PluginGUID);
|
|
|
|
// Initialize static logger for use from static methods
|
|
_log = Log;
|
|
|
|
Log.LogInfo($"{PluginName} v{PluginVersion} loading...");
|
|
|
|
// Initialize JSON stats database
|
|
string dbPath = System.IO.Path.Combine(
|
|
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
|
|
"blackjack_stats.json");
|
|
_statsDatabase = new BlackjackStatsJsonDatabase(dbPath);
|
|
Log.LogInfo($"Stats database initialized at: {dbPath}");
|
|
|
|
var patchTargets = new (string typeName, string methodName, string handler, bool isPostfix)[]
|
|
{
|
|
("YoudaGames.MonopolyPoker.Games.View.BlackjackDecisionView", "ShowBlackjackBetInterface", nameof(OnShowBetInterface), true),
|
|
("YoudaGames.MonopolyPoker.Games.View.BlackjackView", "HandleOnInitRound", nameof(OnInitRound), true),
|
|
("YoudaGames.MonopolyPoker.Games.View.BlackjackView", "HandleOnNextDecision", nameof(OnHandleNextDecision), true),
|
|
("YoudaGames.MonopolyPoker.Games.View.BlackjackView", "HandleOnEndRound", nameof(OnEndRound), true)
|
|
};
|
|
|
|
foreach (var (typeName, methodName, handler, isPostfix) in patchTargets)
|
|
{
|
|
PatchMethod(
|
|
typeName,
|
|
methodName,
|
|
new HarmonyMethod(typeof(Plugin).GetMethod(handler, BindingFlags.Static | BindingFlags.NonPublic)),
|
|
isPostfix: isPostfix);
|
|
}
|
|
|
|
Log.LogInfo($"{PluginName} loaded.");
|
|
|
|
}
|
|
|
|
public override bool Unload()
|
|
{
|
|
try
|
|
{
|
|
// Log final session summary
|
|
if (_sessionRoundsPlayed > 0)
|
|
{
|
|
Log.LogInfo("-------------------------------------------------------");
|
|
Log.LogInfo($"SESSION SUMMARY:");
|
|
Log.LogInfo($" Rounds played: {_sessionRoundsPlayed}");
|
|
Log.LogInfo($" NET profit: {_sessionNetProfit:+#;-#;0} chips");
|
|
if (_sessionRoundsPlayed > 0)
|
|
{
|
|
double avgProfit = (double)_sessionNetProfit / _sessionRoundsPlayed;
|
|
Log.LogInfo($" Average profit per round: {avgProfit:+0.00;-0.00;0.00} chips");
|
|
}
|
|
Log.LogInfo("-------------------------------------------------------");
|
|
}
|
|
|
|
// Log final stats summary
|
|
_statsDatabase?.LogStatsSummary();
|
|
|
|
// Cleanup
|
|
_statsDatabase?.Dispose();
|
|
_harmony?.UnpatchSelf();
|
|
|
|
Log.LogInfo($"{PluginName} unloaded.");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.LogError($"Error during unload: {ex}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Reflection resolvers
|
|
// ---------------------------------------------------------------
|
|
|
|
private static bool ResolveShared()
|
|
{
|
|
if (_sharedResolved) return true;
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
var eventsType = FindType("YoudaGames.MonopolyPoker.Games.BlackjackEvents");
|
|
var userDecisionType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackUserDecision");
|
|
|
|
_blackjackAction = eventsType?.GetMethod("BlackjackAction", bindAll);
|
|
_getActionBet = userDecisionType?.GetMethod("get_ActionBet", bindAll);
|
|
_getActionHit = userDecisionType?.GetMethod("get_ActionHit", bindAll);
|
|
_getActionStand = userDecisionType?.GetMethod("get_ActionStand", bindAll);
|
|
_getActionDouble = userDecisionType?.GetMethod("get_ActionDouble", bindAll);
|
|
_getActionSplit = userDecisionType?.GetMethod("get_ActionSplit", bindAll);
|
|
|
|
_sharedResolved = _blackjackAction != null && _getActionBet != null &&
|
|
_getActionHit != null && _getActionStand != null &&
|
|
_getActionDouble != null && _getActionSplit != null;
|
|
|
|
if (!_sharedResolved)
|
|
{
|
|
_log.LogWarning("Shared resolve failed:");
|
|
if (_blackjackAction == null) _log.LogWarning(" - BlackjackAction");
|
|
if (_getActionBet == null) _log.LogWarning(" - get_ActionBet");
|
|
if (_getActionHit == null) _log.LogWarning(" - get_ActionHit");
|
|
if (_getActionStand == null) _log.LogWarning(" - get_ActionStand");
|
|
if (_getActionDouble == null) _log.LogWarning(" - get_ActionDouble");
|
|
if (_getActionSplit == null) _log.LogWarning(" - get_ActionSplit");
|
|
}
|
|
return _sharedResolved;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"ResolveShared: {ex}"); return false; }
|
|
}
|
|
|
|
private static bool ResolveBet()
|
|
{
|
|
if (_betResolved) return true;
|
|
if (!ResolveShared()) return false;
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
var askBetType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackAskBetData");
|
|
var decViewType = FindType("YoudaGames.MonopolyPoker.Games.View.BlackjackDecisionView");
|
|
|
|
_getBets = askBetType?.GetMethod("get_bets", bindAll);
|
|
_getDecViewEvents = decViewType?.GetMethod("get_m_BlackjackEvents", bindAll);
|
|
_hideBetInterface = decViewType?.GetMethod("HideBlackjackBetInterface", bindAll);
|
|
|
|
_betResolved = _getBets != null && _getDecViewEvents != null;
|
|
|
|
if (!_betResolved)
|
|
{
|
|
_log.LogWarning("Bet resolve failed:");
|
|
if (_getBets == null) _log.LogWarning(" - get_bets");
|
|
if (_getDecViewEvents == null) _log.LogWarning(" - DecisionView.get_m_BlackjackEvents");
|
|
}
|
|
return _betResolved;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"ResolveBet: {ex}"); return false; }
|
|
}
|
|
|
|
private static bool ResolveDecision()
|
|
{
|
|
if (_decResolved) return true;
|
|
if (!ResolveShared()) return false;
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
var nextDecType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackNextDecisionData");
|
|
var bjViewType = FindType("YoudaGames.MonopolyPoker.Games.View.BlackjackView");
|
|
var decViewType = FindType("YoudaGames.MonopolyPoker.Games.View.BlackjackDecisionView");
|
|
var handViewType = FindType("YoudaGames.MonopolyPoker.Games.View.BlackjackHandView");
|
|
var cardDataType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackCardData");
|
|
|
|
_getDecHandId = nextDecType?.GetMethod("get_handId", bindAll);
|
|
_getDecBetAmt = nextDecType?.GetMethod("get_handValue", bindAll);
|
|
_getBjViewEvents = bjViewType?.GetMethod("get_m_BlackjackEvents", bindAll);
|
|
_getHandViews = bjViewType?.GetMethod("get_m_HandViews", bindAll);
|
|
_getDecInterface = bjViewType?.GetMethod("get_m_DecisionInterface", bindAll);
|
|
_hideDecision = decViewType?.GetMethod("HideBlackjackDecision", bindAll);
|
|
_getDecUIGameObject = decViewType?.GetMethod("get_m_BlackjackDecisionInterface", bindAll);
|
|
_getHVHandId = handViewType?.GetMethod("get_HandId", bindAll);
|
|
_getHVCards = handViewType?.GetMethod("get_m_Cards", bindAll);
|
|
_getCardValue = cardDataType?.GetMethod("get_Value", bindAll);
|
|
|
|
_decResolved = _getDecHandId != null && _getDecBetAmt != null &&
|
|
_getBjViewEvents != null && _getHandViews != null &&
|
|
_getHVHandId != null && _getHVCards != null &&
|
|
_getCardValue != null;
|
|
|
|
if (_decResolved)
|
|
_log.LogInfo("All decision reflection targets resolved.");
|
|
else
|
|
{
|
|
_log.LogWarning("Decision resolve failed:");
|
|
|
|
if (_getDecHandId == null) _log.LogWarning(" - NextDecisionData.get_handId");
|
|
if (_getDecBetAmt == null) _log.LogWarning(" - NextDecisionData.get_handValue");
|
|
if (_getBjViewEvents == null) _log.LogWarning(" - BlackjackView.get_m_BlackjackEvents");
|
|
if (_getHandViews == null) _log.LogWarning(" - BlackjackView.get_m_HandViews");
|
|
if (_getDecInterface == null) _log.LogWarning(" - BlackjackView.get_m_DecisionInterface");
|
|
if (_hideDecision == null) _log.LogWarning(" - HideBlackjackDecision");
|
|
if (_getDecUIGameObject == null) _log.LogWarning(" - get_m_BlackjackDecisionInterface");
|
|
if (_getHVHandId == null) _log.LogWarning(" - HandView.get_HandId");
|
|
if (_getHVCards == null) _log.LogWarning(" - HandView.get_m_Cards");
|
|
if (_getCardValue == null) _log.LogWarning(" - CardData.get_Value");
|
|
}
|
|
return _decResolved;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"ResolveDecision: {ex}"); return false; }
|
|
}
|
|
|
|
private static bool ResolveDealer()
|
|
{
|
|
if (_dealerResolved) return true;
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
var roundDataType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackRoundData");
|
|
var handDataType = FindType("YoudaGames.MonopolyPoker.Games.Data.BlackjackHandData");
|
|
|
|
_getRoundDealerHand = roundDataType?.GetMethod("get_dealerHand", bindAll);
|
|
// Primary: List<List<int>> — inner list holds possible values per card (e.g. [1,11] for Ace)
|
|
_getHandValuesPerCard = handDataType?.GetMethod("get_handValuesPerCard", bindAll);
|
|
// Fallback: List<int> — flat per-card point values
|
|
_getHandValues = handDataType?.GetMethod("get_handValues", bindAll);
|
|
|
|
_dealerResolved = _getRoundDealerHand != null &&
|
|
(_getHandValuesPerCard != null || _getHandValues != null);
|
|
|
|
if (!_dealerResolved)
|
|
{
|
|
_log.LogWarning("Dealer resolve failed:");
|
|
if (_getRoundDealerHand == null) _log.LogWarning(" - BlackjackRoundData.get_dealerHand");
|
|
if (_getHandValuesPerCard == null) _log.LogWarning(" - BlackjackHandData.get_handValuesPerCard");
|
|
if (_getHandValues == null) _log.LogWarning(" - BlackjackHandData.get_handValues");
|
|
}
|
|
return _dealerResolved;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"ResolveDealer: {ex}"); return false; }
|
|
}
|
|
|
|
private static bool ResolveEndRound()
|
|
{
|
|
if (_endRoundResolved) return true;
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
var bjViewType = FindType("YoudaGames.MonopolyPoker.Games.View.BlackjackView");
|
|
|
|
// Try various method names for getting player chips
|
|
if (bjViewType != null)
|
|
{
|
|
_getPlayerChips = bjViewType.GetMethod("get_PlayerChips", bindAll)
|
|
?? bjViewType.GetMethod("get_playerChips", bindAll)
|
|
?? bjViewType.GetMethod("get_Chips", bindAll)
|
|
?? bjViewType.GetMethod("get_chips", bindAll);
|
|
}
|
|
|
|
// Don't try to resolve EndRoundData or RoundResult types - use fully dynamic discovery
|
|
_log.LogInfo("Using fully dynamic resolution for EndRound data (types not pre-resolvable)");
|
|
|
|
// Mark as resolved - we'll handle everything dynamically in OnEndRound
|
|
_endRoundResolved = true;
|
|
|
|
return _endRoundResolved;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"ResolveEndRound: {ex}"); return false; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Hook : Auto-bet
|
|
// ---------------------------------------------------------------
|
|
private static void OnShowBetInterface(object __instance, object __0)
|
|
{
|
|
if (!_pluginEnabled) return; // Check if plugin is enabled
|
|
|
|
try
|
|
{
|
|
if (!ResolveBet()) return;
|
|
|
|
var betsList = _getBets.Invoke(__0, null);
|
|
if (betsList == null) { _log.LogWarning("Auto-bet: bets list is null"); return; }
|
|
|
|
int count = Convert.ToInt32(betsList.GetType().GetProperty("Count")?.GetValue(betsList));
|
|
if (count == 0) { _log.LogWarning("Auto-bet: bets list empty"); return; }
|
|
|
|
var getItem = betsList.GetType().GetMethod("get_Item");
|
|
|
|
// Bet amount is in the "Value" property of the bet data objects; we want the minimum bet (index 0).
|
|
|
|
long betAmount = Convert.ToInt64(getItem?.Invoke(betsList, new object[] { bet_index }));
|
|
|
|
var events = _getDecViewEvents.Invoke(__instance, null);
|
|
if (events == null) { _log.LogWarning("Auto-bet: events null"); return; }
|
|
|
|
_log.LogInfo($">>> AUTO-BET: {betAmount} chips <<<");
|
|
_blackjackAction.Invoke(events, new object[] { _getActionBet.Invoke(null, null), 0, betAmount });
|
|
_hideBetInterface?.Invoke(__instance, null);
|
|
}
|
|
catch (Exception ex) { _log.LogError($"OnShowBetInterface: {ex}"); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Hook : Capture dealer upcard from initial deal
|
|
// ---------------------------------------------------------------
|
|
/// <summary>
|
|
/// Postfix on BlackjackView.HandleOnInitRound(BlackjackRoundData).
|
|
///
|
|
/// Dealer upcard reading strategy (confirmed from runtime dumps):
|
|
/// PRIMARY: handValuesPerCard[upcardIdx] is List<int> of possible values.
|
|
/// max(list) gives 11 for Ace, face value for others.
|
|
/// FALLBACK: handValues[upcardIdx] is a direct int point value.
|
|
///
|
|
/// upcardIdx = 1 (American deal: hole first, upcard second).
|
|
/// If only 1 card present, upcardIdx = 0.
|
|
/// </summary>
|
|
private static void OnInitRound(object __instance, object __0)
|
|
{
|
|
if (!_pluginEnabled) return; // Check if plugin is enabled
|
|
|
|
try
|
|
{
|
|
_dealerUpcardValue = -1; // reset state at start of round
|
|
_roundStartChips = 0;
|
|
_roundBetAmount = 0;
|
|
_roundPlayerTotal = 0;
|
|
_roundAction = "";
|
|
|
|
// Try to get player chip balance
|
|
if (_getPlayerChips != null)
|
|
{
|
|
try
|
|
{
|
|
var chips = _getPlayerChips.Invoke(__instance, null);
|
|
if (chips != null)
|
|
_roundStartChips = Convert.ToInt64(chips);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
if (__0 == null) { _log.LogWarning("OnInitRound: BlackjackRoundData is null"); return; }
|
|
if (!ResolveDealer()) return;
|
|
|
|
var dealerHand = _getRoundDealerHand.Invoke(__0, null);
|
|
if (dealerHand == null) { _log.LogWarning("OnInitRound: dealerHand null"); return; }
|
|
|
|
bool readSuccess = false;
|
|
|
|
// ── Primary: handValuesPerCard — List<List<int>> ──────────────────
|
|
if (_getHandValuesPerCard != null)
|
|
{
|
|
var hvpcList = _getHandValuesPerCard.Invoke(dealerHand, null);
|
|
if (hvpcList != null)
|
|
{
|
|
int outerCount = Convert.ToInt32(hvpcList.GetType().GetProperty("Count")?.GetValue(hvpcList));
|
|
var outerGetItem = hvpcList.GetType().GetMethod("get_Item");
|
|
int upcardIdx = outerCount > 1 ? 1 : 0;
|
|
|
|
if (DEBUG)
|
|
{
|
|
_log.LogInfo($"Dealer handValuesPerCard ({outerCount} cards):");
|
|
for (int i = 0; i < outerCount; i++)
|
|
{
|
|
var inner = outerGetItem?.Invoke(hvpcList, new object[] { i });
|
|
if (inner == null) { _log.LogInfo($" [{i}] = null"); continue; }
|
|
int ic = Convert.ToInt32(inner.GetType().GetProperty("Count")?.GetValue(inner));
|
|
var igi = inner.GetType().GetMethod("get_Item");
|
|
string s = "";
|
|
for (int k = 0; k < ic; k++) s += (k > 0 ? "," : "") + igi?.Invoke(inner, new object[] { k });
|
|
_log.LogInfo($" [{i}]{(i == upcardIdx ? "upcard" : "")} = [{s}]");
|
|
}
|
|
}
|
|
|
|
if (upcardIdx < outerCount)
|
|
{
|
|
var innerList = outerGetItem?.Invoke(hvpcList, new object[] { upcardIdx });
|
|
if (innerList != null)
|
|
{
|
|
int ic = Convert.ToInt32(innerList.GetType().GetProperty("Count")?.GetValue(innerList));
|
|
var igi = innerList.GetType().GetMethod("get_Item");
|
|
int maxVal = 0;
|
|
for (int k = 0; k < ic; k++)
|
|
{
|
|
int v = Convert.ToInt32(igi?.Invoke(innerList, new object[] { k }));
|
|
if (v > maxVal) maxVal = v;
|
|
}
|
|
if (maxVal > 0) { _dealerUpcardValue = maxVal; readSuccess = true; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Fallback: handValues — List<int> ─────────────────────────────
|
|
if (!readSuccess && _getHandValues != null)
|
|
{
|
|
var hvList = _getHandValues.Invoke(dealerHand, null);
|
|
if (hvList != null)
|
|
{
|
|
int c = Convert.ToInt32(hvList.GetType().GetProperty("Count")?.GetValue(hvList));
|
|
var gi = hvList.GetType().GetMethod("get_Item");
|
|
int upcardIdx = c > 1 ? 1 : 0;
|
|
|
|
if (DEBUG)
|
|
{
|
|
_log.LogInfo($"Dealer handValues fallback ({c} entries):");
|
|
for (int i = 0; i < c; i++)
|
|
_log.LogInfo($" [{i}]{(i == upcardIdx ? "upcard" : "")} = {gi?.Invoke(hvList, new object[] { i })}");
|
|
}
|
|
|
|
if (upcardIdx < c)
|
|
{
|
|
int v = Convert.ToInt32(gi?.Invoke(hvList, new object[] { upcardIdx }));
|
|
if (v > 0) { _dealerUpcardValue = v; readSuccess = true; }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (readSuccess)
|
|
_log.LogInfo($"Dealer upcard value stored: {_dealerUpcardValue}");
|
|
else
|
|
{
|
|
_log.LogError("OnInitRound: could not determine dealer upcard value. Disabling plugin.");
|
|
_pluginEnabled = false;
|
|
}
|
|
}
|
|
catch (Exception ex) { _log.LogError($"OnInitRound: {ex}"); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Hook : Auto-decide hit/stand using basic strategy
|
|
// ---------------------------------------------------------------
|
|
private static void OnHandleNextDecision(object __instance, object __0, object __1)
|
|
{
|
|
if (!_pluginEnabled) return; // Check if plugin is enabled
|
|
|
|
try
|
|
{
|
|
if (!ResolveDecision()) return;
|
|
|
|
if (DEBUG) DumpRoundData(__0);
|
|
|
|
// ── Check if decision UI is showing (only active for client player) ──
|
|
var decIntf = _getDecInterface?.Invoke(__instance, null);
|
|
if (decIntf == null) return;
|
|
|
|
if (_getDecUIGameObject != null)
|
|
{
|
|
var decUIGO = _getDecUIGameObject.Invoke(decIntf, null);
|
|
if (decUIGO != null)
|
|
{
|
|
var activeSelfProp = decUIGO.GetType().GetProperty("activeSelf");
|
|
if (activeSelfProp != null)
|
|
{
|
|
bool isActive = Convert.ToBoolean(activeSelfProp.GetValue(decUIGO));
|
|
if (!isActive) return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Read decision data ──
|
|
int handId = Convert.ToInt32(_getDecHandId.Invoke(__0, null));
|
|
long betAmt = Convert.ToInt64(_getDecBetAmt.Invoke(__0, null));
|
|
int dealerValue = _dealerUpcardValue; // direct point value (2-11), -1 = unknown
|
|
|
|
// ── Player hand cards + analysis ──
|
|
int handTotal = CalculateHandTotal(__instance, handId, out bool isSoft, out int[] cardEnums);
|
|
if (handTotal < 0)
|
|
{
|
|
_log.LogError($"CRITICAL: Could not read cards for hand {handId} - disabling plugin");
|
|
_pluginEnabled = false; // Disable plugin entirely
|
|
return; // Exit hook
|
|
}
|
|
|
|
// Store round info for end-round logging
|
|
if (_roundBetAmount == 0) // First decision of the round
|
|
{
|
|
_roundBetAmount = betAmt;
|
|
_roundPlayerTotal = handTotal;
|
|
|
|
// Capture starting chips from decision data
|
|
try
|
|
{
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
|
var dataType = __0.GetType();
|
|
var chipsMethod = dataType.GetMethod("get_playersChips", bindAll)
|
|
?? dataType.GetMethod("get_PlayerChips", bindAll);
|
|
|
|
if (chipsMethod != null)
|
|
{
|
|
var chips = chipsMethod.Invoke(__0, null);
|
|
if (chips != null)
|
|
{
|
|
long currentChips = Convert.ToInt64(chips);
|
|
_roundStartChips = currentChips; // Track chips AFTER bet is deducted for net profit
|
|
_log.LogInfo($"Captured starting chips (after bet): current={currentChips}, bet={betAmt}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_log.LogWarning("Could not find get_playersChips method");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogWarning($"Failed to capture starting chips: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// ── Check for pair (for splitting) ──
|
|
bool isPair = cardEnums.Length == 2 &&
|
|
CardEnumToBlackjackValue(cardEnums[0]) ==
|
|
CardEnumToBlackjackValue(cardEnums[1]);
|
|
int pairValue = isPair ? CardEnumToBlackjackValue(cardEnums[0]) : 0;
|
|
|
|
// ── Basic strategy decision ──
|
|
var events = _getBjViewEvents.Invoke(__instance, null);
|
|
if (events == null) { _log.LogWarning("Auto-decide: events null"); return; }
|
|
|
|
string action = dealerValue > 0
|
|
? BasicStrategyAction(handTotal, isSoft, dealerValue, isPair, pairValue, cardEnums.Length)
|
|
: (handTotal <= 11 ? "HIT" : "STAND"); // fallback if dealer upcard still unknown
|
|
|
|
object actionStr;
|
|
string handDesc = $"player={handTotal}{(isSoft ? "(soft)" : "")}{(isPair ? $" pair={pairValue}" : "")} dealer={dealerValue}";
|
|
|
|
switch (action)
|
|
{
|
|
case "SPLIT":
|
|
actionStr = _getActionSplit.Invoke(null, null);
|
|
_log.LogInfo($">>> Hand {handId}: {handDesc} -> SPLIT <<<");
|
|
break;
|
|
case "DOUBLE":
|
|
actionStr = _getActionDouble.Invoke(null, null);
|
|
_log.LogInfo($">>> Hand {handId}: {handDesc} -> DOUBLE <<<");
|
|
break;
|
|
case "HIT":
|
|
actionStr = _getActionHit.Invoke(null, null);
|
|
_log.LogInfo($">>> Hand {handId}: {handDesc} -> HIT <<<");
|
|
break;
|
|
default: // STAND
|
|
actionStr = _getActionStand.Invoke(null, null);
|
|
_log.LogInfo($">>> Hand {handId}: {handDesc} -> STAND <<<");
|
|
break;
|
|
}
|
|
|
|
_blackjackAction.Invoke(events, new object[] { actionStr, handId, betAmt });
|
|
_hideDecision?.Invoke(decIntf, null);
|
|
|
|
// Store last action for end-round logging
|
|
_roundAction = action;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"OnHandleNextDecision: {ex}"); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Hook : Capture round results and log to database
|
|
// ---------------------------------------------------------------
|
|
/// <summary>
|
|
/// Postfix on BlackjackView.HandleOnEndRound(BlackjackEndRoundData).
|
|
/// Captures final chip balance, outcome, and profit to log complete round data.
|
|
/// </summary>
|
|
private static void OnEndRound(object __instance, object __0)
|
|
{
|
|
if (!_pluginEnabled) return;
|
|
|
|
try
|
|
{
|
|
if (__0 == null) { _log.LogWarning("OnEndRound: parameter is null"); return; }
|
|
ResolveEndRound(); // Initialize resolver
|
|
|
|
// Dump the parameter type to discover correct structure
|
|
if (DEBUG)
|
|
{
|
|
_log.LogInfo($"OnEndRound parameter type: {__0.GetType().FullName}");
|
|
DumpRoundData(__0);
|
|
}
|
|
|
|
// Extract data from BlackjackEndRoundData
|
|
string outcome = "UNKNOWN";
|
|
long finalChips = _roundStartChips;
|
|
long amountWon = 0;
|
|
long amountBet = 0;
|
|
bool busted = false;
|
|
bool blackjack = false;
|
|
bool foundData = false;
|
|
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
|
|
|
// Look for the "hands" collection in BlackjackEndRoundData
|
|
try
|
|
{
|
|
var paramType = __0.GetType();
|
|
var handsMethod = paramType.GetMethod("get_hands", bindAll);
|
|
|
|
if (handsMethod != null)
|
|
{
|
|
var hands = handsMethod.Invoke(__0, null);
|
|
if (hands != null)
|
|
{
|
|
var handsType = hands.GetType();
|
|
var countProp = handsType.GetProperty("Count");
|
|
if (countProp != null)
|
|
{
|
|
int count = Convert.ToInt32(countProp.GetValue(hands));
|
|
_log.LogInfo($"Found {count} hand(s) in round results");
|
|
|
|
if (count > 0)
|
|
{
|
|
var getItem = handsType.GetMethod("get_Item");
|
|
var firstHand = getItem?.Invoke(hands, new object[] { 0 });
|
|
|
|
if (firstHand != null)
|
|
{
|
|
var handType = firstHand.GetType();
|
|
|
|
// Extract key data from BlackjackHandData
|
|
var amountWonMethod = handType.GetMethod("get_amountWon", bindAll);
|
|
var amountBetMethod = handType.GetMethod("get_amountBet", bindAll);
|
|
var bustedMethod = handType.GetMethod("get_busted", bindAll);
|
|
var blackjackMethod = handType.GetMethod("get_blackjack", bindAll);
|
|
|
|
if (amountWonMethod != null)
|
|
{
|
|
amountWon = Convert.ToInt64(amountWonMethod.Invoke(firstHand, null));
|
|
_log.LogInfo($"Amount won: {amountWon}");
|
|
}
|
|
|
|
if (amountBetMethod != null)
|
|
{
|
|
amountBet = Convert.ToInt64(amountBetMethod.Invoke(firstHand, null));
|
|
_log.LogInfo($"Amount bet: {amountBet}");
|
|
}
|
|
|
|
if (bustedMethod != null)
|
|
{
|
|
busted = Convert.ToBoolean(bustedMethod.Invoke(firstHand, null));
|
|
_log.LogInfo($"Busted: {busted}");
|
|
}
|
|
|
|
if (blackjackMethod != null)
|
|
{
|
|
blackjack = Convert.ToBoolean(blackjackMethod.Invoke(firstHand, null));
|
|
_log.LogInfo($"Blackjack: {blackjack}");
|
|
}
|
|
|
|
foundData = true;
|
|
|
|
// Derive outcome from the data
|
|
if (blackjack)
|
|
outcome = "BLACKJACK";
|
|
else if (busted)
|
|
outcome = "BUST";
|
|
else if (amountWon > amountBet)
|
|
outcome = "WIN";
|
|
else if (amountWon == amountBet)
|
|
outcome = "PUSH";
|
|
else
|
|
outcome = "LOSE";
|
|
|
|
_log.LogInfo($"Derived outcome: {outcome}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_log.LogWarning("Could not find get_hands method on BlackjackEndRoundData");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogWarning($"Error extracting round data: {ex.Message}");
|
|
if (DEBUG) _log.LogError($"Stack trace: {ex.StackTrace}");
|
|
}
|
|
|
|
// Get current chip balance from BlackjackView
|
|
bool foundChips = false;
|
|
try
|
|
{
|
|
var viewType = __instance.GetType();
|
|
var chipsMethod = viewType.GetMethod("get_playersChips", bindAll)
|
|
?? viewType.GetMethod("get_PlayerChips", bindAll)
|
|
?? viewType.GetMethod("get_playerChips", bindAll)
|
|
?? viewType.GetMethod("get_Chips", bindAll)
|
|
?? viewType.GetMethod("get_chips", bindAll);
|
|
|
|
if (chipsMethod != null)
|
|
{
|
|
var chips = chipsMethod.Invoke(__instance, null);
|
|
if (chips != null)
|
|
{
|
|
finalChips = Convert.ToInt64(chips);
|
|
foundChips = true;
|
|
_log.LogInfo($"Got final chips from BlackjackView: {finalChips}");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogWarning($"Could not get chip balance from view: {ex.Message}");
|
|
}
|
|
|
|
// Calculate profit
|
|
long profit = 0;
|
|
|
|
if (foundData)
|
|
{
|
|
// Primary: Calculate net profit from amountWon and amountBet
|
|
// amountWon is the total returned (includes original bet if won, 0 if lost)
|
|
profit = amountWon - amountBet;
|
|
_log.LogInfo($"Calculated profit: {amountWon} (won) - {amountBet} (bet) = {profit}");
|
|
|
|
// Update final chips if we couldn't read it directly
|
|
if (!foundChips && _roundStartChips > 0)
|
|
{
|
|
finalChips = _roundStartChips + profit;
|
|
_log.LogInfo($"Calculated final chips: {_roundStartChips} + {profit} = {finalChips}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback: Calculate from chip balance change
|
|
if (foundChips && _roundStartChips > 0)
|
|
{
|
|
profit = finalChips - _roundStartChips;
|
|
_log.LogInfo($"Calculated profit from chip change: {finalChips} - {_roundStartChips} = {profit}");
|
|
}
|
|
else
|
|
{
|
|
_log.LogWarning($"Could not calculate profit: foundData={foundData}, foundChips={foundChips}, startChips={_roundStartChips}, bet={_roundBetAmount}");
|
|
}
|
|
}
|
|
|
|
// Log to database
|
|
_statsDatabase?.LogRound(
|
|
betAmount: _roundBetAmount,
|
|
chipBalance: finalChips,
|
|
outcome: outcome,
|
|
playerTotal: _roundPlayerTotal,
|
|
dealerUpcard: _dealerUpcardValue,
|
|
action: _roundAction,
|
|
profit: profit);
|
|
|
|
// Update session statistics
|
|
_sessionNetProfit += profit;
|
|
_sessionRoundsPlayed++;
|
|
|
|
_log.LogInfo($"Round complete: bet={_roundBetAmount}, outcome={outcome}, profit={profit:+#;-#;0}, balance={finalChips}");
|
|
_log.LogInfo($"SESSION: {_sessionRoundsPlayed} rounds, NET profit={_sessionNetProfit:+#;-#;0} ---");
|
|
|
|
// Reset round state
|
|
_roundBetAmount = 0;
|
|
_roundStartChips = 0;
|
|
_roundPlayerTotal = 0;
|
|
_roundAction = "";
|
|
}
|
|
catch (Exception ex) { _log.LogError($"OnEndRound: {ex}"); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Basic strategy (full strategy with double/split)
|
|
// ---------------------------------------------------------------
|
|
/// <summary>
|
|
/// Returns "HIT", "STAND", "DOUBLE", or "SPLIT".
|
|
/// dealerValue: 2-11 where 11 = Ace showing.
|
|
/// isSoft: player hand contains an Ace still counted as 11.
|
|
/// isPair: two cards with same value (for splitting).
|
|
/// pairValue: the value of the pair (2-11).
|
|
/// cardCount: number of cards in hand (only double on 2 cards).
|
|
/// </summary>
|
|
private static string BasicStrategyAction(int playerTotal, bool isSoft, int dealerValue,
|
|
bool isPair, int pairValue, int cardCount)
|
|
{
|
|
bool canDouble = cardCount == 2;
|
|
|
|
// ── Pair splitting (only on initial 2-card hand) ──
|
|
if (isPair && cardCount == 2)
|
|
{
|
|
if (pairValue == 11) // Aces
|
|
return "SPLIT";
|
|
if (pairValue == 10) // 10s
|
|
return "STAND";
|
|
if (pairValue == 9) // 9s: split vs 2-9 except 7
|
|
return (dealerValue >= 2 && dealerValue <= 9 && dealerValue != 7) ? "SPLIT" : "STAND";
|
|
if (pairValue == 8) // 8s
|
|
return "SPLIT";
|
|
if (pairValue == 7) // 7s: split vs 2-7
|
|
return (dealerValue >= 2 && dealerValue <= 7) ? "SPLIT" : "HIT";
|
|
if (pairValue == 6) // 6s: split vs 2-6
|
|
return (dealerValue >= 2 && dealerValue <= 6) ? "SPLIT" : "HIT";
|
|
if (pairValue == 5) // 5s: double vs 2-9
|
|
return (canDouble && dealerValue >= 2 && dealerValue <= 9) ? "DOUBLE" : "HIT";
|
|
if (pairValue == 4) // 4s: split vs 5-6
|
|
return (dealerValue == 5 || dealerValue == 6) ? "SPLIT" : "HIT";
|
|
if (pairValue <= 3) // 2s, 3s: split vs 2-7
|
|
return (dealerValue >= 2 && dealerValue <= 7) ? "SPLIT" : "HIT";
|
|
}
|
|
|
|
// ── Soft totals (Ace counted as 11) ──
|
|
if (isSoft)
|
|
{
|
|
if (playerTotal >= 20) return "STAND"; // Soft 20+
|
|
if (playerTotal == 19) // Soft 19 (A,8)
|
|
return (canDouble && dealerValue == 6) ? "DOUBLE" : "STAND";
|
|
if (playerTotal == 18) // Soft 18 (A,7)
|
|
{
|
|
if (canDouble && dealerValue >= 2 && dealerValue <= 6) return "DOUBLE";
|
|
if (dealerValue >= 9) return "HIT";
|
|
return "STAND";
|
|
}
|
|
if (playerTotal == 17) // Soft 17 (A,6)
|
|
return (canDouble && dealerValue >= 3 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 16) // Soft 16 (A,5)
|
|
return (canDouble && dealerValue >= 4 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 15) // Soft 15 (A,4)
|
|
return (canDouble && dealerValue >= 4 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 14) // Soft 14 (A,3)
|
|
return (canDouble && dealerValue >= 5 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 13) // Soft 13 (A,2)
|
|
return (canDouble && dealerValue >= 5 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
return "HIT"; // Soft 12 or less
|
|
}
|
|
|
|
// ── Hard totals ──
|
|
if (playerTotal >= 17) return "STAND";
|
|
if (playerTotal >= 13 && playerTotal <= 16) // 13-16: stand vs 2-6
|
|
return (dealerValue >= 2 && dealerValue <= 6) ? "STAND" : "HIT";
|
|
if (playerTotal == 12) // 12: stand vs 4-6
|
|
return (dealerValue >= 4 && dealerValue <= 6) ? "STAND" : "HIT";
|
|
if (playerTotal == 11) // 11: always double
|
|
return canDouble ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 10) // 10: double vs 2-9
|
|
return (canDouble && dealerValue >= 2 && dealerValue <= 9) ? "DOUBLE" : "HIT";
|
|
if (playerTotal == 9) // 9: double vs 3-6
|
|
return (canDouble && dealerValue >= 3 && dealerValue <= 6) ? "DOUBLE" : "HIT";
|
|
return "HIT"; // 8 or less
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Card total calculation from hand views
|
|
// ---------------------------------------------------------------
|
|
private static int CardEnumToBlackjackValue(int enumValue)
|
|
{
|
|
if (enumValue == 12) return 11;
|
|
if (enumValue >= 8) return 10;
|
|
if (enumValue >= 0) return enumValue + 2;
|
|
return 0;
|
|
}
|
|
|
|
private static int CardEnumToBlackjackValue(int enumValue, ref int aceCount)
|
|
{
|
|
if (enumValue == 12) { aceCount++; return 11; }
|
|
if (enumValue >= 8) return 10;
|
|
if (enumValue >= 0) return enumValue + 2;
|
|
return 0;
|
|
}
|
|
|
|
private static int CalculateHandTotal(object blackjackView, int handId, out bool isSoft, out int[] cardEnums)
|
|
{
|
|
isSoft = false;
|
|
cardEnums = new int[0];
|
|
try
|
|
{
|
|
var handViewsList = _getHandViews.Invoke(blackjackView, null);
|
|
if (handViewsList == null) { _log.LogWarning("m_HandViews is null"); return -1; }
|
|
|
|
int viewCount = Convert.ToInt32(handViewsList.GetType().GetProperty("Count")?.GetValue(handViewsList));
|
|
var getItem = handViewsList.GetType().GetMethod("get_Item");
|
|
|
|
for (int i = 0; i < viewCount; i++)
|
|
{
|
|
var handView = getItem?.Invoke(handViewsList, new object[] { i });
|
|
if (handView == null) continue;
|
|
|
|
if (Convert.ToInt32(_getHVHandId.Invoke(handView, null)) != handId) continue;
|
|
|
|
var cardsList = _getHVCards.Invoke(handView, null);
|
|
if (cardsList == null) { _log.LogWarning($"m_Cards null for hand {handId}"); return -1; }
|
|
|
|
int cardCount = Convert.ToInt32(cardsList.GetType().GetProperty("Count")?.GetValue(cardsList));
|
|
var getCard = cardsList.GetType().GetMethod("get_Item");
|
|
|
|
cardEnums = new int[cardCount];
|
|
int total = 0, aceCount = 0;
|
|
string cardDebug = "";
|
|
|
|
for (int j = 0; j < cardCount; j++)
|
|
{
|
|
var card = getCard?.Invoke(cardsList, new object[] { j });
|
|
if (card == null) continue;
|
|
int enumValue = Convert.ToInt32(_getCardValue.Invoke(card, null));
|
|
cardEnums[j] = enumValue;
|
|
cardDebug += (j > 0 ? "," : "") + enumValue;
|
|
total += CardEnumToBlackjackValue(enumValue, ref aceCount);
|
|
}
|
|
|
|
while (total > 21 && aceCount > 0) { total -= 10; aceCount--; }
|
|
isSoft = aceCount > 0;
|
|
|
|
_log.LogInfo($"Hand {handId}: cards=[{cardDebug}] total={total} soft={isSoft}");
|
|
return total;
|
|
}
|
|
|
|
_log.LogWarning($"No hand view found with handId={handId} (searched {viewCount} views)");
|
|
return -1;
|
|
}
|
|
catch (Exception ex) { _log.LogError($"CalculateHandTotal: {ex}"); return -1; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Diagnostic: dump any object's getters (guarded by DEBUG)
|
|
// ---------------------------------------------------------------
|
|
private static void DumpRoundData(object roundData)
|
|
{
|
|
if (!DEBUG) return;
|
|
if (!_pluginEnabled) return; // Check if plugin is enabled
|
|
|
|
try
|
|
{
|
|
|
|
_log.LogInfo("Dumping Function ----------------------------------");
|
|
_log.LogInfo("available bets 2: " + _getBets);
|
|
_log.LogInfo("DealerHand: " + _getRoundDealerHand);
|
|
_log.LogInfo("Hand Values: " + _getHandValues);
|
|
_log.LogInfo("Player Chips: " + _getPlayerChips);
|
|
|
|
var bindAll = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static |
|
|
BindingFlags.DeclaredOnly;
|
|
|
|
_log.LogInfo($"=== {roundData.GetType().Name} getters ===");
|
|
foreach (var m in roundData.GetType().GetMethods(bindAll))
|
|
{
|
|
if (!m.Name.StartsWith("get_") || m.GetParameters().Length != 0) continue;
|
|
try { _log.LogInfo($" {m.ReturnType.Name} {m.Name} = {m.Invoke(roundData, null)}"); }
|
|
catch { _log.LogInfo($" {m.ReturnType.Name} {m.Name} = <invoke failed>"); }
|
|
}
|
|
DEBUG = false; // Only dump once per session to avoid log spam
|
|
_log.LogInfo("Dump complete ----------------------------------");
|
|
}
|
|
catch (Exception ex) { _log.LogInfo($"DumpRoundData: {ex}"); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Plugin control methods
|
|
// ---------------------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Enable or disable the plugin at runtime.
|
|
/// </summary>
|
|
public void SetEnabled(bool enabled)
|
|
{
|
|
_pluginEnabled = enabled;
|
|
Log.LogInfo($"{PluginName} {(enabled ? "enabled" : "disabled")}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the plugin is currently enabled.
|
|
/// </summary>
|
|
public bool IsEnabled() => _pluginEnabled;
|
|
|
|
/// <summary>
|
|
/// Reset session statistics (rounds played and net profit).
|
|
/// </summary>
|
|
public void ResetSessionStats()
|
|
{
|
|
Log.LogInfo($"Resetting session stats. Previous: {_sessionRoundsPlayed} rounds, {_sessionNetProfit:+#;-#;0} net profit");
|
|
_sessionNetProfit = 0;
|
|
_sessionRoundsPlayed = 0;
|
|
Log.LogInfo("Session stats reset.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get current session statistics.
|
|
/// </summary>
|
|
public (int rounds, long netProfit) GetSessionStats()
|
|
{
|
|
return (_sessionRoundsPlayed, _sessionNetProfit);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
private static Type FindType(string fullName)
|
|
{
|
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
var t = asm.GetType(fullName);
|
|
if (t != null) return t;
|
|
}
|
|
string shortName = fullName.Contains('.')
|
|
? fullName[(fullName.LastIndexOf('.') + 1)..]
|
|
: fullName;
|
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
try { foreach (var t in asm.GetTypes()) if (t.Name == shortName) return t; }
|
|
catch { }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void PatchMethod(string typeName, string methodName, HarmonyMethod patch, bool isPostfix = false)
|
|
{
|
|
try
|
|
{
|
|
Type targetType = FindType(typeName);
|
|
if (targetType == null) { Log.LogWarning($" Type not found: {typeName}"); return; }
|
|
|
|
var methods = targetType.GetMethods(
|
|
BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.Static |
|
|
BindingFlags.DeclaredOnly);
|
|
|
|
int patched = 0;
|
|
foreach (var method in methods)
|
|
{
|
|
if (method.Name != methodName) continue;
|
|
try
|
|
{
|
|
if (isPostfix) _harmony.Patch(method, postfix: patch);
|
|
else _harmony.Patch(method, prefix: patch);
|
|
patched++;
|
|
Log.LogInfo($" Patched ({(isPostfix ? "postfix" : "prefix")}): {typeName}.{methodName}");
|
|
}
|
|
catch (Exception ex) { Log.LogWarning($" Failed to patch {typeName}.{methodName}: {ex.Message}"); }
|
|
}
|
|
|
|
if (patched == 0)
|
|
Log.LogWarning($" No methods named '{methodName}' found on {typeName}");
|
|
}
|
|
catch (Exception ex) { Log.LogError($" Error patching {typeName}.{methodName}: {ex}"); }
|
|
}
|
|
}
|