From 7c76c271262932a944aef8fd086950c9069530da Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Sun, 1 Aug 2021 14:37:13 -0400 Subject: [PATCH] Add optional ability to stop terrain modification in exclusion zones (for players not allowed to place structures in zone) --- CLre/CLre.csproj | 2 +- CLre_server/API/Config/CLreConfig.cs | 34 +++ CLre_server/API/Engines/ServerEngines.cs | 8 +- CLre_server/API/MainServer/ServerEngines.cs | 3 - .../API/Synergy/ServerHandshakeEngine.cs | 3 - .../API/Synergy/ServerMessagingEngine.cs | 3 - .../SerializedCLreTerrainModifyRejection.cs | 56 +++++ CLre_server/CLre_server.cs | 21 +- CLre_server/CLre_server.csproj | 2 +- .../TerrainModificationExclusionZone.cs | 238 ++++++++++++++++++ CLre_server/WebStatus/StatusEndpoints.cs | 3 - 11 files changed, 338 insertions(+), 35 deletions(-) create mode 100644 CLre_server/API/Synergy/Tweaks/SerializedCLreTerrainModifyRejection.cs create mode 100644 CLre_server/Tweaks/TerrainModificationExclusionZone.cs diff --git a/CLre/CLre.csproj b/CLre/CLre.csproj index d6f2fad..314259a 100644 --- a/CLre/CLre.csproj +++ b/CLre/CLre.csproj @@ -11,7 +11,7 @@ - + diff --git a/CLre_server/API/Config/CLreConfig.cs b/CLre_server/API/Config/CLreConfig.cs index 11f4a88..2922c71 100644 --- a/CLre_server/API/Config/CLreConfig.cs +++ b/CLre_server/API/Config/CLreConfig.cs @@ -1,4 +1,5 @@ using System; +using System.IO; namespace CLre_server.API.Config { @@ -7,6 +8,7 @@ namespace CLre_server.API.Config { public bool clre_clients_only; public bool web_server; + public bool terrain_exclusion_zone; public static CLreConfig Default() { @@ -14,6 +16,7 @@ namespace CLre_server.API.Config { clre_clients_only = false, web_server = false, + terrain_exclusion_zone = false, }; } @@ -22,6 +25,37 @@ namespace CLre_server.API.Config return UnityEngine.JsonUtility.FromJson(s); } + public static CLreConfig FromFile(string path) + { + string s = File.ReadAllText(path); + return FromString(s); + } + + public static CLreConfig FromFileSafely(string path) + { + // try to load config file + CLreConfig conf = Default(); + try + { + string s = File.ReadAllText(path); + conf = FromString(s); + } + catch (Exception e) + { + Utility.Logging.LogWarning($"Failed to load {path}: {e}\n{e.StackTrace}"); + try + { + File.WriteAllText(path, conf.ToString()); + } + catch (Exception e2) + { + Utility.Logging.LogWarning($"Failed to write {path}: {e2}\n{e2.StackTrace}"); + } + } + + return conf; + } + public override string ToString() { return UnityEngine.JsonUtility.ToJson(this, true); diff --git a/CLre_server/API/Engines/ServerEngines.cs b/CLre_server/API/Engines/ServerEngines.cs index 90f598f..fabc9ed 100644 --- a/CLre_server/API/Engines/ServerEngines.cs +++ b/CLre_server/API/Engines/ServerEngines.cs @@ -13,8 +13,8 @@ namespace CLre_server.API.Engines } public abstract void Ready(); - public abstract IEntitiesDB entitiesDB { get; set; } - public abstract IEntityFactory entityFactory { get; set; } + public IEntitiesDB entitiesDB { get; set; } + public IEntityFactory entityFactory { get; set; } public IDataDB dataDB { get; set; } } @@ -26,8 +26,8 @@ namespace CLre_server.API.Engines } public abstract void Ready(); - public abstract IEntitiesDB entitiesDB { get; set; } - public abstract IEntityFactory entityFactory { get; set; } + public IEntitiesDB entitiesDB { get; set; } + public IEntityFactory entityFactory { get; set; } public IDataDB dataDB { get; set; } } diff --git a/CLre_server/API/MainServer/ServerEngines.cs b/CLre_server/API/MainServer/ServerEngines.cs index b55c0c3..85b11b3 100644 --- a/CLre_server/API/MainServer/ServerEngines.cs +++ b/CLre_server/API/MainServer/ServerEngines.cs @@ -28,9 +28,6 @@ namespace CLre_server.API.MainServer }); } - public override IEntitiesDB entitiesDB { get; set; } - public override IEntityFactory entityFactory { get; set; } - public void OnFrameworkInitialized() { GameServerSettings gss = Server.Instance.GameServerSettings; diff --git a/CLre_server/API/Synergy/ServerHandshakeEngine.cs b/CLre_server/API/Synergy/ServerHandshakeEngine.cs index 9578a97..eccb155 100644 --- a/CLre_server/API/Synergy/ServerHandshakeEngine.cs +++ b/CLre_server/API/Synergy/ServerHandshakeEngine.cs @@ -50,9 +50,6 @@ namespace CLre_server.API.Synergy Sender(payload, playerId).Run(); } - public override IEntitiesDB entitiesDB { get; set; } - public override IEntityFactory entityFactory { get; set; } - public IEnumerator Sender(SerializedCLreHandshake payload, int playerId) { yield return null; diff --git a/CLre_server/API/Synergy/ServerMessagingEngine.cs b/CLre_server/API/Synergy/ServerMessagingEngine.cs index 409dc08..8dd863d 100644 --- a/CLre_server/API/Synergy/ServerMessagingEngine.cs +++ b/CLre_server/API/Synergy/ServerMessagingEngine.cs @@ -56,9 +56,6 @@ namespace CLre_server.API.Synergy }); } - public override IEntitiesDB entitiesDB { get; set; } - public override IEntityFactory entityFactory { get; set; } - public ServerMessagingEngine(): base() { MainServer.Server.Instance.Connected += (_, __) => { MessageSender().Run(); }; diff --git a/CLre_server/API/Synergy/Tweaks/SerializedCLreTerrainModifyRejection.cs b/CLre_server/API/Synergy/Tweaks/SerializedCLreTerrainModifyRejection.cs new file mode 100644 index 0000000..e9ba82b --- /dev/null +++ b/CLre_server/API/Synergy/Tweaks/SerializedCLreTerrainModifyRejection.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using NetworkFramework.Shared; + +namespace CLre_server.API.Synergy.Tweaks +{ + public struct SerializedCLreTerrainModifyRejection: ISerializedNetData + { + public RejectionFlag Flags; + + public uint Cell; + + public byte[] Serialize() + { + using (MemoryStream stream = new MemoryStream()) + { + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write((byte)Flags); + writer.Write(Cell); + return stream.ToArray(); + } + } + } + + public void Deserialize(byte[] data) + { + using (MemoryStream stream = new MemoryStream(data)) + { + using (BinaryReader reader = new BinaryReader(stream)) + { + Flags = (RejectionFlag)reader.ReadByte(); + Cell = reader.ReadUInt32(); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Ok() + { + return (Flags & RejectionFlag.Rejection) == RejectionFlag.None; + } + } + + [Flags] + public enum RejectionFlag : byte + { + None = 0, + Rejection = 1, + Proximity = 1 << 1, + Permission = 1 << 2, + AccountNotFound = 1 << 3, + InitError = 1 << 4, + } +} diff --git a/CLre_server/CLre_server.cs b/CLre_server/CLre_server.cs index fb9ce2c..9944e19 100644 --- a/CLre_server/CLre_server.cs +++ b/CLre_server/CLre_server.cs @@ -88,26 +88,11 @@ namespace CLre_server API.MainServer.Server.Instance.InitComplete += (_, __) => API.Utility.Logging.MetaLog("(!) Server successfully initialised"); #endif // try to load config file - try - { - string s = File.ReadAllText("CLre_server.json"); - Config = CLreConfig.FromString(s); - } - catch (Exception e) - { - API.Utility.Logging.LogWarning($"Failed to load CLre_server.json: {e}\n{e.StackTrace}"); - try - { - File.WriteAllText("CLre_server.json", Config.ToString()); - } - catch (Exception e2) - { - API.Utility.Logging.LogWarning($"Failed to write CLre_server.json: {e2}\n{e2.StackTrace}"); - } - } + Config = CLreConfig.FromFileSafely("CLre_server.json"); // init config-dependent functionality WebServer.Init(); API.Synergy.CLreEnforcer.Init(); + Tweaks.TerrainModificationExclusionZone.Init(); // Log info API.Utility.Logging.MetaLog($"{Name} init complete."); } @@ -129,6 +114,7 @@ namespace CLre_server API.Utility.Logging.Log(sb.ToString()); } +#if DEBUG public override void OnGUI() { if (GUI.Button(new Rect(10, 10, 50, 50), "QUIT")) @@ -136,5 +122,6 @@ namespace CLre_server Application.Quit(); // yeet } } +#endif } } diff --git a/CLre_server/CLre_server.csproj b/CLre_server/CLre_server.csproj index 545b744..d5bc888 100644 --- a/CLre_server/CLre_server.csproj +++ b/CLre_server/CLre_server.csproj @@ -11,7 +11,7 @@ - + diff --git a/CLre_server/Tweaks/TerrainModificationExclusionZone.cs b/CLre_server/Tweaks/TerrainModificationExclusionZone.cs new file mode 100644 index 0000000..107193f --- /dev/null +++ b/CLre_server/Tweaks/TerrainModificationExclusionZone.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using CLre_server.API.Synergy.Tweaks; +using Game.DataLoader; +using GameNetworkLayer.Shared; +using HarmonyLib; +using NetworkFramework.Shared; +using Svelto.DataStructures; +using Svelto.ECS; +using UnityEngine; +using User.Server; + +namespace CLre_server.Tweaks +{ + public class TerrainModificationExclusionZone + { + private static TerrainExclusionZoneEngine teze = null; + + internal static object _serverStructureExclusionZoneNode = null; + + internal static void Init() + { + if (!CLre.Config.terrain_exclusion_zone) return; + teze = new TerrainExclusionZoneEngine(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static SerializedCLreTerrainModifyRejection HasExclusionZoneAtLocationWithMember(ref Vector3 location, int playerId) + { + if (_serverStructureExclusionZoneNode == null) + { + SerializedCLreTerrainModifyRejection result = default; + result.Flags = RejectionFlag.InitError | RejectionFlag.Rejection; + return result; // this shouldn't happen (I hope...) + } + return teze.HasExclusionZoneAtLocationWithMember(ref location, playerId, _serverStructureExclusionZoneNode); + } + } + + public class TerrainExclusionZoneEngine : API.Engines.ServerEnginePostBuild + { + public override void Ready() + { + } + + public SerializedCLreTerrainModifyRejection HasExclusionZoneAtLocationWithMember(ref Vector3 digPosition, int playerId, object exclusionZonesNode) + { + SerializedCLreTerrainModifyRejection result = default; + // Similar to Game.Building.ExclusionZone.ServerStructureExclusionZoneEngine.FindPotentialMatchingExclusionZones + // Match player index to Guid + FieldInfo f = AccessTools.Field(AccessTools.TypeByName("User.Server.AccountExclusiveGroups"), "accountGroup"); + ExclusiveGroup accountGroup = (ExclusiveGroup) f.GetValue(null); + ReadOnlyCollectionStruct accounts = + entitiesDB.QueryEntityViews(accountGroup); + if (playerId >= accounts.Count) + { + // playerId isn't in connected accounts + API.Utility.Logging.LogWarning("PlayerId isn't in connected accounts, denying terrain modification"); + result.Flags = RejectionFlag.AccountNotFound | RejectionFlag.Rejection; + return result; // this shouldn't happen (I hope...) + } + Guid playerGuid = accounts[playerId].accountId.publicId; + // find exclusion zone where terrain modification is happening + float cellSize = dataDB.GetDefaultValue().WorldCellRadius * 0.25f; + object structureCellId = GetCellIdFromPosition(ref digPosition, cellSize); + // TODO optimize + Traverse exclusionNodeData = Traverse.Create(exclusionZonesNode) + .Field("serverStructureExclusionZoneDataComponent"); + Traverse exclusionZoneIdsByWorldCell = exclusionNodeData + .Property("exclusionZoneIdsByWorldCell"); // Dictionary> + Traverse exclusionZonesByUniqueId = exclusionNodeData + .Property("exclusionZonesByUniqueId"); // Dictionary + bool exists = exclusionZoneIdsByWorldCell + .Method("ContainsKey", new[] {structureCellId}).GetValue(); + if (exists) + { +#if DEBUG + API.Utility.Logging.MetaLog("Exclusion zone cell found, iterating over zones..."); +#endif + HashSet zoneIds = exclusionZoneIdsByWorldCell + .Property>("Item", index: new[] {structureCellId}) + .Value; + foreach (uint item in zoneIds) + { + Traverse serverStructureExclusionZone = exclusionZonesByUniqueId + .Property("Item", index: new object[] {item}); + Traverse structureExclusionZone = serverStructureExclusionZone + .Property("structureExclusionZone"); + bool isOwner = serverStructureExclusionZone + .Method("CheckIsOwner", playerGuid) + .GetValue(); +#if DEBUG + API.Utility.Logging.MetaLog($"IsOwner? {isOwner}"); +#endif + Game.Building.AABB aabb = structureExclusionZone.Field("_exclusionZoneAabb") + .Field("_aabb").Value; + bool isPointInAABB = IsWithin(ref digPosition, ref aabb); +#if DEBUG + API.Utility.Logging.MetaLog($"IsPointInAABB? {isPointInAABB}"); + API.Utility.Logging.MetaLog($"AABB max:{aabb.max}, min: {aabb.min} dig: {digPosition}"); +#endif + if (isPointInAABB) + { + result.Cell = item; + + if (!isOwner) + { + result.Flags = RejectionFlag.Proximity + | RejectionFlag.Rejection + | RejectionFlag.Permission; + } + return result; + } + } + } +#if DEBUG + API.Utility.Logging.MetaLog("Allowing player to modify terrain"); +#endif + result.Flags = RejectionFlag.None; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object GetCellIdFromPosition(ref Vector3 playerPosition, float cellSize) + { + // This uses decompiled code from Game.WorldGrid.StructureGridUtils:GetCellIdFromPosition + // there's no point in calling that simple function when I have to jump through hoops with reflection + // + // there's also nothing particularly unique (ie copyrightable) about this code, + // so the lawyers can suck it (also suing a benevolent project is a shitty move) + float num = 1f / cellSize; + int x = Mathf.CeilToInt((playerPosition.x - cellSize * 0.5f) * num); + int y = Mathf.CeilToInt((playerPosition.y - cellSize * 0.5f) * num); + int z = Mathf.CeilToInt((playerPosition.z - cellSize * 0.5f) * num); + // TODO optimize + // Create StructureCellId by jumping through hoops + return AccessTools.TypeByName("Game.WorldGrid.StructureCellId") + .GetConstructor(AccessTools.all, null, new[] {typeof(int), typeof(int), typeof(int)}, null) + .Invoke(new object[] {x, y, z}); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsWithin(ref Vector3 point, ref Game.Building.AABB bounds) + { + return point.x > bounds.min.x && point.x < bounds.max.x + && point.y > bounds.min.y && point.y < bounds.max.y + && point.z > bounds.min.z && point.x < bounds.max.z; + } + } + + [HarmonyPatch] + class TerrainModificationEngineServer_RemoveTerrainInput_Patch + { + private static API.Utility.Reflection.INetMsgServerSender_SendMessage + _netMessageSend = null; + + [HarmonyPrefix] + public static bool BeforeMethodCall(int senderPlayerId, ref ISerializedNetData data, object ____netMsgServerSender) + { + if (!CLre.Config.terrain_exclusion_zone) return true; + if (_netMessageSend == null) + { +#if DEBUG + API.Utility.Logging.MetaLog("Building SendMessage delegate optimisation"); +#endif + _netMessageSend = API.Utility.Reflection + .MethodAsDelegate< + API.Utility.Reflection.INetMsgServerSender_SendMessage + >( + "GameNetworkLayer.Server.INetMsgServerSender:SendMessage", + generics: new [] {typeof(SerializedCLreTerrainModifyRejection)}, + instance: ____netMsgServerSender); + } +#if DEBUG + API.Utility.Logging.MetaLog("Intercepting TerrainModificationEngineServer.RemoveTerrainInput()"); +#endif + Vector3 location = Traverse.Create(data).Property("hit").Value; + API.Utility.Logging.MetaLog($"location is null? {location == Vector3.zero}"); + SerializedCLreTerrainModifyRejection modifyPayload = TerrainModificationExclusionZone.HasExclusionZoneAtLocationWithMember(ref location, senderPlayerId); + if (!modifyPayload.Ok()) + { +#if DEBUG + API.Utility.Logging.MetaLog("Rejecting terrain modification"); +#endif + // signal client that stuff failed + _netMessageSend(NetworkDispatcherCode.TerrainModificationFailed, ref modifyPayload, senderPlayerId); + } + return modifyPayload.Ok(); + } + + [HarmonyTargetMethod] + public static MethodBase Target() + { + return AccessTools.Method("GameServer.VoxelFarm.Server.TerrainModificationEngineServer:RemoveTerrainInput"); + } + } + + [HarmonyPatch] + class ServerStructureExclusionZoneEngine_Add_Patch + { + [HarmonyPostfix] + public static void AfterMethodCall(object entityView) + { + TerrainModificationExclusionZone._serverStructureExclusionZoneNode = entityView; +#if DEBUG + API.Utility.Logging.MetaLog("Got TerrainModificationExclusionZone._serverStructureExclusionZoneNode"); +#endif + } + + [HarmonyTargetMethod] + public static MethodBase Target() + { + return AccessTools.Method("Game.Building.ExclusionZone.ServerStructureExclusionZoneEngine:Add", new Type[] {AccessTools.TypeByName("Game.Building.ExclusionZone.ServerStructureExclusionZonesNode")}); + } + } + + [HarmonyPatch] + class ServerStructureExclusionZoneEngine_Remove_Patch + { + [HarmonyPostfix] + public static void AfterMethodCall() + { + TerrainModificationExclusionZone._serverStructureExclusionZoneNode = null; +#if DEBUG + API.Utility.Logging.MetaLog("Yeeted TerrainModificationExclusionZone._serverStructureExclusionZoneNode"); +#endif + } + + [HarmonyTargetMethod] + public static MethodBase Target() + { + return AccessTools.Method("Game.Building.ExclusionZone.ServerStructureExclusionZoneEngine:Remove", new Type[] {AccessTools.TypeByName("Game.Building.ExclusionZone.ServerStructureExclusionZonesNode")}); + } + } +} \ No newline at end of file diff --git a/CLre_server/WebStatus/StatusEndpoints.cs b/CLre_server/WebStatus/StatusEndpoints.cs index 0ffb959..42fefdf 100644 --- a/CLre_server/WebStatus/StatusEndpoints.cs +++ b/CLre_server/WebStatus/StatusEndpoints.cs @@ -77,9 +77,6 @@ namespace CLre_server.WebStatus pollLoop().Run(); } - public override IEntitiesDB entitiesDB { get; set; } - public override IEntityFactory entityFactory { get; set; } - private delegate void PlayerPositionFunc(); private PlayerPositionFunc _playerPositionFunc = null;