// -------------------------------------------------------------------------------------------------- // Copyright (c) 2016 Microsoft Corporation // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the "Software"), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, publish, distribute, // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -------------------------------------------------------------------------------------------------- package com.microsoft.Malmo.MissionHandlers; import io.netty.buffer.ByteBuf; import net.minecraft.block.Block; import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.entity.Entity; import net.minecraft.entity.MoverType; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.server.MinecraftServer; import net.minecraft.util.EnumFacing; import net.minecraft.util.EnumHand; import net.minecraft.util.IThreadListener; import net.minecraft.util.math.AxisAlignedBB; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.math.Vec3d; import net.minecraft.world.WorldServer; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.world.BlockEvent; import net.minecraftforge.fml.common.eventhandler.Event; import net.minecraftforge.fml.common.network.ByteBufUtils; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.fml.relauncher.Side; import com.microsoft.Malmo.MalmoMod; import com.microsoft.Malmo.MissionHandlerInterfaces.ICommandHandler; import com.microsoft.Malmo.Schemas.DiscreteMovementCommand; import com.microsoft.Malmo.Schemas.DiscreteMovementCommands; import com.microsoft.Malmo.Schemas.MissionInit; /** * Fairly dumb command handler that attempts to move the player one block N,S,E * or W.<br> */ public class DiscreteMovementCommandsImplementation extends CommandBase implements ICommandHandler { public static final String MOVE_ATTEMPTED_KEY = "attemptedToMove"; private boolean isOverriding; DiscreteMovementCommands params; public static class DiscretePartialMoveEvent extends Event { public final double x; public final double y; public final double z; public DiscretePartialMoveEvent(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } } public static class UseActionMessage implements IMessage { public BlockPos pos; public ItemStack itemStack; public EnumFacing face; public boolean standOnPlacedBlock; public Vec3d hitVec; public UseActionMessage() { } public UseActionMessage(BlockPos pos, ItemStack itemStack, EnumFacing face, boolean standOnPlacedBlock, Vec3d hitVec) { this.pos = pos; this.itemStack = itemStack; this.face = face; this.standOnPlacedBlock = standOnPlacedBlock; this.hitVec = hitVec; } @Override public void fromBytes(ByteBuf buf) { this.pos = new BlockPos( buf.readInt(), buf.readInt(), buf.readInt() ); this.itemStack = ByteBufUtils.readItemStack(buf); this.face = EnumFacing.values()[buf.readInt()]; this.standOnPlacedBlock = buf.readBoolean(); this.hitVec = new Vec3d(buf.readDouble(), buf.readDouble(), buf.readDouble()); } @Override public void toBytes(ByteBuf buf) { buf.writeInt(this.pos.getX()); buf.writeInt(this.pos.getY()); buf.writeInt(this.pos.getZ()); ByteBufUtils.writeItemStack(buf, this.itemStack); buf.writeInt(this.face.ordinal()); buf.writeBoolean(this.standOnPlacedBlock); buf.writeDouble(this.hitVec.xCoord); buf.writeDouble(this.hitVec.yCoord); buf.writeDouble(this.hitVec.zCoord); } } public static class UseActionMessageHandler implements IMessageHandler<UseActionMessage, IMessage> { @Override public IMessage onMessage(final UseActionMessage message, final MessageContext ctx) { IThreadListener mainThread = null; if (ctx.side == Side.CLIENT) return null; // Not interested. mainThread = (WorldServer)ctx.getServerHandler().playerEntity.world; mainThread.addScheduledTask(new Runnable() { @Override public void run() { EntityPlayerMP player = ctx.getServerHandler().playerEntity; PlayerInteractEvent event = new PlayerInteractEvent.RightClickBlock(player, EnumHand.MAIN_HAND, message.pos, message.face, message.hitVec); MinecraftForge.EVENT_BUS.post(event); if (!event.isCanceled()) { BlockPos pos = message.pos.add( message.face.getDirectionVec() ); Block b = Block.getBlockFromItem( message.itemStack.getItem() ); if( b != null ) { IBlockState blockType = b.getStateFromMeta( message.itemStack.getMetadata() ); if (player.world.setBlockState( pos, blockType )) { BlockSnapshot snapshot = new BlockSnapshot(player.world, pos, blockType); BlockEvent.PlaceEvent placeevent = new BlockEvent.PlaceEvent(snapshot, player.world.getBlockState(message.pos), player); MinecraftForge.EVENT_BUS.post(placeevent); // We set the block, so remove it from the inventory. if (!player.isCreative()) { if (player.inventory.getCurrentItem().getCount() > 1) player.inventory.getCurrentItem().setCount(player.inventory.getCurrentItem().getCount() - 1); else player.inventory.mainInventory.get(player.inventory.currentItem).setCount(0); } if (message.standOnPlacedBlock) { // Eg after a jump-use, the player might expect to stand on the block that was just placed. player.setPosition(pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5); } } } } } }); return null; } } public static class AttackActionMessage implements IMessage { public BlockPos pos; public EnumFacing face; public Vec3d hitVec; public AttackActionMessage() { } public AttackActionMessage(BlockPos hitPos, EnumFacing face, Vec3d hitVec) { this.pos = hitPos; this.face = face; this.hitVec = hitVec; } @Override public void fromBytes(ByteBuf buf) { this.pos = new BlockPos( buf.readInt(), buf.readInt(), buf.readInt() ); this.face = EnumFacing.values()[buf.readInt()]; this.hitVec = new Vec3d(buf.readDouble(), buf.readDouble(), buf.readDouble() ); } @Override public void toBytes(ByteBuf buf) { buf.writeInt(this.pos.getX()); buf.writeInt(this.pos.getY()); buf.writeInt(this.pos.getZ()); buf.writeInt(this.face.ordinal()); buf.writeDouble(this.hitVec.xCoord); buf.writeDouble(this.hitVec.yCoord); buf.writeDouble(this.hitVec.zCoord); } } public static class AttackActionMessageHandler implements IMessageHandler<AttackActionMessage, IMessage> { @Override public IMessage onMessage(final AttackActionMessage message, final MessageContext ctx) { IThreadListener mainThread = null; if (ctx.side == Side.CLIENT) return null; // Not interested. mainThread = (WorldServer)ctx.getServerHandler().playerEntity.world; mainThread.addScheduledTask(new Runnable() { @Override public void run() { EntityPlayerMP player = ctx.getServerHandler().playerEntity; IBlockState iblockstate = player.world.getBlockState(message.pos); Block block = iblockstate.getBlock(); if (iblockstate.getMaterial() != Material.AIR) { PlayerInteractEvent event = new PlayerInteractEvent.LeftClickBlock(player, message.pos, message.face, message.hitVec); MinecraftForge.EVENT_BUS.post(event); if (!event.isCanceled()) { boolean dropBlock = false; // We do things this way, rather than pass true for dropBlock in world.destroyBlock, // because we want this to take instant effect - we don't want the intermediate stage // of spawning a free-floating item that the player must pick up. java.util.List<ItemStack> items = block.getDrops(player.world, message.pos, iblockstate, 0); player.world.destroyBlock( message.pos, dropBlock ); for (ItemStack item : items) { if (!player.inventory.addItemStackToInventory(item)) { Block.spawnAsEntity(player.world, message.pos, item); // Didn't fit in inventory, so spawn it. } } BlockEvent.BreakEvent breakevent = new BlockEvent.BreakEvent(player.world, message.pos, iblockstate, player); MinecraftForge.EVENT_BUS.post(breakevent); } } } }); return null; } } @Override public boolean parseParameters(Object params) { if (params == null || !(params instanceof DiscreteMovementCommands)) return false; this.params = (DiscreteMovementCommands)params; setUpAllowAndDenyLists(this.params.getModifierList()); return true; } private int getDirectionFromYaw(float yaw) { // Initialise direction: int direction = (int)((yaw + 45.0f) / 90.0f); return (direction + 4) % 4; } private DiscreteMovementCommand verbToCommand(String verb) { for (DiscreteMovementCommand com : DiscreteMovementCommand.values()) { if (verb.equalsIgnoreCase(com.value())) return com; } return null; } @Override protected boolean onExecute(String verb, String parameter, MissionInit missionInit) { boolean handled = false; EntityPlayerSP player = Minecraft.getMinecraft().player; if (player != null) { int z = 0; int x = 0; int y = 0; DiscreteMovementCommand command = verbToCommand(verb); if (command == null) return false; // Did not recognise this command. switch (command) { case MOVENORTH: case JUMPNORTH: z = -1; break; case MOVESOUTH: case JUMPSOUTH: z = 1; break; case MOVEEAST: case JUMPEAST: x = 1; break; case MOVEWEST: case JUMPWEST: x = -1; break; case MOVE: case JUMPMOVE: case STRAFE: case JUMPSTRAFE: if (parameter != null && parameter.length() != 0) { float velocity = Float.valueOf(parameter); int offset = (velocity > 0) ? 1 : ((velocity < 0) ? -1 : 0); int direction = getDirectionFromYaw(player.rotationYaw); // For strafing, add one to direction: if (command == DiscreteMovementCommand.STRAFE || command == DiscreteMovementCommand.JUMPSTRAFE) direction = (direction + 1) % 4; switch (direction) { case 0: // North z = offset; break; case 1: // East x = -offset; break; case 2: // South z = -offset; break; case 3: // West x = offset; break; } break; } case TURN: if (parameter != null && parameter.length() != 0) { float yawDelta = Float.valueOf(parameter); int direction = getDirectionFromYaw(player.rotationYaw); direction += (yawDelta > 0) ? 1 : ((yawDelta < 0) ? -1 : 0); direction = (direction + 4) % 4; player.rotationYaw = direction * 90; player.onUpdate(); // Send a message that the ContinuousMovementCommands can pick up on: Event event = new CommandForWheeledRobotNavigationImplementation.ResetPitchAndYawEvent(true, player.rotationYaw, false, 0); MinecraftForge.EVENT_BUS.post(event); handled = true; } break; case LOOK: if (parameter != null && parameter.length() != 0) { float pitchDelta = Float.valueOf(parameter); player.rotationPitch += (pitchDelta < 0) ? -45 : ((pitchDelta > 0) ? 45 : 0); player.onUpdate(); // Send a message that the ContinuousMovementCommands can pick up on: Event event = new CommandForWheeledRobotNavigationImplementation.ResetPitchAndYawEvent(false, 0, true, player.rotationPitch); MinecraftForge.EVENT_BUS.post(event); handled = true; } break; case ATTACK: { RayTraceResult mop = Minecraft.getMinecraft().objectMouseOver; if( mop.typeOfHit == RayTraceResult.Type.BLOCK ) { BlockPos hitPos = mop.getBlockPos(); EnumFacing face = mop.sideHit; IBlockState iblockstate = player.world.getBlockState(hitPos); Block block = iblockstate.getBlock(); if (iblockstate.getMaterial() != Material.AIR) { MalmoMod.network.sendToServer(new AttackActionMessage(hitPos, face, mop.hitVec)); // Trigger a reward for collecting the block java.util.List<ItemStack> items = block.getDrops(player.world, hitPos, iblockstate, 0); for (ItemStack item : items) { RewardForCollectingItemImplementation.GainItemEvent event = new RewardForCollectingItemImplementation.GainItemEvent(item); MinecraftForge.EVENT_BUS.post(event); } } } handled = true; break; } case USE: case JUMPUSE: { RayTraceResult mop = getObjectMouseOver(command); if( mop.typeOfHit == RayTraceResult.Type.BLOCK ) { if( player.inventory.getCurrentItem() != null ) { ItemStack itemStack = player.inventory.getCurrentItem(); Block b = Block.getBlockFromItem( itemStack.getItem() ); if( b != null ) { BlockPos pos = mop.getBlockPos().add( mop.sideHit.getDirectionVec() ); // Can we place this block here? AxisAlignedBB axisalignedbb = b.getDefaultState().getCollisionBoundingBox(player.world, pos); Entity exceptedEntity = (command == DiscreteMovementCommand.USE) ? null : player; // (Not ideal, but needed by jump-use to allow the player to place a block where their feet would be.) if (axisalignedbb == null || player.world.checkNoEntityCollision(axisalignedbb.offset(pos), exceptedEntity)) { boolean standOnBlockPlaced = (command == DiscreteMovementCommand.JUMPUSE && mop.getBlockPos().equals(new BlockPos(player.posX, player.posY - 1, player.posZ))); MalmoMod.network.sendToServer(new UseActionMessage(mop.getBlockPos(), itemStack, mop.sideHit, standOnBlockPlaced, mop.hitVec)); } } } } handled = true; break; } case JUMP: break; // Handled below. } // Handle jumping cases: if (command == DiscreteMovementCommand.JUMP || command == DiscreteMovementCommand.JUMPNORTH || command == DiscreteMovementCommand.JUMPEAST || command == DiscreteMovementCommand.JUMPSOUTH || command == DiscreteMovementCommand.JUMPWEST || command == DiscreteMovementCommand.JUMPMOVE || command == DiscreteMovementCommand.JUMPUSE || command == DiscreteMovementCommand.JUMPSTRAFE) y = 1; if (this.params.isAutoJump() && y == 0 && (z != 0 || x != 0)) { // Do we need to jump? if (!player.world.getCollisionBoxes(player, player.getEntityBoundingBox().offset(x, 0, z)).isEmpty()) y = 1; } if (z != 0 || x != 0 || y != 0) { // Attempt to move the entity: double oldX = player.posX; double oldZ = player.posZ; player.move(MoverType.SELF, (double)x, (double)y, (double)z); player.onUpdate(); if (this.params.isAutoFall()) { // Did we step off a block? If so, attempt to fast-forward our fall. int bailCountdown=256; // Give up after this many attempts // (This is needed because, for example, if the player is caught in a web, the downward movement will have no effect.) while (!player.onGround && !player.capabilities.isFlying && bailCountdown > 0) { // Fast-forward downwards. player.move(MoverType.SELF, 0.0, Math.floor(player.posY-0.0000001) - player.posY, 0.0); player.onUpdate(); bailCountdown--; } } // Now check where we ended up: double newX = player.posX; double newZ = player.posZ; // Are we still in the centre of a square, or did we get shunted? double offsetX = newX - Math.floor(newX); double offsetZ = newZ - Math.floor(newZ); if (Math.abs(offsetX - 0.5) + Math.abs(offsetZ - 0.5) > 0.01) { // We failed to move to the centre of the target square. // This might be because the target square was occupied, and we // were shunted back into our source square, // or it might be that the target square is occupied by something smaller // than one block (eg a fence post), and we're in the target square but // shunted off-centre. // Either way, we can't stay here, so move back to our original position. // Before we do that, fire off a message - this will give the TouchingBlockType handlers // a chance to react to the current position: DiscretePartialMoveEvent event = new DiscretePartialMoveEvent(player.posX, player.posY, player.posZ); MinecraftForge.EVENT_BUS.post(event); // Now adjust the player: player.move(MoverType.SELF, oldX - newX, 0.0, oldZ - newZ); player.onUpdate(); } // Now set the last tick pos values, to turn off inter-tick positional interpolation: player.lastTickPosX = player.posX; player.lastTickPosY = player.posY; player.lastTickPosZ = player.posZ; try { MalmoMod.getPropertiesForCurrentThread().put(MOVE_ATTEMPTED_KEY, true); } catch (Exception e) { // TODO - proper error reporting. System.out.println("Failed to access properties for the client thread after discrete movement - reward may be incorrect."); } handled = true; } } return handled; } private RayTraceResult getObjectMouseOver(DiscreteMovementCommand command) { RayTraceResult mop = null; if (command.equals(DiscreteMovementCommand.USE)) mop = Minecraft.getMinecraft().objectMouseOver; else if (command.equals(DiscreteMovementCommand.JUMPUSE)) { long partialTicks = 0; //Minecraft.timer.renderPartialTicks Entity viewer = Minecraft.getMinecraft().player; double blockReach = Minecraft.getMinecraft().playerController.getBlockReachDistance(); Vec3d eyePos = viewer.getPositionEyes(partialTicks); Vec3d lookVec = viewer.getLook(partialTicks); int yOffset = 1; // For the jump Vec3d searchVec = eyePos.addVector(lookVec.xCoord * blockReach, yOffset + lookVec.yCoord * blockReach, lookVec.zCoord * blockReach); mop = Minecraft.getMinecraft().world.rayTraceBlocks(eyePos, searchVec, false, false, false); } return mop; } @Override public void install(MissionInit missionInit) { } @Override public void deinstall(MissionInit missionInit) { } @Override public boolean isOverriding() { return this.isOverriding; } @Override public void setOverriding(boolean b) { this.isOverriding = b; } }