diff --git a/NebulaNetwork/SaveManager.cs b/NebulaNetwork/SaveManager.cs index eb589e94d..c8b8a8107 100644 --- a/NebulaNetwork/SaveManager.cs +++ b/NebulaNetwork/SaveManager.cs @@ -91,25 +91,31 @@ private static void HandleAutoSave() File.Move(str1, str2); } - public static void LoadServerData() + public static void LoadServerData(bool loadSaveFile) { - var path = GameConfig.gameSaveFolder + DSPGame.LoadFile + FILE_EXTENSION; + playerSaves.Clear(); - // var playerManager = Multiplayer.Session.Network.PlayerManager; + if (!loadSaveFile) + { + return; + } + var path = GameConfig.gameSaveFolder + DSPGame.LoadFile + FILE_EXTENSION; if (!File.Exists(path)) { + Log.Info($"No server file"); return; } - var source = File.ReadAllBytes(path); - var netDataReader = new NetDataReader(source); - ushort revision; try { + var source = File.ReadAllBytes(path); + var netDataReader = new NetDataReader(source); + ushort revision; + var revString = netDataReader.GetString(); if (revString != "REV") { - throw new Exception(); + throw new Exception("Incorrect header"); } revision = netDataReader.GetUShort(); @@ -119,43 +125,45 @@ public static void LoadServerData() // Supported revision: 5~8 if (revision is < 5 or > REVISION) { - throw new Exception(); + throw new Exception($"Unsupported version {revision}"); } } - } - catch (Exception) - { - Log.Warn("Skipping server data from unsupported Nebula version..."); - return; - } - var playerNum = netDataReader.GetInt(); + var playerNum = netDataReader.GetInt(); - for (var i = 0; i < playerNum; i++) - { - var hash = netDataReader.GetString(); - PlayerData playerData = null; - switch (revision) + for (var i = 0; i < playerNum; i++) { - case REVISION: - playerData = netDataReader.Get(() => new PlayerData()); - break; - case >= 5: - playerData = new PlayerData(); - playerData.Import(netDataReader, revision); - break; - } + var hash = netDataReader.GetString(); + PlayerData playerData = null; + switch (revision) + { + case REVISION: + playerData = netDataReader.Get(() => new PlayerData()); + break; + case >= 5: + playerData = new PlayerData(); + playerData.Import(netDataReader, revision); + break; + } - if (!playerSaves.ContainsKey(hash) && playerData != null) - { - playerSaves.Add(hash, playerData); - } - else if (playerData == null) - { - Log.Warn($"could not load player data from unsupported save file revision {revision}"); + if (!playerSaves.ContainsKey(hash) && playerData != null) + { + playerSaves.Add(hash, playerData); + } + else if (playerData == null) + { + Log.Warn($"Could not load player data from unsupported save file revision {revision}"); + } } } + catch (Exception e) + { + playerSaves.Clear(); + Log.WarnInform("Skipping server data due to exception:\n" + e.Message); + Log.Warn(e); + return; + } } public static bool TryAdd(string clientCertHash, IPlayerData playerData) diff --git a/NebulaNetwork/Server.cs b/NebulaNetwork/Server.cs index 029864dc5..4b018736c 100644 --- a/NebulaNetwork/Server.cs +++ b/NebulaNetwork/Server.cs @@ -174,10 +174,7 @@ internal void OnSocketDisconnection(INebulaConnection conn) public void Start() { - if (loadSaveFile) - { - SaveManager.LoadServerData(); - } + SaveManager.LoadServerData(loadSaveFile); foreach (var assembly in AssembliesUtils.GetNebulaAssemblies()) { diff --git a/NebulaPatcher/Patches/Dynamic/ConstructionModuleComponent_Patch.cs b/NebulaPatcher/Patches/Dynamic/ConstructionModuleComponent_Patch.cs index 5b88e9bcc..b84ad05a8 100644 --- a/NebulaPatcher/Patches/Dynamic/ConstructionModuleComponent_Patch.cs +++ b/NebulaPatcher/Patches/Dynamic/ConstructionModuleComponent_Patch.cs @@ -3,6 +3,7 @@ using HarmonyLib; using NebulaModel.Packets.Players; using NebulaWorld; +using NebulaWorld.Player; #endregion @@ -25,4 +26,17 @@ public static void EjectMechaDrone_Prefix(PlanetFactory factory, Player player, var packet = new PlayerEjectMechaDronePacket(playerId, planetId, targetObjectId, next1ObjectId, next2ObjectId, next3ObjectId, priority); Multiplayer.Session.Network.SendPacketToLocalStar(packet); } + + [HarmonyPrefix] + [HarmonyPatch(nameof(ConstructionModuleComponent.InsertTmpBuildTarget))] + public static bool InsertTmpBuildTarget_Prefix(ConstructionModuleComponent __instance, int objectId, float value) + { + if (__instance.entityId != 0 || value < DroneManager.MinSqrDistance) return true; // BAB, or distance is very close + if (!Multiplayer.IsActive) return true; + + // Only send out mecha drones if local player is the closest player to the target prebuild + if (GameMain.localPlanet == null) return true; + var sqrDistToOtherPlayer = Multiplayer.Session.Drones.GetClosestRemotePlayerSqrDistance(GameMain.localPlanet.factory.prebuildPool[objectId].pos); + return value <= sqrDistToOtherPlayer; + } } diff --git a/NebulaPatcher/Patches/Transpilers/ConstructionSystem_Transpiler.cs b/NebulaPatcher/Patches/Transpilers/ConstructionSystem_Transpiler.cs new file mode 100644 index 000000000..5d588f021 --- /dev/null +++ b/NebulaPatcher/Patches/Transpilers/ConstructionSystem_Transpiler.cs @@ -0,0 +1,63 @@ +#region + +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NebulaModel.Logger; +using NebulaModel.Packets.Factory; +using NebulaWorld; +using NebulaWorld.Player; +using UnityEngine; + +#endregion + +namespace NebulaPatcher.Patches.Transpilers; + +[HarmonyPatch(typeof(ConstructionSystem))] +internal class ConstructionSystem_Transpiler +{ + [HarmonyTranspiler] + [HarmonyPatch(nameof(ConstructionSystem.AddBuildTargetToModules))] + public static IEnumerable AddBuildTargetToModules_Transpiler(IEnumerable instructions) + { + try + { + /* Sync Prebuild.itemRequired changes by player, insert local method call after player.package.TakeTailItems + Replace: if (num8 <= num) { this.player.mecha.constructionModule.InsertBuildTarget ... } + With: if (num8 <= num && IsClosestPlayer(ref pos)) { this.player.mecha.constructionModule.InsertBuildTarget ... } + */ + + var codeMatcher = new CodeMatcher(instructions) + .MatchForward(true, + new CodeMatch(OpCodes.Ldloc_S), + new CodeMatch(OpCodes.Ldloc_0), + new CodeMatch(OpCodes.Bgt_Un) + ); + Log.Info(codeMatcher.IsValid); + var sqrDist = codeMatcher.InstructionAt(-2).operand; + var skipLabel = codeMatcher.Operand; + codeMatcher.Advance(1) + .Insert( + new CodeInstruction(OpCodes.Ldloc_S, sqrDist), + new CodeInstruction(OpCodes.Ldarg_2), //ref Vector3 pos + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(ConstructionSystem_Transpiler), nameof(IsClosestPlayer))), + new CodeInstruction(OpCodes.Brfalse_S, skipLabel) + ); + + return codeMatcher.InstructionEnumeration(); + } + catch (System.Exception e) + { + Log.Error("Transpiler ConstructionSystem.AddBuildTargetToModules failed."); + Log.Error(e); + return instructions; + } + } + + static bool IsClosestPlayer(float sqrDist, ref Vector3 pos) + { + if (!Multiplayer.IsActive || sqrDist < DroneManager.MinSqrDistance) return true; + return sqrDist <= Multiplayer.Session.Drones.GetClosestRemotePlayerSqrDistance(pos); + } +} diff --git a/NebulaWorld/Chat/ChatCommandRegistry.cs b/NebulaWorld/Chat/ChatCommandRegistry.cs index d271d91d4..d6d2ed9e3 100644 --- a/NebulaWorld/Chat/ChatCommandRegistry.cs +++ b/NebulaWorld/Chat/ChatCommandRegistry.cs @@ -27,6 +27,7 @@ static ChatCommandRegistry() RegisterCommand("system", new SystemCommandHandler(), "s"); RegisterCommand("reconnect", new ReconnectCommandHandler(), "r"); RegisterCommand("server", new ServerCommandHandler()); + RegisterCommand("playerdata", new PlayerDataCommandHandler()); } private static void RegisterCommand(string commandName, IChatCommandHandler commandHandlerHandler, params string[] aliases) diff --git a/NebulaWorld/Chat/Commands/PlayerDataCommandHandler.cs b/NebulaWorld/Chat/Commands/PlayerDataCommandHandler.cs new file mode 100644 index 000000000..aac8e4af5 --- /dev/null +++ b/NebulaWorld/Chat/Commands/PlayerDataCommandHandler.cs @@ -0,0 +1,167 @@ +#region + +using NebulaWorld.MonoBehaviours.Local.Chat; +using System.Collections.Generic; +using NebulaAPI.GameState; +using HarmonyLib; +using NebulaModel.DataStructures.Chat; +using System.IO; +using NebulaModel; +using NebulaModel.Logger; +using NebulaAPI.DataStructures; +using NebulaModel.Networking; +using NebulaModel.Packets.Players; + +#endregion + +namespace NebulaWorld.Chat.Commands; + +public class PlayerDataCommandHandler : IChatCommandHandler +{ + public void Execute(ChatWindow window, string[] parameters) + { + if (parameters.Length < 1) + { + throw new ChatCommandUsageException("Not enough arguments!".Translate()); + } + + // Due to dependency order, get SaveManager.playerSaves by reflection + var saveManagerType = AccessTools.TypeByName("NebulaNetwork.SaveManager"); + var playerSaves = (Dictionary)AccessTools.Field(saveManagerType, "playerSaves").GetValue(null); + + switch (parameters[0]) + { + case "list": + { + var resp = $"Player count in .server file: {playerSaves.Count}\n"; + foreach (var pair in playerSaves) + { + resp += $"[{pair.Key.Substring(0, 5)}] {pair.Value.Username}"; + } + window.SendLocalChatMessage(resp, ChatMessageType.CommandOutputMessage); + break; + } + case "load" when parameters.Length < 2: + throw new ChatCommandUsageException("Need to specifiy hash string or name of a player!"); + case "load": + { + var input = parameters[1]; + foreach (var pair in playerSaves) + { + if (input == pair.Key.Substring(0, input.Length) || input == pair.Value.Username) + { + window.SendLocalChatMessage($"Load [{pair.Key.Substring(0, 5)}] {pair.Value.Username}", ChatMessageType.CommandOutputMessage); + LoadPlayerData(pair.Value); + return; + } + } + break; + } + case "remove" when parameters.Length < 2: + throw new ChatCommandUsageException("Need to specifiy hash string or name of a player!"); + case "remove": + { + var input = parameters[1]; + var removeHash = ""; + foreach (var pair in playerSaves) + { + if (input == pair.Key.Substring(0, input.Length) || input == pair.Value.Username) + { + window.SendLocalChatMessage($"Remove [{pair.Key.Substring(0, 5)}] {pair.Value.Username}", ChatMessageType.CommandOutputMessage); + removeHash = pair.Key; + break; + } + } + playerSaves.Remove(removeHash); + break; + } + } + } + + public string GetDescription() + { + return "Manage the stored multiplayer player data".Translate(); + } + + public string[] GetUsage() + { + return ["list", "load ", "remove "]; + } + + static void LoadPlayerData(IPlayerData playerData) + { + Log.Info($"Teleporting to target planet {GameMain.localPlanet?.id ?? 0} => {playerData.LocalPlanetId}"); + var actionSail = GameMain.mainPlayer.controller.actionSail; + if (playerData.LocalPlanetId > 0) + { + if (playerData.LocalPlanetId == GameMain.localPlanet?.id) + { + UIRoot.instance.uiGame.globemap.LocalPlanetTeleport(playerData.LocalPlanetPosition.ToVector3()); + } + else + { + var destPlanet = GameMain.galaxy.PlanetById(playerData.LocalPlanetId); + actionSail.fastTravelTargetPlanet = destPlanet; + actionSail.fastTravelTargetLPos = playerData.LocalPlanetPosition.ToVector3(); + actionSail.fastTravelTargetUPos = destPlanet.uPosition + (VectorLF3)(destPlanet.runtimeRotation * actionSail.fastTravelTargetLPos); + actionSail.StartFastTravelToUPosition(actionSail.fastTravelTargetUPos); + } + } + else + { + GameMain.mainPlayer.controller.actionSail.StartFastTravelToUPosition(playerData.UPosition.ToVectorLF3()); + } + + var mechaData = playerData.Mecha; + Log.Info("Loading player inventory..."); + using (var ms = new MemoryStream()) + { + var bw = new BinaryWriter(ms); + mechaData.Inventory.Export(bw); + mechaData.DeliveryPackage.Export(bw); + mechaData.WarpStorage.Export(bw); + + ms.Seek(0, SeekOrigin.Begin); + var br = new BinaryReader(ms); + GameMain.mainPlayer.package.Import(br); + GameMain.mainPlayer.deliveryPackage.Import(br); + GameMain.mainPlayer.mecha.warpStorage.Import(br); + } + if (!Config.Options.SyncSoil) + { + GameMain.mainPlayer.sandCount = mechaData.SandCount; + } + GameMain.mainPlayer.mecha.coreEnergy = mechaData.CoreEnergy; + GameMain.mainPlayer.mecha.reactorEnergy = mechaData.ReactorEnergy; + + if (playerData.Appearance != null) + { + Log.Info("Loading custom appearance..."); + playerData.Appearance.CopyTo(GameMain.mainPlayer.mecha.appearance); + GameMain.mainPlayer.mechaArmorModel.RefreshAllPartObjects(); + GameMain.mainPlayer.mechaArmorModel.RefreshAllBoneObjects(); + GameMain.mainPlayer.mecha.appearance.NotifyAllEvents(); + + var editor = UIRoot.instance.uiMechaEditor; + editor.selection.ClearSelection(); + editor.saveGroup._Close(); + if (editor.mecha.diyAppearance == null) + { + editor.mecha.diyAppearance = new MechaAppearance(); + editor.mecha.diyAppearance.Init(); + } + GameMain.mainPlayer.mecha.appearance.CopyTo(editor.mecha.diyAppearance); + editor.mechaArmorModel.RefreshAllPartObjects(); + editor.mechaArmorModel.RefreshAllBoneObjects(); + editor.mecha.diyAppearance.NotifyAllEvents(); + editor._left_content_height_max = 0f; + editor.SetLeftScrollTop(); + editor.saveGroup._Open(); + + using var writer = new BinaryUtils.Writer(); + GameMain.mainPlayer.mecha.appearance.Export(writer.BinaryWriter); + Multiplayer.Session.Network.SendPacket(new PlayerMechaArmor(Multiplayer.Session.LocalPlayer.Id, + writer.CloseAndGetBytes())); + } + } +} diff --git a/NebulaWorld/Player/DroneManager.cs b/NebulaWorld/Player/DroneManager.cs index de6ba54fa..ff0a77717 100644 --- a/NebulaWorld/Player/DroneManager.cs +++ b/NebulaWorld/Player/DroneManager.cs @@ -15,7 +15,11 @@ namespace NebulaWorld.Player; public class DroneManager : IDisposable { + public const float MinSqrDistance = 225.0f; + private readonly Dictionary cachedPositions = []; + private Vector3[] localPlayerPos = new Vector3[2]; + private int localPlayerCount = 0; private long lastCheckedTick = 0; private readonly List crafts = []; private readonly Stack craftRecyleIds = []; @@ -68,6 +72,8 @@ public void RefreshCachedPositions() if (GameMain.gameTick != lastCheckedTick) { lastCheckedTick = GameMain.gameTick; + localPlayerCount = 0; + var currentLocalPlanetId = GameMain.localPlanet?.id ?? int.MinValue; //CachedPositions.Clear(); using (Multiplayer.Session.World.GetRemotePlayersModels(out var remotePlayersModels)) @@ -89,11 +95,31 @@ public void RefreshCachedPositions() { cachedPositions.Add(model.Movement.PlayerID, new PlayerPosition(ejectPos, model.Movement.localPlanetId)); } + + if (currentLocalPlanetId != localPlanetId) continue; + if (localPlayerCount >= localPlayerPos.Length) + { + var newArray = new Vector3[localPlayerPos.Length * 2]; + Array.Copy(localPlayerPos, newArray, localPlayerPos.Length); + localPlayerPos = newArray; + } + localPlayerPos[localPlayerCount++] = playerPos; } } } } + public float GetClosestRemotePlayerSqrDistance(Vector3 pos) + { + var result = float.MaxValue; + for (var i = 0; i < localPlayerCount; i++) + { + var sqrMagnitude = (pos - localPlayerPos[i]).sqrMagnitude; + if (sqrMagnitude < result) result = sqrMagnitude; + } + return result; + } + public Vector3 GetPlayerEjectPosition(ushort playerId) { return cachedPositions.TryGetValue(playerId, out var value) ? value.Position : GameMain.mainPlayer.position;