CLre/CLre_server/Tweaks/TerrainModificationExclusionZone.cs

265 lines
12 KiB
C#

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<AccountIdServerNode> accounts =
entitiesDB.QueryEntityViews<AccountIdServerNode>(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<WorldCellData>().WorldCellRadius * 0.25f;
object structureCellId = GetCellIdFromPosition(ref digPosition, cellSize);
// TODO optimize
Traverse exclusionNodeData = Traverse.Create(exclusionZonesNode)
.Field("serverStructureExclusionZoneDataComponent");
Traverse exclusionZoneIdsByWorldCell = exclusionNodeData
.Property("exclusionZoneIdsByWorldCell"); // Dictionary<StructureCellId, HashSet<uint>>
Traverse exclusionZonesByUniqueId = exclusionNodeData
.Property("exclusionZonesByUniqueId"); // Dictionary<uint, ServerStructureExclusionZone>
bool exists = exclusionZoneIdsByWorldCell
.Method("ContainsKey", new[] {structureCellId}).GetValue<bool>();
if (exists)
{
#if DEBUG
API.Utility.Logging.MetaLog("Exclusion zone cell found, iterating over zones...");
#endif
HashSet<uint> zoneIds = exclusionZoneIdsByWorldCell
.Property<HashSet<uint>>("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<bool>();
#if DEBUG
API.Utility.Logging.MetaLog($"IsOwner? {isOwner}");
#endif
Game.Building.AABB aabb = structureExclusionZone.Field("_exclusionZoneAabb")
.Field<Game.Building.AABB>("_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<SerializedCLreTerrainModifyRejection>
_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<SerializedCLreTerrainModifyRejection>
>(
"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<Vector3>("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<uint>("resourceId").Value;
modifyPayload.hit = location;
//modifyPayload.materialIndex = tmid.Property<int>("materialIndex").Value;
modifyPayload.toolKey = tmid.Property<string>("toolKey").Value;
modifyPayload.toolMode = tmid.Property<Game.Handhelds.ToolModeType>("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")});
}
}
}