Minecraft Modding: Extending The Reach Of A Player Weapon

Introduction


There are many modders interested in extending the reach of weapons. In fact I think this is a valid method for creating a gun weapon (instead of creating an actual projectile like some people do).

Although there is a Entity#theItemInWorldManager.setBlockReachDistance() method, it actually doesn't seem to do what we want -- apparently it doesn't affect the client. So we have to do something more involved to get the extended reach working as explained below.

Thanks to the Balkon's Weapon Mod source for giving me the idea for this.

The steps are:

  1. Create an IExtendedReach interface that will allow you to identify weapons with extended reach, and provide method to get value of the reach.
  2. Make your custom weapons implement the IExtendedReach interface.
  3. Create and register a custom IMessage packet for telling server about a successful attack.
  4. Handle the MouseEvent where you:
    • Check whether the mouse left button was clicked.
    • Check whether an IExtendedReach item is equipped.
    • Calculate a MovingObjectPosition with the reach of the item.
    • If the MovingObjectPosition returns an entity, send a custom packet to server to call the attack method on that entity.
Note: This tutorial has code for 1.8. For earlier versions, the same technique works but some method names may have changed.

Create An IExtendedReach Interface


This interface will help us identify items with extended reach and provide a method called getReach() to get the reach value. Something simply like this:

public interface IExtendedReach 
{
    public float getReach(); // default is 1.0D
}

Implement the IExtendedReach Interface In Your Custom Weapon Classes


Take your custom weapons and implement the interface (add implements IExtendedReach to your class declaration). This also means you need to add the getReach() method to return the amount of reach. Something like this:

public class ItemSwordExtended extends ItemSword implements IExtendedReach
{
    public ItemSwordExtended(ToolMaterial parMaterial) 
    {
        super(parMaterial);
        setUnlocalizedName("swordExtended");
    }

    @Override
    public float getReach() 
    {
        return 20.0F;
    }
}

All I'm doing here is implementing the interface we made in the previous step.

Key Point: The value returned in the getReach() method is the distance of your weapons reach. In this case I've set it to 4.

Reminder: Don't forget to do the other stuff to make your weapon item: register your item (and in 1.8 also register your item renderer), properly locate a texture file, put an entry into your .lang file, etc.

Create An IMessage Custom Packet


In vanilla Minecraft, just like in this example, the detection of what can be reached is calculated on the client but you need the effect to occur on the server. Therefore Minecraft sends a packet called C02PacketUseEntity when there is an attack. We can't use that packet for extended reach though, because to prevent hacking the server double-checks the distance to the attacked entity to ensure it is within reach.

So we need to make a custom packet that essentially does the same thing, but checks against the extended reach instead of the default amount.

public class MessageExtendedReachAttack implements IMessage 
{
    private int entityId ;

    public MessageExtendedReachAttack() 
    { 
     // need this constructor
    }

    public MessageExtendedReachAttack(int parEntityId) 
    {
     entityId = parEntityId;
        // DEBUG
        System.out.println("Constructor");
    }

    @Override
    public void fromBytes(ByteBuf buf) 
    {
     entityId = ByteBufUtils.readVarInt(buf, 4);
     // DEBUG
     System.out.println("fromBytes");
    }

    @Override
    public void toBytes(ByteBuf buf) 
    {
     ByteBufUtils.writeVarInt(buf, entityId, 4);
        // DEBUG
        System.out.println("toBytes encoded");
    }

    public static class Handler implements IMessageHandler<MessageExtendedReachAttack, 
          IMessage> 
    {
        @Override
        public IMessage onMessage(final MessageExtendedReachAttack message, 
              MessageContext ctx) 
        {
            // DEBUG
            System.out.println("Message received");
            // Know it will be on the server so make it thread-safe
            final EntityPlayerMP thePlayer = (EntityPlayerMP) BlockSmith.proxy.
                  getPlayerEntityFromContext(ctx);
            thePlayer.getServerForPlayer().addScheduledTask(
                  new Runnable()
                  {
                      @Override
                      public void run() 
                      {
                          Entity theEntity = thePlayer.worldObj.
                                getEntityByID(message.entityId);
                          // DEBUG
                          System.out.println("Entity = "+theEntity);
                          
                          // Need to ensure that hackers can't cause trick kills, 
                          // so double check weapon type and reach
                          if (thePlayer.getCurrentEquippedItem() == null)
                          {
                              return;
                          }
                          if (thePlayer.getCurrentEquippedItem().getItem() instanceof 
                                IExtendedReach)
                          {
                              IExtendedReach theExtendedReachWeapon = 
                                    (IExtendedReach)thePlayer.getCurrentEquippedItem().
                                    getItem();
                              double distanceSq = thePlayer.getDistanceSqToEntity(
                                    theEntity);
                              double reachSq =theExtendedReachWeapon.getReach()*
                                    theExtendedReachWeapon.getReach();
                              if (reachSq >= distanceSq)
                              {
                                  thePlayer.attackTargetEntityWithCurrentItem(
                                        theEntity);
                              }
                          }
                          return; // no response in this case
                      }
                }
            );
            return null; // no response message
        }
    }
}

This should be fairly understandable if you're familiar with packets. Basically it provides methods to pack and unpack information into byte buffers which are the payloads for the packets, and then the onMessage() method is called when it is received.

For the toBytes() and fromBytes() methods I'm using ByteBufUtils methods. You can also use ByteBuf methods to read and write integers, but I like to make people aware of ByteBufUtils because it provides methods for more complicated data (especially useful for NBT). The value of 4 in the read and write methods is indicating that I think I only need up to 4 bytes to represent the entity ID since I expect it to be less than 4,294,967,296. In fact you could probably use less bytes, but ultimately there isn't much need to save bytes here -- this packet is only sent occasionally.

In this case, in the onMessage() method I'm preventing hacking by confirming that the server agrees that the player has an extended reach weapon equipped and also that the entity hit is within the reach. If so, I call the method for processing the attack, and note that because it is 1.8 (which does networking in separate thread) I have made the method thread-safe.

Register The Custom Packet


In the init handling method of your common proxy, you should register the packet like this:

BlockSmith.network = NetworkRegistry.INSTANCE.newSimpleChannel(BlockSmith.NETWORK_CHANNEL_NAME);

int packetId = 0;
// register messages from client to server
BlockSmith.network.registerMessage(MessageExtendedReachAttack.Handler.class, 
      MessageExtendedReachAttack.class, packetId++, Side.SERVER);

BlockSmith is the main class of my mod, so you should rename it to the main class of your mod. You'll also need to make a public, static field in your main class for the network.

public static SimpleNetworkWrapper network;

Handle the MouseEvent


If you're not familiar with handling events, check out Jabelar's Event Tutorial. Remember to register your event handler!

For the actual handling method, you'll want something like this:

@SideOnly(Side.CLIENT)
@SubscribeEvent(priority=EventPriority.NORMAL, receiveCanceled=true)
public void onEvent(MouseEvent event)
{ 
    if (event.button == 0 && event.buttonstate)
    {
        Minecraft mc = Minecraft.getMinecraft();
        EntityPlayer thePlayer = mc.thePlayer;
        if (thePlayer != null)
        {
            ItemStack itemstack = thePlayer.getCurrentEquippedItem();
            IExtendedReach ieri;
            if (itemstack != null)
            {
                if (itemstack.getItem() instanceof IExtendedReach)
                {
                    ieri = (IExtendedReach) itemstack.getItem();
                } else
                {
                    ieri = null;
                }
   
                if (ieri != null)
                {
                    float reach = ieri.getReach();
                    MovingObjectPosition mov = getMouseOverExtended(reach); 
                      
                    if (mov != null)
                    {
                        if (mov.entityHit != null && mov.entityHit.hurtResistantTime == 0)
                        {
                            if (mov.entityHit != thePlayer )
                            {
                                BlockSmith.network.sendToServer(new MessageExtendedReachAttack(
                                      mov.entityHit.getEntityId()));
                            }
                        }
                    }
                }
            }
        }
    }
}
        
// This is mostly copied from the EntityRenderer#getMouseOver() method
public static MovingObjectPosition getMouseOverExtended(float dist)
{
    Minecraft mc = FMLClientHandler.instance().getClient();
    Entity theRenderViewEntity = mc.getRenderViewEntity();
    AxisAlignedBB theViewBoundingBox = new AxisAlignedBB(
            theRenderViewEntity.posX-0.5D,
            theRenderViewEntity.posY-0.0D,
            theRenderViewEntity.posZ-0.5D,
            theRenderViewEntity.posX+0.5D,
            theRenderViewEntity.posY+1.5D,
            theRenderViewEntity.posZ+0.5D
            );
    MovingObjectPosition returnMOP = null;
    if (mc.theWorld != null)
    {
        double var2 = dist;
        returnMOP = theRenderViewEntity.rayTrace(var2, 0);
        double calcdist = var2;
        Vec3 pos = theRenderViewEntity.getPositionEyes(0);
        var2 = calcdist;
        if (returnMOP != null)
        {
            calcdist = returnMOP.hitVec.distanceTo(pos);
        }
         
        Vec3 lookvec = theRenderViewEntity.getLook(0);
        Vec3 var8 = pos.addVector(lookvec.xCoord * var2, 
              lookvec.yCoord * var2, 
              lookvec.zCoord * var2);
        Entity pointedEntity = null;
        float var9 = 1.0F;
        @SuppressWarnings("unchecked")
        List<Entity> list = mc.theWorld.getEntitiesWithinAABBExcludingEntity(
              theRenderViewEntity, 
              theViewBoundingBox.addCoord(
                    lookvec.xCoord * var2, 
                    lookvec.yCoord * var2, 
                    lookvec.zCoord * var2).expand(var9, var9, var9));
        double d = calcdist;
            
        for (Entity entity : list)
        {
            if (entity.canBeCollidedWith())
            {
                float bordersize = entity.getCollisionBorderSize();
                AxisAlignedBB aabb = new AxisAlignedBB(
                      entity.posX-entity.width/2, 
                      entity.posY, 
                      entity.posZ-entity.width/2, 
                      entity.posX+entity.width/2, 
                      entity.posY+entity.height, 
                      entity.posZ+entity.width/2);
                aabb.expand(bordersize, bordersize, bordersize);
                MovingObjectPosition mop0 = aabb.calculateIntercept(pos, var8);
                    
                if (aabb.isVecInside(pos))
                {
                    if (0.0D < d || d == 0.0D)
                    {
                        pointedEntity = entity;
                        d = 0.0D;
                    }
                } else if (mop0 != null)
                {
                    double d1 = pos.distanceTo(mop0.hitVec);
                        
                    if (d1 < d || d == 0.0D)
                    {
                        pointedEntity = entity;
                        d = d1;
                    }
                }
            }
        }
           
        if (pointedEntity != null && (d < calcdist || returnMOP == null))
        {
             returnMOP = new MovingObjectPosition(pointedEntity);
        }
    }
    return returnMOP;
}

The code should be fairly self explanatory, I check that the button is the left one (id = 0) and buttonstate is true if pressed, check that the equipped item implements the IExtendedReach interface, find what the mouse is over (using a ray trace from the render view entity) and attacks it if it is an entity within the reach distance.

The mouseOverExtended() method is copied a lot from the EntityRenderer#mouseOver() method but it returns the MovingObjectPosition and it adjusts the distance based on the reach.

The way it works is that it essentially is taking a ray trace along the look vector (which only detects blocks) and then creates a series of bounding boxes along that vector and checks for entities inside where the vector to them intercepts the look vector. I think...

Test It Out


Run the game and confirm that you can hit entities from distance of 20 blocks away. Change the value of the getReach() return value in your item to change its reach to other amounts.

Conclusion


That's it. Pretty simple really. As always, feel free to comment if you need clarifications or have corrections. Happy modding!

10 comments:

  1. how would i make this work for 1.6.4?

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi,
    I've been trying to make this work for 1.7.10, and the code looks fine in Eclipse(no errors and it looks like it should work to me).
    How do I get the EntityPlayerMP from the MessageContext? There doesn't seem to be a method to do that.

    ReplyDelete
  4. What methods are changed in the 1.7.10 code vs this turtorial's 1.8?

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. @TheTrueForce
    ctx.side.isClient() ? Minecraft.getMinecraft().thePlayer : ctx.getServerHandler().playerEntity

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. --Glistre
    Thanks! just what I needed for my mod for mob boss battle. . .would also like to backport to 1.7.10 now

    ReplyDelete
  9. This does not work on any version...

    ReplyDelete
  10. This does not work on any version...

    ReplyDelete