Add optional ability to stop terrain modification in exclusion zones (for players not allowed to place structures in zone)

This commit is contained in:
NGnius (Graham) 2021-08-01 14:37:13 -04:00
parent 34a4c2844f
commit 7c76c27126
11 changed files with 338 additions and 35 deletions

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.0.4" />
<PackageReference Include="Lib.Harmony" Version="2.1.1" />
</ItemGroup>
<ItemGroup>

View file

@ -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<CLreConfig>(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);

View file

@ -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; }
}

View file

@ -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;

View file

@ -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;

View file

@ -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(); };

View file

@ -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,
}
}

View file

@ -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
}
}

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.0.4" />
<PackageReference Include="Lib.Harmony" Version="2.1.1" />
</ItemGroup>
<ItemGroup>

View file

@ -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<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)
{
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<SerializedCLreTerrainModifyRejection>
_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<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
// 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")});
}
}
}

View file

@ -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;