diff --git a/HouseRules_Core/HR.cs b/HouseRules_Core/HR.cs index 387ddaeb..9dc91779 100644 --- a/HouseRules_Core/HR.cs +++ b/HouseRules_Core/HR.cs @@ -18,7 +18,8 @@ public static void SelectRuleset(string ruleset) { if (IsRulesetActive) { - throw new InvalidOperationException("May not select a new ruleset while one is currently active."); + LifecycleDirector.DeactivateReconnect(); + // throw new InvalidOperationException("May not select a new ruleset while one is currently active."); } if (Ruleset.None.Name.Equals(ruleset, StringComparison.OrdinalIgnoreCase)) diff --git a/HouseRules_Core/HouseRules_Core.csproj b/HouseRules_Core/HouseRules_Core.csproj index bee9c864..72732aab 100644 --- a/HouseRules_Core/HouseRules_Core.csproj +++ b/HouseRules_Core/HouseRules_Core.csproj @@ -68,6 +68,7 @@ + diff --git a/HouseRules_Core/LifecycleDirector.cs b/HouseRules_Core/LifecycleDirector.cs index 87387386..67865442 100644 --- a/HouseRules_Core/LifecycleDirector.cs +++ b/HouseRules_Core/LifecycleDirector.cs @@ -2,27 +2,34 @@ { using System; using System.Linq; - using System.Reflection; using System.Text; using Boardgame; + using Boardgame.BoardgameActions; using Boardgame.Networking; using HarmonyLib; using HouseRules.Types; + using Photon.Pun; using Photon.Realtime; internal static class LifecycleDirector { - private const float WelcomeMessageDurationSeconds = 30f; private const string ModdedRoomPropertyKey = "modded"; - + private const float WelcomeMessageDurationSeconds = 30f; private static GameContext _gameContext; private static bool _isCreatingGame; private static bool _isLoadingGame; + private static bool _isReconnect = false; + private static string roomCode; + private static string lastCode; internal static bool IsRulesetActive { get; private set; } internal static void Patch(Harmony harmony) { + harmony.Patch( + original: AccessTools.Method(typeof(GameStateMachine), "OnRoomJoined"), + postfix: new HarmonyMethod(typeof(LifecycleDirector), nameof(GameStateMachine_OnRoomJoined_Postfix))); + harmony.Patch( original: AccessTools.Method(typeof(GameStartup), "InitializeGame"), postfix: new HarmonyMethod(typeof(LifecycleDirector), nameof(GameStartup_InitializeGame_Postfix))); @@ -35,6 +42,10 @@ internal static void Patch(Harmony harmony) original: AccessTools.Method(typeof(CreatingGameState), "OnJoinedRoom"), prefix: new HarmonyMethod(typeof(LifecycleDirector), nameof(CreatingGameState_OnJoinedRoom_Prefix))); + harmony.Patch( + original: AccessTools.Method(typeof(PlayingState), "OnMasterClientChanged"), + prefix: new HarmonyMethod(typeof(LifecycleDirector), nameof(PlayingGameState_OnMasterClientChanged_Prefix))); + harmony.Patch( original: AccessTools.Method(typeof(GameStateMachine), "GoToPlayingState"), postfix: new HarmonyMethod(typeof(LifecycleDirector), nameof(GameStateMachine_GoToPlayingState_Postfix))); @@ -45,9 +56,7 @@ internal static void Patch(Harmony harmony) harmony.Patch( original: AccessTools.Method(typeof(PostGameControllerBase), "OnPlayAgainClicked"), - postfix: new HarmonyMethod( - typeof(LifecycleDirector), - nameof(PostGameControllerBase_OnPlayAgainClicked_Postfix))); + postfix: new HarmonyMethod(typeof(LifecycleDirector), nameof(PostGameControllerBase_OnPlayAgainClicked_Postfix))); harmony.Patch( original: AccessTools.Method(typeof(GameStateMachine), "EndGame"), @@ -55,9 +64,11 @@ internal static void Patch(Harmony harmony) harmony.Patch( original: AccessTools.Method(typeof(SerializableEventQueue), "DisconnectLocalPlayer"), - prefix: new HarmonyMethod( - typeof(LifecycleDirector), - nameof(SerializableEventQueue_DisconnectLocalPlayer_Prefix))); + prefix: new HarmonyMethod(typeof(LifecycleDirector), nameof(SerializableEventQueue_DisconnectLocalPlayer_Prefix))); + + harmony.Patch( + original: AccessTools.Method(typeof(ReconnectState), "OnClickLeaveGameAfterReconnect"), + postfix: new HarmonyMethod(typeof(LifecycleDirector), nameof(ReconnectState_OnClickLeaveGameAfterReconnect_Postfix))); } private static void GameStartup_InitializeGame_Postfix(GameStartup __instance) @@ -66,6 +77,26 @@ private static void GameStartup_InitializeGame_Postfix(GameStartup __instance) _gameContext = gameContext; } + private static void ReconnectState_OnClickLeaveGameAfterReconnect_Postfix() + { + DeactivateReconnect(); + } + + private static void GameStateMachine_OnRoomJoined_Postfix() + { + if (!_isReconnect) + { + return; + } + + lastCode = PhotonNetwork.CurrentRoom.Name; + if (lastCode != roomCode) + { + CoreMod.Logger.Warning($"Room {lastCode} doesn't match original room {roomCode}. Deactivating reconnection rules!"); + DeactivateReconnect(); + } + } + private static void CreatingGameState_TryCreateRoom_Prefix() { if (HR.SelectedRuleset == Ruleset.None) @@ -125,10 +156,47 @@ private static void CreatingGameState_OnJoinedRoom_Prefix() var levelSequence = Traverse.Create(_gameContext.gameStateMachine).Field("levelSequence").Value; MotherbrainGlobalVars.CurrentConfig = levelSequence.gameConfig; + if (_isReconnect) + { + DeactivateReconnect(); + } + + roomCode = PhotonNetwork.CurrentRoom.Name; + CoreMod.Logger.Msg($"New game in room {roomCode} started"); ActivateRuleset(); OnPreGameCreated(); } + private static void PlayingGameState_OnMasterClientChanged_Prefix() + { + if (!_isReconnect) + { + return; + } + + if (!GameStateMachine.IsMasterClient) + { + return; + } + + if (HR.SelectedRuleset == Ruleset.None) + { + return; + } + + if (_gameContext.gameStateMachine.goBackToMenuState) + { + return; + } + + CoreMod.Logger.Warning($"<--- Resuming ruleset after disconnection from room {roomCode} --->"); + + ActivateRuleset(); + OnPreGameCreated(); + OnPostGameCreated(); + _isReconnect = false; + } + private static void GameStateMachine_GoToPlayingState_Postfix() { if (!_isCreatingGame) @@ -172,11 +240,31 @@ private static void GameStateMachine_EndGame_Prefix() DeactivateRuleset(); } - private static void SerializableEventQueue_DisconnectLocalPlayer_Prefix() + private static void SerializableEventQueue_DisconnectLocalPlayer_Prefix(BoardgameActionOnLocalPlayerDisconnect.DisconnectContext context) { - DeactivateRuleset(); - } + if (HR.SelectedRuleset == Ruleset.None) + { + return; + } + + if (!GameStateMachine.IsMasterClient) + { + return; + } + if (context == BoardgameActionOnLocalPlayerDisconnect.DisconnectContext.ReconnectState) + { + CoreMod.Logger.Warning($"<- Disconnected from room {roomCode} ->"); + _isReconnect = true; + DeactivateRuleset(); + } + else + { + CoreMod.Logger.Msg($"<- MANUALLY disconnected from room {roomCode} ->"); + _isReconnect = true; + DeactivateRuleset(); + } + } /// /// Add properties to the room to indicate its modded nature. @@ -201,13 +289,12 @@ private static void AddModdedRoomProperties(RoomOptions roomOptions) newOptions[0] = ModdedRoomPropertyKey; roomOptions.CustomRoomPropertiesForLobby.CopyTo(newOptions, 1); roomOptions.CustomRoomPropertiesForLobby = newOptions; - roomOptions.CustomRoomProperties.Add(ModdedRoomPropertyKey, true); } private static void ActivateRuleset() { - if (IsRulesetActive) + if (IsRulesetActive && !_isReconnect) { CoreMod.Logger.Warning("Ruleset activation was attempted whilst a ruleset was already activated. This should not happen. Please report this to HouseRules developers."); return; @@ -226,13 +313,22 @@ private static void ActivateRuleset() IsRulesetActive = true; - CoreMod.Logger.Msg($"Activating ruleset: {HR.SelectedRuleset.Name} (with {HR.SelectedRuleset.Rules.Count} rules)"); + CoreMod.Logger.Warning($"Activating ruleset: {HR.SelectedRuleset.Name} (with {HR.SelectedRuleset.Rules.Count} rules)"); foreach (var rule in HR.SelectedRuleset.Rules) { try { - CoreMod.Logger.Msg($"Activating rule type: {rule.GetType()}"); - rule.OnActivate(_gameContext); + var isDisabled = rule is IDisableOnReconnect; + if (_isReconnect && isDisabled) + { + CoreMod.Logger.Msg($"Skip activating rule type: {rule.GetType()}"); + continue; + } + else + { + CoreMod.Logger.Msg($"Activating rule type: {rule.GetType()}"); + rule.OnActivate(_gameContext); + } } catch (Exception e) { @@ -249,15 +345,27 @@ private static void DeactivateRuleset() return; } - IsRulesetActive = false; + if (!_isReconnect) + { + IsRulesetActive = false; + } CoreMod.Logger.Msg($"Deactivating ruleset: {HR.SelectedRuleset.Name} (with {HR.SelectedRuleset.Rules.Count} rules)"); foreach (var rule in HR.SelectedRuleset.Rules) { try { - CoreMod.Logger.Msg($"Deactivating rule type: {rule.GetType()}"); - rule.OnDeactivate(_gameContext); + var isDisabled = rule is IDisableOnReconnect; + if (_isReconnect && isDisabled) + { + CoreMod.Logger.Msg($"Skip deactivating rule type: {rule.GetType()}"); + continue; + } + else + { + CoreMod.Logger.Msg($"Deactivating rule type: {rule.GetType()}"); + rule.OnDeactivate(_gameContext); + } } catch (Exception e) { @@ -267,6 +375,32 @@ private static void DeactivateRuleset() } } + public static void DeactivateReconnect() + { + _isReconnect = false; + IsRulesetActive = false; + + CoreMod.Logger.Warning($"Deactivating reconnection: {HR.SelectedRuleset.Name} (with {HR.SelectedRuleset.Rules.Count} rules)"); + + foreach (var rule in HR.SelectedRuleset.Rules) + { + try + { + var isDisabled = rule is IDisableOnReconnect; + if (isDisabled) + { + CoreMod.Logger.Msg($"Deactivating reconnection for rule type: {rule.GetType()}"); + rule.OnDeactivate(_gameContext); + } + } + catch (Exception e) + { + // TODO(orendain): Consider rolling back or disable rule. + CoreMod.Logger.Warning($"Failed to deactivate reconnection for rule [{rule.GetType()}]: {e}"); + } + } + } + private static void OnPreGameCreated() { if (HR.SelectedRuleset == Ruleset.None) @@ -283,8 +417,17 @@ private static void OnPreGameCreated() { try { - CoreMod.Logger.Msg($"Calling OnPreGameCreated for rule type: {rule.GetType()}"); - rule.OnPreGameCreated(_gameContext); + var isDisabled = rule is IDisableOnReconnect; + if (_isReconnect && isDisabled) + { + CoreMod.Logger.Msg($"Skip OnPreGameCreated for rule type: {rule.GetType()}"); + continue; + } + else + { + CoreMod.Logger.Msg($"Calling OnPreGameCreated for rule type: {rule.GetType()}"); + rule.OnPreGameCreated(_gameContext); + } } catch (Exception e) { @@ -310,8 +453,17 @@ private static void OnPostGameCreated() { try { - CoreMod.Logger.Msg($"Calling OnPostGameCreated for rule type: {rule.GetType()}"); - rule.OnPostGameCreated(_gameContext); + var isDisabled = rule is IDisableOnReconnect; + if (_isReconnect && isDisabled) + { + CoreMod.Logger.Msg($"Skip OnPostGameCreated for rule type: {rule.GetType()}"); + continue; + } + else + { + CoreMod.Logger.Msg($"Calling OnPostGameCreated for rule type: {rule.GetType()}"); + rule.OnPostGameCreated(_gameContext); + } } catch (Exception e) { diff --git a/HouseRules_Core/Types/IDisableOnReconnect.cs b/HouseRules_Core/Types/IDisableOnReconnect.cs new file mode 100644 index 00000000..b714f086 --- /dev/null +++ b/HouseRules_Core/Types/IDisableOnReconnect.cs @@ -0,0 +1,9 @@ +namespace HouseRules.Types +{ + /// + /// Represents a rule that is not safe to apply after a disconnect in a multiplayer environment. + /// + public interface IDisableOnReconnect + { + } +} diff --git a/HouseRules_Essentials/Rules/LevelSequenceOverriddenRule.cs b/HouseRules_Essentials/Rules/LevelSequenceOverriddenRule.cs index c3abc8c6..5481b5ef 100644 --- a/HouseRules_Essentials/Rules/LevelSequenceOverriddenRule.cs +++ b/HouseRules_Essentials/Rules/LevelSequenceOverriddenRule.cs @@ -8,7 +8,7 @@ using HarmonyLib; using HouseRules.Types; - public sealed class LevelSequenceOverriddenRule : Rule, IConfigWritable>, IPatchable, IMultiplayerSafe + public sealed class LevelSequenceOverriddenRule : Rule, IConfigWritable>, IPatchable, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "LevelSequence is overridden"; diff --git a/HouseRules_Essentials/Rules/PieceAbilityListOverriddenRule.cs b/HouseRules_Essentials/Rules/PieceAbilityListOverriddenRule.cs index 4c19d9dc..3b3f7f71 100644 --- a/HouseRules_Essentials/Rules/PieceAbilityListOverriddenRule.cs +++ b/HouseRules_Essentials/Rules/PieceAbilityListOverriddenRule.cs @@ -9,7 +9,7 @@ using HouseRules.Types; public sealed class PieceAbilityListOverriddenRule : Rule, - IConfigWritable>>, IMultiplayerSafe + IConfigWritable>>, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece abilities are adjusted"; diff --git a/HouseRules_Essentials/Rules/PieceBehavioursListOverriddenRule.cs b/HouseRules_Essentials/Rules/PieceBehavioursListOverriddenRule.cs index c2c6bfe7..5df2c2cd 100644 --- a/HouseRules_Essentials/Rules/PieceBehavioursListOverriddenRule.cs +++ b/HouseRules_Essentials/Rules/PieceBehavioursListOverriddenRule.cs @@ -10,7 +10,7 @@ using Behaviour = DataKeys.Behaviour; public sealed class PieceBehavioursListOverriddenRule : Rule, - IConfigWritable>>, IMultiplayerSafe + IConfigWritable>>, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece behaviours are adjusted"; diff --git a/HouseRules_Essentials/Rules/PieceConfigAdjustedRule.cs b/HouseRules_Essentials/Rules/PieceConfigAdjustedRule.cs index 6d81299a..3f361c84 100644 --- a/HouseRules_Essentials/Rules/PieceConfigAdjustedRule.cs +++ b/HouseRules_Essentials/Rules/PieceConfigAdjustedRule.cs @@ -9,7 +9,7 @@ using HouseRules.Types; public sealed class PieceConfigAdjustedRule : Rule, IConfigWritable>, - IMultiplayerSafe + IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece configuration is adjusted"; diff --git a/HouseRules_Essentials/Rules/PieceImmunityListAdjustedRule.cs b/HouseRules_Essentials/Rules/PieceImmunityListAdjustedRule.cs index 1c42dd68..6b065465 100644 --- a/HouseRules_Essentials/Rules/PieceImmunityListAdjustedRule.cs +++ b/HouseRules_Essentials/Rules/PieceImmunityListAdjustedRule.cs @@ -9,7 +9,7 @@ using HouseRules.Types; public sealed class PieceImmunityListAdjustedRule : Rule, - IConfigWritable>>, IMultiplayerSafe + IConfigWritable>>, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece immunities are adjusted"; @@ -21,7 +21,7 @@ public sealed class PieceImmunityListAdjustedRule : Rule, /// /// Initializes a new instance of the class. /// - /// Dict of piece name and List + /// Dict of piece name and List. /// Replaces original settings with new list. public PieceImmunityListAdjustedRule(Dictionary> adjustments) { diff --git a/HouseRules_Essentials/Rules/PiecePieceTypeListOverriddenRule.cs b/HouseRules_Essentials/Rules/PiecePieceTypeListOverriddenRule.cs index 46ace698..d918e38b 100644 --- a/HouseRules_Essentials/Rules/PiecePieceTypeListOverriddenRule.cs +++ b/HouseRules_Essentials/Rules/PiecePieceTypeListOverriddenRule.cs @@ -9,7 +9,7 @@ using HouseRules.Types; public sealed class PiecePieceTypeListOverriddenRule : Rule, - IConfigWritable>>, IMultiplayerSafe + IConfigWritable>>, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece piece types are adjusted"; diff --git a/HouseRules_Essentials/Rules/PieceUseWhenKilledOverriddenRule.cs b/HouseRules_Essentials/Rules/PieceUseWhenKilledOverriddenRule.cs index 4e1e259c..6dbdb4e0 100644 --- a/HouseRules_Essentials/Rules/PieceUseWhenKilledOverriddenRule.cs +++ b/HouseRules_Essentials/Rules/PieceUseWhenKilledOverriddenRule.cs @@ -9,7 +9,7 @@ using HouseRules.Types; public sealed class PieceUseWhenKilledOverriddenRule : Rule, - IConfigWritable>>, IMultiplayerSafe + IConfigWritable>>, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Piece UseWhenKilled lists are overridden"; diff --git a/HouseRules_Essentials/Rules/StartCardsModifiedRule.cs b/HouseRules_Essentials/Rules/StartCardsModifiedRule.cs index cf369478..4c461a73 100644 --- a/HouseRules_Essentials/Rules/StartCardsModifiedRule.cs +++ b/HouseRules_Essentials/Rules/StartCardsModifiedRule.cs @@ -9,7 +9,8 @@ using HarmonyLib; using HouseRules.Types; - public sealed class StartCardsModifiedRule : Rule, IConfigWritable>>, IPatchable, IMultiplayerSafe + public sealed class StartCardsModifiedRule : Rule, IConfigWritable>>, + IPatchable, IMultiplayerSafe, IDisableOnReconnect { public override string Description => "Hero start cards are modified";