using Facepunch; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; /* Suggestions * - Add permission based drop chance * */ /* 1.0.2 * Fixed an issue with tome stacks being consumed on use. * Added config option for item split handling. * Fixed an error when running the GiveRandomXPItem command from server console. * ** Fixed a compile error. * * Fixed Stack Modifier support */ namespace Oxide.Plugins { [Info("SkillTreeItems", "imthenewguy", "1.0.3")] [Description("Lootable items that give xp and skill points")] class SkillTreeItems : RustPlugin { #region Config private Configuration config; public class Configuration { [JsonProperty("Prevent bonus xp given when the player consumes the item (such as bonus xp scaling, night time bonuses etc)")] public bool prevent_bonus_xp = true; [JsonProperty("XP items")] public STItemInfo xp_items; [JsonProperty("Skill point items")] public STItemInfo sp_items; [JsonProperty("Manage item stacking for STI items? (set to false if conflicting with a stacks plugin)")] public bool manage_stacks = false; [JsonProperty("Manage item stack splitting for STI items? (set to false if conflicting with a stacks plugin)")] public bool manage_splits = false; public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); } protected override void LoadDefaultConfig() { config = new Configuration(); config.xp_items = DefaultXPItems; config.sp_items = DefaultSPItems; } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) { throw new JsonException(); } if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { PrintToConsole("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch { PrintToConsole($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } SaveConfig(); } protected override void SaveConfig() { PrintToConsole($"Configuration changes saved to {Name}.json"); Config.WriteObject(config, true); } #endregion #region Default Config Dictionary DefaultLootContainersXP { get { return new Dictionary() { ["crate_normal_2"] = 1f, ["crate_normal"] = 3f, ["crate_elite"] = 15f, ["crate_underwater_basic"] = 6f, ["crate_underwater_advanced"] = 12f, ["heli_crate"] = 10f, ["bradley_crate"] = 10f, ["codelockedhackablecrate"] = 10f, ["codelockedhackablecrate_oilrig"] = 10f, ["crate_tools"] = 1.5f }; } } Dictionary DefaultLootContainersSP { get { return new Dictionary() { ["crate_normal_2"] = 0f, ["crate_normal"] = 0f, ["crate_elite"] = 0f, ["crate_underwater_basic"] = 0f, ["crate_underwater_advanced"] = 1f, ["heli_crate"] = 1f, ["bradley_crate"] = 1f, ["codelockedhackablecrate"] = 1f, ["codelockedhackablecrate_oilrig"] = 1f, ["crate_tools"] = 0f }; } } STItemInfo DefaultXPItems { get { return new STItemInfo("xmas.present.small", "unwrap", DefaultSTXPItems, DefaultLootContainersXP); } } STItemInfo DefaultSPItems { get { return new STItemInfo("xmas.present.small", "unwrap", DefaultSTSPItems, DefaultLootContainersSP); } } List DefaultSTXPItems { get { return new List() { new STItem(700, 1100, 2863540162, "research notes", 100), new STItem(1000, 1500, 2863539973, "research notes", 50), new STItem(1400, 2200, 2863540007, "research notes", 20), new STItem(2000, 5000, 2863540048, "research notes", 5) }; } } List DefaultSTSPItems { get { return new List() { new STItem(1, 1, 2863540521, "tome of skill points", 200), new STItem(1, 2, 2863539781, "tome of skill points", 40), new STItem(2, 3, 2863539851, "tome of skill points", 15), new STItem(2, 4, 2863539914, "tome of skill points", 3) }; } } #endregion #region Data [PluginReference] private Plugin SkillTree, StackModifier; const string perm_use = "skilltreeitems.use"; const string perm_admin = "skilltreeitems.admin"; void Init() { permission.RegisterPermission(perm_use, this); permission.RegisterPermission(perm_admin, this); } void UnSub() { Unsubscribe(nameof(CanStackItem)); Unsubscribe(nameof(CanCombineDroppedItem)); Unsubscribe(nameof(OnItemSplit)); } void Sub() { Subscribe(nameof(CanStackItem)); Subscribe(nameof(CanCombineDroppedItem)); Subscribe(nameof(OnItemSplit)); } void OnPluginLoaded(Plugin name) { if (StackModifier != null && name.Name == StackModifier.Name) UnSub(); } void OnPluginUnloaded(Plugin name) { if (StackModifier != null && name.Name == StackModifier.Name) Sub(); } void Unload() { Monitored_actions.Clear(); Monitored_actions = null; WaitTime.Clear(); WaitTime = null; } #endregion; #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["givexpitemUsage"] = "Usage: givexpitem ", ["givespitemUsage"] = "Usage: givespitem ", ["givespitemtoUsage"] = "Usage: givespitemto ", ["givexpitemtoUsage"] = "Usage: givexpitemto ", ["giverandomxpitemUsage"] = "Usage giverandomxpitem ", ["giverandomxpitemNoMatch"] = "No ID matched {0}", ["giverandomxpitemConfirmation"] = "Gave 1x {0} to {1}", ["giverandomxpitemToTarget"] = "You received 1x {0}", ["giverandomspitemUsage"] = "Usage giverandomspitem ", ["giverandomspitemNoMatch"] = "No ID matched {0}", ["giverandomspitemConfirmation"] = "Gave 1x {0} to {1}", ["giverandomspitemToTarget"] = "You received 1x {0}" }, this); } #endregion #region classes public class STItemInfo { [JsonProperty("Base item shortname")] public string shortname; [JsonProperty("Action that is called when using the item")] public string action_string = "unwrap"; [JsonProperty("List of items that will be added to containers")] public List items = new List(); [JsonProperty("List of containers that these items can drop from [%] [0=disabled]")] public Dictionary drop_chance; public STItemInfo(string shortname, string action_string, List items, Dictionary drop_chance) { this.shortname = shortname; this.action_string = action_string; this.items = items; this.drop_chance = drop_chance; } } public class STItem { [JsonProperty("Skin ID")] public ulong skin; [JsonProperty("Minimum XP/Points given for using")] public int min; [JsonProperty("Maximum XP/Points given for using")] public int max; [JsonProperty("Display name of the item")] public string displayName; [JsonProperty("Drop weight [chance]")] public int dropWeight; public STItem(int min, int max, ulong skin = 0, string displayName = null, int dropWeight = 100) { this.skin = skin; this.min = min; this.max = max; this.displayName = displayName; this.dropWeight = dropWeight; } } #endregion #region enums enum ItemType { XP, SP } #endregion #region commands [ChatCommand("givexpitem")] void GiveXPItem(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, perm_admin)) return; if (config.xp_items.items.Count == 0) return; if (args == null || args.Length < 4) { PrintToChat(player, lang.GetMessage("givexpitemUsage", this, player.UserIDString)); return; } if (args == null || args.Length < 3) { PrintToChat(player, lang.GetMessage("givexpitemUsage", this, player.UserIDString)); return; } var amount = Convert.ToInt32(args[0]); if (amount < 1) amount = 1; ulong skin = Convert.ToUInt64(args[1]); string displayName = string.Join(" ", args.Skip(2)); GiveItem(player, amount, skin, displayName, ItemType.XP); } [ChatCommand("givespitem")] void GiveSPItem(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, perm_admin)) return; if (config.sp_items.items.Count == 0) return; if (args == null || args.Length < 4) { PrintToChat(player, lang.GetMessage("givespitemUsage", this, player.UserIDString)); return; } if (args == null || args.Length < 3) { PrintToChat(player, lang.GetMessage("givespitemUsage", this, player.UserIDString)); return; } var amount = Convert.ToInt32(args[0]); if (amount < 1) amount = 1; ulong skin = Convert.ToUInt64(args[1]); string displayName = string.Join(" ", args.Skip(2)); GiveItem(player, amount, skin, displayName, ItemType.SP); } [ConsoleCommand("givespitemto")] void GiveSPItemTo(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, perm_admin)) return; // Target amount skin if (arg.Args == null || arg.Args.Length < 4) { arg.ReplyWith(lang.GetMessage("givespitemtoUsage", this, player.UserIDString ?? null)); return; } var target = FindPlayerByID(arg.Args[0]); if (target == null) return; var amount = Convert.ToInt32(arg.Args[1]); if (amount < 1) amount = 1; ulong skin = Convert.ToUInt64(arg.Args[2]); string displayName = string.Join(" ", arg.Args.Skip(3)); GiveItem(target, amount, skin, displayName, ItemType.SP); } [ConsoleCommand("givexpitemto")] void GiveXPItemTo(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, perm_admin)) return; // Target amount skin if (arg.Args == null || arg.Args.Length < 4) { arg.ReplyWith(lang.GetMessage("givexpitemtoUsage", this, player.UserIDString ?? null)); return; } var target = FindPlayerByID(arg.Args[0]); if (target == null) return; var amount = Convert.ToInt32(arg.Args[1]); if (amount < 1) amount = 1; ulong skin = Convert.ToUInt64(arg.Args[2]); string displayName = string.Join(" ", arg.Args.Skip(3)); GiveItem(target, amount, skin, displayName, ItemType.XP); } [ConsoleCommand("giverandomxpitem")] void GiveRandomXPItem(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, perm_admin)) return; if (arg.Args == null || arg.Args.Length == 0) { arg.ReplyWith(lang.GetMessage("giverandomxpitemUsage", this, player?.UserIDString ?? null)); return; } var target = FindPlayerByID(arg.Args[0], player ?? null); if (target == null) { arg.ReplyWith(string.Format(lang.GetMessage("giverandomxpitemNoMatch", this, player?.UserIDString ?? null), arg.Args[0])); return; } List items = Pool.GetList(); foreach (var item in config.xp_items.items) { if (item.max <= 0) continue; if (!items.Contains(item)) items.Add(item); } if (items.Count > 0) { var randProfile = items.GetRandom(); var randItem = CreateXPItem(1, randProfile.min, randProfile.max, randProfile.skin, randProfile.displayName); if (randItem != null) target.GiveItem(randItem); PrintToChat(target, string.Format(lang.GetMessage("giverandomxpitemToTarget", this, target.UserIDString), randItem.name)); arg.ReplyWith(string.Format(lang.GetMessage("giverandomxpitemConfirmation", this, player?.UserIDString ?? null), randItem.name, target.displayName)); } Pool.FreeList(ref items); } [ConsoleCommand("giverandomspitem")] void GiveRandomSPItem(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, perm_admin)) return; if (arg.Args == null || arg.Args.Length == 0) { arg.ReplyWith(lang.GetMessage("giverandomspitemUsage", this, player?.UserIDString ?? null)); return; } var target = FindPlayerByID(arg.Args[0], player ?? null); if (target == null) { arg.ReplyWith(string.Format(lang.GetMessage("giverandomspitemNoMatch", this, player?.UserIDString ?? null), arg.Args[0])); return; } List items = Pool.GetList(); foreach (var item in config.sp_items.items) { if (item.max <= 0) continue; if (!items.Contains(item)) items.Add(item); } if (items.Count > 0) { var randProfile = items.GetRandom(); var randItem = CreateSPItem(1, randProfile.min, randProfile.max, randProfile.skin, randProfile.displayName); if (randItem != null) target.GiveItem(randItem); PrintToChat(target, string.Format(lang.GetMessage("giverandomspitemToTarget", this, target.UserIDString ?? null), randItem.name)); arg.ReplyWith(string.Format(lang.GetMessage("giverandomspitemConfirmation", this, player?.UserIDString ?? null), randItem.name, target.displayName)); } Pool.FreeList(ref items); } #endregion #region oxide hooks void OnServerInitialized() { if (SkillTree == null || !SkillTree.IsLoaded) { Puts("SkillTree is required to run this plugin."); Interface.Oxide.UnloadPlugin(Name); return; } if (StackModifier != null && StackModifier.IsLoaded) UnSub(); else { if (!config.manage_stacks) { Unsubscribe(nameof(CanStackItem)); Unsubscribe(nameof(CanCombineDroppedItem)); } if (!config.manage_splits) { Unsubscribe(nameof(OnItemSplit)); } } bool require_save = false; if (config.xp_items == null || config.xp_items.items?.Count == 0) { config.xp_items = DefaultXPItems; require_save = true; } if (config.sp_items == null || config.sp_items.items?.Count == 0) { config.sp_items = DefaultSPItems; require_save = true; } if (config.xp_items.drop_chance == null || config.xp_items.drop_chance.Count == 0) { config.xp_items.drop_chance = DefaultLootContainersXP; require_save = true; } if (config.sp_items.drop_chance == null || config.sp_items.drop_chance.Count == 0) { config.sp_items.drop_chance = DefaultLootContainersSP; require_save = true; } if (require_save) SaveConfig(); if (!Monitored_actions.Contains(config.xp_items.action_string)) Monitored_actions.Add(config.xp_items.action_string); if (!Monitored_actions.Contains(config.sp_items.action_string)) Monitored_actions.Add(config.sp_items.action_string); } object CanStackItem(Item item, Item targetItem) { if (item == null || targetItem == null) return null; if (IsXPItem(targetItem) || IsXPItem(item)) return false; return null; } object CanCombineDroppedItem(DroppedItem item, DroppedItem targetItem) { return CanStackItem(item.item, targetItem.item); } private object OnItemSplit(Item item, int amount) { if (!IsXPItem(item)) return null; Item x = ItemManager.CreateByItemID(item.info.itemid); item.amount -= amount; x.name = item.name; x.skin = item.skin; x.amount = amount; x.MarkDirty(); item.MarkDirty(); return x; } List Monitored_actions = new List(); void OnServerSave() { WaitTime.Clear(); } Dictionary WaitTime = new Dictionary(); object OnItemAction(Item item, string action, BasePlayer player) { if (item == null || player == null) return null; if (Monitored_actions.Contains(action)) { if (IsXPItem(item)) { if (!permission.UserHasPermission(player.UserIDString, perm_use)) { PrintToChat(player, "You do not have permission to use this item."); return true; } float wait; if (WaitTime.TryGetValue(player, out wait) && wait > Time.time) { PrintToChat(player, "Please wait a second before trying again."); return true; } WaitTime[player] = Time.time + 1; var xp = (double)GetXPAmount(item.name); SkillTree.Call("AwardXP", player, xp, (BaseEntity)null, config.prevent_bonus_xp); PrintToChat(player, $"You gained {xp} xp from your {item.name.Split('[', ']')[0]}."); if (item.amount > 1) { item.SplitItem(1)?.Remove(); } else { NextTick(() => item.Remove()); } return true; } else if (IsSPItem(item)) { if (!permission.UserHasPermission(player.UserIDString, perm_use)) { PrintToChat(player, "You do not have permission to use this item."); return true; } var sp = GetSPAmount(item.name); SkillTree.Call("GiveSkillPoints", player, sp); PrintToChat(player, $"You received {sp} skill points from your {item.name.Split('(', ')')[0]}."); if (item.amount > 1) { item.SplitItem(1)?.Remove(); } else { NextTick(() => item.Remove()); } return true; } } if (action == "upgrade_item" && (IsXPItem(item) || IsSPItem(item))) return true; return null; } void OnEntityKill(LootContainer entity) { if (entity == null) return; looted_containers.Remove(entity); } #endregion #region methods BasePlayer FindPlayerByID(string id, BasePlayer searchingPlayer = null) { if (!id.IsSteamId()) return null; var player = BasePlayer.activePlayerList.Where(x => x.UserIDString == id).FirstOrDefault(); if (player == null) { if (searchingPlayer != null) PrintToChat(searchingPlayer, $"No player found matching ID: {id}"); else Puts($"No player found matching ID: {id}"); } return player ?? null; } bool IsSPItem(Item item) { return item.info.shortname == config.sp_items.shortname && item.name != null && GetSPAmount(item.name) > 0; } int GetSPAmount(string name) { if (string.IsNullOrEmpty(name)) return 0; var splitString = name.Split('(', ')'); if (splitString == null || splitString.Length < 2) return 0; var result = Convert.ToInt32(splitString[1]); return result; } bool IsXPItem(Item item) { return item.info.shortname == config.xp_items.shortname && item.name != null && GetXPAmount(item.name) > 0; } int GetXPAmount(string name) { if (string.IsNullOrEmpty(name)) return 0; var splitString = name.Split('[', ']'); if (splitString == null || splitString.Length < 2) return 0; var result = Convert.ToInt32(splitString[1]); return result; } Item CreateXPItem(int amount, int xp_min, int xp_max, ulong skin = 0, string displayName = null) { var item = ItemManager.CreateByName(config.xp_items.shortname, amount, skin); if (item == null) return null; int xp = UnityEngine.Random.Range(xp_min, xp_max + 1); item.name = !string.IsNullOrEmpty(displayName) ? $"{displayName} [{xp}]" : $"{item.info.displayName.english} [{xp}]"; return item; } Item CreateSPItem(int amount, int xp_min, int xp_max, ulong skin = 0, string displayName = null) { var item = ItemManager.CreateByName(config.sp_items.shortname, amount, skin); if (item == null) return null; int xp = UnityEngine.Random.Range(xp_min, xp_max + 1); item.name = !string.IsNullOrEmpty(displayName) ? $"{displayName} ({xp})" : $"{item.info.displayName.english} ({xp})"; return item; } // player == target. void GiveItem(BasePlayer player, STItem def, int amount, ItemType type) { if (def == null) return; var item = type == ItemType.XP ? CreateXPItem(1, def.min, def.max, def.skin, def.displayName ?? null) : CreateSPItem(Math.Max(amount, 1), def.min, def.max, def.skin, def.displayName ?? null); if (item != null) player.GiveItem(item); } void GiveItem(BasePlayer player, int quantity, ulong skin, string displayName, ItemType type) { var item = type == ItemType.XP ? CreateXPItem(1, quantity, quantity, skin, displayName ?? null) : CreateSPItem(1, quantity, quantity, skin, displayName ?? null); if (item != null) player.GiveItem(item); } bool RollSuccessful(float luck) { var roll = UnityEngine.Random.Range(0f, 100f); return (roll > 100f - luck); } #endregion #region loot List looted_containers = new List(); object CanLootEntity(BasePlayer player, LootContainer lootContainer) { if (lootContainer == null) return null; if (looted_containers.Contains(lootContainer)) return null; looted_containers.Add(lootContainer); if (permission.UserHasPermission(player.UserIDString, perm_use)) { float chance; if (config.xp_items.drop_chance.TryGetValue(lootContainer.ShortPrefabName, out chance) && RollSuccessful(chance)) RollItem(ItemType.XP, lootContainer); if (config.sp_items.drop_chance.TryGetValue(lootContainer.ShortPrefabName, out chance) && RollSuccessful(chance)) RollItem(ItemType.SP, lootContainer); } return null; } void RollItem(ItemType type, LootContainer container) { if (type == ItemType.XP) { int totalWeight = config.xp_items.items.Sum(x => x.dropWeight); int count = 0; int roll = UnityEngine.Random.Range(0, totalWeight + 1); foreach (var data in config.xp_items.items) { count += data.dropWeight; if (roll <= count) { var item = CreateXPItem(1, data.min, data.max, data.skin, data.displayName); if (item != null) { container.inventory.capacity++; container.inventorySlots++; item.MoveToContainer(container.inventory); } return; } } } else { int totalWeight = config.sp_items.items.Sum(x => x.dropWeight); int count = 0; int roll = UnityEngine.Random.Range(0, totalWeight + 1); foreach (var data in config.sp_items.items) { count += data.dropWeight; if (roll <= count) { var item = CreateSPItem(1, data.min, data.max, data.skin, data.displayName); if (item != null) { container.inventory.capacity++; container.inventorySlots++; item.MoveToContainer(container.inventory); } return; } } } } #endregion } }