From 5285add77e2d3c88b6de2a493f8ddfe5583c4316 Mon Sep 17 00:00:00 2001 From: Jack O'Sullivan Date: Mon, 16 Sep 2024 01:42:19 +0100 Subject: [PATCH] Working mod --- AntiYoink/AntiYoink.cs | 178 ++++++++++-------- .../plugins/AntiYoink/AntiYoink.language | 2 +- 2 files changed, 96 insertions(+), 84 deletions(-) diff --git a/AntiYoink/AntiYoink.cs b/AntiYoink/AntiYoink.cs index 1c41f2c..8cced04 100644 --- a/AntiYoink/AntiYoink.cs +++ b/AntiYoink/AntiYoink.cs @@ -1,16 +1,13 @@ using BepInEx; +using BepInEx.Configuration; using R2API; +using R2API.Utils; using RoR2; using UnityEngine; -using UnityEngine.AddressableAssets; +using UnityEngine.Networking; namespace AntiYoink { - // This is an example plugin that can be put in - // BepInEx/plugins/ExamplePlugin/ExamplePlugin.dll to test out. - // It's a small plugin that adds a relatively simple item to the game, - // and gives you that item whenever you press F2. - // This attribute specifies that we have a dependency on a given BepInEx Plugin, // We need the R2API ItemAPI dependency because we are using for adding our item to the game. // You don't need this if you're not using R2API in your plugin, @@ -24,12 +21,15 @@ namespace AntiYoink // This attribute is required, and lists metadata for your plugin. [BepInPlugin(PluginGUID, PluginName, PluginVersion)] + // We only do server-side stuff :) + [NetworkCompatibility(compatibility: CompatibilityLevel.NoNeedForSync)] + // This is the main declaration of our plugin class. // BepInEx searches for all classes inheriting from BaseUnityPlugin to initialize on startup. // BaseUnityPlugin itself inherits from MonoBehaviour, // so you can use this as a reference for what you can declare and use in your plugin class // More information in the Unity Docs: https://docs.unity3d.com/ScriptReference/MonoBehaviour.html - public class ExamplePlugin : BaseUnityPlugin + public class AntiYoinkPlugin : BaseUnityPlugin { // The Plugin GUID should be a unique ID for this plugin, // which is human readable (as it is used in places like the config). @@ -38,8 +38,11 @@ namespace AntiYoink public const string PluginName = "AntiYoink"; public const string PluginVersion = "0.1.0"; - // We need our item definition to persist through our functions, and therefore make it a class field. - private static ItemDef myItemDef; + private const bool DevMode = true; + + public static ConfigEntry MaxItemCountDelta { get; set; } + + private float naughtyLast = 0; // The Awake() method is run at the very start when the game is initialized. public void Awake() @@ -47,94 +50,103 @@ namespace AntiYoink // Init our logging class so that we can properly log for debugging Log.Init(Logger); - // First let's define our item - myItemDef = ScriptableObject.CreateInstance(); + MaxItemCountDelta = Config.Bind( + "General", + "MaxItemCountDelta", + 3, + "Maximum delta between player with the lowest number of items"); - // Language Tokens, explained there https://risk-of-thunder.github.io/R2Wiki/Mod-Creation/Assets/Localization/ - myItemDef.name = "EXAMPLE_CLOAKONKILL_NAME"; - myItemDef.nameToken = "EXAMPLE_CLOAKONKILL_NAME"; - myItemDef.pickupToken = "EXAMPLE_CLOAKONKILL_PICKUP"; - myItemDef.descriptionToken = "EXAMPLE_CLOAKONKILL_DESC"; - myItemDef.loreToken = "EXAMPLE_CLOAKONKILL_LORE"; - - // The tier determines what rarity the item is: - // Tier1=white, Tier2=green, Tier3=red, Lunar=Lunar, Boss=yellow, - // and finally NoTier is generally used for helper items, like the tonic affliction -#pragma warning disable Publicizer001 // Accessing a member that was not originally public. Here we ignore this warning because with how this example is setup we are forced to do this - myItemDef._itemTierDef = Addressables.LoadAssetAsync("RoR2/Base/Common/Tier2Def.asset").WaitForCompletion(); -#pragma warning restore Publicizer001 - // Instead of loading the itemtierdef directly, you can also do this like below as a workaround - // myItemDef.deprecatedTier = ItemTier.Tier2; - - // You can create your own icons and prefabs through assetbundles, but to keep this boilerplate brief, we'll be using question marks. - myItemDef.pickupIconSprite = Addressables.LoadAssetAsync("RoR2/Base/Common/MiscIcons/texMysteryIcon.png").WaitForCompletion(); - myItemDef.pickupModelPrefab = Addressables.LoadAssetAsync("RoR2/Base/Mystery/PickupMystery.prefab").WaitForCompletion(); - - // Can remove determines - // if a shrine of order, - // or a printer can take this item, - // generally true, except for NoTier items. - myItemDef.canRemove = true; - - // Hidden means that there will be no pickup notification, - // and it won't appear in the inventory at the top of the screen. - // This is useful for certain noTier helper items, such as the DrizzlePlayerHelper. - myItemDef.hidden = false; - - // You can add your own display rules here, - // where the first argument passed are the default display rules: - // the ones used when no specific display rules for a character are found. - // For this example, we are omitting them, - // as they are quite a pain to set up without tools like https://thunderstore.io/package/KingEnderBrine/ItemDisplayPlacementHelper/ - var displayRules = new ItemDisplayRuleDict(null); - - // Then finally add it to R2API - ItemAPI.Add(new CustomItem(myItemDef, displayRules)); - - // But now we have defined an item, but it doesn't do anything yet. So we'll need to define that ourselves. - GlobalEventManager.onCharacterDeathGlobal += GlobalEventManager_onCharacterDeathGlobal; + if (DevMode) + { + On.RoR2.Networking.NetworkManagerSystemSteam.OnClientConnect += (s, u, t) => { }; + } } - private void GlobalEventManager_onCharacterDeathGlobal(DamageReport report) + public void OnEnable() { - // If a character was killed by the world, we shouldn't do anything. - if (!report.attacker || !report.attackerBody) + On.RoR2.GenericPickupController.AttemptGrant += GenericPickupController_AttemptGrant; + On.RoR2.PickupPickerController.OnInteractionBegin += PickupPickerController_OnInteractionBegin; + } + + public void OnDisable() + { + On.RoR2.GenericPickupController.AttemptGrant -= GenericPickupController_AttemptGrant; + On.RoR2.PickupPickerController.OnInteractionBegin -= PickupPickerController_OnInteractionBegin; + } + + public static int GetTotalItems(Inventory inv) + { + int itemCount = 0; + foreach (var i in inv.itemAcquisitionOrder) + { + if (ItemCatalog.GetItemDef(i).hidden) continue; + itemCount += inv.GetItemCount(i); + } + + return itemCount; + } + private bool AllowedToPickUp(CharacterBody body) + { + CharacterBody lowestPlayer = body; + int yoinkerItemCount = GetTotalItems(body.inventory); + int delta = 0; + foreach (var player in PlayerCharacterMasterController.instances) + { + var playerBody = player.master.GetBody(); + if (!playerBody || !playerBody.healthComponent.alive || playerBody == body) continue; + + int itemCount = GetTotalItems(playerBody.inventory); + if (yoinkerItemCount - itemCount > delta) + { + lowestPlayer = playerBody; + delta = yoinkerItemCount - itemCount; + } + } + if (lowestPlayer != body && delta > MaxItemCountDelta.Value) + { + if (Run.instance.time - naughtyLast > 1) + { + ChatMessage.Send($"Naughty, naughty! {body.GetUserName()} has {delta} more items than poor {lowestPlayer.GetUserName()}"); + naughtyLast = Run.instance.time; + } + return false; + } + + return true; + } + + private void GenericPickupController_AttemptGrant(On.RoR2.GenericPickupController.orig_AttemptGrant orig, GenericPickupController self, CharacterBody body) + { + if (!NetworkServer.active) + { + Debug.LogWarning("[Server] AntiYoink hook function 'GenericPickupController_AttemptGrant' called on client"); + return; + } + TeamComponent component = body.GetComponent(); + if (!component || component.teamIndex != TeamIndex.Player) { return; } - var attackerCharacterBody = report.attackerBody; - - // We need an inventory to do check for our item - if (attackerCharacterBody.inventory) - { - // Store the amount of our item we have - var garbCount = attackerCharacterBody.inventory.GetItemCount(myItemDef.itemIndex); - if (garbCount > 0 && - // Roll for our 50% chance. - Util.CheckRoll(50, attackerCharacterBody.master)) - { - // Since we passed all checks, we now give our attacker the cloaked buff. - // Note how we are scaling the buff duration depending on the number of the custom item in our inventory. - attackerCharacterBody.AddTimedBuff(RoR2Content.Buffs.Cloak, 3 + garbCount); - } + if (!AllowedToPickUp(body)) { + return; } + + orig(self, body); } - // The Update() method is run on every frame of the game. - private void Update() + // For Artifact of Command + private void PickupPickerController_OnInteractionBegin(On.RoR2.PickupPickerController.orig_OnInteractionBegin orig, PickupPickerController self, Interactor activator) { - // This if statement checks if the player has currently pressed F2. - if (Input.GetKeyDown(KeyCode.F2)) + if (NetworkServer.active) { - // Get the player body to use a position: - var transform = PlayerCharacterMasterController.instances[0].master.GetBodyObject().transform; - - // And then drop our defined item in front of the player. - - Log.Info($"Player pressed F2. Spawning our custom item at coordinates {transform.position}"); - PickupDropletController.CreatePickupDroplet(PickupCatalog.FindPickupIndex(myItemDef.itemIndex), transform.position, transform.forward * 20f); + CharacterBody body = activator.GetComponent(); + if (body && !AllowedToPickUp(body)) + { + return; + } } + orig(self, activator); } } } diff --git a/Thunderstore/plugins/AntiYoink/AntiYoink.language b/Thunderstore/plugins/AntiYoink/AntiYoink.language index e1f16c2..3f9b2f2 100644 --- a/Thunderstore/plugins/AntiYoink/AntiYoink.language +++ b/Thunderstore/plugins/AntiYoink/AntiYoink.language @@ -17,4 +17,4 @@ "EXAMPLE_CLOAKONKILL_DESC": "ENGLISH Whenever you kill an enemy, you have a 50% chance to cloak for 4s (+1s per stack).", "EXAMPLE_CLOAKONKILL_LORE": "ENGLISH Those who visit in the night are either praying for a favour, or preying on a neighbour." } -} \ No newline at end of file +}