using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using UnityEngine; using System.Reflection; /* Changelog 1.1.5: - Fixed: Updated for 1st May Rust update 1.1.4: - Fixed: Players being able to claim horses without buying in some cases - Added: Config option to set minimum time before warning player about unlock time (reduce chat notification spam) 1.1.3: - Fixed: Players being able to claim horses at Ranch without using a saddle 1.1.2: - Fixed: Unauthorised players are now unable to loot horse inventory 1.1.1: - Fixed: Use Friends, Clans and Teams options now work as intended when set to false */ namespace Oxide.Plugins { [Info("Horse Lock", "ZEODE", "1.1.5")] [Description("Lock ridable horses to players to prevent unauthorised use.")] public class HorseLock: RustPlugin { #region Plugin References [PluginReference] Plugin Friends, Clans, VehicleLicence; #endregion Plugin References #region Consts private const string horsePrefab = "assets/content/vehicles/horse/ridablehorse.prefab"; private const string saddlePrefab = "assets/prefabs/vehicle/seats/horsesaddle.prefab"; #endregion Consts #region Oxide Hooks private void Init() { try { storedData = Interface.Oxide.DataFileSystem.ReadObject(Name); if (storedData == null) { Puts("Data is null. Creating blank data file..."); storedData = new StoredData(); SaveData(); } } catch (Exception ex) { if (ex is JsonSerializationException || ex is NullReferenceException || ex is JsonReaderException || ex is KeyNotFoundException) { Puts("Data file invalid. Creating blank data file..."); storedData = new StoredData(); SaveData(); return; } throw; } } private void Unload() { SaveData(); } private void OnServerSave() { SaveData(); } private void OnNewSave() { PrintWarning("Server wipe detected, clearing all player horse data."); storedData = new StoredData(); SaveData(); } private object CanMountEntity(BasePlayer player, BaseMountable mount) { if (player == null || mount == null) return null; if (mount.name.Equals(saddlePrefab) && player.userID.IsSteamId()) { RidableHorse horse = mount?.GetParentEntity() as RidableHorse; if (horse == null) return null; return CanMountOrLead(horse, player, false); } return null; } private object OnHorseLead(RidableHorse horse, BasePlayer player) { if (player == null || horse == null) return null; if (horse.name.Equals(horsePrefab) && player.userID.IsSteamId()) return CanMountOrLead(horse, player, true); return null; } private object CanLootEntity(BasePlayer player, RidableHorse horse) { if (player == null || horse == null) return null; if (horse.name.Equals(horsePrefab) && player.userID.IsSteamId()) { if (horse.IsForSale) return null; return CanMountOrLead(horse, player, false); } return null; } private object OnEntityDismounted(BaseMountable entity, BasePlayer player) { if (player == null || entity == null) return null; if (entity.name.Equals(saddlePrefab) && player.userID.IsSteamId()) { RidableHorse horse = entity?.GetParentEntity() as RidableHorse; if (horse == null || player == null) return null; if (VehicleLicence && (bool)VehicleLicence?.CallHook("IsLicensedVehicle", horse)) return null; ulong ownerId = horse.OwnerID; ulong horseNetId = horse.net.ID.Value; float dismountTime; if (!storedData.playerData[ownerId].Horses.TryGetValue(horseNetId, out dismountTime)) return null; BasePlayer owner = FindPlayerByPartialNameOrId(ownerId.ToString()); if (owner == null) return null; if (Time.realtimeSinceStartup - storedData.playerData[ownerId].UnlockWarningTime >= config.options.warningTimeout) { storedData.playerData[ownerId].UnlockWarningTime = Time.realtimeSinceStartup; var horsePriv = horse?.GetBuildingPrivilege(); if (!horsePriv || !horsePriv.IsAuthed(ownerId)) { TimeSpan time = TimeSpan.FromSeconds(config.options.lockTime); Message(player, "DismountTime", time.ToString("hh\\:mm\\:ss")); } } AddHorseData(owner.userID, owner.displayName, horseNetId); } return null; } #endregion Oxide Hooks #region Helpers private object CanMountOrLead(RidableHorse horse, BasePlayer player, bool lead = false) { if (player == null || horse == null) return null; ulong playerId = player.userID; ulong ownerId = horse.OwnerID; string ownerName = GetOwnerDisplayName(horse); ulong horseNetId = horse.net.ID.Value; if (horse.name.Equals(horsePrefab) && player.userID.IsSteamId()) { if (ownerId == 0) { horse.OwnerID = playerId; AddHorseData(playerId, player.displayName, horseNetId); Message(player, "Claimed"); return null; } else if (VehicleLicence && (bool)VehicleLicence.CallHook("IsLicensedVehicle", horse)) { return null; } if(!storedData.playerData.ContainsKey(ownerId)) AddHorseData(ownerId, ownerName, horseNetId); float dismountTime; if (!storedData.playerData[ownerId].Horses.TryGetValue(horseNetId, out dismountTime)) { if (!IsFriend(ownerId, playerId)) { //if (config.options.doRear) // HorseRear(horse, player); // debug: coming back soon! Message(player, lead ? "NoLead" : "NoMountOrLoot", ownerName); return true; } AddHorseData(ownerId, ownerName, horseNetId); } else if (storedData.playerData[ownerId].Horses.TryGetValue(horseNetId, out dismountTime)) { var horsePriv = horse?.GetBuildingPrivilege(); if (horsePriv && horsePriv.IsAuthed(ownerId)) { if (!IsFriend(ownerId, playerId)) { //if (config.options.doRear) // HorseRear(horse, player); // debug: coming back soon! Message(player, lead ? "NoLead" : "NoMountOrLoot", ownerName); return true; } } else { float timeSinceDismount = Time.realtimeSinceStartup - storedData.playerData[ownerId].Horses[horseNetId]; if (timeSinceDismount > config.options.lockTime) { RemoveHorseData(ownerId, horseNetId); horse.OwnerID = playerId; AddHorseData(playerId, player.displayName, horseNetId); Message(player, "TimedOut"); return null; } else if (!IsFriend(ownerId, playerId)) { //if (config.options.doRear) // HorseRear(horse, player); // debug: coming back soon! Message(player, lead ? "NoLead" : "NoMountOrLoot", ownerName); return true; } } } } return null; } private bool IsFriend(ulong playerId, ulong targetId) { if (playerId == 0 || targetId == 0) return false; if (playerId == targetId) return true; if (config.options.useClans && Clans) { var result = Clans?.Call("IsMemberOrAlly", playerId, targetId); if (result != null && Convert.ToBoolean(result)) return true; } if (config.options.useFriends && Friends) { var result = Friends?.Call("AreFriends", playerId, targetId); if (result != null && Convert.ToBoolean(result)) return true; } if (config.options.useTeams) { RelationshipManager.PlayerTeam team; RelationshipManager.ServerInstance.playerToTeam.TryGetValue(playerId, out team); if (team == null) return false; if (team.members.Contains(targetId)) return true; } return false; } BasePlayer FindPlayerByPartialNameOrId(string nameOrId) { if (string.IsNullOrEmpty(nameOrId)) return null; IPlayer player = covalence.Players.FindPlayer(nameOrId); if (player != null) return (BasePlayer)player.Object; return null; } private string GetOwnerDisplayName(BaseEntity entity) { var playerID = entity.OwnerID; if (playerID.IsSteamId()) { var player = FindPlayerByPartialNameOrId(playerID.ToString()); if (player) return player.displayName; var p = covalence.Players.FindPlayerById(playerID.ToString()); if (p != null) return p.Name; } return $"Unknown: {playerID}"; } #endregion Helpers #region Language protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["NoLead"] = "You cannot lead this horse, it belongs to {0}.", ["NoMountOrLoot"] = "You cannot mount or loot this horse, it belongs to {0}.", ["Claimed"] = "This horse was unclaimed, you now own it!", ["DismountTime"] = "Horse will be unlocked in {0} (hh:mm:ss) unless you mount again in that time or store within your building privilege.", ["TimedOut"] = "This horse was abandoned by it's previous owner, you now own it!" }, this); } private string Lang(string messageKey, string playerId, params object[] args) { return string.Format(lang.GetMessage(messageKey, this, playerId), args); } private void Message(BasePlayer player, string messageKey, params object[] args) { if (player == null || !player.userID.IsSteamId()) return; var message = Lang(messageKey, player.UserIDString, args); Player.Message(player, message, config.options.usePrefix ? config.options.chatPrefix : null, config.options.chatIcon); } #endregion Language #region Stored Data private static StoredData storedData; private class StoredData { public Dictionary playerData = new Dictionary(); } private class PlayerData { public string PlayerName; public ulong UserID; public float UnlockWarningTime; public Dictionary Horses = new Dictionary(); } private void AddHorseData(ulong playerId, string playerName, ulong horseId) { if(!storedData.playerData.ContainsKey(playerId)) { storedData.playerData.Add(playerId, new PlayerData()); storedData.playerData[playerId].PlayerName = playerName; storedData.playerData[playerId].UserID = playerId; storedData.playerData[playerId].Horses.Add(horseId, Time.realtimeSinceStartup); } else { if (storedData.playerData[playerId].Horses.ContainsKey(horseId)) storedData.playerData[playerId].Horses[horseId] = Time.realtimeSinceStartup; else storedData.playerData[playerId].Horses.Add(horseId, Time.realtimeSinceStartup); } } private void RemoveHorseData(ulong playerId, ulong horseId) { if (storedData.playerData.ContainsKey(playerId)) { if (storedData.playerData[playerId].Horses.ContainsKey(horseId)) storedData.playerData[playerId].Horses.Remove(horseId); } } private void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(Name, storedData); } #endregion Stored Data #region Config private static ConfigData config; private class ConfigData { [JsonProperty(PropertyName = "Options")] public Options options; public class Options { [JsonProperty(PropertyName = "Use Friends Plugin (Friends Can Mount)")] public bool useFriends { get; set; } [JsonProperty(PropertyName = "Use Clans Plugin (Clan Can Mount)")] public bool useClans { get; set; } [JsonProperty(PropertyName = "Use Teams (Team Can Mount)")] public bool useTeams { get; set; } [JsonProperty(PropertyName = "Custom Chat Icon (Default = 0)")] public ulong chatIcon { get; set; } [JsonProperty(PropertyName = "Time Horse Locked After Dismount (Seconds)")] public float lockTime { get; set; } [JsonProperty(PropertyName = "Minimum Time Between Dismount Unlock Warnings (Seconds)")] public float warningTimeout { get; set; } [JsonProperty(PropertyName = "Make Horse Rear When Unauthorised Player Attempts to Mount")] public bool doRear { get; set; } [JsonProperty(PropertyName = "Use Chat Prefix")] public bool usePrefix { get; set; } [JsonProperty(PropertyName = "Chat Prefix")] public string chatPrefix { get; set; } } [JsonProperty(PropertyName = "Plugin Version")] public VersionNumber pluginVersion { get; set; } } private ConfigData GetDefaultConfig() { return new ConfigData { options = new ConfigData.Options { useFriends = false, useClans = false, useTeams = false, chatIcon = 0, lockTime = 900f, warningTimeout = 120f, doRear = true, usePrefix = true, chatPrefix = "[Horse Lock]: " }, pluginVersion = Version }; } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) LoadDefaultConfig(); else if (config.pluginVersion < Version) UpdateConfigValues(); } catch (Exception ex) { if (ex is JsonSerializationException || ex is NullReferenceException || ex is JsonReaderException) { Puts($"ERROR: {ex}"); return; } throw; } } protected override void LoadDefaultConfig() { Puts("Configuration file missing or corrupt, creating default config file."); config = GetDefaultConfig(); } protected override void SaveConfig() { Config.WriteObject(config); } private void UpdateConfigValues() { ConfigData defaultConfig = GetDefaultConfig(); Puts("Config update detected! Updating config file..."); if (config.pluginVersion < new VersionNumber(1, 0, 1)) { config = defaultConfig; } if (config.pluginVersion < new VersionNumber(1, 1, 4)) { config.options.warningTimeout = 120f; } Puts("Config update completed!"); config.pluginVersion = Version; SaveConfig(); } #endregion Config } }