using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using CLre_server.API.Synergy.Tweaks; using CLre_server.API.Utility; using Game.DataLoader; using GameNetworkLayer.Shared; using HarmonyLib; using NetworkFramework.Shared; using Svelto.DataStructures; using Svelto.ECS; using UnityEngine; using User.Server; using voxelfarm; 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) { 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 object _netMsgServerSender; // reflection caching private static API.Utility.Reflection.INetMsgServerSender_SendMessage _netMessageSend_CLre = null; [HarmonyPrefix] public static bool BeforeMethodCall(int senderPlayerId, ref ISerializedNetData data, object ____netMsgServerSender) { if (!CLre.Config.terrain_exclusion_zone) return true; _netMsgServerSender = ____netMsgServerSender; if (_netMessageSend_CLre == null) { // cache reflection operations on first run #if DEBUG API.Utility.Logging.MetaLog("Building SendMessage delegate optimisation"); #endif _netMessageSend_CLre = 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 // TODO optimize Traverse tmid = Traverse.Create(data); modifyPayload.resourceId = tmid.Property("resourceId").Value; modifyPayload.hit = location; //modifyPayload.materialIndex = tmid.Property("materialIndex").Value; modifyPayload.toolKey = tmid.Property("toolKey").Value; modifyPayload.toolMode = tmid.Property("toolMode").Value; switch (modifyPayload.toolMode) { case Game.Handhelds.ToolModeType.Block: modifyPayload.hit.y -= 0.125f * 2; // each layer is 0.125 thick, block is 3 layers break; case Game.Handhelds.ToolModeType.Disc: break; case Game.Handhelds.ToolModeType.Voxel: modifyPayload.toolMode = Game.Handhelds.ToolModeType.Disc; // voxels aren't replaced properly break; } // signal client that stuff failed _netMessageSend_CLre(NetworkDispatcherCode.TerrainModificationFailed, ref modifyPayload, senderPlayerId); // build terrain data for terrain replacement #if DEBUG API.Utility.Logging.MetaLog("Filling hole left by terrain modification"); #endif } 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")}); } } }