diff --git a/Fika.Core/Coop/ClientClasses/CoopClientAchievementController.cs b/Fika.Core/Coop/ClientClasses/CoopClientAchievementController.cs new file mode 100644 index 00000000..2f62e7ba --- /dev/null +++ b/Fika.Core/Coop/ClientClasses/CoopClientAchievementController.cs @@ -0,0 +1,28 @@ +using Comfort.Common; +using EFT; +using EFT.Quests; +using Fika.Core.Coop.Matchmaker; +using Fika.Core.Networking; + +namespace Fika.Core.Coop.ClientClasses +{ + public class CoopClientAchievementController : AchievementControllerClass + { + private readonly FikaClient _fikaClient = Singleton.Instance; + + public CoopClientAchievementController(Profile profile, InventoryControllerClass inventoryController, ISession session, bool fromServer) : base(profile, inventoryController, session, fromServer) + { + } + + public override void OnConditionValueChanged(IConditionCounter conditional, EQuestStatus status, Condition condition, bool notify) + { + if (MatchmakerAcceptPatches.IsClient) + { + ConditionChangePacket packet = new(_fikaClient.MyPlayer.NetId, condition.id, condition.value); + _fikaClient.SendData(_fikaClient.DataWriter, ref packet, LiteNetLib.DeliveryMethod.ReliableUnordered); + } + + base.OnConditionValueChanged(conditional, status, condition, notify); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Coop/ClientClasses/CoopClientQuestController.cs b/Fika.Core/Coop/ClientClasses/CoopClientQuestController.cs new file mode 100644 index 00000000..9bc047d2 --- /dev/null +++ b/Fika.Core/Coop/ClientClasses/CoopClientQuestController.cs @@ -0,0 +1,28 @@ +using Comfort.Common; +using EFT; +using EFT.Quests; +using Fika.Core.Coop.Matchmaker; +using Fika.Core.Networking; + +namespace Fika.Core.Coop.ClientClasses +{ + public class CoopClientQuestController : GClass3206 + { + private FikaClient fikaClient = Singleton.Instance; + + public CoopClientQuestController(Profile profile, InventoryControllerClass inventoryController, ISession session, bool fromServer) : base(profile, inventoryController, session, fromServer) + { + } + + public override void OnConditionValueChanged(IConditionCounter conditional, EQuestStatus status, Condition condition, bool notify) + { + if (MatchmakerAcceptPatches.IsClient) + { + ConditionChangePacket packet = new(fikaClient.MyPlayer.NetId, condition.id, condition.value); + fikaClient.SendData(fikaClient.DataWriter, ref packet, LiteNetLib.DeliveryMethod.ReliableUnordered); + } + + base.OnConditionValueChanged(conditional, status, condition, notify); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Coop/Components/CoopHandler.cs b/Fika.Core/Coop/Components/CoopHandler.cs index b881b263..5c47dc5a 100644 --- a/Fika.Core/Coop/Components/CoopHandler.cs +++ b/Fika.Core/Coop/Components/CoopHandler.cs @@ -61,6 +61,7 @@ public class SpawnObject(Profile profile, Vector3 position, bool isAlive, bool i internal static GameObject CoopHandlerParent; private Coroutine PingRoutine; + public bool StartSpawning = false; #endregion @@ -283,6 +284,11 @@ private async Task ReadFromServerCharactersLoop() { while (RunAsyncTasks) { + while (!StartSpawning) + { + await Task.Delay(1000); + } + CoopGame coopGame = (CoopGame)Singleton.Instance; int waitTime = 2500; if (coopGame.Status == GameStatus.Started) @@ -294,7 +300,6 @@ private async Task ReadFromServerCharactersLoop() if (Players == null) { continue; - } ReadFromServerCharacters(); @@ -369,7 +374,7 @@ await Singleton.Instance.LoadBundlesAndCreatePools(PoolManager.Pool if (!spawnObject.IsAlive) { - // TODO: Spawn them as corpses? + otherPlayer.OnDead(EDamageType.Undefined); } if (MatchmakerAcceptPatches.IsServer) @@ -402,6 +407,11 @@ private IEnumerator ProcessSpawnQueue() { while (true) { + if (!StartSpawning) + { + yield return new WaitUntil(() => StartSpawning); + } + yield return new WaitForSeconds(1f); if (Singleton.Instantiated) @@ -456,7 +466,7 @@ public WorldInteractiveObject GetInteractiveObject(string objectId, out WorldInt private ObservedCoopPlayer SpawnObservedPlayer(Profile profile, Vector3 position, int playerId, bool isAI, int netId) { ObservedCoopPlayer otherPlayer = ObservedCoopPlayer.CreateObservedPlayer(playerId, position, Quaternion.identity, - "Player", isAI == true ? "Bot_" : $"Player_{profile.Nickname}_", EPointOfView.ThirdPerson, profile, isAI, + "Player", isAI == true ? $"Bot_{netId}_" : $"Player_{profile.Nickname}_", EPointOfView.ThirdPerson, profile, isAI, EUpdateQueue.Update, Player.EUpdateMode.Manual, Player.EUpdateMode.Auto, GClass549.Config.CharacterController.ObservedPlayerMode, () => Singleton.Instance.Control.Settings.MouseSensitivity, diff --git a/Fika.Core/Coop/GameMode/CoopGame.cs b/Fika.Core/Coop/GameMode/CoopGame.cs index 3e368d87..794271a1 100644 --- a/Fika.Core/Coop/GameMode/CoopGame.cs +++ b/Fika.Core/Coop/GameMode/CoopGame.cs @@ -38,6 +38,7 @@ using Fika.Core.UI.Models; using HarmonyLib; using JsonType; +using LiteNetLib; using LiteNetLib.Utils; using Newtonsoft.Json; using System; @@ -486,7 +487,7 @@ private bool TryDespawnFurthest(Profile profile, Vector3 position, CoopHandler c if (botKey == string.Empty) { #if DEBUG - Logger.LogWarning("TryDespawnFurthest: botKey was empty"); + Logger.LogWarning("TryDespawnFurthest: botKey was empty"); #endif return false; } @@ -677,7 +678,7 @@ private async Task SendOrReceiveSpawnPoint() if (!string.IsNullOrEmpty(name)) { - Logger.LogInfo($"Retrieved Spawn Point '{name}' from server"); + Logger.LogInfo($"Retrieved Spawn Point {name} from server"); Dictionary allSpawnPoints = Traverse.Create(spawnPoints).Field("dictionary_0").GetValue>(); foreach (ISpawnPoint spawnPointObject in allSpawnPoints.Keys) @@ -727,7 +728,37 @@ public override async Task vmethod_2(int playerId, Vector3 position await CreateCoopHandler(); CoopHandler.GetCoopHandler().LocalGameInstance = this; - LocalPlayer myPlayer = await CoopPlayer.Create(playerId, spawnPoint.Position, spawnPoint.Rotation, "Player", "Main_", EPointOfView.FirstPerson, profile, + Vector3 PosToSpawn = spawnPoint.Position; + Quaternion RotToSpawn = spawnPoint.Rotation; + if (MatchmakerAcceptPatches.IsClient && MatchmakerAcceptPatches.IsReconnect) + { + ReconnectRequestPacket reconnectPacket = new(ProfileId); + MatchmakerAcceptPatches.GClass3163?.ChangeStatus($"Sending Reconnect Request..."); + + int retryCount = 0; + while (MatchmakerAcceptPatches.ReconnectPacket == null && retryCount < 5) + { + Singleton.Instance?.SendData(new NetDataWriter(), ref reconnectPacket, DeliveryMethod.ReliableUnordered); + MatchmakerAcceptPatches.GClass3163?.ChangeStatus($"Requests Sent for reconnect... {retryCount + 1}"); + await Task.Delay(3000); + retryCount++; + } + + if (MatchmakerAcceptPatches.ReconnectPacket == null && retryCount == 5) + { + MatchmakerAcceptPatches.GClass3163?.ChangeStatus($"Failed to Reconnect..."); + Singleton.Instance.ShowCriticalErrorScreen("Network Error", "[EXPERIMENTAL] Unable to reconnect to the host. Please try again after returning to main menu.", + ErrorScreen.EButtonType.OkButton, 10f, ReconnectFailed, ReconnectFailed); + } + + MatchmakerAcceptPatches.GClass3163?.ChangeStatus($"Reconnecting to host..."); + + PosToSpawn = MatchmakerAcceptPatches.ReconnectPacket.Value.Position; + RotToSpawn = MatchmakerAcceptPatches.ReconnectPacket.Value.Rotation; + profile = MatchmakerAcceptPatches.ReconnectPacket.Value.Profile.Profile; + } + + LocalPlayer myPlayer = await CoopPlayer.Create(playerId, PosToSpawn, RotToSpawn, "Player", "Main_", EPointOfView.FirstPerson, profile, false, UpdateQueue, armsUpdateMode, bodyUpdateMode, GClass549.Config.CharacterController.ClientPlayerMode, getSensitivity, getAimingSensitivity, new GClass1445(), MatchmakerAcceptPatches.IsServer ? 0 : 1000, statisticsManager); @@ -740,13 +771,21 @@ public override async Task vmethod_2(int playerId, Vector3 position throw new MissingComponentException("CoopHandler was missing during CoopGame init"); } + CoopPlayer coopPlayer = (CoopPlayer)myPlayer; + + if (MatchmakerAcceptPatches.IsClient && MatchmakerAcceptPatches.IsReconnect) + { + coopPlayer.NetId = MatchmakerAcceptPatches.ReconnectPacket.Value.NetId; + myPlayer.MovementContext.SetPoseLevel(MatchmakerAcceptPatches.ReconnectPacket.Value.PoseLevel, true); + myPlayer.MovementContext.IsInPronePose = MatchmakerAcceptPatches.ReconnectPacket.Value.IsProne; + } + if (RaidSettings.MetabolismDisabled) { myPlayer.HealthController.DisableMetabolism(); NotificationManagerClass.DisplayMessageNotification("Metabolism disabled", iconType: EFT.Communications.ENotificationIconType.Alert); } - CoopPlayer coopPlayer = (CoopPlayer)myPlayer; coopHandler.Players.Add(coopPlayer.NetId, coopPlayer); PlayerSpawnRequest body = new(myPlayer.ProfileId, MatchmakerAcceptPatches.GetGroupId()); @@ -822,15 +861,18 @@ public override async Task vmethod_2(int playerId, Vector3 position } } - SendCharacterPacket packet = new(new FikaSerialization.PlayerInfoPacket() { Profile = myPlayer.Profile }, myPlayer.HealthController.IsAlive, false, myPlayer.Transform.position, (myPlayer as CoopPlayer).NetId); - - if (MatchmakerAcceptPatches.IsServer) + if (!MatchmakerAcceptPatches.IsReconnect) { - await SetStatus(myPlayer, LobbyEntry.ELobbyStatus.COMPLETE); - } - else - { - Singleton.Instance.SendData(new NetDataWriter(), ref packet, LiteNetLib.DeliveryMethod.ReliableUnordered); + SendCharacterPacket packet = new(new FikaSerialization.PlayerInfoPacket() { Profile = myPlayer.Profile }, myPlayer.HealthController.IsAlive, false, myPlayer.Transform.position, (myPlayer as CoopPlayer).NetId); + + if (MatchmakerAcceptPatches.IsServer) + { + await SetStatus(myPlayer, LobbyEntry.ELobbyStatus.COMPLETE); + } + else + { + Singleton.Instance.SendData(new NetDataWriter(), ref packet, LiteNetLib.DeliveryMethod.ReliableUnordered); + } } if (MatchmakerAcceptPatches.IsServer) @@ -848,6 +890,7 @@ public override async Task vmethod_2(int playerId, Vector3 position } } + coopHandler.StartSpawning = true; await WaitForPlayers(); Destroy(customButton); @@ -861,6 +904,12 @@ public override async Task vmethod_2(int playerId, Vector3 position return myPlayer; } + private void ReconnectFailed() + { + ClientAppUtils.GetMainApp().method_48().HandleExceptions(); + Logger.LogError($"Failed to reconnect to the host. Returning to main menu..."); + } + private void MainPlayerDied(EDamageType obj) { EndByTimerScenario endByTimerScenario = GetComponent(); @@ -960,6 +1009,8 @@ private async Task CreateLocalPlayer() { spawnPoint = SpawnSystem.SelectSpawnPoint(ESpawnCategory.Player, Profile_0.Info.Side); await SendOrReceiveSpawnPoint(); + MatchmakerAcceptPatches.IsReconnect = false; + MatchmakerAcceptPatches.ReconnectPacket = null; } if (MatchmakerAcceptPatches.IsClient) @@ -1086,6 +1137,8 @@ private async Task WaitForPlayers() client.SendData(writer, ref packet, LiteNetLib.DeliveryMethod.ReliableOrdered); await Task.Delay(1000); } while (numbersOfPlayersToWaitFor > 0 && !forceStart); + + MatchmakerAcceptPatches.SpawnedPlayersComplete = true; } } @@ -1166,7 +1219,7 @@ public override IEnumerator vmethod_4(float startDelay, BotControllerSettings co if (limits > 0) { botsController_0.BotSpawner.SetMaxBots(limits); - } + } } DynamicAI = gameObject.AddComponent(); @@ -1605,6 +1658,13 @@ public override void Stop(string profileId, ExitStatus exitStatus, string exitNa { Logger.LogInfo("CoopGame::Stop"); + if (MatchmakerAcceptPatches.IsReconnect) + { + MatchmakerAcceptPatches.IsReconnect = false; + MatchmakerAcceptPatches.ReconnectPacket = null; + MatchmakerAcceptPatches.SpawnedPlayersComplete = false; + } + CoopPlayer myPlayer = (CoopPlayer)Singleton.Instance.MainPlayer; myPlayer.PacketSender.DestroyThis(); diff --git a/Fika.Core/Coop/Matchmaker/MatchmakerAccept/MatchmakerAcceptPatches.cs b/Fika.Core/Coop/Matchmaker/MatchmakerAccept/MatchmakerAcceptPatches.cs index 5aac7c1c..8a27bdd6 100644 --- a/Fika.Core/Coop/Matchmaker/MatchmakerAccept/MatchmakerAcceptPatches.cs +++ b/Fika.Core/Coop/Matchmaker/MatchmakerAccept/MatchmakerAcceptPatches.cs @@ -1,5 +1,6 @@ using EFT; using EFT.UI.Matchmaker; +using Fika.Core.Networking; using Fika.Core.Networking.Http; using Fika.Core.Networking.Http.Models; using System; @@ -32,6 +33,10 @@ public static class MatchmakerAcceptPatches public static WeatherClass[] Nodes { get; set; } = null; private static string groupId; private static long timestamp; + public static bool IsReconnect = false; + public static ReconnectResponsePacket? ReconnectPacket; + public static bool SpawnedPlayersComplete = false; + #endregion #region Static Fields diff --git a/Fika.Core/Coop/Patches/BaseLocalGame/BaseLocalGame_method_6_Patch.cs b/Fika.Core/Coop/Patches/BaseLocalGame/BaseLocalGame_method_6_Patch.cs new file mode 100644 index 00000000..6ecadadf --- /dev/null +++ b/Fika.Core/Coop/Patches/BaseLocalGame/BaseLocalGame_method_6_Patch.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using Aki.Reflection.Patching; +using EFT; +using Fika.Core.Coop.Matchmaker; + +namespace Fika.Core.Coop.Patches +{ + internal class BaseLocalGame_method_6_Patch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(BaseLocalGame).GetMethod(nameof(BaseLocalGame.method_11)); + } + + [PatchPrefix] + public static bool PatchPrefix(ref LocationSettingsClass.Location location) + { + if (MatchmakerAcceptPatches.IsClient && MatchmakerAcceptPatches.IsReconnect) + { + location.Loot = MatchmakerAcceptPatches.ReconnectPacket.Value.Items; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Fika.Core/Coop/Players/CoopPlayer.cs b/Fika.Core/Coop/Players/CoopPlayer.cs index 5da9fe75..ef0c3e4c 100644 --- a/Fika.Core/Coop/Players/CoopPlayer.cs +++ b/Fika.Core/Coop/Players/CoopPlayer.cs @@ -66,11 +66,11 @@ public static async Task Create(int playerId, Vector3 position, Qua ISession session = Singleton>.Instance.GetClientBackEndSession(); - GClass3206 questController = new(profile, inventoryController, session, true); + CoopClientQuestController questController = new(profile, inventoryController, session, true); questController.Init(); questController.Run(); - AchievementControllerClass achievementsController = new(profile, inventoryController, session, true); + CoopClientAchievementController achievementsController = new(profile, inventoryController, session, true); achievementsController.Init(); achievementsController.Run(); @@ -844,7 +844,6 @@ public override void TryInteractionCallback(LootableContainer container) protected virtual void Start() { Profile.Info.GroupId = "Fika"; - if (Side != EPlayerSide.Savage) { if (Equipment.GetSlot(EquipmentSlot.Dogtag).ContainedItem != null) diff --git a/Fika.Core/Coop/Players/ObservedCoopPlayer.cs b/Fika.Core/Coop/Players/ObservedCoopPlayer.cs index 0df67055..8de9d058 100644 --- a/Fika.Core/Coop/Players/ObservedCoopPlayer.cs +++ b/Fika.Core/Coop/Players/ObservedCoopPlayer.cs @@ -139,6 +139,7 @@ public static async Task CreateObservedPlayer(int playerId, CoopObservedStatisticsManager statisticsManager = new(); + await player.Init(rotation, layerName, pointOfView, profile, inventoryController, healthController, statisticsManager, null, null, filter, EVoipState.NotAvailable, aiControl, false); @@ -746,7 +747,10 @@ public override void OnDead(EDamageType damageType) } else { - NotificationManagerClass.DisplayWarningNotification($"Group member '{nickname}' has died"); + if (!MatchmakerAcceptPatches.IsReconnect) + { + NotificationManagerClass.DisplayWarningNotification($"Group member '{Profile.Nickname}' has died"); + } } } if (IsBoss(Profile.Info.Settings.Role, out string name) && IsObservedAI && LastAggressor != null) diff --git a/Fika.Core/FikaPlugin.cs b/Fika.Core/FikaPlugin.cs index d351c479..0ddba265 100644 --- a/Fika.Core/FikaPlugin.cs +++ b/Fika.Core/FikaPlugin.cs @@ -206,6 +206,7 @@ protected void Awake() new BotCacher_Patch().Enable(); new InventoryScroll_Patch().Enable(); new AbstractGame_InRaid_Patch().Enable(); + new BaseLocalGame_method_6_Patch().Enable(); #if GOLDMASTER new TOS_Patch().Enable(); #endif diff --git a/Fika.Core/Networking/FikaClient.cs b/Fika.Core/Networking/FikaClient.cs index dce47735..afcb0869 100644 --- a/Fika.Core/Networking/FikaClient.cs +++ b/Fika.Core/Networking/FikaClient.cs @@ -11,6 +11,7 @@ using EFT.Weather; using Fika.Core.Coop.Components; using Fika.Core.Coop.GameMode; +using Fika.Core.Coop.Matchmaker; using Fika.Core.Coop.Players; using Fika.Core.Modding; using Fika.Core.Modding.Events; @@ -22,10 +23,12 @@ using LiteNetLib; using LiteNetLib.Utils; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; using UnityEngine; namespace Fika.Core.Networking @@ -80,6 +83,7 @@ protected void Start() packetProcessor.SubscribeNetSerializable(OnAssignNetIdPacketReceived); packetProcessor.SubscribeNetSerializable(OnSyncNetIdPacketReceived); packetProcessor.SubscribeNetSerializable(OnOperationCallbackPacketReceived); + packetProcessor.SubscribeNetSerializable(OnReconnectResponsePacketReceived); _netClient = new NetManager(this) { @@ -124,6 +128,12 @@ private void OnOperationCallbackPacketReceived(OperationCallbackPacket packet) private void OnSyncNetIdPacketReceived(SyncNetIdPacket packet) { + if (MatchmakerAcceptPatches.IsClient && MatchmakerAcceptPatches.IsReconnect) + { + FikaPlugin.Instance.FikaLogger.LogInfo($"OnSyncNetIdPacketReceived: Client is reconnecting, ignore Sync."); + return; + } + Dictionary newPlayers = Players; if (Players.TryGetValue(packet.NetId, out CoopPlayer player)) { @@ -153,6 +163,12 @@ private void OnSyncNetIdPacketReceived(SyncNetIdPacket packet) private void OnAssignNetIdPacketReceived(AssignNetIdPacket packet) { + if (MatchmakerAcceptPatches.IsClient && MatchmakerAcceptPatches.IsReconnect) + { + FikaPlugin.Instance.FikaLogger.LogInfo($"OnAssignNetIdPacketReceived: Client is reconnecting, ignore assignment."); + return; + } + FikaPlugin.Instance.FikaLogger.LogInfo($"OnAssignNetIdPacketReceived: Assigned NetId {packet.NetId} to my own client."); MyPlayer.NetId = packet.NetId; int i = -1; @@ -176,6 +192,67 @@ private void OnAssignNetIdPacketReceived(AssignNetIdPacket packet) Players[packet.NetId] = MyPlayer; } + private void OnReconnectResponsePacketReceived(ReconnectResponsePacket packet) + { + MatchmakerAcceptPatches.IsReconnect = true; + MatchmakerAcceptPatches.ReconnectPacket = packet; + + StartCoroutine(SyncClientToHost(packet)); + } + + public IEnumerator SyncClientToHost(ReconnectResponsePacket packet) + { + while (!Singleton.Instantiated) + { + yield return null; + } + + ClientGameWorld gameWorld = Singleton.Instance as ClientGameWorld; + CoopGame coopGame = (CoopGame)Singleton.Instance; + + // interactables + WorldInteractiveObject[] interactiveObjects = FindObjectsOfType(); + for (int i = 0; i < packet.InteractiveObjectAmount; i++) + { + WorldInteractiveObject.GStruct385 packetInteractiveObject = packet.InteractiveObjects[i]; + // find interactive object with id + WorldInteractiveObject interactiveObject = interactiveObjects.FirstOrDefault(x => x.Id == packetInteractiveObject.Id); + interactiveObject?.SetFromStatusInfo(packetInteractiveObject); + } + + // Windows + for (int i = 0; i < packet.WindowBreakerAmount; i++) + { + gameWorld.method_20(packet.Windows[i].Id.GetHashCode(), packet.Windows[i].FirstHitPosition.Value); + } + + // lights + LampController[] clientLights = LocationScene.GetAllObjects(true).ToArray(); + for (int i = 0; i < packet.LightAmount; i++) + { + LampController lampController = packet.Lights[i]; + LampController clientLightToChange = clientLights.FirstOrDefault(x => x.NetId == lampController.NetId); + clientLightToChange?.Switch(lampController.LampState); + } + + // smokes + List smokes = new(); + for (int i = 0; i < packet.SmokeAmount; i++) + { + smokes.Add(packet.Smokes[i]); + } + + // ------------------- Anything that needs to come after player spawns ------------------------------- + while (coopGame.Status != GameStatus.Started) + { + yield return null; + } + gameWorld.OnSmokeGrenadesDeserialized(smokes); + // TODO: Smokes do spawn on the ground, but no visible smoke effect shows, im using BSG's method of doing this currently, so this might be a BSG thing. + // Decide if to fix or not. + + } + private void OnSendCharacterPacketReceived(SendCharacterPacket packet) { if (packet.PlayerInfo.Profile.ProfileId != MyPlayer.ProfileId) diff --git a/Fika.Core/Networking/FikaSerializationExtensions.cs b/Fika.Core/Networking/FikaSerializationExtensions.cs index 2a3ce5b6..2c811a42 100644 --- a/Fika.Core/Networking/FikaSerializationExtensions.cs +++ b/Fika.Core/Networking/FikaSerializationExtensions.cs @@ -1,5 +1,6 @@ using Comfort.Common; using EFT; +using EFT.Interactive; using EFT.InventoryLogic; using EFT.SynchronizableObjects; using LiteNetLib.Utils; @@ -239,5 +240,96 @@ public static Item GetItem(this NetDataReader reader) return GClass1524.DeserializeItem(Singleton.Instance, [], binaryReader.ReadEFTItemDescriptor()); } + + public static void PutInteractiveObjectState(this NetDataWriter writer, WorldInteractiveObject worldInteractiveObject) + { + writer.Put(worldInteractiveObject.Id); + writer.Put((byte)worldInteractiveObject.DoorState); + writer.Put(Mathf.FloorToInt(worldInteractiveObject.CurrentAngle)); + writer.Put(worldInteractiveObject as Door is Door door ? door.IsBroken : false); + } + + public static WorldInteractiveObject.GStruct385 GetInteractiveObjectState(this NetDataReader reader) + { + return new() + { + Id = reader.GetString(), + State = reader.GetByte(), + Angle = reader.GetInt(), + IsBroken = reader.GetBool() + }; + } + + public static void PutWindowBreakerState(this NetDataWriter writer, WindowBreaker windowBreaker) + { + writer.Put(windowBreaker.Id); + writer.Put(windowBreaker.FirstHitPosition.Value); + } + + public static WindowBreaker GetWindowBreakerState(this NetDataReader reader) + { + return new() + { + Id = reader.GetString(), + FirstHitPosition = reader.GetVector3(), + }; + } + + public static void PutLightState(this NetDataWriter writer, LampController windowBreaker) + { + writer.Put(windowBreaker.NetId); + writer.Put((byte)windowBreaker.LampState); + } + + public static LampController GetLightState(this NetDataReader reader) + { + return new() + { + NetId = reader.GetInt(), + LampState = (Turnable.EState)reader.GetByte() + }; + } + + public static void PutSmokeState(this NetDataWriter writer, Throwable smoke) + { + var smokeToUse = smoke as SmokeGrenade; + GStruct34 data = smokeToUse.NetworkData; + + writer.Put(data.Id); + writer.Put(data.Position); + writer.Put(data.Template); + writer.Put(data.Time); + writer.Put(data.Orientation); + writer.Put(data.PlatformId); + } + + public static GStruct34 GetSmokeState(this NetDataReader reader) + { + return new() + { + Id = reader.GetString(), + Position = reader.GetVector3(), + Template = reader.GetString(), + Time = reader.GetInt(), + Orientation = reader.GetQuaternion(), + PlatformId = reader.GetShort() + }; + } + + public static void PutLocationItem(this NetDataWriter writer, LootItemPositionClass[] locationItem) + { + using MemoryStream memoryStream = new(); + using BinaryWriter binaryWriter = new(memoryStream); + binaryWriter.Write(GClass1524.SerializeLootData(locationItem)); + writer.PutByteArray(memoryStream.ToArray()); + } + + public static GClass1202 GetLocationItem(this NetDataReader reader) + { + using MemoryStream memoryStream = new(reader.GetByteArray()); + using BinaryReader binaryReader = new(memoryStream); + + return GClass1524.DeserializeLootData(Singleton.Instance, binaryReader.ReadEFTLootDataDescriptor()); + } } } diff --git a/Fika.Core/Networking/FikaServer.cs b/Fika.Core/Networking/FikaServer.cs index 68da25bc..ecb1ea4a 100644 --- a/Fika.Core/Networking/FikaServer.cs +++ b/Fika.Core/Networking/FikaServer.cs @@ -21,6 +21,7 @@ using LiteNetLib.Utils; using Open.Nat; using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -83,6 +84,8 @@ public async void Start() packetProcessor.SubscribeNetSerializable(OnMinePacketReceived); packetProcessor.SubscribeNetSerializable(OnBorderZonePacketReceived); packetProcessor.SubscribeNetSerializable(OnSendCharacterPacketReceived); + packetProcessor.SubscribeNetSerializable(OnReconnectRequestPacketReceived); + packetProcessor.SubscribeNetSerializable(OnConditionChangedPacketReceived); _netServer = new NetManager(this) { @@ -164,6 +167,12 @@ await Task.Run(async () => ServerReady = true; } + private void OnConditionChangedPacketReceived(ConditionChangePacket packet, NetPeer peer) + { + Players.FirstOrDefault(x => x.Key == packet.NetId).Value.Profile.TaskConditionCounters + .FirstOrDefault(c => c.Key == packet.ConditionId).Value.Value = (int)packet.ConditionValue; + } + public int PopNetId() { int netId = _currentNetId; @@ -172,6 +181,78 @@ public int PopNetId() return netId; } + private void OnReconnectRequestPacketReceived(ReconnectRequestPacket packet, NetPeer peer) + { + serverLogger.LogError($"Player Wanting to reconnect {packet.ProfileId}"); + StartCoroutine(SyncClientToHost(packet, peer)); + } + + public IEnumerator SyncClientToHost(ReconnectRequestPacket packet, NetPeer peer) + { + while (!Singleton.Instantiated) + { + yield return null; + } + GameWorld gameWorld = Singleton.Instance; + ClientGameWorld ClientgameWorld = Singleton.Instance as ClientGameWorld; + CoopGame coopGame = (CoopGame)Singleton.Instance; + + while (string.IsNullOrEmpty(ClientgameWorld.MainPlayer.Location)) + { + yield return null; + } + + ObservedCoopPlayer playerToUse = (ObservedCoopPlayer)Players.FirstOrDefault((v) => v.Value.ProfileId == packet.ProfileId).Value; + + if (playerToUse == null) + { + serverLogger.LogError($"Player was not found"); + } + + WorldInteractiveObject[] interactiveObjects = FindObjectsOfType().Where(x => x.DoorState != x.InitialDoorState + && x.DoorState != EDoorState.Interacting && x.DoorState != EDoorState.Interacting).ToArray(); + + WindowBreaker[] windows = ClientgameWorld?.Windows.Where(x => x.AvailableToSync && x.IsDamaged).ToArray(); + + LampController[] lights = LocationScene.GetAllObjects(false).ToArray(); + + Throwable[] smokes = ClientgameWorld.Grenades.Where(x => x as SmokeGrenade is not null).ToArray(); + + LootItemPositionClass[] items = gameWorld.GetJsonLootItems().Where(x => x as GClass1200 is null).ToArray(); // will ignore corpses + // LootItemPositionClass[] items = gameWorld.GetJsonLootItems().ToArray(); // will include corpses + + Profile.GClass1756 health = playerToUse.NetworkHealthController.Store(null); + GClass2417.GClass2420[] effects = playerToUse.NetworkHealthController.IReadOnlyList_0.ToArray(); + + foreach (GClass2417.GClass2420 effect in effects) + { + if (!effect.Active) + { + continue; + } + + if (health.BodyParts[effect.BodyPart].Effects == null) + { + health.BodyParts[effect.BodyPart].Effects = new Dictionary(); + } + + if (!health.BodyParts[effect.BodyPart].Effects.ContainsKey(effect.GetType().Name) && effect is GInterface245) + { + health.BodyParts[effect.BodyPart].Effects.Add(effect.GetType().Name, new Profile.GClass1756.GClass1757 { Time = -1f, ExtraData = effect.StoreObj }); + } + } + + playerToUse.Profile.Health = health; + playerToUse.Profile.Info.EntryPoint = coopGame.InfiltrationPoint; + + ReconnectResponsePacket responsePacket = new(playerToUse.NetId, playerToUse.Transform.position, + playerToUse.Transform.rotation, playerToUse.Pose, playerToUse.PoseLevel, playerToUse.IsInPronePose, + interactiveObjects, windows, lights, smokes, + new FikaSerialization.PlayerInfoPacket() { Profile = playerToUse.Profile }, items); + + SendDataToPeer(peer, _dataWriter, ref responsePacket, DeliveryMethod.ReliableUnordered); + } + private void OnSendCharacterPacketReceived(SendCharacterPacket packet, NetPeer peer) { int netId = PopNetId(); diff --git a/Fika.Core/Networking/Packets/Player/ConditionChangePacket.cs b/Fika.Core/Networking/Packets/Player/ConditionChangePacket.cs new file mode 100644 index 00000000..2895690d --- /dev/null +++ b/Fika.Core/Networking/Packets/Player/ConditionChangePacket.cs @@ -0,0 +1,26 @@ +using LiteNetLib.Utils; + +namespace Fika.Core.Networking +{ + // used for both quests and achievements + public struct ConditionChangePacket(int netId, string conditionId, float conditionValue) : INetSerializable + { + public int NetId; + public string ConditionId; + public float ConditionValue; + + public void Deserialize(NetDataReader reader) + { + NetId = reader.GetInt(); + ConditionId = reader.GetString(); + ConditionValue = reader.GetFloat(); + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(netId); + writer.Put(conditionId); + writer.Put(conditionValue); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/Packets/Player/ReconnectRequestPacket.cs b/Fika.Core/Networking/Packets/Player/ReconnectRequestPacket.cs new file mode 100644 index 00000000..29a215aa --- /dev/null +++ b/Fika.Core/Networking/Packets/Player/ReconnectRequestPacket.cs @@ -0,0 +1,19 @@ +using LiteNetLib.Utils; + +namespace Fika.Core.Networking +{ + public struct ReconnectRequestPacket(string profileId): INetSerializable + { + public string ProfileId = profileId; + + public void Deserialize(NetDataReader reader) + { + ProfileId = reader.GetString(); + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(ProfileId); + } + } +} \ No newline at end of file diff --git a/Fika.Core/Networking/Packets/Player/ReconnectResponsePacket.cs b/Fika.Core/Networking/Packets/Player/ReconnectResponsePacket.cs new file mode 100644 index 00000000..eaa7e254 --- /dev/null +++ b/Fika.Core/Networking/Packets/Player/ReconnectResponsePacket.cs @@ -0,0 +1,143 @@ +using EFT; +using EFT.Interactive; +using LiteNetLib.Utils; +using UnityEngine; +using static Fika.Core.Networking.FikaSerialization; + +namespace Fika.Core.Networking +{ + public struct ReconnectResponsePacket(int netId, Vector3 position, Quaternion rotation, EPlayerPose playerPose, float poseLevel, + bool isProne, WorldInteractiveObject[] interactiveObjects, WindowBreaker[] windows, LampController[] lights, Throwable[] smokes + , PlayerInfoPacket profile, LootItemPositionClass[] items): INetSerializable + { + public int NetId; + public Vector3 Position; + public Quaternion Rotation; + public EPlayerPose PlayerPose; + public float PoseLevel; + public bool IsProne = isProne; + public int InteractiveObjectAmount; + public WorldInteractiveObject.GStruct385[] InteractiveObjects; + public int WindowBreakerAmount; + public WindowBreaker[] Windows; + public int LightAmount; + public LampController[] Lights; + public int SmokeAmount; + public GStruct34[] Smokes; + public PlayerInfoPacket Profile; + public int ItemAmount; + public GClass1202 Items; + + public void Deserialize(NetDataReader reader) + { + NetId = reader.GetInt(); + Position = reader.GetVector3(); + Rotation = reader.GetQuaternion(); + PlayerPose = (EPlayerPose)reader.GetByte(); + PoseLevel = reader.GetFloat(); + IsProne = reader.GetBool(); + InteractiveObjectAmount = reader.GetInt(); + + if (InteractiveObjectAmount > 0) + { + InteractiveObjects = new WorldInteractiveObject.GStruct385[InteractiveObjectAmount]; + for (int i = 0; i < InteractiveObjectAmount; i++) + { + InteractiveObjects[i] = reader.GetInteractiveObjectState(); + } + } + + WindowBreakerAmount = reader.GetInt(); + + if (WindowBreakerAmount > 0) + { + Windows = new WindowBreaker[WindowBreakerAmount]; + for (int i = 0; i < WindowBreakerAmount; i++) + { + Windows[i] = reader.GetWindowBreakerState(); + } + } + + LightAmount = reader.GetInt(); + + if (LightAmount > 0) + { + Lights = new LampController[LightAmount]; + for (int i = 0; i < LightAmount; i++) + { + Lights[i] = reader.GetLightState(); + } + } + + SmokeAmount = reader.GetInt(); + + if (SmokeAmount > 0) + { + Smokes = new GStruct34[SmokeAmount]; + for (int i = 0; i < SmokeAmount; i++) + { + Smokes[i] = reader.GetSmokeState(); + } + } + + Profile = PlayerInfoPacket.Deserialize(reader); + Items = reader.GetLocationItem(); + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(netId); + writer.Put(position); + writer.Put(rotation); + writer.Put((byte)playerPose); + writer.Put(poseLevel); + writer.Put(IsProne); + writer.Put(interactiveObjects.Length); + + if (interactiveObjects.Length > 0) + { + for (int i = 0; i < interactiveObjects.Length; i++) + { + writer.PutInteractiveObjectState(interactiveObjects[i]); + } + } + + writer.Put(windows.Length); + + if (windows.Length > 0) + { + for (int i = 0; i < windows.Length; i++) + { + writer.PutWindowBreakerState(windows[i]); + } + } + + writer.Put(lights.Length); + + if (lights.Length > 0) + { + for (int i = 0; i < lights.Length; i++) + { + writer.PutLightState(lights[i]); + } + } + + writer.Put(smokes.Length); + + if (smokes.Length > 0) + { + for (int i = 0; i < smokes.Length; i++) + { + writer.PutSmokeState(smokes[i]); + } + } + + PlayerInfoPacket.Serialize(writer, profile); + + if (items.Length > 0) + { + writer.PutLocationItem(items); + } + } + } +} \ No newline at end of file diff --git a/Fika.Core/UI/Custom/MatchMakerUIScript.cs b/Fika.Core/UI/Custom/MatchMakerUIScript.cs index 18697328..4ccddabe 100644 --- a/Fika.Core/UI/Custom/MatchMakerUIScript.cs +++ b/Fika.Core/UI/Custom/MatchMakerUIScript.cs @@ -300,6 +300,14 @@ private void RefreshUI() } Singleton.Instance.PlayUISound(EUISoundType.ButtonClick); + if (entry.Status == LobbyEntry.ELobbyStatus.REJOIN) + { + MatchmakerAcceptPatches.IsReconnect = true; + } + else + { + MatchmakerAcceptPatches.IsReconnect = false; + } StartCoroutine(JoinMatch(ProfileId, server.name, button)); }); @@ -379,23 +387,31 @@ private void RefreshUI() switch (entry.Status) { - case LobbyEntry.ELobbyStatus.LOADING: + case LobbyEntry.ELobbyStatus.LOADING: + tooltipTextGetter = new() { - tooltipTextGetter = new() - { - TooltipText = "Host is still loading." - }; - - button.enabled = false; - if (image != null) - { - image.color = new(0.5f, image.color.g / 2, image.color.b / 2, 0.75f); - } - - tooltipArea = joinButton.GetOrAddComponent(); - tooltipArea.enabled = true; - tooltipArea.SetMessageText(new Func(tooltipTextGetter.GetText)); + TooltipText = "Host is still loading." + }; + + button.enabled = false; + if (image != null) + { + image.color = new(0.5f, image.color.g / 2, image.color.b / 2, 0.75f); } + + tooltipArea = joinButton.GetOrAddComponent(); + tooltipArea.enabled = true; + tooltipArea.SetMessageText(new Func(tooltipTextGetter.GetText)); + break; + case LobbyEntry.ELobbyStatus.REJOIN: + tooltipTextGetter = new() + { + TooltipText = "Click to Rejoin raid." + }; + + tooltipArea = joinButton.GetOrAddComponent(); + tooltipArea.enabled = true; + tooltipArea.SetMessageText(new Func(tooltipTextGetter.GetText)); break; case LobbyEntry.ELobbyStatus.IN_GAME: tooltipTextGetter = new() diff --git a/Fika.Core/UI/Models/LobbyEntry.cs b/Fika.Core/UI/Models/LobbyEntry.cs index 5b7ef5dd..f20bf229 100644 --- a/Fika.Core/UI/Models/LobbyEntry.cs +++ b/Fika.Core/UI/Models/LobbyEntry.cs @@ -43,7 +43,8 @@ public enum ELobbyStatus { LOADING = 0, IN_GAME = 1, - COMPLETE = 2 + COMPLETE = 2, + REJOIN = 3 } } } \ No newline at end of file