diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index a3b5f8b..07aab73 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.84")] -[assembly: AssemblyFileVersion("0.1.84")] +[assembly: AssemblyVersion("0.1.85")] +[assembly: AssemblyFileVersion("0.1.85")] diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index c3b3274..8c7e398 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -29,7 +29,7 @@ namespace PraetorisClient public class PraetorisClientPlugin : BaseUnityPlugin { private const string ModName = "PraetorisClient"; - private const string ModVersion = "0.1.84"; + private const string ModVersion = "0.1.85"; private const string Author = "warpalicious"; private const string ModGUID = Author + "." + ModName; private const string EpicLootGuid = "randyknapp.mods.epicloot"; @@ -197,6 +197,7 @@ private void Update() RpcTraceTelemetry.BackgroundUpdate(); SurtlingBoatFeature.Update(); + ServerChestPlacement.Update(); ServerGuideFeature.ServerGuide.Update(); } diff --git a/Source/ServerChest/ServerChest.cs b/Source/ServerChest/ServerChest.cs index ad5c7e9..98e1f0f 100644 --- a/Source/ServerChest/ServerChest.cs +++ b/Source/ServerChest/ServerChest.cs @@ -143,11 +143,6 @@ internal static bool TryGetByInventory(Inventory inventory, out ServerChest serv return InventoryOwners.TryGetValue(inventory, out serverChest); } - internal static void ForgetInventory(Inventory inventory) - { - InventoryOwners.Remove(inventory); - } - internal static bool IsServerChestPrefab(ZDO zdo) { return zdo != null && zdo.GetPrefab() == PrefabName.GetStableHashCode(); diff --git a/Source/ServerChest/ServerChestIdentity.cs b/Source/ServerChest/ServerChestIdentity.cs index 820c8f6..81371ec 100644 --- a/Source/ServerChest/ServerChestIdentity.cs +++ b/Source/ServerChest/ServerChestIdentity.cs @@ -49,6 +49,10 @@ internal static bool TryGetSenderIdentity(long sender, string requestedName, str platformId = localPlatformId; } } + else + { + return false; + } return !string.IsNullOrWhiteSpace(characterName) && !string.IsNullOrWhiteSpace(platformId); } diff --git a/Source/ServerChest/ServerChestPatches.cs b/Source/ServerChest/ServerChestPatches.cs index 6f51882..e6089ca 100644 --- a/Source/ServerChest/ServerChestPatches.cs +++ b/Source/ServerChest/ServerChestPatches.cs @@ -69,18 +69,6 @@ private static void Postfix(InventoryGui __instance) return; } - if (ServerChestPiece.TryRestoreVanillaWoodChest(currentContainer, out bool restoredVanillaWoodChest)) - { - if (GridWasCompacted || restoredVanillaWoodChest) - { - RestoreGrid(containerGrid); - containerGrid.UpdateInventory(currentContainer.GetInventory(), null, null); - GridWasCompacted = false; - } - - return; - } - if (!ServerChest.IsServerChest(currentContainer)) { if (GridWasCompacted) diff --git a/Source/ServerChest/ServerChestPiece.cs b/Source/ServerChest/ServerChestPiece.cs index 3611ef6..624befb 100644 --- a/Source/ServerChest/ServerChestPiece.cs +++ b/Source/ServerChest/ServerChestPiece.cs @@ -7,9 +7,8 @@ namespace PraetorisClient.ServerChestFeature { internal static class ServerChestPiece { - internal const string BasePrefabName = "piece_chest_wood"; + private const string BasePrefabName = "TreasureChest_dvergrtower"; private static bool _registered; - private static ContainerShape? _vanillaWoodChestShape; internal static void Initialize() { @@ -33,24 +32,31 @@ private static void Register() PieceConfig pieceConfig = new() { Name = "Server Chest", - Description = "Receives admin-delivered items for one registered player.", + Description = "A chest used to receive items from server admins. Limit one per player.", PieceTable = PieceTables.Hammer, - Category = PieceCategories.Misc + Category = PieceCategories.Misc, + CraftingStation = "piece_workbench", + Requirements = new[] { new RequirementConfig("Wood", 10) } }; - _vanillaWoodChestShape ??= CaptureVanillaWoodChestShape(); - - CustomPiece customPiece = new(ServerChest.PrefabName, BasePrefabName, pieceConfig); - GameObject prefab = customPiece.PiecePrefab; + GameObject prefab = PrefabManager.Instance.CreateClonedPrefab(ServerChest.PrefabName, BasePrefabName); if (prefab == null) { PraetorisClientPlugin.Log.LogError("Failed to create ServerChest prefab from " + BasePrefabName + "."); return; } + Piece buildPiece = prefab.GetComponent() ?? prefab.AddComponent(); + buildPiece.m_canBeRemoved = true; + if (!Application.isBatchMode) + { + Sprite icon = RenderManager.Instance.Render(prefab); + if (icon != null) + buildPiece.m_icon = icon; + } ConfigurePrefab(prefab); + CustomPiece customPiece = new(prefab, false, pieceConfig); PieceManager.Instance.AddPiece(customPiece); - RestoreVanillaWoodChestPrefab(); _registered = true; PrefabManager.OnVanillaPrefabsAvailable -= Register; } @@ -63,14 +69,16 @@ private static void ConfigurePrefab(GameObject prefab) container.m_name = "Server Chest"; container.m_width = ServerChest.MaxColumns; container.m_height = ServerChest.MaxRows; - container.m_defaultItems.m_drops.Clear(); + container.m_defaultItems = new DropTable(); + container.m_autoDestroyEmpty = false; + container.m_discoverStat = PlayerStatType.None; } Piece piece = prefab.GetComponent(); if (piece != null) { piece.m_name = "Server Chest"; - piece.m_description = "Receives admin-delivered items for one registered player."; + piece.m_description = "A chest used to receive items from server admins. Limit one per player."; } if (prefab.GetComponent() == null) @@ -79,135 +87,5 @@ private static void ConfigurePrefab(GameObject prefab) } } - internal static bool TryRestoreVanillaWoodChest(Container container, out bool restored) - { - restored = false; - if (!IsVanillaWoodChest(container)) - { - return false; - } - - ContainerShape? shape = _vanillaWoodChestShape; - if (shape == null) - { - return true; - } - - restored |= RestoreContainerComponent(container, shape.Value); - Inventory inventory = container.GetInventory(); - ServerChest serverChest = container.GetComponent(); - if (serverChest != null) - { - if (inventory != null) - { - ServerChest.ForgetInventory(inventory); - } - - Object.Destroy(serverChest); - restored = true; - } - - if (inventory == null || (inventory.GetWidth() == shape.Value.Width && inventory.GetHeight() == shape.Value.Height)) - { - return true; - } - - int capacity = shape.Value.Width * shape.Value.Height; - if (inventory.NrOfItems() > capacity) - { - PraetorisClientPlugin.Log.LogWarning("Leaving " + BasePrefabName + " at expanded size because it has more stacks than vanilla capacity."); - return true; - } - - ServerChest.CompactInventory(inventory, shape.Value.Width); - ServerChest.ApplyInventoryShape(inventory, shape.Value.Width, shape.Value.Height); - restored = true; - return true; - } - - private static ContainerShape? CaptureVanillaWoodChestShape() - { - GameObject basePrefab = PrefabManager.Instance.GetPrefab(BasePrefabName); - Container? container = basePrefab != null ? basePrefab.GetComponent() : null; - if (container == null) - { - PraetorisClientPlugin.Log.LogWarning("Unable to capture vanilla wood chest container shape."); - return null; - } - - return new ContainerShape(container.m_name, container.m_width, container.m_height); - } - - private static void RestoreVanillaWoodChestPrefab() - { - ContainerShape? shape = _vanillaWoodChestShape; - if (shape == null) - { - return; - } - - GameObject basePrefab = PrefabManager.Instance.GetPrefab(BasePrefabName); - Container? container = basePrefab != null ? basePrefab.GetComponent() : null; - if (container == null) - { - return; - } - - RestoreContainerComponent(container, shape.Value); - ServerChest serverChest = container.GetComponent(); - if (serverChest != null) - { - Object.Destroy(serverChest); - } - } - - private static bool RestoreContainerComponent(Container container, ContainerShape shape) - { - bool restored = false; - if (container.m_name != shape.Name) - { - container.m_name = shape.Name; - restored = true; - } - - if (container.m_width != shape.Width) - { - container.m_width = shape.Width; - restored = true; - } - - if (container.m_height != shape.Height) - { - container.m_height = shape.Height; - restored = true; - } - - return restored; - } - - private static bool IsVanillaWoodChest(Container container) - { - if (container == null) - { - return false; - } - - string objectName = container.gameObject.name; - return objectName == BasePrefabName || objectName.StartsWith(BasePrefabName + "("); - } - - private readonly struct ContainerShape - { - internal ContainerShape(string name, int width, int height) - { - Name = name; - Width = width; - Height = height; - } - - internal string Name { get; } - internal int Width { get; } - internal int Height { get; } - } } } diff --git a/Source/ServerChest/ServerChestPlacement.cs b/Source/ServerChest/ServerChestPlacement.cs new file mode 100644 index 0000000..28a97cd --- /dev/null +++ b/Source/ServerChest/ServerChestPlacement.cs @@ -0,0 +1,252 @@ +using System; +using HarmonyLib; +using UnityEngine; + +namespace PraetorisClient.ServerChestFeature +{ + internal static class ServerChestPlacement + { + private const string CountRequest = "Praetoris_ServerChestCount"; + private const string CountResponse = "Praetoris_ServerChestCountResult"; + private const string PlacementRequest = "Praetoris_ServerChestPlace"; + private const string PlacementResponse = "Praetoris_ServerChestPlaceResult"; + private static Player? _player; + private static float _nextRequest; + private static float _receivedAt = -100f; + private static int _count; + private static ZDOID _pendingChest; + private static float _placementDeadline; + private static float _nextPlacementRequest; + + internal static void Register(ZRoutedRpc rpc) + { + _player = null; + _receivedAt = -100f; + _pendingChest = ZDOID.None; + rpc.Register(CountRequest, OnCountRequest); + rpc.Register(CountResponse, OnCountResponse); + rpc.Register(PlacementRequest, OnPlacementRequest); + rpc.Register(PlacementResponse, OnPlacementResponse); + } + + internal static void RequestPlacement(ZDO zdo) + { + if (ZNet.instance.IsServer()) + { + AcceptNewChest(zdo, ZNet.GetUID()); + return; + } + _player = Player.m_localPlayer; + _pendingChest = zdo.m_uid; + _placementDeadline = Time.unscaledTime + 15f; + _nextPlacementRequest = 0f; + } + + private static void UpdatePlacementRequest() + { + if (_pendingChest.IsNone() || ZRoutedRpc.instance == null || ZDOMan.instance == null) + return; + if (Time.unscaledTime >= _placementDeadline) + { + _pendingChest = ZDOID.None; + ServerChest.ShowMessage("Server Chest registration timed out. Use manual registration or rebuild the chest."); + return; + } + if (Time.unscaledTime < _nextPlacementRequest) + return; + _nextPlacementRequest = Time.unscaledTime + 1f; + long server = ZRoutedRpc.instance.GetServerPeerID(); + ZDOMan.instance.ForceSendZDO(server, _pendingChest); + ZRoutedRpc.instance.InvokeRoutedRPC(server, PlacementRequest, _pendingChest); + } + + private static void OnPlacementRequest(long sender, ZDOID chestId) + { + if (ZNet.instance == null || !ZNet.instance.IsServer() || ZDOMan.instance == null || + chestId.IsNone() || chestId.UserID != sender) + return; + // Object synchronization can arrive after this request. The client retries only this chest. + ZDO chest = ZDOMan.instance.GetZDO(chestId); + if (chest == null) + return; + bool accepted = AcceptNewChest(chest, sender); + ZRoutedRpc.instance.InvokeRoutedRPC(sender, PlacementResponse, chestId, accepted); + } + + private static void OnPlacementResponse(long sender, ZDOID chestId, bool accepted) + { + if (ZRoutedRpc.instance == null || sender != ZRoutedRpc.instance.GetServerPeerID() || + chestId != _pendingChest) + return; + _pendingChest = ZDOID.None; + _nextRequest = 0f; + if (accepted) + { + _count = Math.Max(1, _count); + _receivedAt = Time.unscaledTime; + } + ServerChest.ShowMessage(accepted ? "Server Chest registered." : "Server Chest placement rejected."); + } + + internal static bool IsChest(Piece piece) + { + return piece != null && Utils.GetPrefabName(piece.gameObject) == ServerChest.PrefabName; + } + + internal static int Count(long creator, string platform, ZDOID exclude = default) + { + // The server's saved network objects include chests in unloaded zones. + int count = 0; + string owner = ServerChest.NormalizeLookup(platform); + foreach (ZDO chest in ServerChest.FindAllZdos()) + { + if (chest.m_uid != exclude && + ((creator != 0L && chest.GetLong(ZDOVars.s_creator) == creator) || + (owner.Length > 0 && ServerChest.OwnerLookup(chest) == owner))) + { + count++; + } + } + return count; + } + + internal static void Update() + { + Player player = Player.m_localPlayer; + if (player != _player) + { + _player = player; + _receivedAt = -100f; + _nextRequest = 0f; + _pendingChest = ZDOID.None; + } + UpdatePlacementRequest(); + if (player == null || ZRoutedRpc.instance == null || ZNet.instance == null || + ZNet.instance.IsServer() || !IsChest(player.GetSelectedPiece()) || Time.unscaledTime < _nextRequest) + { + return; + } + _nextRequest = Time.unscaledTime + 2f; + ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), CountRequest, player.GetPlayerID()); + } + + private static void OnCountRequest(long sender, long creator) + { + if (ZNet.instance == null || !ZNet.instance.IsServer() || + !IsSenderCreator(sender, creator) || + !ServerChestIdentity.TryGetSenderIdentity(sender, "", "", out _, out string platform)) + { + return; + } + ZRoutedRpc.instance.InvokeRoutedRPC(sender, CountResponse, creator, Count(creator, platform)); + } + + private static bool IsSenderCreator(long sender, long creator) + { + if (sender == ZNet.GetUID()) + return Player.m_localPlayer != null && Player.m_localPlayer.GetPlayerID() == creator; + ZNetPeer? peer = PlayerResolver.FindPeerBySender(sender); + ZDO? player = peer != null && ZDOMan.instance != null ? ZDOMan.instance.GetZDO(peer.m_characterID) : null; + return creator != 0L && player != null && player.GetLong(ZDOVars.s_playerID) == creator; + } + + private static void OnCountResponse(long sender, long creator, int count) + { + if (ZRoutedRpc.instance == null || sender != ZRoutedRpc.instance.GetServerPeerID() || + Player.m_localPlayer == null || Player.m_localPlayer.GetPlayerID() != creator) + { + return; + } + _count = count; + _receivedAt = Time.unscaledTime; + } + + internal static bool CanPlace(Player player) + { + if (!_pendingChest.IsNone()) + { + ServerChest.ShowMessage("Waiting for Server Chest registration."); + return false; + } + string platform = ServerChestIdentity.GetLocalPlatformId(); + int count = Count(player.GetPlayerID(), platform); + if (ZNet.instance != null && !ZNet.instance.IsServer()) + { + if (Time.unscaledTime - _receivedAt > 6f) + { + ServerChest.ShowMessage("Checking your Server Chest count. Please try again."); + _nextRequest = 0f; + return false; + } + count = Math.Max(count, _count); + } + if (count > 0) + { + ServerChest.ShowMessage("You can only place one Server Chest. Remove your existing chest first."); + return false; + } + return true; + } + + private static bool AcceptNewChest(ZDO zdo, long sender) + { + if (!ServerChest.IsServerChestPrefab(zdo) || ZNet.instance == null || !ZNet.instance.IsServer()) + { + return false; + } + long creator = zdo.GetLong(ZDOVars.s_creator); + bool resolved = ServerChestIdentity.TryGetSenderIdentity(sender, "", "", out string name, out string platform); + if (!resolved || !IsSenderCreator(sender, creator)) + return false; + // A repeated request must not register or delete an already accepted chest. + if (zdo.GetOwner() == ZDOMan.GetSessionID() && + ServerChest.OwnerLookup(zdo) == ServerChest.NormalizeLookup(platform)) + return true; + if (zdo.GetOwner() != sender) + return false; + if (Count(creator, platform, zdo.m_uid) > 0) + { + zdo.SetOwner(ZDOMan.GetSessionID()); + ZDOMan.instance.DestroyZDO(zdo); + PraetorisClientPlugin.Log.LogInfo("Rejected duplicate Server Chest placement."); + return false; + } + zdo.SetOwner(ZDOMan.GetSessionID()); + ServerChest.SetRegistration(zdo, name, platform); + PraetorisClientPlugin.Log.LogInfo("Automatically registered new Server Chest for " + name + "."); + return true; + } + } + + [HarmonyPatch(typeof(Player), nameof(Player.TryPlacePiece))] + internal static class ServerChestTryPlacePatch + { + private static bool Prefix(Player __instance, Piece piece, ref bool __result) + { + if (!ServerChestPlacement.IsChest(piece) || ServerChestPlacement.CanPlace(__instance)) + return true; + __result = false; + return false; + } + } + + [HarmonyPatch(typeof(Piece), nameof(Piece.SetCreator))] + internal static class ServerChestCreatorPatch + { + private static void Prefix(Piece __instance, out bool __state) + { + __state = ServerChestPlacement.IsChest(__instance) && __instance.GetCreator() == 0L; + } + + private static void Postfix(Piece __instance, long uid, bool __state) + { + Player player = Player.m_localPlayer; + if (!__state || player == null || player.GetPlayerID() != uid || __instance.GetCreator() != uid) + return; + ZNetView view = __instance.GetComponent(); + if (view == null || !view.IsValid() || !view.IsOwner()) + return; + ServerChestPlacement.RequestPlacement(view.GetZDO()); + } + } +} diff --git a/Source/ServerChest/ServerChestRpc.cs b/Source/ServerChest/ServerChestRpc.cs index 71d788e..e6138fb 100644 --- a/Source/ServerChest/ServerChestRpc.cs +++ b/Source/ServerChest/ServerChestRpc.cs @@ -11,6 +11,7 @@ internal static class ServerChestRpc internal static void Register(ZRoutedRpc rpc) { + ServerChestPlacement.Register(rpc); rpc.Register(RpcNames.ServerChestRegisterRequest, OnRegisterRequest); rpc.Register(RpcNames.ServerChestRegisterResponse, OnRegisterResponse); rpc.Register(RpcNames.ServerChestCommandRequest, OnCommandRequest); diff --git a/manifest.json b/manifest.json index 556b06a..b024e2d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "PraetorisClient", "description": "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes.", - "version_number": "0.1.84", + "version_number": "0.1.85", "website_url": "https://discord.gg/3aaru2VyHJ", "dependencies": [ "denikson-BepInExPack_Valheim-5.4.2333", diff --git a/thunderstore.toml b/thunderstore.toml index 6efbc67..d60afd3 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -4,7 +4,7 @@ schemaVersion = "0.0.1" [package] namespace = "praetoris" name = "PraetorisClient" -versionNumber = "0.1.84" +versionNumber = "0.1.85" description = "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes." websiteUrl = "https://discord.gg/3aaru2VyHJ" containsNsfwContent = false