using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Oxide.Plugins { [Info("Vehicle Deployed Locks", "WhiteThunder", "1.14.0")] [Description("Allows players to deploy code locks and key locks to vehicles.")] internal class VehicleDeployedLocks : CovalencePlugin { #region Fields [PluginReference] private Plugin Clans, Friends; private const string Permission_CodeLock_Prefix = "vehicledeployedlocks.codelock"; private const string Permission_KeyLock_Prefix = "vehicledeployedlocks.keylock"; private const string Permission_MasterKey = "vehicledeployedlocks.masterkey"; private const string Prefab_CodeLock_DeployedEffect = "assets/prefabs/locks/keypad/effects/lock-code-deploy.prefab"; private const string Prefab_CodeLock_DeniedEffect = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab"; private const string Prefab_CodeLock_UnlockEffect = "assets/prefabs/locks/keypad/effects/lock.code.unlock.prefab"; private const float MaxDeployDistance = 3; private Configuration _config; private CooldownManager _craftCodeLockCooldowns; private CooldownManager _craftKeyLockCooldowns; private readonly VehicleInfoManager _vehicleInfoManager; private readonly LockedVehicleTracker _lockedVehicleTracker; private readonly AutoUnlockManager _autoUnlockManager; private readonly ReskinManager _reskinManager; private readonly object False = false; private enum PayType { Item, Resources, Free } public VehicleDeployedLocks() { _vehicleInfoManager = new VehicleInfoManager(this); _lockedVehicleTracker = new LockedVehicleTracker(_vehicleInfoManager); _autoUnlockManager = new AutoUnlockManager(this, _lockedVehicleTracker); _reskinManager = new ReskinManager(_vehicleInfoManager, _lockedVehicleTracker); } #endregion #region Hooks private void Init() { permission.RegisterPermission(Permission_MasterKey, this); permission.RegisterPermission(LockType.Code.PermissionFree, this); permission.RegisterPermission(LockType.Code.PermissionAllVehicles, this); permission.RegisterPermission(LockType.Key.PermissionFree, this); permission.RegisterPermission(LockType.Key.PermissionAllVehicles, this); _craftKeyLockCooldowns = new CooldownManager(_config.CraftCooldownSeconds); _craftCodeLockCooldowns = new CooldownManager(_config.CraftCooldownSeconds); if (_config.AllowPushWhileLockedOut) { Unsubscribe(nameof(OnVehiclePush)); } Unsubscribe(nameof(OnEntityKill)); } private void OnServerInitialized() { _vehicleInfoManager.OnServerInitialized(); _lockedVehicleTracker.OnServerInitialized(); _autoUnlockManager.OnServerInitialized(_config.AutoUnlockSettings); if (_config.UpdateLockPositions) { foreach (var networkable in BaseNetworkable.serverEntities) { var entity = networkable as BaseEntity; if ((object)entity == null) continue; var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(entity); if (vehicleInfo == null) continue; var lockEntity = GetVehicleLock(entity); if (lockEntity == null) continue; var transform = lockEntity.transform; transform.localPosition = vehicleInfo.LockPosition; transform.localRotation = vehicleInfo.LockRotation; lockEntity.SendNetworkUpdate_Position(); } } Subscribe(nameof(OnEntityKill)); } private object CanMountEntity(BasePlayer player, BaseMountable entity) { // Don't lock taxi modules if (entity is ModularCarSeat carSeat && IsTaxiSeat(carSeat)) return null; return CanPlayerInteractWithParentVehicle(player, entity); } private object CanLootEntity(BasePlayer player, StorageContainer container) { // Don't lock taxi module shopfronts if (container is ModularVehicleShopFront) return null; return CanPlayerInteractWithParentVehicle(player, container); } private object CanLootEntity(BasePlayer player, ContainerIOEntity container) { return CanPlayerInteractWithParentVehicle(player, container); } private object CanLootEntity(BasePlayer player, RidableHorse horse) { return CanPlayerInteractWithVehicle(player, horse); } private object CanLootEntity(BasePlayer player, ModularCarGarage carLift) { if (carLift == null || _config.ModularCarSettings.AllowEditingWhileLockedOut || !carLift.PlatformIsOccupied) return null; return CanPlayerInteractWithVehicle(player, carLift.carOccupant); } private object OnHorseLead(RidableHorse horse, BasePlayer player) { return CanPlayerInteractWithVehicle(player, horse); } private object OnHotAirBalloonToggle(HotAirBalloon hab, BasePlayer player) { return CanPlayerInteractWithVehicle(player, hab); } private object OnSwitchToggle(ElectricSwitch electricSwitch, BasePlayer player) { if (electricSwitch == null) return null; var autoTurret = electricSwitch.GetParentEntity() as AutoTurret; if (autoTurret != null) return CanPlayerInteractWithParentVehicle(player, autoTurret); return null; } private object OnTurretAuthorize(AutoTurret entity, BasePlayer player) { return CanPlayerInteractWithParentVehicle(player, entity); } private object OnTurretTarget(AutoTurret autoTurret, BasePlayer player) { if (autoTurret == null || player == null || player.UserIDString == null) return null; var turretParent = autoTurret.GetParentEntity(); var vehicle = turretParent as BaseVehicle ?? (turretParent as BaseVehicleModule)?.Vehicle; if (vehicle == null) return null; var baseLock = GetVehicleLock(vehicle); if (baseLock == null) return null; if (CanPlayerBypassLock(player, baseLock, provideFeedback: false)) return False; return null; } private object CanSwapToSeat(BasePlayer player, ModularCarSeat carSeat) { // Don't lock taxi modules if (IsTaxiSeat(carSeat)) return null; return CanPlayerInteractWithParentVehicle(player, carSeat, provideFeedback: false); } private object OnVehiclePush(BaseVehicle vehicle, BasePlayer player) { return CanPlayerInteractWithVehicle(player, vehicle); } private void OnEntityKill(BaseLock baseLock) { var vehicle = GetParentVehicle(baseLock); if (vehicle == null) return; _lockedVehicleTracker.OnLockRemoved(vehicle); } // Handle the case where a cockpit is removed but the car remains // If a lock is present, either move the lock to another cockpit or destroy it private void OnEntityKill(VehicleModuleSeating seatingModule) { if (seatingModule == null || !seatingModule.HasADriverSeat()) return; var car = seatingModule.Vehicle as ModularCar; if (car == null) return; var baseLock = seatingModule.GetComponentInChildren(); if (baseLock == null) return; baseLock.SetParent(null); var car2 = car; var baseLock2 = baseLock; NextTick(() => { if (car2 == null) { _lockedVehicleTracker.OnLockRemoved(car2); baseLock2.Kill(); } else { var driverModule = FindFirstDriverModule(car2); if (driverModule == null) { _lockedVehicleTracker.OnLockRemoved(car2); baseLock2.Kill(); } else { baseLock2.SetParent(driverModule); } } }); } // Allow players to deploy locks directly without any commands. private object CanDeployItem(BasePlayer basePlayer, Deployer deployer, NetworkableId entityId) { if (basePlayer == null || deployer == null) return null; var deployable = deployer.GetDeployable(); if (deployable == null) return null; var activeItem = basePlayer.GetActiveItem(); if (activeItem == null) return null; var itemId = activeItem.info.itemid; LockInfo lockInfo; if (itemId == LockInfo.CodeLock.ItemId) { lockInfo = LockInfo.CodeLock; } else if (itemId == LockInfo.PilotCodeLock.ItemId) { lockInfo = LockInfo.PilotCodeLock; } else if (itemId == LockInfo.KeyLock.ItemId) { lockInfo = LockInfo.KeyLock; } else { return null; } var vehicleInfo = GetVehicleAndInfo(BaseNetworkable.serverEntities.Find(entityId) as BaseEntity, basePlayer, out var vehicle, fromDeployHook: true); if (vehicleInfo == null) return null; var player = basePlayer.IPlayer; // Trick to make sure the replies are in chat instead of console. player.LastCommand = CommandType.Chat; if (!VerifyCanDeploy(player, vehicle, vehicleInfo, lockInfo.LockType, out _, out var payType) || !VerifyDeployDistance(player, vehicle)) return False; DeployLockForPlayer(vehicle, vehicleInfo, lockInfo, basePlayer, payType); return False; } private object OnEntityReskin(Snowmobile snowmobile, ItemSkinDirectory.Skin skin, BasePlayer player) { var baseLock = GetVehicleLock(snowmobile); if (baseLock == null) return null; if (_vehicleInfoManager.GetVehicleInfo(snowmobile) == null) return null; if (baseLock.IsLocked() && !CanPlayerBypassLock(player, baseLock, provideFeedback: true)) return False; _reskinManager.HandleReskinPre(snowmobile, baseLock); // In case another plugin blocks the pre-hook, add back or destroy the lock. NextTick(_reskinManager.CleanupAction); return null; } private void OnEntityReskinned(Snowmobile snowmobile, ItemSkinDirectory.Skin skin, BasePlayer player) { _reskinManager.HandleReskinPost(snowmobile); } #endregion #region API [HookMethod(nameof(API_DeployCodeLock))] public CodeLock API_DeployCodeLock(BaseEntity vehicle, BasePlayer player, bool isFree = true) { return DeployLockForAPI(vehicle, player, LockType.Code, isFree) as CodeLock; } [HookMethod(nameof(API_DeployKeyLock))] public KeyLock API_DeployKeyLock(BaseEntity vehicle, BasePlayer player, bool isFree = true) { return DeployLockForAPI(vehicle, player, LockType.Key, isFree) as KeyLock; } [HookMethod(nameof(API_CanPlayerDeployCodeLock))] public bool API_CanPlayerDeployCodeLock(BasePlayer player, BaseEntity vehicle) { return CanPlayerDeployLockForAPI(player, vehicle, LockType.Code); } [HookMethod(nameof(API_CanPlayerDeployKeyLock))] public bool API_CanPlayerDeployKeyLock(BasePlayer player, BaseEntity vehicle) { return CanPlayerDeployLockForAPI(player, vehicle, LockType.Key); } [HookMethod(nameof(API_CanAccessVehicle))] public bool API_CanAccessVehicle(BasePlayer player, BaseEntity vehicle, bool provideFeedback = true) { return CanPlayerInteractWithVehicle(player, vehicle, provideFeedback) == null; } [HookMethod(nameof(API_RegisterCustomVehicleType))] public void API_RegisterCustomVehicleType(string vehicleType, Vector3 lockPosition, Quaternion lockRotation, string parentBone, Func determineLockParent) { _vehicleInfoManager.RegisterCustomVehicleType(this, new VehicleInfo() { VehicleType = vehicleType, LockPosition = lockPosition, LockRotation = lockRotation, ParentBone = parentBone, DetermineLockParent = determineLockParent, }); } #endregion #region Commands [Command("vehiclecodelock", "vcodelock", "vlock")] private void CodeLockCommand(IPlayer player, string cmd, string[] args) { LockCommand(player, LockType.Code); } [Command("vehiclekeylock", "vkeylock")] private void KeyLockCommand(IPlayer player, string cmd, string[] args) { LockCommand(player, LockType.Key); } private void LockCommand(IPlayer player, LockType lockType) { if (player.IsServer) return; var basePlayer = player.Object as BasePlayer; var vehicleInfo = GetVehicleAndInfo(GetLookEntity(basePlayer, MaxDeployDistance), basePlayer, out var vehicle); if (vehicleInfo == null) { ReplyToPlayer(player, Lang.DeployErrorNoVehicleFound); return; } if (!VerifyCanDeploy(player, vehicle, vehicleInfo, lockType, out var lockInfo, out var payType)) return; DeployLockForPlayer(vehicle, vehicleInfo, lockInfo, basePlayer, payType); } #endregion #region Helper Methods - General private static bool HasPermissionAny(IPlayer player, params string[] permissionNames) { foreach (var perm in permissionNames) { if (player.HasPermission(perm)) return true; } return false; } private static BaseLock GetVehicleLock(BaseEntity vehicle) { return vehicle.GetSlot(BaseEntity.Slot.Lock) as BaseLock; } private static bool IsLockableEntity(BaseEntity entity) { if (entity.IsBusy()) return false; return entity.HasSlot(BaseEntity.Slot.Lock); } private static string[] FindPrefabsOfType() where T : BaseEntity { var prefabList = new List(); foreach (var assetPath in GameManifest.Current.entities) { var entity = GameManager.server.FindPrefab(assetPath)?.GetComponent(); if (entity == null) continue; prefabList.Add(entity.PrefabName); } return prefabList.ToArray(); } private static bool IsTaxiSeat(ModularCarSeat carSeat) { return carSeat is { associatedSeatingModule.DoorsAreLockable: false }; } private static bool IsSameTeam(BasePlayer player, ulong userId) { if (player.currentTeam == 0) return false; var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam); return team != null && team.members.Contains(userId); } #endregion #region Helper Methods - Lock Authorization private bool IsPlayerAuthorizedToCodeLock(ulong userID, CodeLock codeLock) { return codeLock.whitelistPlayers.Contains(userID) || codeLock.guestPlayers.Contains(userID); } private bool IsPlayerAuthorizedToLock(BasePlayer player, BaseLock baseLock) { return (baseLock as KeyLock)?.HasLockPermission(player) ?? IsPlayerAuthorizedToCodeLock(player.userID, baseLock as CodeLock); } private bool PlayerHasMasterKeyForLock(BasePlayer player) { return permission.UserHasPermission(player.UserIDString, Permission_MasterKey); } private bool IsLockSharedWithPlayer(BasePlayer player, BaseLock baseLock) { var ownerId = baseLock.OwnerID; if (ownerId == 0 || ownerId == player.userID) return false; // In case the owner was locked out for some reason var codeLock = baseLock as CodeLock; if (codeLock != null && !IsPlayerAuthorizedToCodeLock(ownerId, codeLock)) return false; var sharingSettings = _config.SharingSettings; if (sharingSettings.Team && player.currentTeam != 0) { if (IsSameTeam(player, ownerId)) return true; } if (sharingSettings.Friends && Friends != null) { var friendsResult = Friends.Call("HasFriend", baseLock.OwnerID, player.userID); if (friendsResult is true) return true; } if ((sharingSettings.Clan || sharingSettings.ClanOrAlly) && Clans != null) { var clanMethodName = sharingSettings.ClanOrAlly ? "IsMemberOrAlly" : "IsClanMember"; var clanResult = Clans.Call(clanMethodName, ownerId.ToString(), player.UserIDString); if (clanResult is true) return true; } return false; } private bool CanPlayerBypassLock(BasePlayer player, BaseLock baseLock, bool provideFeedback) { var hookResult = Interface.CallHook("CanUseLockedEntity", player, baseLock); if (hookResult is bool result) return result; if (_config.AllowNPCsToBypassLocks && (player.IsNpc || !player.userID.IsSteamId())) return true; var canAccessLock = IsPlayerAuthorizedToLock(player, baseLock) || IsLockSharedWithPlayer(player, baseLock) || PlayerHasMasterKeyForLock(player); if (canAccessLock) { if (provideFeedback && !(baseLock is KeyLock)) { Effect.server.Run(Prefab_CodeLock_UnlockEffect, baseLock, 0, Vector3.zero, Vector3.forward); } return true; } if (provideFeedback) { Effect.server.Run(Prefab_CodeLock_DeniedEffect, baseLock, 0, Vector3.zero, Vector3.forward); ChatMessage(player, Lang.GenericErrorVehicleLocked); } return false; } private object CanPlayerInteractWithVehicle(BasePlayer player, BaseEntity vehicle, bool provideFeedback = true) { if (player == null || vehicle == null) return null; var baseLock = GetVehicleLock(vehicle); if (baseLock == null || !baseLock.IsLocked()) return null; if (CanPlayerBypassLock(player, baseLock, provideFeedback)) return null; return False; } private BaseEntity GetParentVehicle(BaseEntity entity) { var parent = entity.GetParentEntity(); if (parent == null) return null; // Check for a vehicle module first since they are considered vehicles. var parentModule = parent as BaseVehicleModule; if (parentModule != null) return parentModule.Vehicle; if (parent is HotAirBalloon or BaseVehicle) return parent; return _vehicleInfoManager.GetCustomVehicleParent(entity); } private object CanPlayerInteractWithParentVehicle(BasePlayer player, BaseEntity entity, bool provideFeedback = true) { return CanPlayerInteractWithVehicle(player, GetParentVehicle(entity), provideFeedback); } #endregion #region Helper Methods - Deploying Locks private static bool DeployWasBlocked(BaseEntity vehicle, BasePlayer player, LockType lockType) { return Interface.CallHook(lockType.PreHookName, vehicle, player) is false; } private static BaseEntity GetLookEntity(BasePlayer player, float maxDistance) { if (!Physics.Raycast(player.eyes.HeadRay(), out var hit, maxDistance, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore)) return null; return hit.GetEntity(); } private static bool IsDead(BaseEntity entity) { return (entity as BaseCombatEntity)?.IsDead() ?? false; } private static VehicleModuleSeating FindFirstDriverModule(ModularCar car) { for (var socketIndex = 0; socketIndex < car.TotalSockets; socketIndex++) { if (car.TryGetModuleAt(socketIndex, out var module)) { var seatingModule = module as VehicleModuleSeating; if (seatingModule != null && seatingModule.HasADriverSeat()) return seatingModule; } } return null; } private static bool CanCarHaveLock(ModularCar car) { return FindFirstDriverModule(car) != null; } private static bool CanVehicleHaveALock(BaseEntity vehicle) { // Only modular cars have restrictions var car = vehicle as ModularCar; return car == null || CanCarHaveLock(car); } private static Item GetPlayerLockItem(BasePlayer player, LockType lockType, out LockInfo lockInfo) { if (lockType == LockType.Key) { lockInfo = LockInfo.KeyLock; return GetPlayerLockItem(player, LockInfo.KeyLock); } // Prioritize the original code lock over the pilot code lock. var codeLockItem = GetPlayerLockItem(player, LockInfo.CodeLock); if (codeLockItem != null) { lockInfo = LockInfo.CodeLock; return codeLockItem; } var pilotCodeLockItem = GetPlayerLockItem(player, LockInfo.PilotCodeLock); if (pilotCodeLockItem != null) { lockInfo = LockInfo.PilotCodeLock; return pilotCodeLockItem; } lockInfo = LockInfo.CodeLock; return null; } private static Item GetPlayerLockItem(BasePlayer player, LockInfo lockInfo) { return player.inventory.FindItemByItemID(lockInfo.ItemId); } private static PayType DeterminePayType(IPlayer player, LockType lockType, out LockInfo lockInfo) { // Find a matching lock item in order to determine which code lock type should be deployed. var lockItem = GetPlayerLockItem(player.Object as BasePlayer, lockType, out lockInfo); if (player.HasPermission(lockType.PermissionFree)) return PayType.Free; return lockItem != null ? PayType.Item : PayType.Resources; } private static bool CanPlayerAffordLock(BasePlayer player, LockType lockType, out LockInfo lockInfo, out PayType payType) { payType = DeterminePayType(player.IPlayer, lockType, out lockInfo); if (payType != PayType.Resources) return true; return player.inventory.crafting.CanCraft(lockInfo.ItemDefinition); } private static RidableHorse GetClosestHorse(HitchTrough hitchTrough, BasePlayer player) { var closestDistance = float.MaxValue; RidableHorse closestHorse = null; foreach (var hitchSpot in hitchTrough.hitchSpots) { if (!hitchSpot.IsOccupied()) continue; var distance = Vector3.Distance(player.transform.position, hitchSpot.tr.position); if (distance < closestDistance) { closestDistance = distance; if (hitchSpot.hitchableEntRef.Get(serverside: true) is RidableHorse ridableHorse) { closestHorse = ridableHorse; } } } return closestHorse; } private static void ClaimVehicle(BaseEntity vehicle, ulong ownerId) { vehicle.OwnerID = ownerId; Interface.CallHook("OnVehicleOwnershipChanged", vehicle); } private bool IsVehicleConnectedEntity(BasePlayer basePlayer, BaseEntity entity, out BaseEntity vehicle) { vehicle = null; if (entity is BaseVehicleModule module) { vehicle = module.Vehicle ?? module.GetParentEntity(); return true; } if (entity is ModularCarGarage carLift) { vehicle = carLift.carOccupant; return true; } if (entity is HitchTrough hitchTrough) { vehicle = GetClosestHorse(hitchTrough, basePlayer); return true; } return false; } private VehicleInfo GetVehicleAndInfo(BaseEntity entity, BasePlayer basePlayer, out BaseEntity vehicle, bool fromDeployHook = false) { if (entity == null) { vehicle = null; return null; } var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(entity); if (vehicleInfo != null) { vehicle = entity; return vehicleInfo; } if (IsVehicleConnectedEntity(basePlayer, entity, out vehicle) && vehicle != null) return _vehicleInfoManager.GetVehicleInfo(vehicle); if (fromDeployHook && IsLockableEntity(entity)) { // Let the game decide whether to lock the entity, instead of resolving the parent vehicle. vehicle = null; return null; } vehicle = entity.GetParentEntity(); if (vehicle == null) return null; return _vehicleInfoManager.GetVehicleInfo(vehicle); } private bool AllowNoOwner(BaseEntity vehicle) { return _config.AllowIfNoOwner || vehicle.OwnerID != 0; } private bool AllowDifferentOwner(BasePlayer player, BaseEntity vehicle) { return _config.AllowIfDifferentOwner || vehicle.OwnerID == 0 || vehicle.OwnerID == player.userID || (_config.AllowIfOwnedByTeammate && IsSameTeam(player, vehicle.OwnerID)); } private void MaybeChargePlayerForLock(BasePlayer player, LockInfo lockInfo, PayType payType) { if (payType == PayType.Free) return; if (payType == PayType.Item) { // Prefer taking the item they are holding in case they are deploying directly. var heldItem = player.GetActiveItem(); if (heldItem != null && heldItem.info.itemid == lockInfo.ItemId) { heldItem.UseItem(1); } else { player.inventory.Take(null, lockInfo.ItemId, 1); } player.Command("note.inv", lockInfo.ItemId, -1); return; } foreach (var ingredient in lockInfo.Blueprint.ingredients) { player.inventory.Take(null, ingredient.itemid, (int)ingredient.amount); player.Command("note.inv", ingredient.itemid, -ingredient.amount); GetCooldownManager(lockInfo.LockType).UpdateLastUsedForPlayer(player.UserIDString); } } private BaseLock DeployLock(BaseEntity vehicle, VehicleInfo vehicleInfo, LockInfo lockInfo, ulong ownerId = 0) { var parentToEntity = vehicleInfo.DetermineLockParent(vehicle); if (parentToEntity == null) return null; var baseLock = GameManager.server.CreateEntity(lockInfo.Prefab, vehicleInfo.LockPosition, vehicleInfo.LockRotation) as BaseLock; if (baseLock == null) return null; var keyLock = baseLock as KeyLock; if (keyLock != null) { keyLock.keyCode = UnityEngine.Random.Range(1, 100000); } // Assign lock ownership when the lock is being deployed by/for a player. if (ownerId != 0) { baseLock.OwnerID = ownerId; } baseLock.SetParent(parentToEntity, vehicleInfo.ParentBone); baseLock.Spawn(); vehicle.SetSlot(BaseEntity.Slot.Lock, baseLock); // Auto lock key locks to be consistent with vanilla. if (ownerId != 0 && keyLock != null) { keyLock.SetFlag(BaseEntity.Flags.Locked, true); } Effect.server.Run(Prefab_CodeLock_DeployedEffect, baseLock.transform.position); Interface.CallHook("OnVehicleLockDeployed", vehicle, baseLock); _lockedVehicleTracker.OnLockAdded(vehicle); return baseLock; } private BaseLock DeployLockForPlayer(BaseEntity vehicle, VehicleInfo vehicleInfo, LockInfo lockInfo, BasePlayer player, PayType payType) { var originalVehicleOwnerId = vehicle.OwnerID; // Temporarily set the player as the owner of the vehicle, for compatibility with AutoCodeLock (OnItemDeployed). vehicle.OwnerID = player.userID; var baseLock = DeployLock(vehicle, vehicleInfo, lockInfo, player.userID); if (baseLock == null) { vehicle.OwnerID = originalVehicleOwnerId; return null; } // Allow other plugins to detect the code lock being deployed (e.g., to auto lock). var lockItem = GetPlayerLockItem(player, lockInfo); if (lockItem != null) { Interface.CallHook("OnItemDeployed", lockItem.GetHeldEntity(), vehicle, baseLock); } else { // Temporarily increase the player inventory capacity to ensure there is enough space. player.inventory.containerMain.capacity++; var temporaryLockItem = ItemManager.CreateByItemID(lockInfo.ItemId); if (player.inventory.GiveItem(temporaryLockItem)) { Interface.CallHook("OnItemDeployed", temporaryLockItem.GetHeldEntity(), vehicle, baseLock); temporaryLockItem.RemoveFromContainer(); } temporaryLockItem.Remove(); player.inventory.containerMain.capacity--; } // Revert the vehicle owner to the original, after OnItemDeployed is called. vehicle.OwnerID = originalVehicleOwnerId; MaybeChargePlayerForLock(player, lockInfo, payType); // Potentially assign vehicle ownership when the lock is being deployed by/for a player. if (vehicle.OwnerID == 0) { if (_config.AutoClaimUnownedVehicles) { ClaimVehicle(vehicle, player.userID); } } else if (vehicle.OwnerID != player.userID && _config.AutoReplaceVehicleOwnership) { ClaimVehicle(vehicle, player.userID); } return baseLock; } private BaseLock DeployLockForAPI(BaseEntity vehicle, BasePlayer player, LockType lockType, bool isFree) { if (vehicle == null || IsDead(vehicle)) return null; var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(vehicle); if (vehicleInfo == null || GetVehicleLock(vehicle) != null || !CanVehicleHaveALock(vehicle)) return null; GetPlayerLockItem(player, lockType, out var lockInfo); PayType payType; if (isFree) { payType = PayType.Free; } else if (!VerifyPlayerCanDeployLock(player.IPlayer, lockType, out _, out payType)) { return null; } if (DeployWasBlocked(vehicle, player, lockType)) return null; return player != null ? DeployLockForPlayer(vehicle, vehicleInfo, lockInfo, player, payType) : DeployLock(vehicle, vehicleInfo, lockInfo); } private bool CanPlayerDeployLockForAPI(BasePlayer player, BaseEntity vehicle, LockType lockType) { return vehicle != null && !IsDead(vehicle) && _vehicleInfoManager.GetVehicleInfo(vehicle) != null && AllowNoOwner(vehicle) && AllowDifferentOwner(player, vehicle) && player.CanBuild() && GetVehicleLock(vehicle) == null && CanVehicleHaveALock(vehicle) && CanPlayerAffordLock(player, lockType, out _, out _) && !DeployWasBlocked(vehicle, player, lockType); } #endregion #region Helper Methods - Command Checks private bool VerifyDeployDistance(IPlayer player, BaseEntity vehicle) { if (vehicle.Distance(player.Object as BasePlayer) <= MaxDeployDistance) return true; ReplyToPlayer(player, Lang.DeployErrorDistance); return false; } private bool VerifyPermissionToVehicleAndLockType(IPlayer player, VehicleInfo vehicleInfo, LockType lockType) { var vehiclePerm = lockType == LockType.Code ? vehicleInfo.CodeLockPermission : vehicleInfo.KeyLockPermission; if (vehiclePerm != null && HasPermissionAny(player, lockType.PermissionAllVehicles, vehiclePerm)) return true; ReplyToPlayer(player, Lang.GenericErrorNoPermission); return false; } private bool VerifyVehicleIsNotDead(IPlayer player, BaseEntity vehicle) { if (!IsDead(vehicle)) return true; ReplyToPlayer(player, Lang.DeployErrorVehicleDead); return false; } private bool VerifyNotForSale(IPlayer player, BaseEntity vehicle) { var horse = vehicle as RidableHorse; if (horse == null || !horse.IsForSale) return true; ReplyToPlayer(player, Lang.DeployErrorOther); return false; } private bool VerifyNoOwnershipRestriction(IPlayer player, BasePlayer basePlayer, BaseEntity vehicle) { if (!AllowNoOwner(vehicle)) { ReplyToPlayer(player, Lang.DeployErrorNoOwner); return false; } if (!AllowDifferentOwner(basePlayer, vehicle)) { ReplyToPlayer(player, Lang.DeployErrorDifferentOwner); return false; } return true; } private bool VerifyCanBuild(IPlayer player, BaseEntity vehicle) { var basePlayer = player.Object as BasePlayer; if (basePlayer == null) return false; if (vehicle.OwnerID == 0 && _config.RequireTCIfNoOwner) { if (!basePlayer.IsBuildingAuthed() || !basePlayer.IsBuildingAuthed(vehicle.WorldSpaceBounds())) { ReplyToPlayer(player, Lang.DeployErrorNoOwnerRequiresTC); return false; } } else if (basePlayer.IsBuildingBlocked() || basePlayer.IsBuildingBlocked(vehicle.WorldSpaceBounds())) { ReplyToPlayer(player, Lang.GenericErrorBuildingBlocked); return false; } return true; } private bool VerifyVehicleHasNoLock(IPlayer player, BaseEntity vehicle) { if (GetVehicleLock(vehicle) == null) return true; ReplyToPlayer(player, Lang.DeployErrorHasLock); return false; } private bool VerifyVehicleCanHaveALock(IPlayer player, BaseEntity vehicle) { if (CanVehicleHaveALock(vehicle)) return true; ReplyToPlayer(player, Lang.DeployErrorModularCarNoCockpit); return false; } private bool VerifyPlayerCanAffordLock(BasePlayer player, LockType lockType, out LockInfo lockInfo, out PayType payType) { if (CanPlayerAffordLock(player, lockType, out lockInfo, out payType)) return true; ChatMessage(player, Lang.DeployErrorInsufficientResources, lockInfo.ItemDefinition.displayName.translated); return false; } private bool VerifyOffCooldown(IPlayer player, LockType lockType, PayType payType) { if (payType != PayType.Resources) return true; var secondsRemaining = GetCooldownManager(lockType).GetSecondsRemaining(player.Id); if (secondsRemaining <= 0) return true; ChatMessage(player.Object as BasePlayer, Lang.GenericErrorCooldown, Math.Ceiling(secondsRemaining)); return false; } private bool VerifyPlayerCanDeployLock(IPlayer player, LockType lockType, out LockInfo lockInfo, out PayType payType) { return VerifyPlayerCanAffordLock(player.Object as BasePlayer, lockType, out lockInfo, out payType) && VerifyOffCooldown(player, lockType, payType); } private bool VerifyNotMounted(IPlayer player, BaseEntity vehicle, VehicleInfo vehicleInfo) { if (!vehicleInfo.IsMounted(vehicle)) return true; ReplyToPlayer(player, Lang.DeployErrorMounted); return false; } private bool VerifyCanDeploy(IPlayer player, BaseEntity vehicle, VehicleInfo vehicleInfo, LockType lockType, out LockInfo lockInfo, out PayType payType) { var basePlayer = player.Object as BasePlayer; payType = PayType.Item; if (!VerifyPermissionToVehicleAndLockType(player, vehicleInfo, lockType) || !VerifyVehicleIsNotDead(player, vehicle) || !VerifyNotForSale(player, vehicle) || !VerifyNoOwnershipRestriction(player, basePlayer, vehicle) || !VerifyCanBuild(player, vehicle) || !VerifyVehicleHasNoLock(player, vehicle) || !VerifyVehicleCanHaveALock(player, vehicle)) { lockInfo = null; return false; } return VerifyPlayerCanDeployLock(player, lockType, out lockInfo, out payType) && VerifyNotMounted(player, vehicle, vehicleInfo) && !DeployWasBlocked(vehicle, basePlayer, lockType); } #endregion #region Vehicle Info private class VehicleInfo { public string VehicleType; public string[] PrefabPaths; public Vector3 LockPosition; public Quaternion LockRotation; public string ParentBone; public string CodeLockPermission { get; private set; } public string KeyLockPermission { get; private set; } public uint[] PrefabIds { get; private set; } public Func DetermineLockParent = entity => entity; public Func TimeSinceLastUsed = _ => 0; public void OnServerInitialized(VehicleDeployedLocks plugin) { CodeLockPermission = $"{Permission_CodeLock_Prefix}.{VehicleType}"; KeyLockPermission = $"{Permission_KeyLock_Prefix}.{VehicleType}"; if (!plugin.permission.PermissionExists(CodeLockPermission, plugin)) { plugin.permission.RegisterPermission(CodeLockPermission, plugin); } if (!plugin.permission.PermissionExists(KeyLockPermission, plugin)) { plugin.permission.RegisterPermission(KeyLockPermission, plugin); } // Custom vehicles aren't currently allowed to specify prefabs since they reuse existing prefabs. if (PrefabPaths != null) { var prefabIds = new List(); foreach (var prefabName in PrefabPaths) { var prefabId = StringPool.Get(prefabName); if (prefabId != 0) { prefabIds.Add(prefabId); } else { plugin.LogError($"Invalid prefab. Please alert the plugin maintainer -- {prefabName}"); } } PrefabIds = prefabIds.ToArray(); } } // In the future, custom vehicles may be able to pass in a method to override this. public bool IsMounted(BaseEntity entity) { var vehicle = entity as BaseVehicle; if (vehicle != null) return vehicle.AnyMounted(); var mountable = entity as BaseMountable; if (mountable != null) return mountable.AnyMounted(); return false; } } private class VehicleInfoManager { private readonly VehicleDeployedLocks _plugin; private readonly Dictionary _prefabIdToVehicleInfo = new(); private readonly Dictionary _customVehicleTypes = new(); public VehicleInfoManager(VehicleDeployedLocks plugin) { _plugin = plugin; } public void OnServerInitialized() { var allVehicles = new[] { new VehicleInfo { VehicleType = "attackhelicopter", PrefabPaths = new[] { "assets/content/vehicles/attackhelicopter/attackhelicopter.entity.prefab" }, LockPosition = new Vector3(-0.6f, 1.08f, 1.01f), TimeSinceLastUsed = vehicle => Time.time - (vehicle as AttackHelicopter)?.lastEngineOnTime ?? Time.time, }, new VehicleInfo { VehicleType = "chinook", PrefabPaths = new[] { "assets/prefabs/npc/ch47/ch47.entity.prefab" }, LockPosition = new Vector3(-1.175f, 2, 6.5f), TimeSinceLastUsed = vehicle => Time.time - (vehicle as CH47Helicopter)?.lastPlayerInputTime ?? Time.time, }, new VehicleInfo { VehicleType = "duosub", PrefabPaths = new[] { "assets/content/vehicles/submarine/submarineduo.entity.prefab" }, LockPosition = new Vector3(-0.455f, 1.29f, 0.75f), LockRotation = Quaternion.Euler(0, 180, 10), TimeSinceLastUsed = vehicle => (vehicle as SubmarineDuo)?.timeSinceLastUsed ?? 0, }, new VehicleInfo { VehicleType = "hotairballoon", PrefabPaths = new[] { "assets/prefabs/deployable/hot air balloon/hotairballoon.prefab" }, LockPosition = new Vector3(1.45f, 0.9f, 0), TimeSinceLastUsed = vehicle => Time.time - (vehicle as HotAirBalloon)?.sinceLastBlast ?? Time.time, }, new VehicleInfo { VehicleType = "kayak", PrefabPaths = new[] { "assets/content/vehicles/boats/kayak/kayak.prefab" }, LockPosition = new Vector3(-0.43f, 0.2f, 0.2f), LockRotation = Quaternion.Euler(0, 90, 90), TimeSinceLastUsed = vehicle => (vehicle as Kayak)?.timeSinceLastUsed ?? 0, }, new VehicleInfo { VehicleType = "locomotive", PrefabPaths = new[] { "assets/content/vehicles/trains/locomotive/locomotive.entity.prefab" }, LockPosition = new Vector3(-0.11f, 2.89f, 4.95f), TimeSinceLastUsed = vehicle => (vehicle as TrainEngine)?.decayingFor ?? 0, }, new VehicleInfo { VehicleType = "magnetcrane", PrefabPaths = new[] { "assets/content/vehicles/crane_magnet/magnetcrane.entity.prefab" }, LockPosition = new Vector3(-1.735f, -1.445f, 0.79f), LockRotation = Quaternion.Euler(0, 0, 90), ParentBone = "Top", TimeSinceLastUsed = vehicle => Time.realtimeSinceStartup - (vehicle as MagnetCrane)?.lastDrivenTime ?? Time.realtimeSinceStartup, }, new VehicleInfo { VehicleType = "minicopter", PrefabPaths = new[] { "assets/content/vehicles/minicopter/minicopter.entity.prefab" }, LockPosition = new Vector3(-0.15f, 0.7f, -0.1f), TimeSinceLastUsed = vehicle => Time.time - (vehicle as Minicopter)?.lastEngineOnTime ?? Time.time, }, new VehicleInfo { VehicleType = "modularcar", // There are at least 37 valid Modular Car prefabs. PrefabPaths = FindPrefabsOfType(), LockPosition = new Vector3(-0.9f, 0.35f, -0.5f), DetermineLockParent = vehicle => FindFirstDriverModule((ModularCar)vehicle), TimeSinceLastUsed = vehicle => Time.time - (vehicle as ModularCar)?.lastEngineOnTime ?? Time.time, }, new VehicleInfo { VehicleType = "motorbike.sidecar", PrefabPaths = new[] { "assets/content/vehicles/bikes/motorbike_sidecar.prefab" }, LockPosition = new Vector3(-0.09f, 0.65f, 0.03f), TimeSinceLastUsed = vehicle => (vehicle as Bike).timeSinceLastUsed, }, new VehicleInfo { VehicleType = "motorbike", PrefabPaths = new[] { "assets/content/vehicles/bikes/motorbike.prefab" }, LockPosition = new Vector3(-0.09f, 0.65f, 0.03f), TimeSinceLastUsed = vehicle => (vehicle as Bike).timeSinceLastUsed, }, new VehicleInfo { VehicleType = "pedalbike", PrefabPaths = new[] { "assets/content/vehicles/bikes/pedalbike.prefab" }, LockPosition = new Vector3(0, 0.6f, 0.1f), TimeSinceLastUsed = vehicle => (vehicle as Bike).timeSinceLastUsed, }, new VehicleInfo { VehicleType = "pedaltrike", PrefabPaths = new[] { "assets/content/vehicles/bikes/pedaltrike.prefab" }, LockPosition = new Vector3(0, 0.6f, 0.1f), TimeSinceLastUsed = vehicle => (vehicle as Bike).timeSinceLastUsed, }, new VehicleInfo { VehicleType = "ptboat", PrefabPaths = new[] { "assets/content/vehicles/boats/ptboat/ptboat.prefab", "assets/content/vehicles/boats/ptboat/ptboat.deepsea.prefab", }, LockPosition = new Vector3(-1.05f, 2.10f, -0.75f), LockRotation = Quaternion.Euler(0, 90, 0), TimeSinceLastUsed = vehicle => (vehicle as RHIB)?.timeSinceLastUsedFuel ?? 0, }, new VehicleInfo { VehicleType = "rhib", PrefabPaths = new[] { "assets/content/vehicles/boats/rhib/rhib.prefab", "assets/content/vehicles/boats/rhib/rhib.deepsea.prefab", }, LockPosition = new Vector3(-0.68f, 2.00f, 0.7f), TimeSinceLastUsed = vehicle => (vehicle as RHIB)?.timeSinceLastUsedFuel ?? 0, }, new VehicleInfo { VehicleType = "ridablehorse", PrefabPaths = new[] { "assets/content/vehicles/horse/ridablehorse.prefab" }, LockPosition = new Vector3(-0.6f, 0.25f, -0.1f), LockRotation = Quaternion.Euler(0, 95, 90), ParentBone = "Horse_RootBone", TimeSinceLastUsed = vehicle => Time.time - (vehicle as RidableHorse)?.lastRiddenTime ?? Time.time, }, new VehicleInfo { VehicleType = "rowboat", PrefabPaths = new[] { "assets/content/vehicles/boats/rowboat/rowboat.prefab" }, LockPosition = new Vector3(-0.83f, 0.51f, -0.57f), TimeSinceLastUsed = vehicle => (vehicle as MotorRowboat)?.timeSinceLastUsedFuel ?? 0, }, new VehicleInfo { VehicleType = "scraptransport", PrefabPaths = new[] { "assets/content/vehicles/scrap heli carrier/scraptransporthelicopter.prefab" }, LockPosition = new Vector3(-1.25f, 1.22f, 1.99f), TimeSinceLastUsed = vehicle => Time.time - (vehicle as ScrapTransportHelicopter)?.lastEngineOnTime ?? Time.time, }, new VehicleInfo { VehicleType = "sedan", PrefabPaths = new[] { "assets/content/vehicles/sedan_a/sedantest.entity.prefab" }, LockPosition = new Vector3(-1.09f, 0.79f, 0.5f), }, new VehicleInfo { VehicleType = "sedanrail", PrefabPaths = new[] { "assets/content/vehicles/sedan_a/sedanrail.entity.prefab" }, LockPosition = new Vector3(-1.09f, 1.025f, -0.26f), TimeSinceLastUsed = vehicle => (vehicle as TrainEngine)?.decayingFor ?? 0, }, new VehicleInfo { VehicleType = "snowmobile", PrefabPaths = new[] { "assets/content/vehicles/snowmobiles/snowmobile.prefab" }, LockPosition = new Vector3(-0.205f, 0.59f, 0.4f), TimeSinceLastUsed = vehicle => (vehicle as Snowmobile)?.timeSinceLastUsed ?? 0, }, new VehicleInfo { VehicleType = "solosub", PrefabPaths = new[] { "assets/content/vehicles/submarine/submarinesolo.entity.prefab" }, LockPosition = new Vector3(0f, 1.85f, 0f), LockRotation = Quaternion.Euler(0, 90, 90), TimeSinceLastUsed = vehicle => (vehicle as BaseSubmarine)?.timeSinceLastUsed ?? 0, }, new VehicleInfo { VehicleType = "tomaha", PrefabPaths = new[] { "assets/content/vehicles/snowmobiles/tomahasnowmobile.prefab" }, LockPosition = new Vector3(-0.37f, 0.4f, 0.125f), TimeSinceLastUsed = vehicle => (vehicle as Snowmobile)?.timeSinceLastUsed ?? 0, }, new VehicleInfo { VehicleType = "tugboat", PrefabPaths = new[] { "assets/content/vehicles/boats/tugboat/tugboat.prefab" }, LockPosition = new Vector3(0.065f, 6.8f, 4.12f), LockRotation = Quaternion.Euler(0, 90, 60), TimeSinceLastUsed = vehicle => (vehicle as Tugboat)?.timeSinceLastUsedFuel ?? 0, }, new VehicleInfo { VehicleType = "workcart", PrefabPaths = new[] { "assets/content/vehicles/trains/workcart/workcart.entity.prefab" }, LockPosition = new Vector3(-0.2f, 2.35f, 2.7f), TimeSinceLastUsed = vehicle => (vehicle as TrainEngine)?.decayingFor ?? 0, }, new VehicleInfo { VehicleType = "workcartaboveground", PrefabPaths = new[] { "assets/content/vehicles/trains/workcart/workcart_aboveground.entity.prefab" }, LockPosition = new Vector3(-0.2f, 2.35f, 2.7f), TimeSinceLastUsed = vehicle => (vehicle as TrainEngine)?.decayingFor ?? 0, }, new VehicleInfo { VehicleType = "workcartcovered", PrefabPaths = new[] { "assets/content/vehicles/trains/workcart/workcart_aboveground2.entity.prefab" }, LockPosition = new Vector3(-0.2f, 2.35f, 2.7f), TimeSinceLastUsed = vehicle => (vehicle as TrainEngine)?.decayingFor ?? 0, }, }; foreach (var vehicleInfo in allVehicles) { vehicleInfo.OnServerInitialized(_plugin); foreach (var prefabId in vehicleInfo.PrefabIds) { _prefabIdToVehicleInfo[prefabId] = vehicleInfo; } } } public void RegisterCustomVehicleType(VehicleDeployedLocks plugin, VehicleInfo vehicleInfo) { vehicleInfo.OnServerInitialized(plugin); _customVehicleTypes[vehicleInfo.VehicleType] = vehicleInfo; } public VehicleInfo GetVehicleInfo(BaseEntity entity) { if (_prefabIdToVehicleInfo.TryGetValue(entity.prefabID, out var vehicleInfo)) return vehicleInfo; foreach (var customVehicleInfo in _customVehicleTypes.Values) { if (customVehicleInfo.DetermineLockParent(entity) != null) return customVehicleInfo; } return null; } public BaseEntity GetCustomVehicleParent(BaseEntity entity) { foreach (var vehicleInfo in _customVehicleTypes.Values) { var lockParent = vehicleInfo.DetermineLockParent(entity); if (lockParent != null) return lockParent; } return null; } } #endregion #region Lock Info private class LockType { public static readonly LockType Code = new() { PermissionAllVehicles = $"{Permission_CodeLock_Prefix}.allvehicles", PermissionFree = $"{Permission_CodeLock_Prefix}.free", PreHookName = "CanDeployVehicleCodeLock", }; public static readonly LockType Key = new() { PermissionAllVehicles = $"{Permission_KeyLock_Prefix}.allvehicles", PermissionFree = $"{Permission_KeyLock_Prefix}.free", PreHookName = "CanDeployVehicleKeyLock", }; public string PermissionAllVehicles; public string PermissionFree; public string PreHookName; } private class LockInfo { public static readonly LockInfo CodeLock = new() { LockType = LockType.Code, ItemId = 1159991980, Prefab = "assets/prefabs/locks/keypad/lock.code.prefab", }; public static readonly LockInfo PilotCodeLock = new() { LockType = LockType.Code, ItemId = 1586884551, Prefab = "assets/prefabs/locks/keypad/skins/codelock_a_pilot/lock.code.a.pilot.prefab", }; public static readonly LockInfo KeyLock = new() { LockType = LockType.Key, ItemId = -850982208, Prefab = "assets/prefabs/locks/keylock/lock.key.prefab", }; public LockType LockType; public int ItemId; public string Prefab; public ItemDefinition ItemDefinition => ItemManager.FindItemDefinition(ItemId); public ItemBlueprint Blueprint => ItemManager.FindBlueprint(ItemDefinition); } #endregion #region Locked Vehicle Tracker private class LockedVehicleTracker { public Dictionary> VehiclesWithLocksByType { get; } = new(); private readonly VehicleInfoManager _vehicleInfoManager; public LockedVehicleTracker(VehicleInfoManager vehicleInfoManager) { _vehicleInfoManager = vehicleInfoManager; } public void OnServerInitialized() { foreach (var entity in BaseNetworkable.serverEntities) { var baseEntity = entity as BaseEntity; if (baseEntity == null) continue; var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(baseEntity); if (vehicleInfo == null || GetVehicleLock(baseEntity) == null) continue; OnLockAdded(baseEntity); } } public void OnLockAdded(BaseEntity vehicle) { GetEntityListForVehicle(vehicle)?.Add(vehicle); } public void OnLockRemoved(BaseEntity vehicle) { GetEntityListForVehicle(vehicle)?.Remove(vehicle); } private HashSet EnsureEntityList(VehicleInfo vehicleInfo) { if (!VehiclesWithLocksByType.TryGetValue(vehicleInfo, out var vehicleList)) { vehicleList = new HashSet(); VehiclesWithLocksByType[vehicleInfo] = vehicleList; } return vehicleList; } private HashSet GetEntityListForVehicle(BaseEntity entity) { var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(entity); if (vehicleInfo == null) return null; return EnsureEntityList(vehicleInfo); } } #endregion #region Auto Unlock Manager private class AutoUnlockManager { private VehicleDeployedLocks _plugin; private LockedVehicleTracker _lockedVehicleTracker; private AutoUnlockSettings _autoUnlockSettings; public AutoUnlockManager(VehicleDeployedLocks plugin, LockedVehicleTracker lockedVehicleTracker) { _plugin = plugin; _lockedVehicleTracker = lockedVehicleTracker; } public void OnServerInitialized(AutoUnlockSettings settings) { _autoUnlockSettings = settings; if (!settings.Enabled) return; _plugin.timer.Every(settings.CheckIntervalSeconds, CheckVehicles); } private void CheckVehicles() { List remainingVehicles = null; foreach (var (vehicleInfo, vehicleList) in _lockedVehicleTracker.VehiclesWithLocksByType) { foreach (var vehicle in vehicleList) { if (vehicle == null || vehicle.IsDestroyed) continue; if (_autoUnlockSettings.ExemptOwnedVehicles && vehicle.OwnerID != 0) continue; var baseLock = GetVehicleLock(vehicle); if (baseLock == null || baseLock.IsDestroyed || !baseLock.IsLocked()) continue; var timeSinceUsed = vehicleInfo.TimeSinceLastUsed(vehicle); if (timeSinceUsed < _autoUnlockSettings.IdleSeconds) continue; if (_autoUnlockSettings.ExemptNearTC) { remainingVehicles ??= new List(); remainingVehicles.Add(vehicle); continue; } Unlock(baseLock); } } if (remainingVehicles == null) return; var vehicleIndex = 0; // The remaining vehicles need more expensive checks, so spread them out across multiple frames. _plugin.timer.Repeat(0, remainingVehicles.Count, () => { var vehicle = remainingVehicles[vehicleIndex++]; if (vehicle == null || vehicle.IsDestroyed) return; var baseLock = GetVehicleLock(vehicle); if (baseLock == null || baseLock.IsDestroyed || !baseLock.IsLocked()) return; if (vehicle.GetBuildingPrivilege() != null) return; Unlock(baseLock); }); } private void Unlock(BaseLock baseLock) { baseLock.SetFlag(BaseEntity.Flags.Locked, false); Effect.server.Run(Prefab_CodeLock_UnlockEffect, baseLock, 0, Vector3.zero, Vector3.forward); } } #endregion #region Reskin Management private class ReskinEvent { public BaseEntity Entity; public BaseLock BaseLock; public Vector3 Position; public void Assign(BaseEntity entity, BaseLock baseLock) { Entity = entity; BaseLock = baseLock; Position = entity?.transform.position ?? Vector3.zero; } public void Reset() { Assign(null, null); } } private class ReskinManager { private VehicleInfoManager _vehicleInfoManager; private LockedVehicleTracker _lockedVehicleTracker; // Pool only a single reskin event since usually there will be at most a single event per frame. private ReskinEvent _pooledReskinEvent; // Keep track of all reskin events happening in a frame, in case there are multiple. private List _reskinEvents = new(); public readonly Action CleanupAction; public ReskinManager(VehicleInfoManager vehicleInfoManager, LockedVehicleTracker lockedVehicleTracker) { _vehicleInfoManager = vehicleInfoManager; _lockedVehicleTracker = lockedVehicleTracker; CleanupAction = CleanupEvents; } public void HandleReskinPre(BaseEntity entity, BaseLock baseLock) { _pooledReskinEvent ??= new ReskinEvent(); var reskinEvent = _pooledReskinEvent.Entity == null ? _pooledReskinEvent : new ReskinEvent(); // Unparent the lock to prevent it from being destroyed. // It will later be parented to the newly spawned entity. baseLock.SetParent(null); reskinEvent.Assign(entity, baseLock); _reskinEvents.Add(reskinEvent); } public void HandleReskinPost(BaseEntity entity) { var reskinEvent = FindReskinEventForPosition(entity.transform.position); if (reskinEvent == null) return; var baseLock = reskinEvent.BaseLock; if (baseLock == null || baseLock.IsDestroyed) return; var vehicleInfo = _vehicleInfoManager.GetVehicleInfo(entity); if (vehicleInfo == null) return; _reskinEvents.Remove(reskinEvent); baseLock.SetParent(entity, vehicleInfo.ParentBone); entity.SetSlot(BaseEntity.Slot.Lock, baseLock); _lockedVehicleTracker.OnLockAdded(entity); var lockTransform = baseLock.transform; lockTransform.localPosition = vehicleInfo.LockPosition; lockTransform.localRotation = vehicleInfo.LockRotation; baseLock.SendNetworkUpdateImmediate(); if (reskinEvent == _pooledReskinEvent) { reskinEvent.Reset(); } } private ReskinEvent FindReskinEventForPosition(Vector3 position) { foreach (var reskinEvent in _reskinEvents) { if (reskinEvent.Position == position) return reskinEvent; } return null; } private void CleanupEvents() { if (_reskinEvents.Count == 0) return; foreach (var reskinEvent in _reskinEvents) { var baseLock = reskinEvent.BaseLock; if (baseLock == null || baseLock.IsDestroyed || baseLock.HasParent()) continue; var entity = reskinEvent.Entity; if (entity != null && !entity.IsDestroyed) { // The reskin event must have been blocked, so reparent the lock to it. baseLock.SetParent(reskinEvent.Entity); continue; } // The post event wasn't called, and the original entity is gone, so destroy the lock. baseLock.Kill(); } _pooledReskinEvent.Reset(); _reskinEvents.Clear(); } } #endregion #region Cooldown Manager private class CooldownManager { private readonly Dictionary _cooldownMap = new(); private readonly float _cooldownDuration; public CooldownManager(float duration) { _cooldownDuration = duration; } public void UpdateLastUsedForPlayer(string userID) { _cooldownMap[userID] = Time.realtimeSinceStartup; } public float GetSecondsRemaining(string userID) { return _cooldownMap.TryGetValue(userID, out var duration) ? duration + _cooldownDuration - Time.realtimeSinceStartup : 0; } } private CooldownManager GetCooldownManager(LockType lockType) { return lockType == LockType.Code ? _craftCodeLockCooldowns : _craftKeyLockCooldowns; } #endregion #region Configuration private class AutoUnlockSettings { [JsonProperty("Enabled")] public bool Enabled; [JsonProperty("Unlock after idle time (seconds)")] public float IdleSeconds = 3600; [JsonProperty("IdleSeconds")] private float DeprecatedIdleSeconds { set => IdleSeconds = value; } [JsonProperty("Check interval seconds")] public float CheckIntervalSeconds = 300; [JsonProperty("CheckIntervalSeconds")] private float DeprecatedCheckIntervalSeconds { set => CheckIntervalSeconds = value; } [JsonProperty("Exempt owned vehicles")] public bool ExemptOwnedVehicles = true; [JsonProperty("ExemptOwnedVehicles")] private bool DeprecatedExemptOwnedVehicles { set => ExemptOwnedVehicles = value; } [JsonProperty("Exempt vehicles near cupboards")] public bool ExemptNearTC = true; [JsonProperty("ExemptNearTC")] private bool DeprecatedExemptNearTC { set => ExemptNearTC = value; } } private class ModularCarSettings { [JsonProperty("Allow editing while locked out")] public bool AllowEditingWhileLockedOut = true; [JsonProperty("AllowEditingWhileLockedOut")] private bool DeprecatedAllowEditingWhileLockedOut { set => AllowEditingWhileLockedOut = value; } } private class SharingSettings { [JsonProperty("Clan")] public bool Clan; [JsonProperty("Clan or ally")] public bool ClanOrAlly; [JsonProperty("ClanOrAlly")] private bool DeprecatedClanOrAlly { set => ClanOrAlly = value; } [JsonProperty("Friends")] public bool Friends; [JsonProperty("Team")] public bool Team; } private class Configuration : BaseConfiguration { [JsonProperty("Update lock positions", DefaultValueHandling = DefaultValueHandling.Ignore)] public bool UpdateLockPositions; [JsonProperty("Allow NPCs to bypass locks")] public bool AllowNPCsToBypassLocks; [JsonProperty("Allow deploying locks onto unowned vehicles")] public bool AllowIfNoOwner = true; [JsonProperty("AllowIfNoOwner")] private bool DeprecatedAllowIfNoOwner { set => AllowIfNoOwner = value; } [JsonProperty("Allow deploying locks onto vehicles owned by teammates")] public bool AllowIfOwnedByTeammate; [JsonProperty("Allow deploying locks onto vehicles owned by other players")] public bool AllowIfDifferentOwner; [JsonProperty("AllowIfDifferentOwner")] private bool DeprecatedAllowIfDifferentOwner { set => AllowIfDifferentOwner = value; } [JsonProperty("Require cupboard auth to deploy locks onto unowned vehicles")] public bool RequireTCIfNoOwner; [JsonProperty("RequireTCIfNoOwner")] private bool DeprecatedRequireTCIfNoOwner { set => RequireTCIfNoOwner = value; } [JsonProperty("Auto claim unowned vehicles when deploying locks")] public bool AutoClaimUnownedVehicles; [JsonProperty("Auto replace vehicle ownership when deploying locks")] public bool AutoReplaceVehicleOwnership; [JsonProperty("Allow pushing vehicles while locked out")] public bool AllowPushWhileLockedOut = true; [JsonProperty("AllowPushWhileLockedOut")] private bool DeprecatedAllowPushWhileLockedOut { set => AllowPushWhileLockedOut = value; } [JsonProperty("Cooldown to auto craft locks (seconds)")] public float CraftCooldownSeconds = 10; [JsonProperty("CraftCooldownSeconds")] private float DeprecatedCraftCooldownSeconds { set => CraftCooldownSeconds = value; } [JsonProperty("Modular car settings")] public ModularCarSettings ModularCarSettings = new(); [JsonProperty("ModularCarSettings")] private ModularCarSettings DeprecatedModularCarSettings { set => ModularCarSettings = value; } [JsonProperty("Lock sharing settings")] public SharingSettings SharingSettings = new(); [JsonProperty("DefaultSharingSettings")] private SharingSettings DeprecatedSharingSettings { set => SharingSettings = value; } [JsonProperty("Auto unlock idle vehicles")] public AutoUnlockSettings AutoUnlockSettings = new(); [JsonProperty("AutoUnlockIdleVehicles")] private AutoUnlockSettings DeprecatedAutoUnlockSettings { set => AutoUnlockSettings = value; } } private Configuration GetDefaultConfig() => new(); #endregion #region Configuration Helpers private class BaseConfiguration { public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonHelper.Deserialize(ToJson()) as Dictionary; } private static class JsonHelper { public static object Deserialize(string json) => ToObject(JToken.Parse(json)); private static object ToObject(JToken token) { switch (token.Type) { case JTokenType.Object: return token.Children() .ToDictionary(prop => prop.Name, prop => ToObject(prop.Value)); case JTokenType.Array: return token.Select(ToObject).ToList(); default: return ((JValue)token).Value; } } } private bool MaybeUpdateConfig(BaseConfiguration config) { var currentWithDefaults = config.ToDictionary(); var currentRaw = Config.ToDictionary(x => x.Key, x => x.Value); return MaybeUpdateConfigDict(currentWithDefaults, currentRaw); } private bool MaybeUpdateConfigDict(Dictionary currentWithDefaults, Dictionary currentRaw) { var changed = false; foreach (var key in currentWithDefaults.Keys) { if (currentRaw.TryGetValue(key, out var currentRawValue)) { var currentDictValue = currentRawValue as Dictionary; if (currentWithDefaults[key] is Dictionary defaultDictValue) { if (currentDictValue == null) { currentRaw[key] = currentWithDefaults[key]; changed = true; } else if (MaybeUpdateConfigDict(defaultDictValue, currentDictValue)) changed = true; } } else { currentRaw[key] = currentWithDefaults[key]; changed = true; } } return changed; } protected override void LoadDefaultConfig() => _config = GetDefaultConfig(); protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { throw new JsonException(); } if (MaybeUpdateConfig(_config)) { LogWarning("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch (Exception e) { LogError(e.Message); LogWarning($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() { Log($"Configuration changes saved to {Name}.json"); Config.WriteObject(_config, true); } #endregion #region Localization private string GetMessage(string playerId, string messageName, params object[] args) { var message = lang.GetMessage(messageName, this, playerId); return args.Length > 0 ? string.Format(message, args) : message; } private string GetMessage(IPlayer player, string messageName, params object[] args) => GetMessage(player.Id, messageName, args); private void ReplyToPlayer(IPlayer player, string messageName, params object[] args) => player.Reply(string.Format(GetMessage(player, messageName), args)); private void ChatMessage(BasePlayer player, string messageName, params object[] args) => player.ChatMessage(string.Format(GetMessage(player.UserIDString, messageName), args)); private static class Lang { public const string GenericErrorNoPermission = "Generic.Error.NoPermission"; public const string GenericErrorBuildingBlocked = "Generic.Error.BuildingBlocked"; public const string GenericErrorCooldown = "Generic.Error.Cooldown"; public const string GenericErrorVehicleLocked = "Generic.Error.VehicleLocked"; public const string DeployErrorNoVehicleFound = "Deploy.Error.NoVehicleFound"; public const string DeployErrorVehicleDead = "Deploy.Error.VehicleDead"; public const string DeployErrorOther = "Deploy.Error.Other"; public const string DeployErrorDifferentOwner = "Deploy.Error.DifferentOwner"; public const string DeployErrorNoOwner = "Deploy.Error.NoOwner"; public const string DeployErrorNoOwnerRequiresTC = "Deploy.Error.NoOwner.NoBuildingPrivilege"; public const string DeployErrorHasLock = "Deploy.Error.HasLock"; public const string DeployErrorInsufficientResources = "Deploy.Error.InsufficientResources"; public const string DeployErrorMounted = "Deploy.Error.Mounted"; public const string DeployErrorModularCarNoCockpit = "Deploy.Error.ModularCar.NoCockpit"; public const string DeployErrorDistance = "Deploy.Error.Distance"; } protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { [Lang.GenericErrorNoPermission] = "You don't have permission to do that.", [Lang.GenericErrorBuildingBlocked] = "Error: Cannot do that while building blocked.", [Lang.GenericErrorCooldown] = "Please wait {0}s and try again.", [Lang.GenericErrorVehicleLocked] = "That vehicle is locked.", [Lang.DeployErrorNoVehicleFound] = "Error: No vehicle found.", [Lang.DeployErrorVehicleDead] = "Error: That vehicle is dead.", [Lang.DeployErrorOther] = "Error: You cannot do that.", [Lang.DeployErrorDifferentOwner] = "Error: Someone else owns that vehicle.", [Lang.DeployErrorNoOwner] = "Error: You do not own that vehicle.", [Lang.DeployErrorNoOwnerRequiresTC] = "Error: Locking unowned vehicles requires building privilege.", [Lang.DeployErrorHasLock] = "Error: That vehicle already has a lock.", [Lang.DeployErrorInsufficientResources] = "Error: Not enough resources to craft a {0}.", [Lang.DeployErrorMounted] = "Error: That vehicle is currently occupied.", [Lang.DeployErrorModularCarNoCockpit] = "Error: That car needs a cockpit module to receive a lock.", [Lang.DeployErrorDistance] = "Error: Too far away." }, this, "en"); lang.RegisterMessages(new Dictionary { [Lang.GenericErrorNoPermission] = "Você não tem permissão para fazer isso.", [Lang.GenericErrorBuildingBlocked] = "Erro: Não é possível fazer isso enquanto o edifício está bloqueado.", [Lang.GenericErrorCooldown] = "Aguarde {0} seg e tente novamente.", [Lang.GenericErrorVehicleLocked] = "Esse veículo está trancado.", [Lang.DeployErrorNoVehicleFound] = "Erro: Nenhum veículo encontrado.", [Lang.DeployErrorVehicleDead] = "Erro: esse veículo está destruido.", [Lang.DeployErrorOther] = "Erro: Você não pode fazer isso.", [Lang.DeployErrorDifferentOwner] = "Erro: outra pessoa é proprietária desse veículo.", [Lang.DeployErrorNoOwner] = "Erro: você não possui esse veículo.", [Lang.DeployErrorNoOwnerRequiresTC] = "Erro: o bloqueio de veículos sem proprietário requer privilégio de construção.", [Lang.DeployErrorHasLock] = "Erro: esse veículo já tem fechadura.", [Lang.DeployErrorInsufficientResources] = "Erro: recursos insuficientes para criar um {0}.", [Lang.DeployErrorMounted] = "Erro: esse veículo está ocupado no momento.", [Lang.DeployErrorModularCarNoCockpit] = "Erro: esse carro precisa de um módulo de cabine para receber um bloqueio.", [Lang.DeployErrorDistance] = "Erro: muito longe." }, this, "pt-BR"); } #endregion } }