using BepInEx; using BepInEx.Unity.IL2CPP; using HarmonyLib; using System; using System.Reflection; using UnityEngine; using static Il2CppSystem.Reflection.RuntimePropertyInfo; namespace AutoBlackjack; /// /// 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 (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 /// [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 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 // 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> private static MethodInfo _getHandValues; // BlackjackHandData.get_handValues() -> List (fallback) // ── Reflection handles: end round hook ────────────────────── private static bool _endRoundResolved; private static MethodInfo _getEndRoundData; // BlackjackEndRoundData.get_roundResults() -> List 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> — inner list holds possible values per card (e.g. [1,11] for Ace) _getHandValuesPerCard = handDataType?.GetMethod("get_handValuesPerCard", bindAll); // Fallback: List — 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 // --------------------------------------------------------------- /// /// Postfix on BlackjackView.HandleOnInitRound(BlackjackRoundData). /// /// Dealer upcard reading strategy (confirmed from runtime dumps): /// PRIMARY: handValuesPerCard[upcardIdx] is List 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. /// 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> ────────────────── 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 ───────────────────────────── 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 // --------------------------------------------------------------- /// /// Postfix on BlackjackView.HandleOnEndRound(BlackjackEndRoundData). /// Captures final chip balance, outcome, and profit to log complete round data. /// 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) // --------------------------------------------------------------- /// /// 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). /// 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} = "); } } 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 // --------------------------------------------------------------- /// /// Enable or disable the plugin at runtime. /// public void SetEnabled(bool enabled) { _pluginEnabled = enabled; Log.LogInfo($"{PluginName} {(enabled ? "enabled" : "disabled")}"); } /// /// Check if the plugin is currently enabled. /// public bool IsEnabled() => _pluginEnabled; /// /// Reset session statistics (rounds played and net profit). /// public void ResetSessionStats() { Log.LogInfo($"Resetting session stats. Previous: {_sessionRoundsPlayed} rounds, {_sessionNetProfit:+#;-#;0} net profit"); _sessionNetProfit = 0; _sessionRoundsPlayed = 0; Log.LogInfo("Session stats reset."); } /// /// Get current session statistics. /// 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}"); } } }