Resource [NMS] Customizing the behavior of anvils

Discussion in 'Plugin Help/Development/Requests' started by TeeePeee, Jan 6, 2015.

Thread Status:
Not open for further replies.
  1. Offline

    TeeePeee

    Hello again everybody,

    As per usual, I've been digging around in the world of NMS in order to make some fun new features for my plugin, MyZ 4.0. And it wouldn't be right to keep this to myself, so here it is: my newest NMS tutorial: How to customize the behavior of anvils.

    In order to do this, there are a number of challenges to first overcome. The first one we'll tackle (because it's the only part that doesn't require NMS) is overriding the default action of right-clicking an anvil. To do this, we could just write a custom Block that extends BlockAnvil, but this would be a pain to do, as block manipulation usually is. So instead, we'll just make use of the Bukkit API for the PlayerInteractEvent.

    Your First Bit of Code (open)
    Code:
    package you.your.package;
    imports...
    
    public class AnvilListener implements Listener {
    
        @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
        private void onRightClickAnvil(PlayerInteractEvent e) {
            if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.getClickedBlock().getType() == Material.ANVIL) {
                e.setCancelled(true);
                interactAnvil(e.getPlayer(), e.getClickedBlock());
            }
        }
    }


    Simply enough. We listen in on when a Player right clicks an anvil, and if they do, cancel the normal behavior and run the method interactAnvil(Player, Block). Let's implement that method.

    Not so fast, though. Before we can open an Anvil Inventory, we need to see how it's really done. So let's go through it logically. The Player must know how to handle each action it performs, so somewhere in the net.minecraft.server source code for EntityHuman, there must be some method that allows the player to perform this action of opening an Anvil!

    Now usually, the next course of action would be to dig into the mc-dev source code on GitHub. However, I'll be writing this tutorial for 1.8 - and a few things have changed. We could take a look at this mc-dev source on GitHub, but it's obfuscated and not worth the time. I will not endorse decompiling a Craftbukkit/alternative jar file built for 1.8, but for the breadth of this tutorial, I will be providing the necessary methods.

    So first things first, let's see how the anvil is opened. We'll first visit BlockAnvil, because each block has an interact method. So when the Player interacts with a BlockAnvil, we can see that the method executed is:
    Code:
    EntityHuman#openTileEntity(new TileEntityContainerAnvil(World, BlockPosition));
    And so, we arrive at the first point where we have to write some more code. Keep the above code in your mind, and let's go back into our source, which should look somewhat like the following:
    AnvilListener.java (open)
    Code:
    package you.your.package;
    imports...
    
    public class AnvilListener implements Listener {
    
        @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
    private void onRightClickAnvil(PlayerInteractEvent e) {
            if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.getClickedBlock().getType() == Material.ANVIL) {
                e.setCancelled(true);
                interactAnvil(e.getPlayer(), e.getClickedBlock());
            }
        }
    
        private void interactAnvil(Player player, Block block) {
            // TODO We still need to implement this!
        }
    }


    Review again what you know:
    1. You know how an Anvil is opened.
    2. You know that, when an Anvil should open, you cancel it, and run your own method.
    So let's test our knowledge, and implement the interactAnvil method such that it opens a normal Anvil. We know from the interact method, that we must pass a World and a BlockPosition. We must also have a reference to the NMS version of a Player.

    Updating the interactAnvil Method (open)
    Code:
    private void interactAnvil(Player player, Block block) {
        // The NMS version of our Player.
        EntityHuman human = ((CraftPlayer)player).getHandle();
    
        // Our BlockPosition
        BlockPosition position = new BlockPosition(block.getX(), block.getY(), block.getZ());
    
        // Make sure the world that the Player is in isn't static when we try to open an Inventory, otherwise we might crash the client.
        if(!human.world.isStatic) {
            // Let's construct an Anvil TileEntityContainer which is the housing for the Anvil Inventory.
            ITileEntityContainer container = new TileEntityContainerAnvil(human.world, position);
            human.openTileEntity(container);
    }


    If you'd like, at this point, you can compile and run your code. You shouldn't notice any difference in behavior; a Player right-clicks an anvil -> the player opens an Inventory. But there is one major difference.

    You're telling the Player what Inventory to open - not the Anvil.

    So let's check out how we can modify this to open a different inventory, and we should quickly arrive at the option of creating a wrapper for the TileEntityContainerAnvil class. So let's find that class and open it up.
    TileEntityContainerAnvil.java (open)
    Code:
    package net.minecraft.server.v1_8_R1;
    
    public class TileEntityContainerAnvil implements ITileEntityContainer {
        private final World a;
        private final BlockPosition b;
     
        public TileEntityContainerAnvil(World paramWorld, BlockPosition paramBlockPosition) {
            this.a = paramWorld;
            this.b = paramBlockPosition;
        }
     
        public String getName() {
            return "anvil";
        }
     
        public boolean hasCustomName() {
            return false;
        }
     
        public IChatBaseComponent getScoreboardDisplayName() {
            return new ChatMessage(Blocks.ANVIL.a() + ".name", new Object[0]);
        }
     
        public Container createContainer(PlayerInventory paramPlayerInventory, EntityHuman paramEntityHuman) {
            return new ContainerAnvil(paramPlayerInventory, this.a, this.b, paramEntityHuman);
        }
     
        public String getContainerName() {
            return "minecraft:anvil";
        }
    }


    This class is pretty accessible. We can clearly see that it is just a simple little Anvil that has one method that we're interested in: createContainer. Why are we interested in this particular method? Well, it should be quite obvious: the ContainerAnvil should jump off the page for you.

    So let's wrap the TileEntityContainerAnvil in a class we'll call CustomTileEntityContainerAnvil, which I'll provide below with a few slight changes, and one major one. See if you can spot it.

    CustomTileEntityContainerAnvil.java (open)
    Code:
    package you.your.package;
    imports...
    
    public class CustomTileEntityContainerAnvil extends TileEntityContainerAnvil  {
        private final World world;
        private final BlockPosition position;
     
        public TileEntityContainerAnvil(World paramWorld, BlockPosition paramBlockPosition) {
            this.world = paramWorld;
            this.position = paramBlockPosition;
        }
     
        public String getName() {
            return "anvil";
        }
     
        public boolean hasCustomName() {
            return false;
        }
     
        public IChatBaseComponent getScoreboardDisplayName() {
            return new ChatMessage(Blocks.ANVIL.a() + ".name", new Object[0]);
        }
     
        public Container createContainer(PlayerInventory paramPlayerInventory, EntityHuman paramEntityHuman) {
            return new CustomContainerAnvil(paramPlayerInventory, this.world, this.position, paramEntityHuman);
        }
     
        public String getContainerName() {
            return "minecraft:anvil";
        }
    }


    Apart from changing the package declaration (by the way, make sure you make the correct imports! They should be net.minecraft.server, not org.bukkit) and renaming a few variables, there's one big change... It's in the createContainer method...

    That's right - we've made another wrapper. Because the ContainerAnvil is the main class for the Anvil Inventory, I'm not going to post it's original source code - only the code after our modifications. It's a big file.
    CustomContainerAnvil.java (open)
    Code:
    package you.your.package;
    
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    
    import net.minecraft.server.v1_8_R1.BlockAnvil;
    import net.minecraft.server.v1_8_R1.BlockPosition;
    import net.minecraft.server.v1_8_R1.Blocks;
    import net.minecraft.server.v1_8_R1.Container;
    import net.minecraft.server.v1_8_R1.Enchantment;
    import net.minecraft.server.v1_8_R1.EnchantmentManager;
    import net.minecraft.server.v1_8_R1.EntityHuman;
    import net.minecraft.server.v1_8_R1.IBlockData;
    import net.minecraft.server.v1_8_R1.ICrafting;
    import net.minecraft.server.v1_8_R1.IInventory;
    import net.minecraft.server.v1_8_R1.InventoryCraftResult;
    import net.minecraft.server.v1_8_R1.InventorySubcontainer;
    import net.minecraft.server.v1_8_R1.ItemStack;
    import net.minecraft.server.v1_8_R1.Items;
    import net.minecraft.server.v1_8_R1.PlayerInventory;
    import net.minecraft.server.v1_8_R1.Slot;
    import net.minecraft.server.v1_8_R1.World;
    
    import org.apache.commons.lang3.StringUtils;
    import org.bukkit.craftbukkit.v1_8_R1.entity.CraftHumanEntity;
    import org.bukkit.craftbukkit.v1_8_R1.inventory.CraftInventory;
    import org.bukkit.craftbukkit.v1_8_R1.inventory.CraftInventoryAnvil;
    import org.bukkit.craftbukkit.v1_8_R1.inventory.CraftInventoryView;
    import org.bukkit.entity.HumanEntity;
    import org.bukkit.entity.Player;
    import org.bukkit.inventory.InventoryHolder;
    
    public class CustomContainerAnvil extends Container {
    
       private final IInventory resultSlot = new InventoryCraftResult();
       private final IInventory processSlots = new CustomContainerAnvilInventory(this, "Repair", true, 2);
       private final World inWorld;
       private final BlockPosition position;
       public int expCost;
       private int iDontKnow;
       private String textbox;
       private final EntityHuman human;
       private CraftInventoryView bukkitEntity = null;
       private final PlayerInventory pInventory;
    
       public CustomContainerAnvil(PlayerInventory playerinventory, World world, BlockPosition blockposition, EntityHuman entityhuman) {
         pInventory = playerinventory;
         position = blockposition;
         inWorld = world;
         human = entityhuman;
         a(new Slot(processSlots, 0, 27, 47));
         a(new Slot(processSlots, 1, 76, 47));
         a(new CustomSlotAnvilResult(this, resultSlot, 2, 134, 47, world, blockposition));
         for (int i = 0; i < 3; i++) {
           for (int j = 0; j < 9; j++) {
             a(new Slot(playerinventory, j + i * 9 + 9, 8 + j * 18, 84 + i * 18));
           }
         }
         for (int i = 0; i < 9; i++) {
           a(new Slot(playerinventory, i, 8 + i * 18, 142));
         }
       }
    
       @Override
       /**
        * This is called when a player places an item in the anvil inventory.
        */
       public void a(IInventory iinventory) {
         super.a(iinventory);
         if (iinventory == processSlots) {
           updateAnvilDisplay();
         }
       }
    
       @SuppressWarnings("unchecked")
       public void updateAnvilDisplay() {
         ItemStack leftSlot = processSlots.getItem(0);
    
         expCost = 1;
         int reRepairCostAddition = 0;
         byte costOffsetModifier = 0;
    
         // If the item in the left-most slot is null...
         if (leftSlot == null) {
           // Make sure we don't have a result item.
           resultSlot.setItem(0, (ItemStack) null);
           expCost = 0;
         } else {
           ItemStack resultItem = leftSlot.cloneItemStack();
           ItemStack rightSlot = processSlots.getItem(1);
           Map<Integer, Integer> leftEnchantments = EnchantmentManager.a(resultItem);
           boolean usingEnchantedBook = false;
           int existingReRepairCost = leftSlot.getRepairCost() + (rightSlot == null ? 0 : rightSlot.getRepairCost());
    
           iDontKnow = 0;
           // If we have an item in the right-most slot...
           if (rightSlot != null) {
             usingEnchantedBook = rightSlot.getItem() == Items.ENCHANTED_BOOK && Items.ENCHANTED_BOOK.h(rightSlot).size() > 0;
             if (resultItem.e() && resultItem.getItem().a(leftSlot, rightSlot)) {
               int k = Math.min(resultItem.h(), resultItem.j() / 4);
               if (k <= 0) {
                 resultSlot.setItem(0, (ItemStack) null);
                 expCost = 0;
                 return;
               }
               int someVariable;
               for (someVariable = 0; k > 0 && someVariable < rightSlot.count; someVariable++) {
                 int new_durability = resultItem.h() - k;
                 resultItem.setData(new_durability);
                 reRepairCostAddition++;
                 k = Math.min(resultItem.h(), resultItem.j() / 4);
               }
               iDontKnow = someVariable;
             } else {
               // If we're not apply an enchantment and we're not repairing
               // an item...
               if (!usingEnchantedBook && (resultItem.getItem() != rightSlot.getItem() || !resultItem.e())) {
                 // Make sure we don't have a result item.
                 resultSlot.setItem(0, (ItemStack) null);
                 expCost = 0;
                 return;
               }
    
               // If we're not using an enchanted book (therefore, we must
               // be repairing at this point)...
               if (resultItem.e() && !usingEnchantedBook) {
                 // Compute the new durability.
                 int leftDurability = leftSlot.j() - leftSlot.h();
                 int rightDurability = rightSlot.j() - rightSlot.h();
                 int i1 = rightDurability + resultItem.j() * 12 / 100;
                 int k1 = leftDurability + i1;
    
                 int j1 = resultItem.j() - k1;
                 if (j1 < 0) {
                   j1 = 0;
                 }
                 if (j1 < resultItem.getData()) {
                   resultItem.setData(j1);
                   reRepairCostAddition += 2;
                 }
               }
    
               Map<Integer, Integer> rightEnchantments = EnchantmentManager.a(rightSlot);
               Iterator<Integer> rightEnchantIter = rightEnchantments.keySet().iterator();
               while (rightEnchantIter.hasNext()) {
                 int enchantmentID = rightEnchantIter.next().intValue();
                 Enchantment enchantment = Enchantment.getById(enchantmentID);
                 if (enchantment != null) {
                   // Compute a new enchantment level.
                   int leftLevel = leftEnchantments.containsKey(Integer.valueOf(enchantmentID)) ? leftEnchantments.get(
                       Integer.valueOf(enchantmentID)).intValue() : 0;
                   int rightLevel = rightEnchantments.get(Integer.valueOf(enchantmentID)).intValue();
                   int newLevel;
                   if (leftLevel == rightLevel) {
                     rightLevel++;
                     newLevel = rightLevel;
                   } else {
                     newLevel = Math.max(rightLevel, leftLevel);
                   }
                   rightLevel = newLevel;
                   boolean enchantable = enchantment.canEnchant(leftSlot);
                   if (human.abilities.canInstantlyBuild || leftSlot.getItem() == Items.ENCHANTED_BOOK) {
                     enchantable = true;
                   }
                   Iterator<Integer> leftEnchantIter = leftEnchantments.keySet().iterator();
                   while (leftEnchantIter.hasNext()) {
                     int enchantID = leftEnchantIter.next().intValue();
                     if (enchantID != enchantmentID && !enchantment.a(Enchantment.getById(enchantID))) {
                       enchantable = false;
                       reRepairCostAddition++;
                     }
                   }
                   if (enchantable) {
                     // Make sure we don't apply a level too high.
                     if (rightLevel > enchantment.getMaxLevel()) {
                       rightLevel = enchantment.getMaxLevel();
                     }
                     leftEnchantments.put(Integer.valueOf(enchantmentID), Integer.valueOf(rightLevel));
                     int randomCostModifier = 0;
                     switch (enchantment.getRandomWeight()) {
                     case 1:
                       randomCostModifier = 8;
                       break;
                     case 2:
                       randomCostModifier = 4;
                     case 3:
                     case 4:
                     case 6:
                     case 7:
                     case 8:
                     case 9:
                     default:
                       break;
                     case 5:
                       randomCostModifier = 2;
                       break;
                     case 10:
                       randomCostModifier = 1;
                     }
                     if (usingEnchantedBook) {
                       randomCostModifier = Math.max(1, randomCostModifier / 2);
                     }
                     reRepairCostAddition += randomCostModifier * rightLevel;
                   }
                 }
               }
             }
           }
    
           // If the textbox is blank...
           if (StringUtils.isBlank(textbox)) {
             // Remove the name on our item.
             if (leftSlot.hasName()) {
               costOffsetModifier = 1;
               reRepairCostAddition += costOffsetModifier;
               resultItem.r();
             }
           } else if (!textbox.equals(leftSlot.getName())) {
             // Name the item.
             costOffsetModifier = 1;
             reRepairCostAddition += costOffsetModifier;
             resultItem.c(textbox);
           }
    
           // Apply the costs for re-repairing the items.
           expCost = existingReRepairCost + reRepairCostAddition;
           if (reRepairCostAddition <= 0) {
             resultItem = null;
           }
           if (costOffsetModifier == reRepairCostAddition && costOffsetModifier > 0 && expCost >= 40) {
             expCost = 39;
           }
    
           // Max out at exp-cost 40 repairs.
           if (expCost >= 40 && !human.abilities.canInstantlyBuild) {
             resultItem = null;
           }
    
           // Apply everything to our result item.
           if (resultItem != null) {
             int repairCost = resultItem.getRepairCost();
             if (rightSlot != null && repairCost < rightSlot.getRepairCost()) {
               repairCost = rightSlot.getRepairCost();
             }
             repairCost = repairCost * 2 + 1;
             resultItem.setRepairCost(repairCost);
             EnchantmentManager.a(leftEnchantments, resultItem);
           }
           resultSlot.setItem(0, resultItem);
           b();
         }
       }
    
       @Override
       /**
        * Called when the player opens this inventory.
        */
       public void addSlotListener(ICrafting icrafting) {
         super.addSlotListener(icrafting);
         icrafting.setContainerData(this, 0, expCost);
       }
    
       @Override
       /**
        * Called when the player closes this inventory.
        */
       public void b(EntityHuman entityhuman) {
         super.b(entityhuman);
         if (!inWorld.isStatic) {
           for (int i = 0; i < processSlots.getSize(); i++) {
             ItemStack itemstack = processSlots.splitWithoutUpdate(i);
             if (itemstack != null) {
               entityhuman.drop(itemstack, false);
             }
           }
         }
       }
    
       @Override
       /**
        * Called while this is open.
        *
        * @return TRUE if the player can view this inventory, FALSE if the player
        *  must close the inventory.
        */
       public boolean a(EntityHuman entityhuman) {
         if (!checkReachable) { return true; }
         return inWorld.getType(position).getBlock() == Blocks.ANVIL;
       }
    
       @Override
       /**
        * Called when shift-clicking an item into this inventory.
        */
       public ItemStack b(EntityHuman entityhuman, int index) {
         ItemStack itemResult = null;
         Slot slot = (Slot) c.get(index);
         if (slot != null && slot.hasItem()) {
           ItemStack inSlot = slot.getItem();
    
           itemResult = inSlot.cloneItemStack();
           if (index == 2) {
             if (!a(inSlot, 3, 39, true)) { return null; }
             slot.a(inSlot, itemResult);
           } else if (index != 0 && index != 1) {
             if (index >= 3 && index < 39 && !a(inSlot, 0, 2, false)) { return null; }
           } else if (!a(inSlot, 3, 39, false)) { return null; }
           if (inSlot.count == 0) {
             slot.set((ItemStack) null);
           } else {
             slot.f();
           }
           if (inSlot.count == itemResult.count) { return null; }
           slot.a(entityhuman, inSlot);
         }
         return itemResult;
       }
    
       static IInventory a(CustomContainerAnvil containeranvil) {
         return containeranvil.processSlots;
       }
    
       static int b(CustomContainerAnvil containeranvil) {
         return containeranvil.iDontKnow;
       }
    
       @Override
       public CraftInventoryView getBukkitView() {
         if (bukkitEntity != null) { return bukkitEntity; }
         CraftInventory inventory = new CraftInventoryAnvil(processSlots, resultSlot);
         bukkitEntity = new CraftInventoryView(pInventory.player.getBukkitEntity(), inventory, this);
         return bukkitEntity;
       }
    
       private class CustomContainerAnvilInventory extends InventorySubcontainer {
    
         final CustomContainerAnvil anvil;
         public List<HumanEntity> transaction = new ArrayList<HumanEntity>();
         public Player player;
         private int maxStack = 64;
    
         @Override
         public ItemStack[] getContents() {
           return items;
         }
    
         @Override
         public void onOpen(CraftHumanEntity who) {
           transaction.add(who);
         }
    
         @Override
         public void onClose(CraftHumanEntity who) {
           transaction.remove(who);
         }
    
         @Override
         public List<HumanEntity> getViewers() {
           return transaction;
         }
    
         @Override
         public InventoryHolder getOwner() {
           return player;
         }
    
         @Override
         public void setMaxStackSize(int size) {
           maxStack = size;
         }
    
         CustomContainerAnvilInventory(CustomContainerAnvil containeranvil, String title, boolean customName, int size) {
           super(title, customName, size);
           anvil = containeranvil;
         }
    
         @Override
         public void update() {
           super.update();
           anvil.a(this);
         }
    
         @Override
         public int getMaxStackSize() {
           return maxStack;
         }
       }
    
       private class CustomSlotAnvilResult extends Slot {
    
         private final CustomContainerAnvil anvil;
         private final World world;
         private final BlockPosition position;
    
         CustomSlotAnvilResult(CustomContainerAnvil paramContainerAnvil, IInventory paramIInventory, int paramInt1, int paramInt2, int paramInt3,
             World paramWorld, BlockPosition paramBlockPosition) {
           super(paramIInventory, paramInt1, paramInt2, paramInt3);
    
           anvil = paramContainerAnvil;
           world = paramWorld;
           position = paramBlockPosition;
         }
    
         @Override
         /**
          * Whether or not an ItemStack is allowed to be placed in this slot.
          */
         public boolean isAllowed(ItemStack paramItemStack) {
           return false;
         }
    
         @Override
         /**
          * Whether or not a human is allowed to click in this slot.
          */
         public boolean isAllowed(EntityHuman paramEntityHuman) {
           return (paramEntityHuman.abilities.canInstantlyBuild || paramEntityHuman.expLevel >= anvil.expCost) && anvil.expCost > 0 && hasItem();
         }
    
         @Override
         /**
          * Called when the player takes the result item from the anvil.
          */
         public void a(EntityHuman paramEntityHuman, ItemStack paramItemStack) {
           if (!paramEntityHuman.abilities.canInstantlyBuild) {
             paramEntityHuman.levelDown(-anvil.expCost);
           }
           // Take the item.
           CustomContainerAnvil.a(anvil).setItem(0, null);
           if (CustomContainerAnvil.b(anvil) > 0) {
             ItemStack resultObject = CustomContainerAnvil.a(anvil).getItem(1);
             if (resultObject != null && resultObject.count > CustomContainerAnvil.b(anvil)) {
               resultObject.count -= CustomContainerAnvil.b(anvil);
               CustomContainerAnvil.a(anvil).setItem(1, resultObject);
             } else {
               CustomContainerAnvil.a(anvil).setItem(1, null);
             }
           } else {
             CustomContainerAnvil.a(anvil).setItem(1, null);
           }
           anvil.expCost = 0;
    
           // Damage the anvil.
           IBlockData block = world.getType(position);
           if (!paramEntityHuman.abilities.canInstantlyBuild && !world.isStatic && block.getBlock() == Blocks.ANVIL
               && paramEntityHuman.bb().nextFloat() < 0.12F) {
             int damage = ((Integer) block.get(BlockAnvil.DAMAGE)).intValue();
             damage++;
             if (damage > 2) {
               world.setAir(position);
               world.triggerEffect(1020, position, 0);
             } else {
               world.setTypeAndData(position, block.set(BlockAnvil.DAMAGE, Integer.valueOf(damage)), 2);
               world.triggerEffect(1021, position, 0);
             }
           } else if (!world.isStatic) {
             world.triggerEffect(1021, position, 0);
           }
         }
       }
    }


    That's a lot of code, but it's commented in a lot of places so you should have too much trouble picking through it. For now, copy-paste that into a new class and you'll have all the pieces you need to start customizing.

    Let's again analyze the flow of events to see what we've accomplished:
    1. Player right-clicks anvil (onRightClickAnvil)
    2. Default action is cancelled (onRightClickAnvil)
    3. Custom anvil inventory is constructed (interactAnvil)
    4. Player opens custom anvil inventory (interactAnvil)
    Combine the three classes, AnvilListener, CustomTileEntityContainerAnvil and CustomContainerAnvil and you should have a functioning custom anvil inventory!

    The only problem is, is that currently, it's still exactly the same.

    In order to make modifications, all you need to do is pick through CustomContainerAnvil.java and see what you want to change. Most of the interactions I've commented, and tried to rename variables as aptly as I could.

    If, for example, you wanted to prevent anvil damage, you would simply remove a few lines from public void a(EntityHuman paramEntityHuman, ItemStack paramItemStack).

    Perhaps, if there's enough demand, I'll add some resources to this tutorial on specific modifications you can make, but until then, happy messing with anvils!
     
  2. Offline

    chasechocolate

    Solidly written tutorial :)
     
  3. @TeeePeee This tutorial is written for 1.8 - I suggest rewriting it for 1.7 if you're going to have it on Bukkit :)
     
  4. Offline

    mrCookieSlime

    Moved to Alternatives Section since this Tutorial was specifically written for an Alternative.
     
  5. Offline

    TeeePeee

    @AdamQpzm
    I've been away from the Bukkit scene for about 6 months now, but I'm running Craftbukkit 1.8-R0.1-SNAPSHOT (git-Bukkit-262c777), so, I'm confused. I must have an unofficial build? :confused:


    @mrCookieSlime
    Whoopsies. Thanks for the move though :)


    @chasechocolate
    Grazie :)
     
  6. Indeed - there is no official CraftBukkit 1.8 and I have my doubts there ever will be :)
     
Thread Status:
Not open for further replies.

Share This Page