p1ut0nium-git / Rough-Mobs-Revamped

Rough Mobs Revamped for Minecraft
https://www.curseforge.com/minecraft/mc-mods/rough-mobs-revamped
4 stars 8 forks source link

[Bug] equipment pools are not working exactly right! #11

Closed 0xebjc closed 4 years ago

0xebjc commented 4 years ago

So I think I solved why this magic loops are hear:

            int rnd = RND.nextInt(entries.size());
            T entry = entries.get(rnd);
            String dimension = dimensions.get(rnd);

            /* TODO: I don't understand why Lellson was looping through this 100 times
            But it doesn't seem to be needed?
            Why does the magic number 100 represent?

            int i = 100;
            while (!isDimension(entity, dimension) && i > 0) {
                rnd = RND.nextInt(entries.size());
                entry = entries.get(rnd);
                dimension = dimensions.get(rnd);
                i--;
            }
            */
0xebjc commented 4 years ago

So the problem is that the code logic isn't the best as solving the desired problem: 1) The first rand int is set between the pool size, for example: // Main Hand wooden_sword;6;0 stone_sword;4;0 iron_sword;3 golden_sword;2;-1 diamond_sword;1;-1

2) The pool size is 5, so the random selection is between 0-4, based on the random int, lets say it's 1, then the stone_sword is selected, and since there was only one try, 3) The code then drops to check the dimension, if the mob is in the nether (-1) the weapon isn't selected. 4) Even if the setting to always select an item was set, no item would be equipped in this case.

*Somehow the logic needs to be changed to get a random item from the pool for only the items for that specific dimension and items with no dimension set.

I would probably go about this by creating a local variable (local pool) with a function that gets all items that would be acceptable for the dimension filter, then create a random int to select that local pool if items, and return null if no items are applicable for that dimension or any-dimension.

0xebjc commented 4 years ago

this may be the same problem in logic where you commented out for enchanting pools also,... I haven't looked at that code in a while, so not sure what's going on there.

p1ut0nium-git commented 4 years ago

I'll give this some more thought and look into making it a more reliable system.

0xebjc commented 4 years ago

so I dug through the original logic, took some time to wrap my brain around what was going on, data structures, etc. and identify what the problem was and figure out how best I thought I could improve and fix this issue. I think I've got a pretty solid solution, of course peer review is always important. I've taken the logic from building a full list of all entries, whether valid or not for the specific case filters. Now the filters determine a list of only valid entries, then a random valid choice is selected.

I'll paste my code in following posts, the first will be all the changes, with a bunch of comments and commented out junk code, so you can read the notes if you want. The second post will be with all the comments cleaned up and mostly removed.

Thanks for keeping a good mod alive. -jc

0xebjc commented 4 years ago

All code with junk verbose comments:

package com.p1ut0nium.roughmobsrevamped.misc;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.p1ut0nium.roughmobsrevamped.RoughMobs;
import com.p1ut0nium.roughmobsrevamped.compat.CompatHandler;
import com.p1ut0nium.roughmobsrevamped.compat.GameStagesCompat;
import com.p1ut0nium.roughmobsrevamped.config.RoughConfig;

import java.util.Random;

import net.darkhax.gamestages.GameStageHelper;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemSeeds;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTException;
import net.minecraft.util.ResourceLocation;

public class EquipHelper {

    private static final String KEY_APPLIED = Constants.unique("equipApplied");
    private static final Random RND = new Random();

    private static Boolean gameStagesLoaded;
    private static Boolean playerHasEnchantStage;
    private static Boolean enchantStageEnabled;

    public static class EquipmentApplier {

        private final String name;
        private final int chancePerWeaponDefault;
        private final int chancePerPieceDefault;
        private final int enchChanceDefault;
        private final float enchMultiplierDefault;
        private final float dropChanceDefault;

        private EquipmentPool poolMainhand;
        private EquipmentPool poolOffhand;

        private EquipmentPool poolHelmet;
        private EquipmentPool poolChestplate;
        private EquipmentPool poolLeggings;
        private EquipmentPool poolBoots;

        private int chancePerWeapon;
        private int chancePerPiece;
        private int enchChance;
        private float enchMultiplier;
        private float dropChance;

        private String[] equipMainhand;
        private String[] equipOffhand;

        private String[] equipHelmet;
        private String[] equipChestplate;
        private String[] equipLeggings;
        private String[] equipBoots;

        private String[] equipWeaponEnchants;
        private String[] equipArmorEnchants;

        public EquipmentApplier(String name, int chancePerWeaponDefault, int chancePerPieceDefault, int enchChanceDefault, float enchMultiplierDefault, float dropChanceDefault) {
            this.name = name;
            this.chancePerWeaponDefault = chancePerWeaponDefault;
            this.chancePerPieceDefault = chancePerPieceDefault;
            this.enchChanceDefault = enchChanceDefault;
            this.enchMultiplierDefault = enchMultiplierDefault;
            this.dropChanceDefault = dropChanceDefault;
        }

        public EquipmentPool getPoolMainhand() {
            return poolMainhand;
        }

        public EquipmentPool getPoolOffhand() {
            return poolOffhand;
        }

        public EquipmentPool getPoolHelmet() {
            return poolHelmet;
        }

        public EquipmentPool getPoolChestplate() {
            return poolChestplate;
        }

        public EquipmentPool getPoolLeggings() {
            return poolLeggings;
        }

        public EquipmentPool getPoolBoots() {
            return poolBoots;
        }

        public void setPoolMainhand(EquipmentPool poolMainhand) {
            this.poolMainhand = poolMainhand;
        }

        public void setPoolOffhand(EquipmentPool poolOffhand) {
            this.poolOffhand = poolOffhand;
        }

        public void setPoolHelmet(EquipmentPool poolHelmet) {
            this.poolHelmet = poolHelmet;
        }

        public void setPoolChestplate(EquipmentPool poolChestplate) {
            this.poolChestplate = poolChestplate;
        }

        public void setPoolLeggings(EquipmentPool poolLeggings) {
            this.poolLeggings = poolLeggings;
        }

        public void setPoolBoots(EquipmentPool poolBoots) {
            this.poolBoots = poolBoots;
        }

        public boolean getChance(int chance) {
            if (chance <= 0) {
                return false;
            }
            return Math.random() <= (float)1/(float)chance; 
        }

        public void equipEntity(EntityLiving entity) {

            if (entity == null || entity.getEntityData().getBoolean(KEY_APPLIED))
                return;

            // Get nearest player to the spawned mob
            EntityPlayer playerClosest = entity.world.getClosestPlayerToEntity(entity, -1.0D);

            // Get all Game Stage related info
            gameStagesLoaded = CompatHandler.isGameStagesLoaded();
            enchantStageEnabled = GameStagesCompat.useEnchantStage();           

            // Test to see if player has enchantment stage unlocked
            if (gameStagesLoaded && enchantStageEnabled) {
                playerHasEnchantStage = GameStageHelper.hasAnyOf(playerClosest, Constants.ROUGHMOBSALL, Constants.ROUGHMOBSENCHANT);
            } else {
                playerHasEnchantStage = false;
            }

            EquipmentPool[] pools = new EquipmentPool[] {
                    poolMainhand, poolOffhand, poolBoots, poolLeggings, poolChestplate, poolHelmet
            };

            // If getChance succeeds, then equip entity with complete set of armor
            // Code/idea thanks to 0xebjc (divided by 4 to reduce the full armor set
            // chance compared to individual change roles for each piece.
            boolean completeArmorSet = getChance(chancePerPiece/4);

            // Attempt to add weapons and armor)
            for (int i = 0; i < pools.length; i++) {
                EquipmentPool pool = pools[i];
                EntityEquipmentSlot slot = EntityEquipmentSlot.values()[i];

                // For slots 0 and 1, use chancePerWeapon, for all others, use chancePerPiece
                int rnd = i <= 1 ? chancePerWeapon : chancePerPiece;

                // Test for each weapon and each piece of armor, or if entity should have complete armor set
                if (getChance(rnd) || (completeArmorSet && i > 1)) {
                    ItemStack stack = pool.getRandom(entity, enchChance, enchMultiplier);

                    if (stack != null) {
                        entity.setItemStackToSlot(slot, stack);
                        entity.setDropChance(slot, dropChance);
                    }
                }
            }

            entity.getEntityData().setBoolean(KEY_APPLIED, true);
        }

        public String initConfig(String[] defaultMainhand, String[] defaultOffhand, String[] defaultHelmets, String[] defaultChestplates, String[] defaultLeggings, String[] defaultBoots, String[] defaultWeaponEnchants, String[] defaultArmorEnchants, boolean skipChanceOptions) {

            String formatName = name.toLowerCase().replace(" ", "") + "Equipment";
            RoughConfig.getConfig().addCustomCategoryComment(formatName, "Add enchanted armor and weapons to a newly spawned " + name + ". Takes 2-3 values seperated by a semicolon:\n"
                                                                                 + "Format: item or enchantment;chance;dimension\n"
                                                                                 + "item or enchantment:\tthe item/enchantment id\n"
                                                                                 + "chance:\t\t\t\tthe higher this number the more this item/enchantment gets selected\n"
                                                                                 + "dimension:\t\t\tdimension (ID) in which the item/enchantment can be selected (optional! Leave this blank for any dimension)");

            if (skipChanceOptions) 
            {
                chancePerWeapon = chancePerWeaponDefault;
                chancePerPiece = chancePerPieceDefault;
                enchChance = enchChanceDefault;
            }
            else
            {
                chancePerWeapon = RoughConfig.getInteger(formatName, "WeaponChance", chancePerWeaponDefault, 0, Short.MAX_VALUE, "Chance (1 in X per hand) to give a " + name + " new weapons on spawn\nSet to 0 to disable new weapons", true);
                chancePerPiece = RoughConfig.getInteger(formatName, "ArmorChance", chancePerPieceDefault, 0, Short.MAX_VALUE, "Chance (1 in X per piece) to give a " + name + " new armor on spawn\nSet to 0 to disable new armor", true);
                enchChance = RoughConfig.getInteger(formatName, "EnchantChance", enchChanceDefault, 0, Short.MAX_VALUE, "Chance (1 in X per item) to enchant newly given items\nSet to 0 to disable item enchanting", true);
            }

            enchMultiplier = RoughConfig.getFloat(formatName, "EnchantMultiplier", enchMultiplierDefault, 0F, 1F, "Multiplier for the applied enchantment level with the max. level. The level can still be a bit lower\ne.g. 0.5 would make sharpness to be at most level 3 (5 x 0.5 = 2.5 and [2.5] = 3) and fire aspect would always be level 1 (2 x 0.5 = 1)", true);
            dropChance = RoughConfig.getFloat(formatName, "DropChance", dropChanceDefault, 0F, 1F, "Chance (per slot) that the " + name + " drops the equipped item (1 = 100%, 0 = 0%)", true);

            equipMainhand = RoughConfig.getStringArray(formatName, "Mainhand", defaultMainhand, "Items which can be wielded by a " + name + " in their mainhand");
            equipOffhand = RoughConfig.getStringArray(formatName, "Offhand", defaultOffhand, "Items which can be wielded by a " + name + " in their offhand");
            equipHelmet = RoughConfig.getStringArray(formatName, "Helmet", defaultHelmets, "Helmets which can be worn by a " + name + " in their helmet slot");
            equipChestplate = RoughConfig.getStringArray(formatName, "Chestplate", defaultChestplates, "Chestplates which can be worn by a " + name + " in their chestplate slot");
            equipLeggings = RoughConfig.getStringArray(formatName, "Leggings", defaultLeggings, "Leggings which can be worn by a " + name + " in their leggings slot");
            equipBoots = RoughConfig.getStringArray(formatName, "Boots", defaultBoots, "Boots which can be worn by a " + name + " in their boots slot");

            equipWeaponEnchants = RoughConfig.getStringArray(formatName, "WeaponEnchants", defaultWeaponEnchants, "Enchantments which can be applied to mainhand and offhand items");
            equipArmorEnchants = RoughConfig.getStringArray(formatName, "ArmorEnchants", defaultArmorEnchants, "Enchantments which can be applied to armor items");

            return formatName;
        }

        public void createPools() {

            setPoolMainhand(EquipmentPool.createEquipmentPool("mainhand", equipMainhand, equipWeaponEnchants));
            setPoolOffhand(EquipmentPool.createEquipmentPool("offhand", equipOffhand, equipWeaponEnchants));
            setPoolHelmet(EquipmentPool.createEquipmentPool("helmet", equipHelmet, equipArmorEnchants));
            setPoolChestplate(EquipmentPool.createEquipmentPool("chestplate", equipChestplate, equipArmorEnchants));
            setPoolLeggings(EquipmentPool.createEquipmentPool("leggings", equipLeggings, equipArmorEnchants));
            setPoolBoots(EquipmentPool.createEquipmentPool("boots", equipBoots, equipArmorEnchants));
        }
    }

    public static class EquipmentPool {

        public final EntryPool<ItemStack> ITEM_POOL = new EntryPool<ItemStack>();
        public final EntryPool<Enchantment> ENCHANTMENT_POOL = new EntryPool<Enchantment>();

        public static EquipmentPool createEquipmentPool(String name, String[] arrayItems, String[] arrayEnchants) {

            EquipmentPool pool = new EquipmentPool();

            List<String> errorItems = pool.addItemsFromNames(arrayItems);
            if (!errorItems.isEmpty()) 
                RoughMobs.logError(Constants.MODNAME + ": error on creating the " + name + " item pool! " + String.join(", ", errorItems));

            List<String> errorEnchants = pool.addEnchantmentsFromNames(arrayEnchants);
            if (!errorEnchants.isEmpty()) 
                RoughMobs.logError(Constants.MODNAME + ": error on creating the " + name + " enchantment pool! " + String.join(", ", errorEnchants));

            return pool;
        }

        public List<String> addEnchantmentsFromNames(String[] array) {

            List<String> errors = new ArrayList<String>();
            for (String s : array)
            {
                String error = addEnchantmentFromName(s);
                if (error != null)
                    errors.add(error);
            }   

            return errors;
        }

        private String addEnchantmentFromName(String s) {

            String[] parts = s.split(";");

            if (parts.length >= 2) 
            {
                try 
                {
                    Enchantment ench = Enchantment.getEnchantmentByLocation(parts[0]);
                    int probability = Integer.parseInt(parts[1]);
                    int dimension = parts.length > 2 ? Integer.parseInt(parts[2]) : Integer.MIN_VALUE;

                    if (ench == null)
                        return "Invalid enchantment: " + parts[0] + " in line: " + s;
                    else    
                        addEnchantment(ench, probability, dimension);
                }
                catch(NumberFormatException e) 
                {
                    return "Invalid numbers in line: " + s;
                }
            }
            else
            {
                return "Invalid format for line: \"" + s + "\" Please change to enchantment;probability;dimensionID";
            }

            return null;
        }

        public List<String> addItemsFromNames(String[] array) {

            List<String> errors = new ArrayList<String>();
            for (String s : array)
            {
                String error = addItemFromName(s);
                if (error != null)
                    errors.add(error);
            }

            return errors;
        }

        private String addItemFromName(String s) {

            String[] parts = s.split(";");

            if (parts.length >= 2) 
            {
                try 
                {
                    Item item = Item.REGISTRY.getObject(new ResourceLocation(parts[0]));
                    int probability = Integer.parseInt(parts[1]);
                    int dimension = parts.length >= 3 ? Integer.parseInt(parts[2]) : Integer.MIN_VALUE;
                    int meta = parts.length >= 4 ? Integer.parseInt(parts[3]) : 0;
                    String nbt = parts.length >= 5 ? parts[4] : "";

                    if (item == null)
                        return "Invalid item: " + parts[0] + " in line: " + s;
                    else
                        addItem(new ItemStack(item, 1, meta), probability, dimension, nbt);
                }
                catch(NumberFormatException e) 
                {
                    return "Invalid numbers in line: " + s;
                }
            }
            else
            {
                return "Invalid format for line: \"" + s + "\" Please change to item;probability;meta";
            }

            return null;
        }

        public void addItem(ItemStack stack, int probability, int dimension, String nbt) {

            ITEM_POOL.addEntry(stack, probability, dimension == Integer.MIN_VALUE ? "ALL" : dimension, nbt);
        }

        public void addEnchantment(Enchantment enchantment, int probability, int dimension) {
            ENCHANTMENT_POOL.addEntry(enchantment, probability, dimension == Integer.MIN_VALUE ? "ALL" : dimension);
        }

        public ItemStack getRandom(Entity entity, int enchChance, float levelMultiplier) {

            if (ITEM_POOL.POOL.isEmpty()) 
                return null;

            ItemStack randomStack = ITEM_POOL.getRandom(entity);

            // Test to see if player has Enchantment stage
            if (gameStagesLoaded == false || enchantStageEnabled == false || enchantStageEnabled && playerHasEnchantStage) {

                // Test to see if there are no items to be enchanted
                if(randomStack != null) {

                    if (!ENCHANTMENT_POOL.POOL.isEmpty() && enchChance > 0 && RND.nextInt(enchChance) == 0) 
                    {
                        Enchantment ench = ENCHANTMENT_POOL.getRandom(entity, randomStack);

                        // TODO - Clean this up some
                        // If there is no enchantment, skip this
                        //
                        // Valid enchantments were already checked
                        // against the item (randomStack), if none
                        // valid, then ench would be null
                        if (ench != null) {

                            // Not sure why this while loop exists
                            // What is the magic number 10 for?

                            // 0xEBJC:  So the original logic is performing a random lookup of random enchantments
                            //          for each iteration it checks if the enchantment is a valid one for the item
                            //          if it fails after 10 iterations then no enchantment is applied.
                            //
                            //  Some kind of logic like this is needed for example if you had sword, axe, pickaxe,
                            //  and bow as equippable hand items and some enchantments only applied to some of these
                            //  different items exclusively. A problem with doing say a do until valid enchantment is
                            //  what if the enchantment pool is not empty, but no enchantments can be applied to
                            //  the specific item?

                            /*
                            int i = 10;
                            boolean canApply = !ench.canApply(randomStack);

                            while (canApply && i > 0) 
                            {
                                ench = ENCHANTMENT_POOL.getRandom(entity);
                                i--;
                            }

                            if (!canApply) {
                                return randomStack;
                            }

                            // If there is no enchantment, skip this
                            if (ench != null) {
                                double maxLevel = Math.max(ench.getMinLevel(), Math.min(ench.getMaxLevel(), Math.round(ench.getMaxLevel() * levelMultiplier)));
                                int level = (int)Math.round(maxLevel * (0.5 + Math.random()/2));

                                if (!randomStack.isItemEnchanted())
                                    randomStack.addEnchantment(ench, level);
                            }

                            */

                            double maxLevel = Math.max(ench.getMinLevel(), Math.min(ench.getMaxLevel(), Math.round(ench.getMaxLevel() * levelMultiplier)));
                            int level = (int)Math.round(maxLevel * (0.5 + Math.random()/2));

                            if (!randomStack.isItemEnchanted()) {
                                randomStack.addEnchantment(ench, level);
                            }
                        }
                    }
                }
            }

            return randomStack;
        }
    }

    public static class EntryPool<T> {

        public final Map<T, Object[]> POOL = new HashMap<T, Object[]>();
        private List<T> entries = null;
        //private List<String> dimensions = new ArrayList<String>();
        //private boolean needsReload;

        public void addEntry(T t, Object... data) {
            POOL.put(t, data);
            //needsReload = true;
        }

        // Allows for original method to be called while now enabling an item
        // to be passed in to check and build a valid list of enchantments
        // with the overloaded method
        public T getRandom(Entity entity) {
            return getRandom(entity, null);
        }

        public T getRandom(Entity entity, ItemStack item) {

            // Create a new valid entry pool for every entity spawn
            entries = new ArrayList<T>();

            // Loop through each entry in the POOL
            for (Entry<T, Object[]> entry : POOL.entrySet())
            {
                Object[] data = entry.getValue();

                T key = entry.getKey();
                if (key instanceof ItemStack && data.length > 2 && ((String)data[2]).length() != 0)
                {
                    try
                    {
                        ((ItemStack)key).setTagCompound(JsonToNBT.getTagFromJson((String) data[2]));
                    }
                    catch (NBTException e)
                    {
                        RoughMobs.logError("NBT Tag invalid: %s", e.toString());
                        e.printStackTrace();
                    }
                }

                // Exclude non-matching dimensions for entries associated entity
                if (isDimension(entity, String.valueOf(data[1]))) {
                    boolean validEntry = true;

                    // Additionally check if getRandom is getting a random
                    // enchantment for a valid item passed into the method
                    // if so then build a list of enchantments only valid
                    // for the item passed in.
                    if (item != null && key instanceof Enchantment) {
                        validEntry = ((Enchantment) key).canApply(item);
                    }

                    // Store entry n number of times where : (n = chance value)
                    // for valid entries
                    if (validEntry) {
                        for (int i = 0; i < (int) data[0]; i++) {
                            entries.add(key);
                        }
                    }
                }
            }

            int entrySize = entries.size();

            // If there are valid entries, return one randomly else return none (null)
            if(entrySize != 0) {
                int rnd = RND.nextInt(entrySize);
                return entries.get(rnd);
            }

            return null;

            /*
            int rnd = RND.nextInt(entries.size());
            T entry = entries.get(rnd);
            String dimension = dimensions.get(rnd);
            */

            /* TODO: I don't understand why Lellson was looping through this 100 times,

            // Note: by 0xEBJC: so the original entry pool was generated with all entries for the entity, the
            // entry pool is generated only once, then used for every lookup to get a random item/enchantment
            //
            // Example:
            //
            // stone_sword;3;0
            // iron_sword;2
            // diamond_sword;1;-1
            //
            // Entry Pool = :
            //      item        |   Dimension
            //  stone_sword     |       0
            //  stone_sword     |       0
            //  stone_sword     |       0
            //  iron_sword      |     'ALL'
            //  iron_sword      |     'ALL'
            //  diamond_sword   |       -1
            //
            //
            //  Then the i = 100 for loop loops through the list above trying each random until
            //  an entry that is picked at random matches the entities dimension, so if you created
            //  an entry with chance = 80, then there would be 80 entries in the pool for that item
            //  but the higher number of entries and possible invalid dimensions creates a very random
            //  and less probable likely hood that a valid entry is selected, this is compounded by the
            //  fact that before getRandom() is called a chance roll is already made in the equipEntity()
            //
            //  So the int 1 = 100 is a workaround because not every random entry will have a valid dimension
            //  0xEBJC: my suggest fix, code above, create an entry pool of only valid entries that match
            //  against the dimension filter, then get a random entry from that list.

            But it doesn't seem to be needed?
            Why does the magic number 100 represent?

            int i = 100;
            while (!isDimension(entity, dimension) && i > 0) {
                rnd = RND.nextInt(entries.size());
                entry = entries.get(rnd);
                dimension = dimensions.get(rnd);
                i--;
            }
            */

            /*
            // If entity is in wrong dimension, then don't return entries
            if (!isDimension(entity, dimension)) {
                entry = null;
                return entry;
            }

            // Otherwise, return entries
            return entry;
            */
        }

        private static boolean isDimension(Entity entity, String dimension) {
            return dimension.trim().toUpperCase().equals("ALL") || String.valueOf(entity.dimension).equals(dimension);
        }
    }
}
0xebjc commented 4 years ago

Cleaned up:

package com.p1ut0nium.roughmobsrevamped.misc;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.p1ut0nium.roughmobsrevamped.RoughMobs;
import com.p1ut0nium.roughmobsrevamped.compat.CompatHandler;
import com.p1ut0nium.roughmobsrevamped.compat.GameStagesCompat;
import com.p1ut0nium.roughmobsrevamped.config.RoughConfig;

import java.util.Random;

import net.darkhax.gamestages.GameStageHelper;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemSeeds;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTException;
import net.minecraft.util.ResourceLocation;

public class EquipHelper {

    private static final String KEY_APPLIED = Constants.unique("equipApplied");
    private static final Random RND = new Random();

    private static Boolean gameStagesLoaded;
    private static Boolean playerHasEnchantStage;
    private static Boolean enchantStageEnabled;

    public static class EquipmentApplier {

        private final String name;
        private final int chancePerWeaponDefault;
        private final int chancePerPieceDefault;
        private final int enchChanceDefault;
        private final float enchMultiplierDefault;
        private final float dropChanceDefault;

        private EquipmentPool poolMainhand;
        private EquipmentPool poolOffhand;

        private EquipmentPool poolHelmet;
        private EquipmentPool poolChestplate;
        private EquipmentPool poolLeggings;
        private EquipmentPool poolBoots;

        private int chancePerWeapon;
        private int chancePerPiece;
        private int enchChance;
        private float enchMultiplier;
        private float dropChance;

        private String[] equipMainhand;
        private String[] equipOffhand;

        private String[] equipHelmet;
        private String[] equipChestplate;
        private String[] equipLeggings;
        private String[] equipBoots;

        private String[] equipWeaponEnchants;
        private String[] equipArmorEnchants;

        public EquipmentApplier(String name, int chancePerWeaponDefault, int chancePerPieceDefault, int enchChanceDefault, float enchMultiplierDefault, float dropChanceDefault) {
            this.name = name;
            this.chancePerWeaponDefault = chancePerWeaponDefault;
            this.chancePerPieceDefault = chancePerPieceDefault;
            this.enchChanceDefault = enchChanceDefault;
            this.enchMultiplierDefault = enchMultiplierDefault;
            this.dropChanceDefault = dropChanceDefault;
        }

        public EquipmentPool getPoolMainhand() {
            return poolMainhand;
        }

        public EquipmentPool getPoolOffhand() {
            return poolOffhand;
        }

        public EquipmentPool getPoolHelmet() {
            return poolHelmet;
        }

        public EquipmentPool getPoolChestplate() {
            return poolChestplate;
        }

        public EquipmentPool getPoolLeggings() {
            return poolLeggings;
        }

        public EquipmentPool getPoolBoots() {
            return poolBoots;
        }

        public void setPoolMainhand(EquipmentPool poolMainhand) {
            this.poolMainhand = poolMainhand;
        }

        public void setPoolOffhand(EquipmentPool poolOffhand) {
            this.poolOffhand = poolOffhand;
        }

        public void setPoolHelmet(EquipmentPool poolHelmet) {
            this.poolHelmet = poolHelmet;
        }

        public void setPoolChestplate(EquipmentPool poolChestplate) {
            this.poolChestplate = poolChestplate;
        }

        public void setPoolLeggings(EquipmentPool poolLeggings) {
            this.poolLeggings = poolLeggings;
        }

        public void setPoolBoots(EquipmentPool poolBoots) {
            this.poolBoots = poolBoots;
        }

        public boolean getChance(int chance) {
            if (chance <= 0) {
                return false;
            }
            return Math.random() <= (float)1/(float)chance; 
        }

        public void equipEntity(EntityLiving entity) {

            if (entity == null || entity.getEntityData().getBoolean(KEY_APPLIED))
                return;

            // Get nearest player to the spawned mob
            EntityPlayer playerClosest = entity.world.getClosestPlayerToEntity(entity, -1.0D);

            // Get all Game Stage related info
            gameStagesLoaded = CompatHandler.isGameStagesLoaded();
            enchantStageEnabled = GameStagesCompat.useEnchantStage();           

            // Test to see if player has enchantment stage unlocked
            if (gameStagesLoaded && enchantStageEnabled) {
                playerHasEnchantStage = GameStageHelper.hasAnyOf(playerClosest, Constants.ROUGHMOBSALL, Constants.ROUGHMOBSENCHANT);
            } else {
                playerHasEnchantStage = false;
            }

            EquipmentPool[] pools = new EquipmentPool[] {
                    poolMainhand, poolOffhand, poolBoots, poolLeggings, poolChestplate, poolHelmet
            };

            // If getChance succeeds, then equip entity with complete set of armor
            // 4 x less than individual armor piece roles
            // Code/idea thanks to 0xebjc
            boolean completeArmorSet = getChance(chancePerPiece/4);

            // Attempt to add weapons and armor)
            for (int i = 0; i < pools.length; i++) {
                EquipmentPool pool = pools[i];
                EntityEquipmentSlot slot = EntityEquipmentSlot.values()[i];

                // For slots 0 and 1, use chancePerWeapon, for all others, use chancePerPiece
                int rnd = i <= 1 ? chancePerWeapon : chancePerPiece;

                // Test for each weapon and each piece of armor, or if entity should have complete armor set
                if (getChance(rnd) || (completeArmorSet && i > 1)) {
                    ItemStack stack = pool.getRandom(entity, enchChance, enchMultiplier);

                    if (stack != null) {
                        entity.setItemStackToSlot(slot, stack);
                        entity.setDropChance(slot, dropChance);
                    }
                }
            }

            entity.getEntityData().setBoolean(KEY_APPLIED, true);
        }

        public String initConfig(String[] defaultMainhand, String[] defaultOffhand, String[] defaultHelmets, String[] defaultChestplates, String[] defaultLeggings, String[] defaultBoots, String[] defaultWeaponEnchants, String[] defaultArmorEnchants, boolean skipChanceOptions) {

            String formatName = name.toLowerCase().replace(" ", "") + "Equipment";
            RoughConfig.getConfig().addCustomCategoryComment(formatName, "Add enchanted armor and weapons to a newly spawned " + name + ". Takes 2-3 values seperated by a semicolon:\n"
                                                                                 + "Format: item or enchantment;chance;dimension\n"
                                                                                 + "item or enchantment:\tthe item/enchantment id\n"
                                                                                 + "chance:\t\t\t\tthe higher this number the more this item/enchantment gets selected\n"
                                                                                 + "dimension:\t\t\tdimension (ID) in which the item/enchantment can be selected (optional! Leave this blank for any dimension)");

            if (skipChanceOptions) 
            {
                chancePerWeapon = chancePerWeaponDefault;
                chancePerPiece = chancePerPieceDefault;
                enchChance = enchChanceDefault;
            }
            else
            {
                chancePerWeapon = RoughConfig.getInteger(formatName, "WeaponChance", chancePerWeaponDefault, 0, Short.MAX_VALUE, "Chance (1 in X per hand) to give a " + name + " new weapons on spawn\nSet to 0 to disable new weapons", true);
                chancePerPiece = RoughConfig.getInteger(formatName, "ArmorChance", chancePerPieceDefault, 0, Short.MAX_VALUE, "Chance (1 in X per piece) to give a " + name + " new armor on spawn\nSet to 0 to disable new armor", true);
                enchChance = RoughConfig.getInteger(formatName, "EnchantChance", enchChanceDefault, 0, Short.MAX_VALUE, "Chance (1 in X per item) to enchant newly given items\nSet to 0 to disable item enchanting", true);
            }

            enchMultiplier = RoughConfig.getFloat(formatName, "EnchantMultiplier", enchMultiplierDefault, 0F, 1F, "Multiplier for the applied enchantment level with the max. level. The level can still be a bit lower\ne.g. 0.5 would make sharpness to be at most level 3 (5 x 0.5 = 2.5 and [2.5] = 3) and fire aspect would always be level 1 (2 x 0.5 = 1)", true);
            dropChance = RoughConfig.getFloat(formatName, "DropChance", dropChanceDefault, 0F, 1F, "Chance (per slot) that the " + name + " drops the equipped item (1 = 100%, 0 = 0%)", true);

            equipMainhand = RoughConfig.getStringArray(formatName, "Mainhand", defaultMainhand, "Items which can be wielded by a " + name + " in their mainhand");
            equipOffhand = RoughConfig.getStringArray(formatName, "Offhand", defaultOffhand, "Items which can be wielded by a " + name + " in their offhand");
            equipHelmet = RoughConfig.getStringArray(formatName, "Helmet", defaultHelmets, "Helmets which can be worn by a " + name + " in their helmet slot");
            equipChestplate = RoughConfig.getStringArray(formatName, "Chestplate", defaultChestplates, "Chestplates which can be worn by a " + name + " in their chestplate slot");
            equipLeggings = RoughConfig.getStringArray(formatName, "Leggings", defaultLeggings, "Leggings which can be worn by a " + name + " in their leggings slot");
            equipBoots = RoughConfig.getStringArray(formatName, "Boots", defaultBoots, "Boots which can be worn by a " + name + " in their boots slot");

            equipWeaponEnchants = RoughConfig.getStringArray(formatName, "WeaponEnchants", defaultWeaponEnchants, "Enchantments which can be applied to mainhand and offhand items");
            equipArmorEnchants = RoughConfig.getStringArray(formatName, "ArmorEnchants", defaultArmorEnchants, "Enchantments which can be applied to armor items");

            return formatName;
        }

        public void createPools() {

            setPoolMainhand(EquipmentPool.createEquipmentPool("mainhand", equipMainhand, equipWeaponEnchants));
            setPoolOffhand(EquipmentPool.createEquipmentPool("offhand", equipOffhand, equipWeaponEnchants));
            setPoolHelmet(EquipmentPool.createEquipmentPool("helmet", equipHelmet, equipArmorEnchants));
            setPoolChestplate(EquipmentPool.createEquipmentPool("chestplate", equipChestplate, equipArmorEnchants));
            setPoolLeggings(EquipmentPool.createEquipmentPool("leggings", equipLeggings, equipArmorEnchants));
            setPoolBoots(EquipmentPool.createEquipmentPool("boots", equipBoots, equipArmorEnchants));
        }
    }

    public static class EquipmentPool {

        public final EntryPool<ItemStack> ITEM_POOL = new EntryPool<ItemStack>();
        public final EntryPool<Enchantment> ENCHANTMENT_POOL = new EntryPool<Enchantment>();

        public static EquipmentPool createEquipmentPool(String name, String[] arrayItems, String[] arrayEnchants) {

            EquipmentPool pool = new EquipmentPool();

            List<String> errorItems = pool.addItemsFromNames(arrayItems);
            if (!errorItems.isEmpty()) 
                RoughMobs.logError(Constants.MODNAME + ": error on creating the " + name + " item pool! " + String.join(", ", errorItems));

            List<String> errorEnchants = pool.addEnchantmentsFromNames(arrayEnchants);
            if (!errorEnchants.isEmpty()) 
                RoughMobs.logError(Constants.MODNAME + ": error on creating the " + name + " enchantment pool! " + String.join(", ", errorEnchants));

            return pool;
        }

        public List<String> addEnchantmentsFromNames(String[] array) {

            List<String> errors = new ArrayList<String>();
            for (String s : array)
            {
                String error = addEnchantmentFromName(s);
                if (error != null)
                    errors.add(error);
            }   

            return errors;
        }

        private String addEnchantmentFromName(String s) {

            String[] parts = s.split(";");

            if (parts.length >= 2) 
            {
                try 
                {
                    Enchantment ench = Enchantment.getEnchantmentByLocation(parts[0]);
                    int probability = Integer.parseInt(parts[1]);
                    int dimension = parts.length > 2 ? Integer.parseInt(parts[2]) : Integer.MIN_VALUE;

                    if (ench == null)
                        return "Invalid enchantment: " + parts[0] + " in line: " + s;
                    else    
                        addEnchantment(ench, probability, dimension);
                }
                catch(NumberFormatException e) 
                {
                    return "Invalid numbers in line: " + s;
                }
            }
            else
            {
                return "Invalid format for line: \"" + s + "\" Please change to enchantment;probability;dimensionID";
            }

            return null;
        }

        public List<String> addItemsFromNames(String[] array) {

            List<String> errors = new ArrayList<String>();
            for (String s : array)
            {
                String error = addItemFromName(s);
                if (error != null)
                    errors.add(error);
            }

            return errors;
        }

        private String addItemFromName(String s) {

            String[] parts = s.split(";");

            if (parts.length >= 2) 
            {
                try 
                {
                    Item item = Item.REGISTRY.getObject(new ResourceLocation(parts[0]));
                    int probability = Integer.parseInt(parts[1]);
                    int dimension = parts.length >= 3 ? Integer.parseInt(parts[2]) : Integer.MIN_VALUE;
                    int meta = parts.length >= 4 ? Integer.parseInt(parts[3]) : 0;
                    String nbt = parts.length >= 5 ? parts[4] : "";

                    if (item == null)
                        return "Invalid item: " + parts[0] + " in line: " + s;
                    else
                        addItem(new ItemStack(item, 1, meta), probability, dimension, nbt);
                }
                catch(NumberFormatException e) 
                {
                    return "Invalid numbers in line: " + s;
                }
            }
            else
            {
                return "Invalid format for line: \"" + s + "\" Please change to item;probability;meta";
            }

            return null;
        }

        public void addItem(ItemStack stack, int probability, int dimension, String nbt) {

            ITEM_POOL.addEntry(stack, probability, dimension == Integer.MIN_VALUE ? "ALL" : dimension, nbt);
        }

        public void addEnchantment(Enchantment enchantment, int probability, int dimension) {
            ENCHANTMENT_POOL.addEntry(enchantment, probability, dimension == Integer.MIN_VALUE ? "ALL" : dimension);
        }

        public ItemStack getRandom(Entity entity, int enchChance, float levelMultiplier) {

            if (ITEM_POOL.POOL.isEmpty()) 
                return null;

            ItemStack randomStack = ITEM_POOL.getRandom(entity);

            // Test to see if player has Enchantment stage
            if (gameStagesLoaded == false || enchantStageEnabled == false || enchantStageEnabled && playerHasEnchantStage) {

                // Test to see if there are no items to be enchanted
                if(randomStack != null) {

                    if (!ENCHANTMENT_POOL.POOL.isEmpty() && enchChance > 0 && RND.nextInt(enchChance) == 0) 
                    {
                        Enchantment ench = ENCHANTMENT_POOL.getRandom(entity, randomStack);

                        // If there is no enchantment, skip this Valid enchantments were already checked
                        // against the item (randomStack), if none  valid, then ench would be null
                        if (ench != null) {

                            double maxLevel = Math.max(ench.getMinLevel(), Math.min(ench.getMaxLevel(), Math.round(ench.getMaxLevel() * levelMultiplier)));
                            int level = (int)Math.round(maxLevel * (0.5 + Math.random()/2));

                            if (!randomStack.isItemEnchanted()) {
                                randomStack.addEnchantment(ench, level);
                            }
                        }
                    }
                }
            }

            return randomStack;
        }
    }

    public static class EntryPool<T> {

        public final Map<T, Object[]> POOL = new HashMap<T, Object[]>();
        private List<T> entries = null;

        public void addEntry(T t, Object... data) {
            POOL.put(t, data);
        }

        // Allows for original method to be called while now enabling an itemto be passed 
        // in to check and build a valid list of enchantments with the overloaded method
        public T getRandom(Entity entity) {
            return getRandom(entity, null);
        }

        public T getRandom(Entity entity, ItemStack item) {

            // Create a new valid entry pool for every entity spawn
            entries = new ArrayList<T>();

            // Loop through each entry in the POOL
            for (Entry<T, Object[]> entry : POOL.entrySet())
            {
                Object[] data = entry.getValue();

                T key = entry.getKey();
                if (key instanceof ItemStack && data.length > 2 && ((String)data[2]).length() != 0)
                {
                    try
                    {
                        ((ItemStack)key).setTagCompound(JsonToNBT.getTagFromJson((String) data[2]));
                    }
                    catch (NBTException e)
                    {
                        RoughMobs.logError("NBT Tag invalid: %s", e.toString());
                        e.printStackTrace();
                    }
                }

                // Exclude non-matching dimensions for entries associated entity
                if (isDimension(entity, String.valueOf(data[1]))) {
                    boolean validEntry = true;

                    // Additionally check if getRandom is getting a random
                    // enchantment for a valid item passed into the method
                    // if so then build a list of enchantments only valid
                    // for the item passed in.
                    if (item != null && key instanceof Enchantment) {
                        validEntry = ((Enchantment) key).canApply(item);
                    }

                    // Store entry n number of times where : (n = chance value)
                    // for valid entries
                    if (validEntry) {
                        for (int i = 0; i < (int) data[0]; i++) {
                            entries.add(key);
                        }
                    }
                }
            }

            int entrySize = entries.size();

            // If there are valid entries, return one randomly else return none (null)
            if(entrySize != 0) {
                int rnd = RND.nextInt(entrySize);
                return entries.get(rnd);
            }

            return null;

        }

        private static boolean isDimension(Entity entity, String dimension) {
            return dimension.trim().toUpperCase().equals("ALL") || String.valueOf(entity.dimension).equals(dimension);
        }
    }
}
p1ut0nium-git commented 4 years ago

Thanks for this. I was planning to completely redo this logic, and your code will certainly help.

0xebjc commented 4 years ago

one mistake the chance value is a denominator, so this line should be fixed to this:

boolean completeArmorSet = getChance(chancePerPiece*4);

that is if you want the per whole armor set to be less likely then the per pieces, I had it as divide by 4, making a whole armor set 4 x times more likely :p

p1ut0nium-git commented 4 years ago

Fixed as of version 2.3.0 - https://www.curseforge.com/minecraft/mc-mods/rough-mobs-revamped/files/2886313