diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 3c6cb52..26b61a8 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.86")] -[assembly: AssemblyFileVersion("0.1.86")] +[assembly: AssemblyVersion("0.1.87")] +[assembly: AssemblyFileVersion("0.1.87")] diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index b7e6312..a824853 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.86"; + private const string ModVersion = "0.1.87"; private const string Author = "warpalicious"; private const string ModGUID = Author + "." + ModName; private const string EpicLootGuid = "randyknapp.mods.epicloot"; diff --git a/Source/Network/RpcNames.cs b/Source/Network/RpcNames.cs index 7ec518d..6af87e7 100644 --- a/Source/Network/RpcNames.cs +++ b/Source/Network/RpcNames.cs @@ -21,9 +21,11 @@ internal static class RpcNames public const string ServerChestCommandRequest = "PraetorisClient_ServerChest_CommandRequest"; public const string ServerChestCommandResponse = "PraetorisClient_ServerChest_CommandResponse"; public const string ShipPasswordSetRequest = "PraetorisClient_ShipPassword_SetRequest"; - public const string ShipPasswordSetGrant = "PraetorisClient_ShipPassword_SetGrant"; + public const string ShipPasswordSetGrant = "PraetorisClient_ShipPassword_SetGrantV2"; public const string ShipPasswordControlRequest = "PraetorisClient_ShipPassword_ControlRequest"; - public const string ShipPasswordControlGrant = "PraetorisClient_ShipPassword_ControlGrant"; + public const string ShipPasswordControlGrant = "PraetorisClient_ShipPassword_ControlGrantV2"; + public const string ShipPasswordStorageRequest = "PraetorisClient_ShipPassword_StorageRequest"; + public const string ShipPasswordStorageGrant = "PraetorisClient_ShipPassword_StorageGrant"; public const string ShipPasswordOwnerResponse = "PraetorisClient_ShipPassword_OwnerResponse"; public const string ShipPasswordResponse = "PraetorisClient_ShipPassword_Response"; public const string MaintenanceNotice = "PraetorisClient_MaintenanceNotice"; diff --git a/Source/ShipPassword/ShipPasswordData.cs b/Source/ShipPassword/ShipPasswordData.cs index 075bbcc..0c6675d 100644 --- a/Source/ShipPassword/ShipPasswordData.cs +++ b/Source/ShipPassword/ShipPasswordData.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Security.Cryptography; namespace PraetorisClient.ShipPasswordFeature @@ -12,6 +13,28 @@ internal static class ShipPasswordData internal static readonly int SaltHash = "PraetorisShipPassword_salt".GetStableHashCode(); internal static readonly int VerifierHash = "PraetorisShipPassword_verifier".GetStableHashCode(); + private static readonly int AuthorizedPlayersHash = "PraetorisShipPassword_players".GetStableHashCode(); + + internal static bool HasAccess(ZDO? zdo, long playerId) + { + if (zdo == null || playerId == 0L) + { + return false; + } + + string entry = "|" + playerId.ToString(CultureInfo.InvariantCulture) + "|"; + return zdo.GetString(AuthorizedPlayersHash, "").Contains(entry); + } + + internal static void GrantAccess(ZDO zdo, long playerId) + { + if (playerId != 0L && !HasAccess(zdo, playerId)) + { + // The boat owns this record. Password changes preserve it; destruction removes it. + zdo.Set(AuthorizedPlayersHash, zdo.GetString(AuthorizedPlayersHash, "") + + "|" + playerId.ToString(CultureInfo.InvariantCulture) + "|"); + } + } internal static bool IsProtected(ZDO? zdo) { diff --git a/Source/ShipPassword/ShipPasswordInteraction.cs b/Source/ShipPassword/ShipPasswordInteraction.cs index 58e3b4e..5303979 100644 --- a/Source/ShipPassword/ShipPasswordInteraction.cs +++ b/Source/ShipPassword/ShipPasswordInteraction.cs @@ -10,11 +10,14 @@ private enum InputMode { None, SetPassword, - EnterPassword + EnterPassword, + EnterStoragePassword } private ShipControlls _controls = null!; private InputMode _mode; + private Container? _container; + private float _storageRetryUntil; private void Awake() { @@ -45,9 +48,57 @@ internal bool BeginEnterPassword(Player player) return false; } + ZNetView nview = _controls.m_ship.GetComponent(); + if (nview != null && nview.IsValid() && ShipPasswordData.HasAccess(nview.GetZDO(), player.GetPlayerID())) + { + ShipPasswordRpc.RequestControl(nview.GetZDO().m_uid, ""); + return false; + } + return Open(InputMode.EnterPassword, "Enter ship password"); } + internal bool BeginStoragePassword(Player player, Container container) + { + if (player != Player.m_localPlayer || !InStorageRange(player, container)) + { + return false; + } + + _container = container; + _storageRetryUntil = 0f; + return Open(InputMode.EnterStoragePassword, "Enter ship password"); + } + + private void Update() + { + if (_storageRetryUntil == 0f) + { + return; + } + + Player player = Player.m_localPlayer; + if (Time.realtimeSinceStartup > _storageRetryUntil || _container == null || player == null || + !InStorageRange(player, _container)) + { + _storageRetryUntil = 0f; + return; + } + + ZDO? zdo = ShipPasswordStorage.GetShipZdo(_container!); + if (ShipPasswordData.HasAccess(zdo, player.GetPlayerID())) + { + _storageRetryUntil = 0f; + _container.Interact(player, false, false); + } + } + + private static bool InStorageRange(Player player, Container container) + { + return Vector3.Distance(player.transform.position, container.transform.position) <= + player.m_maxInteractDistance + container.m_hoverOffset; + } + public string GetText() { return ""; @@ -70,7 +121,7 @@ public void SetText(string text) { ShipPasswordRpc.RequestSetPassword(nview.GetZDO().m_uid, password); } - else if (mode == InputMode.EnterPassword) + else if (mode == InputMode.EnterPassword || mode == InputMode.EnterStoragePassword) { if (password.Length == 0) { @@ -78,7 +129,21 @@ public void SetText(string text) return; } - ShipPasswordRpc.RequestControl(nview.GetZDO().m_uid, password); + if (mode == InputMode.EnterStoragePassword) + { + if (_container == null || Player.m_localPlayer == null || + !InStorageRange(Player.m_localPlayer, _container)) + { + return; + } + + _storageRetryUntil = Time.realtimeSinceStartup + 10f; + ShipPasswordRpc.RequestStorageAccess(nview.GetZDO().m_uid, password); + } + else + { + ShipPasswordRpc.RequestControl(nview.GetZDO().m_uid, password); + } } } diff --git a/Source/ShipPassword/ShipPasswordRpc.cs b/Source/ShipPassword/ShipPasswordRpc.cs index abfed1e..24b2750 100644 --- a/Source/ShipPassword/ShipPasswordRpc.cs +++ b/Source/ShipPassword/ShipPasswordRpc.cs @@ -19,6 +19,10 @@ internal static void Register(ZRoutedRpc rpc) rpc.Register(RpcNames.ShipPasswordSetGrant, OnSetGrant); rpc.Register(RpcNames.ShipPasswordControlRequest, OnControlRequest); rpc.Register(RpcNames.ShipPasswordControlGrant, OnControlGrant); + rpc.Register(RpcNames.ShipPasswordStorageRequest, + (sender, package) => OnAccessRequest(sender, package, false)); + rpc.Register(RpcNames.ShipPasswordStorageGrant, + (sender, package) => OnAccessGrant(sender, package, false)); rpc.Register(RpcNames.ShipPasswordOwnerResponse, OnOwnerResponse); rpc.Register(RpcNames.ShipPasswordResponse, OnResponse); } @@ -33,6 +37,11 @@ internal static void RequestControl(ZDOID shipId, string password) SendRequest(RpcNames.ShipPasswordControlRequest, shipId, password); } + internal static void RequestStorageAccess(ZDOID shipId, string password) + { + SendRequest(RpcNames.ShipPasswordStorageRequest, shipId, password); + } + private static void SendRequest(string rpcName, ZDOID shipId, string password) { if (ZRoutedRpc.instance == null) @@ -65,7 +74,7 @@ private static void OnSetRequest(long sender, ZPackage package) return; } - if (!TryGetAuthorizedShip(sender, shipId, creatorRequired: true, out ZDO? shipZdo, out _)) + if (!TryGetAuthorizedShip(sender, shipId, creatorRequired: true, out ZDO? shipZdo, out long playerId)) { SendResponse(sender, false, "Only the nearby ship creator can change its password."); return; @@ -84,11 +93,17 @@ private static void OnSetRequest(long sender, ZPackage package) grant.Write(sender); grant.Write(saltValue); grant.Write(verifierValue); + grant.Write(playerId); RegisterPendingOwnerResponse(ownerPeerId, sender, shipId); ZRoutedRpc.instance.InvokeRoutedRPC(ownerPeerId, RpcNames.ShipPasswordSetGrant, grant); } private static void OnControlRequest(long sender, ZPackage package) + { + OnAccessRequest(sender, package, true); + } + + private static void OnAccessRequest(long sender, ZPackage package, bool takeControl) { if (!IsServer()) { @@ -99,7 +114,7 @@ private static void OnControlRequest(long sender, ZPackage package) string password = package.ReadString(); if (!TryGetAuthorizedShip(sender, shipId, creatorRequired: false, out ZDO? shipZdo, out long playerId)) { - SendResponse(sender, false, "You must be aboard the ship to use its helm."); + SendResponse(sender, false, "You must be near the ship to request access."); return; } @@ -109,7 +124,9 @@ private static void OnControlRequest(long sender, ZPackage package) return; } - if (!ShipPasswordData.IsProtected(shipZdo) || !ShipPasswordData.Verify(shipZdo!, password)) + if (password.Length > ShipPasswordData.MaximumPasswordLength || + !ShipPasswordData.IsProtected(shipZdo) || + (!ShipPasswordData.HasAccess(shipZdo, playerId) && !ShipPasswordData.Verify(shipZdo!, password))) { PraetorisClientPlugin.Log.LogInfo("Rejected ship password for " + shipId + "."); SendResponse(sender, false, "Incorrect ship password."); @@ -122,8 +139,10 @@ private static void OnControlRequest(long sender, ZPackage package) grant.Write(shipId); grant.Write(sender); grant.Write(playerId); + grant.Write(shipZdo!.GetString(ShipPasswordData.VerifierHash, "")); RegisterPendingOwnerResponse(ownerPeerId, sender, shipId); - ZRoutedRpc.instance.InvokeRoutedRPC(ownerPeerId, RpcNames.ShipPasswordControlGrant, grant); + ZRoutedRpc.instance.InvokeRoutedRPC(ownerPeerId, + takeControl ? RpcNames.ShipPasswordControlGrant : RpcNames.ShipPasswordStorageGrant, grant); PraetorisClientPlugin.Log.LogInfo("Accepted ship password for " + shipId + "."); } @@ -138,6 +157,7 @@ private static void OnSetGrant(long sender, ZPackage package) long requestingPeerId = package.ReadLong(); string saltValue = package.ReadString(); string verifierValue = package.ReadString(); + long playerId = package.ReadLong(); GameObject? shipObject = ZNetScene.instance != null ? ZNetScene.instance.FindInstance(shipId) : null; ZNetView? nview = shipObject != null ? shipObject.GetComponent() : null; if (nview == null || !nview.IsValid() || !nview.IsOwner()) @@ -148,6 +168,11 @@ private static void OnSetGrant(long sender, ZPackage package) ShipPasswordData.ApplyVerifier(nview.GetZDO(), saltValue, verifierValue); bool passwordSet = verifierValue.Length > 0; + if (passwordSet) + { + ShipPasswordData.GrantAccess(nview.GetZDO(), playerId); + ZDOMan.instance.ForceSendZDO(requestingPeerId, shipId); + } PraetorisClientPlugin.Log.LogInfo( (passwordSet ? "Set" : "Cleared") + " ship password for " + shipId + "."); SendOwnerResponse( @@ -158,6 +183,11 @@ private static void OnSetGrant(long sender, ZPackage package) } private static void OnControlGrant(long sender, ZPackage package) + { + OnAccessGrant(sender, package, true); + } + + private static void OnAccessGrant(long sender, ZPackage package, bool takeControl) { if (ZRoutedRpc.instance == null || sender != ZRoutedRpc.instance.GetServerPeerID()) { @@ -167,19 +197,30 @@ private static void OnControlGrant(long sender, ZPackage package) ZDOID shipId = package.ReadZDOID(); long requestingPeerId = package.ReadLong(); long playerId = package.ReadLong(); + string verifier = package.ReadString(); GameObject? shipObject = ZNetScene.instance != null ? ZNetScene.instance.FindInstance(shipId) : null; Ship? ship = shipObject != null ? shipObject.GetComponent() : null; ZNetView? nview = shipObject != null ? shipObject.GetComponent() : null; ShipControlls? controls = ship != null ? ship.m_shipControlls : null; - if (ship == null || nview == null || controls == null || !nview.IsOwner() || - !ShipPasswordData.IsProtected(nview.GetZDO()) || !ship.IsPlayerInBoat(playerId)) + if (ship == null || nview == null || !nview.IsValid() || !nview.IsOwner() || + !ShipPasswordData.IsProtected(nview.GetZDO()) || + nview.GetZDO().GetString(ShipPasswordData.VerifierHash, "") != verifier || + (takeControl && (controls == null || !ship.IsPlayerInBoat(playerId)))) { SendOwnerResponse(shipId, requestingPeerId, false, "Ship control request expired. Try again."); return; } - if (controls.GetUser() != playerId && controls.HaveValidUser()) + ShipPasswordData.GrantAccess(nview.GetZDO(), playerId); + ZDOMan.instance.ForceSendZDO(requestingPeerId, shipId); + if (!takeControl) + { + SendOwnerResponse(shipId, requestingPeerId, true, "Ship access saved. You can use its helm and storage."); + return; + } + + if (controls!.GetUser() != playerId && controls.HaveValidUser()) { SendOwnerResponse(shipId, requestingPeerId, false, "$msg_inuse"); return; diff --git a/Source/ShipPassword/ShipPasswordStorage.cs b/Source/ShipPassword/ShipPasswordStorage.cs new file mode 100644 index 0000000..9de29d1 --- /dev/null +++ b/Source/ShipPassword/ShipPasswordStorage.cs @@ -0,0 +1,114 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; + +namespace PraetorisClient.ShipPasswordFeature +{ + internal static class ShipPasswordStorage + { + internal static Ship? GetShip(Container container) + { + Ship? ship = container.GetComponentInParent(); + return ship != null ? ship : container.m_rootObjectOverride != null + ? container.m_rootObjectOverride.GetComponent() : null; + } + + internal static ZDO? GetShipZdo(Container container) + { + Ship? ship = GetShip(container); + ZNetView? nview = ship != null ? ship.GetComponent() : null; + return nview != null && nview.IsValid() ? nview.GetZDO() : null; + } + } + + [HarmonyPatch(typeof(Container), nameof(Container.Interact))] + internal static class ShipPasswordStorageInteractPatch + { + private static bool Prefix(Container __instance, Humanoid character, bool hold, ref bool __result) + { + ZDO? zdo = ShipPasswordStorage.GetShipZdo(__instance); + if (!ShipPasswordData.IsProtected(zdo)) + { + return true; + } + + Player? player = character as Player; + if (player != null && ShipPasswordData.HasAccess(zdo, player.GetPlayerID())) + { + return true; + } + + __result = false; + if (!hold && player != null && + (!__instance.m_checkGuardStone || PrivateArea.CheckAccess(__instance.transform.position))) + { + Ship? ship = ShipPasswordStorage.GetShip(__instance); + ShipPasswordInput? input = ship != null && ship.m_shipControlls != null + ? ship.m_shipControlls.GetComponent() : null; + input?.BeginStoragePassword(player, __instance); + } + + return false; + } + } + + // Vanilla open, stack, and take-all requests all call CheckAccess on the owner. + // Keeping the check here also covers direct requests that do not show the prompt. + [HarmonyPatch(typeof(Container), "CheckAccess")] + internal static class ShipPasswordStorageAccessPatch + { + private static void Postfix(Container __instance, long playerID, ref bool __result) + { + ZDO? zdo = ShipPasswordStorage.GetShipZdo(__instance); + if (ShipPasswordData.IsProtected(zdo) && !ShipPasswordData.HasAccess(zdo, playerID)) + { + __result = false; + } + } + } + + [HarmonyPatch] + internal static class ShipPasswordStorageSenderPatch + { + private static IEnumerable TargetMethods() + { + yield return AccessTools.Method(typeof(Container), "RPC_RequestOpen"); + yield return AccessTools.Method(typeof(Container), "RPC_RequestStack"); + yield return AccessTools.Method(typeof(Container), "RPC_RequestTakeAll"); + } + + private static bool Prefix(Container __instance, long uid, long playerID) + { + if (!ShipPasswordData.IsProtected(ShipPasswordStorage.GetShipZdo(__instance))) + { + return true; + } + + // A remote caller must not borrow another character's saved access. + // Other clients are routed through the server, so resolve the nearby + // character's ZDO owner rather than requiring a direct network peer. + foreach (Player player in Player.GetAllPlayers()) + { + if (player.GetPlayerID() == playerID) + { + ZDO? character = ZDOMan.instance.GetZDO(player.GetZDOID()); + return character != null && character.GetOwner() == uid; + } + } + + return false; + } + } + + [HarmonyPatch(typeof(Container), nameof(Container.GetHoverText))] + internal static class ShipPasswordStorageHoverPatch + { + private static void Postfix(Container __instance, ref string __result) + { + if (ShipPasswordData.IsProtected(ShipPasswordStorage.GetShipZdo(__instance))) + { + __result += "\nPassword protected"; + } + } + } +} diff --git a/Source/Ships/ShipRudderOwnershipPatch.cs b/Source/Ships/ShipRudderOwnershipPatch.cs deleted file mode 100644 index f665cf5..0000000 --- a/Source/Ships/ShipRudderOwnershipPatch.cs +++ /dev/null @@ -1,24 +0,0 @@ -using HarmonyLib; - -namespace PraetorisClient -{ - [HarmonyPatch(typeof(ShipControlls), "RPC_RequestRespons")] - internal static class ShipRudderOwnershipPatch - { - private static void Postfix(ShipControlls __instance, bool granted, ZNetView ___m_nview) - { - Player player = Player.m_localPlayer; - if (!granted || player == null || !ReferenceEquals(player.GetDoodadController(), __instance)) - { - return; - } - - if (___m_nview != null && ___m_nview.IsValid()) - { - ___m_nview.ClaimOwnership(); - // The grant can arrive before the previous owner's user value is synchronized. - ___m_nview.GetZDO().Set(ZDOVars.s_user, player.GetPlayerID()); - } - } - } -} diff --git a/manifest.json b/manifest.json index f976c72..840242a 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.86", + "version_number": "0.1.87", "website_url": "https://discord.gg/3aaru2VyHJ", "dependencies": [ "denikson-BepInExPack_Valheim-5.4.2333", diff --git a/thunderstore.toml b/thunderstore.toml index d60afd3..69f9393 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -4,7 +4,7 @@ schemaVersion = "0.0.1" [package] namespace = "praetoris" name = "PraetorisClient" -versionNumber = "0.1.85" +versionNumber = "0.1.87" 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