diff --git a/src/main/java/li/cil/oc2/client/ClientSetup.java b/src/main/java/li/cil/oc2/client/ClientSetup.java index 026986e1..661f171f 100644 --- a/src/main/java/li/cil/oc2/client/ClientSetup.java +++ b/src/main/java/li/cil/oc2/client/ClientSetup.java @@ -6,11 +6,13 @@ import li.cil.oc2.client.gui.ComputerContainerScreen; import li.cil.oc2.client.item.CustomItemModelProperties; import li.cil.oc2.client.model.BusCableModelLoader; import li.cil.oc2.client.renderer.NetworkCableRenderer; +import li.cil.oc2.client.renderer.entity.RobotEntityRenderer; import li.cil.oc2.client.renderer.tileentity.ComputerTileEntityRenderer; import li.cil.oc2.client.renderer.tileentity.NetworkConnectorTileEntityRenderer; import li.cil.oc2.common.Constants; import li.cil.oc2.common.bus.device.DeviceTypes; import li.cil.oc2.common.container.Containers; +import li.cil.oc2.common.entity.Entities; import li.cil.oc2.common.tileentity.TileEntities; import net.minecraft.client.gui.ScreenManager; import net.minecraft.inventory.container.PlayerContainer; @@ -20,6 +22,7 @@ import net.minecraftforge.client.event.TextureStitchEvent; import net.minecraftforge.client.model.ModelLoaderRegistry; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.client.registry.RenderingRegistry; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; public final class ClientSetup { @@ -32,6 +35,8 @@ public final class ClientSetup { ClientRegistry.bindTileEntityRenderer(TileEntities.COMPUTER_TILE_ENTITY.get(), ComputerTileEntityRenderer::new); ClientRegistry.bindTileEntityRenderer(TileEntities.NETWORK_CONNECTOR_TILE_ENTITY.get(), NetworkConnectorTileEntityRenderer::new); + + RenderingRegistry.registerEntityRenderingHandler(Entities.ROBOT.get(), RobotEntityRenderer::new); } @SubscribeEvent diff --git a/src/main/java/li/cil/oc2/client/renderer/entity/RobotEntityRenderer.java b/src/main/java/li/cil/oc2/client/renderer/entity/RobotEntityRenderer.java new file mode 100644 index 00000000..d7b76c86 --- /dev/null +++ b/src/main/java/li/cil/oc2/client/renderer/entity/RobotEntityRenderer.java @@ -0,0 +1,110 @@ +package li.cil.oc2.client.renderer.entity; + +import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.vertex.IVertexBuilder; +import li.cil.oc2.api.API; +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.IRenderTypeBuffer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererManager; +import net.minecraft.client.renderer.entity.model.EntityModel; +import net.minecraft.client.renderer.model.ModelRenderer; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.EntityRayTraceResult; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RayTraceResult; +import net.minecraft.util.math.vector.Vector3f; +import net.minecraft.util.text.StringTextComponent; +import net.minecraftforge.common.model.TransformationHelper; + +public final class RobotEntityRenderer extends EntityRenderer { + private final RobotModel model = new RobotModel(); + + /////////////////////////////////////////////////////////////////// + + public RobotEntityRenderer(final EntityRendererManager renderManager) { + super(renderManager); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public ResourceLocation getEntityTexture(final RobotEntity entity) { + return new ResourceLocation(API.MOD_ID, "textures/entity/robot/robot.png"); + } + + @Override + public void render(final RobotEntity entity, final float entityYaw, final float partialTicks, final MatrixStack matrixStack, final IRenderTypeBuffer buffer, final int packedLight) { + final RobotEntity.AnimationState state = entity.getAnimationState(); + state.update(partialTicks, entity.world.rand); + + matrixStack.push(); + // NB: we don't entityYaw given to use because that uses a plain lerp which can lead to ugly + // jumps in case we get a wrapped rotationYaw synced from the server (leading to ~360 + // degree delta to the last known previous rotation). Haven't figured out where to + // alternatively prevent this wrapping or patch the prev value instead. + final float partialRotation = MathHelper.degreesDifferenceAbs(entity.prevRotationYaw, entity.rotationYaw) * partialTicks; + final float rotation = MathHelper.approachDegrees(entity.prevRotationYaw, entity.rotationYaw, partialRotation); + matrixStack.rotate(Vector3f.YN.rotationDegrees(rotation)); + + model.setRotationAngles(entity, 0, 0, 0, 0, 0); + + final IVertexBuilder builder = buffer.getBuffer(model.getRenderType(getEntityTexture(entity))); + model.render(matrixStack, builder, packedLight, OverlayTexture.NO_OVERLAY, 1, 1, 1, 1); + + matrixStack.pop(); + + final RayTraceResult hit = Minecraft.getInstance().objectMouseOver; + if (hit instanceof EntityRayTraceResult && entity == ((EntityRayTraceResult) hit).getEntity()) { + super.renderName(entity, new StringTextComponent("hi"), matrixStack, buffer, packedLight); + } + } + + /////////////////////////////////////////////////////////////////// + + private static final class RobotModel extends EntityModel { + private final ModelRenderer topRenderer; + private final ModelRenderer baseRenderer; + private final ModelRenderer coreRenderer; + private float baseY, topY; + private final float[] topRotation = new float[3]; + + public RobotModel() { + topRenderer = new ModelRenderer(this, 1, 1) + .setTextureSize(64, 64) + .addBox(-7, 8, -7, 14, 6, 14); + baseRenderer = new ModelRenderer(this, 1, 23) + .setTextureSize(64, 64) + .addBox(-7, 0, -7, 14, 7, 14); + coreRenderer = new ModelRenderer(this, 1, 34) + .setTextureSize(64, 64) + .addBox(-6, 7, -6, 12, 1, 12); + } + + @Override + public void setRotationAngles(final RobotEntity entity, final float limbSwing, final float limbSwingAmount, final float ageInTicks, final float netHeadYaw, final float headPitch) { + final RobotEntity.AnimationState state = entity.getAnimationState(); + baseY = state.baseRenderOffsetY; + topY = state.topRenderOffsetY; + topRotation[1] = state.topRenderRotationY; + } + + @Override + public void render(final MatrixStack matrixStack, final IVertexBuilder buffer, final int packedLight, final int packedOverlay, final float red, final float green, final float blue, final float alpha) { + matrixStack.push(); + matrixStack.translate(0, topY, 0); + matrixStack.rotate(TransformationHelper.quatFromXYZ(topRotation, true)); + topRenderer.render(matrixStack, buffer, packedLight, packedOverlay); + matrixStack.pop(); + + matrixStack.push(); + matrixStack.translate(0, baseY, 0); + baseRenderer.render(matrixStack, buffer, packedLight, packedOverlay); + coreRenderer.render(matrixStack, buffer, LightTexture.packLight(15, 15), packedOverlay); + matrixStack.pop(); + } + } +} diff --git a/src/main/java/li/cil/oc2/client/renderer/entity/package-info.java b/src/main/java/li/cil/oc2/client/renderer/entity/package-info.java new file mode 100644 index 00000000..a9e7eb39 --- /dev/null +++ b/src/main/java/li/cil/oc2/client/renderer/entity/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.client.renderer.entity; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/li/cil/oc2/common/Constants.java b/src/main/java/li/cil/oc2/common/Constants.java index 9bf3b989..c68fb0a9 100644 --- a/src/main/java/li/cil/oc2/common/Constants.java +++ b/src/main/java/li/cil/oc2/common/Constants.java @@ -41,6 +41,10 @@ public final class Constants { /////////////////////////////////////////////////////////////////// + public static final String ROBOT_ENTITY_NAME = "robot"; + + /////////////////////////////////////////////////////////////////// + public static final String CONFIG_MAX_ALLOCATED_MEMORY = "config.oc2.maxAllocatedMemory"; public static final String CONFIG_MAX_MEMORY_SIZE = "config.oc2.maxMemorySize"; public static final String CONFIG_MAX_HARD_DRIVE_SIZE = "config.oc2.maxHardDriveSize"; diff --git a/src/main/java/li/cil/oc2/common/Main.java b/src/main/java/li/cil/oc2/common/Main.java index fa32de5e..9b6e290f 100644 --- a/src/main/java/li/cil/oc2/common/Main.java +++ b/src/main/java/li/cil/oc2/common/Main.java @@ -9,6 +9,7 @@ import li.cil.oc2.common.bus.device.data.BaseBlockDevices; import li.cil.oc2.common.bus.device.data.Firmwares; import li.cil.oc2.common.bus.device.provider.Providers; import li.cil.oc2.common.container.Containers; +import li.cil.oc2.common.entity.Entities; import li.cil.oc2.common.item.Items; import li.cil.oc2.common.serialization.serializers.Serializers; import li.cil.oc2.common.tileentity.TileEntities; @@ -28,6 +29,7 @@ public final class Main { Items.initialize(); Blocks.initialize(); TileEntities.initialize(); + Entities.initialize(); Containers.initialize(); Providers.initialize(); DeviceTypes.initialize(); diff --git a/src/main/java/li/cil/oc2/common/entity/Entities.java b/src/main/java/li/cil/oc2/common/entity/Entities.java new file mode 100644 index 00000000..11b66c08 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/Entities.java @@ -0,0 +1,33 @@ +package li.cil.oc2.common.entity; + +import li.cil.oc2.api.API; +import li.cil.oc2.common.Constants; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityClassification; +import net.minecraft.entity.EntityType; +import net.minecraftforge.fml.RegistryObject; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; + +import java.util.function.Function; + +public final class Entities { + private static final DeferredRegister> ENTITIES = DeferredRegister.create(ForgeRegistries.ENTITIES, API.MOD_ID); + + /////////////////////////////////////////////////////////////////// + + public static final RegistryObject> ROBOT = register(Constants.ROBOT_ENTITY_NAME, RobotEntity::new, EntityClassification.MISC, b -> b.size(14f / 16f, 14f / 16f).immuneToFire().disableSummoning()); + + /////////////////////////////////////////////////////////////////// + + public static void initialize() { + ENTITIES.register(FMLJavaModLoadingContext.get().getModEventBus()); + } + + /////////////////////////////////////////////////////////////////// + + private static RegistryObject> register(final String name, final EntityType.IFactory factory, final EntityClassification classification, final Function, EntityType.Builder> customizer) { + return ENTITIES.register(name, () -> customizer.apply(EntityType.Builder.create(factory, classification)).build(name)); + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/RobotEntity.java b/src/main/java/li/cil/oc2/common/entity/RobotEntity.java new file mode 100644 index 00000000..123c1214 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/RobotEntity.java @@ -0,0 +1,323 @@ +package li.cil.oc2.common.entity; + +import li.cil.oc2.common.entity.robot.*; +import li.cil.oc2.common.integration.Wrenches; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.oc2.common.util.WorldUtils; +import net.minecraft.block.SoundType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.network.IPacket; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.ActionResultType; +import net.minecraft.util.Hand; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.network.NetworkHooks; + +import javax.annotation.Nullable; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.Random; + +public final class RobotEntity extends Entity { + public static int MAX_QUEUED_ACTIONS = 16; + + private static final String COMMAND_PROCESSOR_TAG_NAME = "commands"; + + private static final DataParameter IS_RUNNING = EntityDataManager.createKey(RobotEntity.class, DataSerializers.BOOLEAN); + + /////////////////////////////////////////////////////////////////// + + private final AnimationState animationState = new AnimationState(); + private final CommandProcessor commandProcessor = new CommandProcessor(); + + /////////////////////////////////////////////////////////////////// + + public RobotEntity(final EntityType type, final World world) { + super(type, world); + this.preventEntitySpawning = true; + setNoGravity(true); + } + + /////////////////////////////////////////////////////////////////// + + @OnlyIn(Dist.CLIENT) + public AnimationState getAnimationState() { + return animationState; + } + + public boolean isRunning() { + return dataManager.get(IS_RUNNING); + } + + public void start() { + // todo start vm + } + + public void stop() { + // todo stop vm + + commandProcessor.clear(); + } + + @Override + public void tick() { + super.tick(); + + commandProcessor.tick(); + } + + @Override + public ActionResultType processInitialInteract(final PlayerEntity player, final Hand hand) { + final ItemStack stack = player.getHeldItem(hand); + if (Wrenches.isWrench(stack)) { + if (!world.isRemote()) { + if (player.isSneaking()) { + remove(); + WorldUtils.playSound(world, getPosition(), SoundType.METAL, SoundType::getBreakSound); + } else { + // todo open container + } + } + } else { + if (player.isSneaking()) { + // TODO start machine + if (!world.isRemote()) { + dataManager.set(IS_RUNNING, !dataManager.get(IS_RUNNING)); + } + } else { +// if (rand.nextBoolean()) { +// commandProcessor.move(MovementDirection.values()[rand.nextInt(MovementDirection.values().length)]); +// } else { +// commandProcessor.rotate(rand.nextBoolean() ? RotationDirection.LEFT : RotationDirection.RIGHT); + commandProcessor.rotate(RotationDirection.RIGHT); +// } + // TODO open terminal + inventory screen + } + } + + return ActionResultType.SUCCESS; + } + + @Override + public IPacket createSpawnPacket() { + return NetworkHooks.getEntitySpawningPacket(this); + } + + @Override + public boolean canBeCollidedWith() { + return true; + } + + @Override + public boolean canCollide(final Entity entity) { + return entity != this; + } + + @Override + public void applyEntityCollision(final Entity entity) { + } + + @Override + public boolean func_241845_aY() { + return true; + } + + @Override + public boolean shouldSpawnRunningEffects() { + return false; + } + + /////////////////////////////////////////////////////////////////// + + @Override + protected void registerData() { + getDataManager().register(IS_RUNNING, false); + RobotActions.registerData(getDataManager()); + } + + @Override + protected void writeAdditional(final CompoundNBT compound) { + compound.put(COMMAND_PROCESSOR_TAG_NAME, commandProcessor.serialize()); + } + + @Override + protected void readAdditional(final CompoundNBT compound) { + commandProcessor.deserialize(compound.getCompound(COMMAND_PROCESSOR_TAG_NAME)); + } + + @Override + protected boolean canTriggerWalking() { + return false; + } + + @Override + protected void doBlockCollisions() { + } + + /////////////////////////////////////////////////////////////////// + + private static float lerpClamped(final float from, final float to, final float delta) { + if (from < to) { + return Math.min(from + delta, to); + } else if (from > to) { + return Math.max(from - delta, to); + } else { + return from; + } + } + + private static float remapFrom01To(final float x, final float a1, final float b1) { + if (a1 == b1) { + return a1; + } else { + return x * (b1 - a1) + a1; + } + } + + /////////////////////////////////////////////////////////////////// + + public final class AnimationState { + private static final float TOP_IDLE_Y = -2f / 16f; + private static final float BASE_IDLE_Y = -1f / 16f; + + private static final float TRANSLATION_SPEED = 0.005f; + private static final float ROTATION_SPEED = 1f; + private static final float MAX_ROTATION = 5f; + private static final float MIN_ROTATION_SPEED = 0.055f; + private static final float MAX_ROTATION_SPEED = 0.060f; + private static final float HOVER_ANIMATION_SPEED = 0.01f; + + public float topRenderOffsetY = TOP_IDLE_Y; + public float baseRenderOffsetY = BASE_IDLE_Y; + public float topRenderRotationY; + public float topRenderTargetRotationY; + public float topRenderRotationSpeed; + public float topRenderHover = -(hashCode() & 0xFFFF); // init to "random" to avoid synchronous hovering + + public void update(final float deltaTime, final Random random) { + if (isRunning() || commandProcessor.hasQueuedActions()) { + topRenderHover = topRenderHover + deltaTime * HOVER_ANIMATION_SPEED; + final float topOffsetY = MathHelper.sin(topRenderHover) / 32f; + + topRenderOffsetY = lerpClamped(topRenderOffsetY, topOffsetY, deltaTime * TRANSLATION_SPEED); + baseRenderOffsetY = lerpClamped(baseRenderOffsetY, topOffsetY, deltaTime * TRANSLATION_SPEED); + + topRenderRotationY = lerpClamped(topRenderRotationY, topRenderTargetRotationY, deltaTime * topRenderRotationSpeed); + if (topRenderRotationY == topRenderTargetRotationY) { + topRenderTargetRotationY = remapFrom01To(random.nextFloat(), -MAX_ROTATION, MAX_ROTATION); + topRenderRotationSpeed = remapFrom01To(random.nextFloat(), MIN_ROTATION_SPEED, MAX_ROTATION_SPEED); + } + } else { + topRenderOffsetY = lerpClamped(topRenderOffsetY, TOP_IDLE_Y, deltaTime * TRANSLATION_SPEED * 2); + baseRenderOffsetY = lerpClamped(baseRenderOffsetY, BASE_IDLE_Y, deltaTime * TRANSLATION_SPEED); + + topRenderRotationY = lerpClamped(topRenderRotationY, 0, deltaTime * ROTATION_SPEED); + } + } + } + + private final class CommandProcessor { + private static final String QUEUE_TAG_NAME = "queue"; + private static final String ACTION_TAG_NAME = "action"; + + private final Queue queue = new ArrayDeque<>(MAX_QUEUED_ACTIONS); + @Nullable private AbstractRobotAction action; + + public boolean hasQueuedActions() { + return action != null || !queue.isEmpty(); + } + + public boolean move(final MovementDirection direction) { + return addAction(new RobotMovementAction(direction)); + } + + public boolean rotate(final RotationDirection direction) { + return addAction(new RobotRotationAction(direction)); + } + + public void tick() { + if (getEntityWorld().isRemote()) { + RobotActions.performClient(RobotEntity.this); + } else { + if (action != null) { + if (action.perform(RobotEntity.this)) { + action = null; + } + } + if (action == null) { + action = queue.poll(); + if (action != null) { + action.initialize(RobotEntity.this); + } + } + } + } + + public void clear() { + queue.clear(); + } + + public CompoundNBT serialize() { + final CompoundNBT tag = new CompoundNBT(); + + final ListNBT queueTag = new ListNBT(); + for (final AbstractRobotAction action : queue) { + queueTag.add(RobotActions.serialize(action)); + } + tag.put(QUEUE_TAG_NAME, queueTag); + + if (action != null) { + tag.put(ACTION_TAG_NAME, RobotActions.serialize(action)); + } + + return tag; + } + + public void deserialize(final CompoundNBT tag) { + queue.clear(); + action = null; + + final ListNBT queueTag = tag.getList(QUEUE_TAG_NAME, NBTTagIds.TAG_COMPOUND); + for (int i = 0; i < queueTag.size(); i++) { + final AbstractRobotAction action = RobotActions.deserialize(queueTag.getCompound(i)); + if (action != null) { + queue.add(action); + } + } + + if (tag.contains(ACTION_TAG_NAME, NBTTagIds.TAG_COMPOUND)) { + action = RobotActions.deserialize(tag.getCompound(ACTION_TAG_NAME)); + action.initialize(RobotEntity.this); + } + } + + /////////////////////////////////////////////////////////////////// + + private boolean addAction(final AbstractRobotAction action) { + if (getEntityWorld().isRemote()) { + return false; + } + + if (!isRunning()) { + return false; + } + + if (queue.size() < MAX_QUEUED_ACTIONS) { + queue.add(action); + return true; + } else { + return false; + } + } + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/package-info.java b/src/main/java/li/cil/oc2/common/entity/package-info.java new file mode 100644 index 00000000..62dae114 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.common.entity; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotAction.java b/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotAction.java new file mode 100644 index 00000000..bd2a6186 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotAction.java @@ -0,0 +1,39 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.nbt.CompoundNBT; + +public abstract class AbstractRobotAction { + private final AbstractRobotActionType type; + + /////////////////////////////////////////////////////////////////// + + public AbstractRobotAction(final AbstractRobotActionType type) { + this.type = type; + } + + public AbstractRobotAction(final AbstractRobotActionType type, final CompoundNBT tag) { + this(type); + deserialize(tag); + } + + /////////////////////////////////////////////////////////////////// + + public AbstractRobotActionType getType() { + return type; + } + + public void initialize(final RobotEntity robot) { + } + + public abstract boolean perform(RobotEntity robot); + + public CompoundNBT serialize() { + return new CompoundNBT(); + } + + /////////////////////////////////////////////////////////////////// + + protected void deserialize(final CompoundNBT tag) { + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotActionType.java b/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotActionType.java new file mode 100644 index 00000000..79fd1485 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotActionType.java @@ -0,0 +1,32 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.EntityDataManager; + +public abstract class AbstractRobotActionType { + private final int id; + + /////////////////////////////////////////////////////////////////// + + protected AbstractRobotActionType(final int id) { + this.id = id; + } + + /////////////////////////////////////////////////////////////////// + + public int getId() { + return id; + } + + public void registerData(final EntityDataManager dataManager) { + } + + public void initializeData(final RobotEntity robot) { + } + + public void performClient(final RobotEntity robot) { + } + + public abstract AbstractRobotAction deserialize(final CompoundNBT tag); +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/MovementDirection.java b/src/main/java/li/cil/oc2/common/entity/robot/MovementDirection.java new file mode 100644 index 00000000..3d026fb0 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/MovementDirection.java @@ -0,0 +1,8 @@ +package li.cil.oc2.common.entity.robot; + +public enum MovementDirection { + UP, + DOWN, + FORWARD, + BACKWARD, +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RobotActions.java b/src/main/java/li/cil/oc2/common/entity/robot/RobotActions.java new file mode 100644 index 00000000..822e9f83 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RobotActions.java @@ -0,0 +1,65 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.EntityDataManager; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.function.IntFunction; + +public final class RobotActions { + private static final String ACTION_TYPE_TAG_NAME = "action_type"; + + private static final ArrayList ACTIONS = new ArrayList<>(); + + /////////////////////////////////////////////////////////////////// + + public static final AbstractRobotActionType MOVEMENT = register(RobotMovementActionType::new); + public static final AbstractRobotActionType ROTATION = register(RobotRotationActionType::new); + + /////////////////////////////////////////////////////////////////// + + public static void registerData(final EntityDataManager dataManager) { + for (final AbstractRobotActionType type : ACTIONS) { + type.registerData(dataManager); + } + } + + public static void initializeData(final RobotEntity robot) { + for (final AbstractRobotActionType type : ACTIONS) { + type.initializeData(robot); + } + } + + public static void performClient(final RobotEntity robot) { + for (final AbstractRobotActionType type : ACTIONS) { + type.performClient(robot); + } + } + + public static CompoundNBT serialize(final AbstractRobotAction action) { + final CompoundNBT actionTag = action.serialize(); + actionTag.putInt(ACTION_TYPE_TAG_NAME, action.getType().getId()); + return actionTag; + } + + @Nullable + public static AbstractRobotAction deserialize(final CompoundNBT tag) { + final int type = tag.getInt(ACTION_TYPE_TAG_NAME); + if (type < 1 || type > ACTIONS.size()) { + return null; + } + + final AbstractRobotActionType actionType = ACTIONS.get(type - 1); + return actionType.deserialize(tag); + } + + /////////////////////////////////////////////////////////////////// + + private static AbstractRobotActionType register(final IntFunction factory) { + final AbstractRobotActionType type = factory.apply(ACTIONS.size() + 1); + ACTIONS.add(type); + return type; + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementAction.java b/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementAction.java new file mode 100644 index 00000000..5cb678fd --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementAction.java @@ -0,0 +1,142 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.Constants; +import li.cil.oc2.common.entity.Entities; +import li.cil.oc2.common.entity.RobotEntity; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.oc2.common.util.NBTUtils; +import net.minecraft.entity.MoverType; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; + +import javax.annotation.Nullable; + +public final class RobotMovementAction extends AbstractRobotAction { + public static final DataParameter TARGET_POSITION = EntityDataManager.createKey(RobotEntity.class, DataSerializers.BLOCK_POS); + public static final double TARGET_EPSILON = 0.0001; + + /////////////////////////////////////////////////////////////////// + + private static final float MOVEMENT_SPEED = 0.5f / Constants.TICK_SECONDS; // In blocks per second. + + private static final String DIRECTION_TAG_NAME = "direction"; + private static final String START_TAG_NAME = "start"; + private static final String TARGET_TAG_NAME = "start"; + + /////////////////////////////////////////////////////////////////// + + private MovementDirection direction; + @Nullable private BlockPos start; + @Nullable private Vector3d target; + + /////////////////////////////////////////////////////////////////// + + public RobotMovementAction(final MovementDirection direction) { + super(RobotActions.MOVEMENT); + this.direction = direction; + } + + RobotMovementAction(final CompoundNBT tag) { + super(RobotActions.MOVEMENT); + deserialize(tag); + } + + /////////////////////////////////////////////////////////////////// + + public static Vector3d getTargetPositionInBlock(final BlockPos position) { + return Vector3d.copyCenteredHorizontally(position).add(0, 0.5f * (1 - Entities.ROBOT.get().getHeight()), 0); + } + + public static void moveTowards(final RobotEntity robot, final Vector3d targetPosition) { + Vector3d delta = targetPosition.subtract(robot.getPositionVec()); + if (delta.lengthSquared() > MOVEMENT_SPEED * MOVEMENT_SPEED) { + delta = delta.normalize().scale(MOVEMENT_SPEED); + } + + robot.move(MoverType.SELF, delta); + } + + @Override + public void initialize(final RobotEntity robot) { + if (target == null) { + start = robot.getPosition(); + BlockPos targetPosition = start; + switch (direction) { + case UP: + targetPosition = targetPosition.offset(Direction.UP); + break; + case DOWN: + targetPosition = targetPosition.offset(Direction.DOWN); + break; + case FORWARD: + targetPosition = targetPosition.offset(robot.getHorizontalFacing()); + break; + case BACKWARD: + targetPosition = targetPosition.offset(robot.getHorizontalFacing().getOpposite()); + break; + } + + target = getTargetPositionInBlock(targetPosition); + } + + robot.getDataManager().set(TARGET_POSITION, new BlockPos(target)); + } + + @Override + public boolean perform(final RobotEntity robot) { + if (target == null) { + return true; + } + + moveTowards(robot, target); + + final boolean didCollide = robot.collidedHorizontally || robot.collidedVertically; + if (didCollide && !robot.getEntityWorld().isRemote()) { + if (start != null) { + target = getTargetPositionInBlock(start); + robot.getDataManager().set(TARGET_POSITION, start); + + start = null; + } else { + // todo if it's a block, try to break it. or just drop ourselves? + } + } + + return robot.getPositionVec().squareDistanceTo(target) < TARGET_EPSILON; + } + + /////////////////////////////////////////////////////////////////// + + @Override + public CompoundNBT serialize() { + final CompoundNBT tag = super.serialize(); + + NBTUtils.putEnum(tag, DIRECTION_TAG_NAME, direction); + if (start != null) { + NBTUtils.putBlockPos(tag, START_TAG_NAME, start); + } + if (target != null) { + NBTUtils.putVector3d(tag, TARGET_TAG_NAME, target); + } + + return tag; + } + + @Override + protected void deserialize(final CompoundNBT tag) { + super.deserialize(tag); + + direction = NBTUtils.getEnum(tag, DIRECTION_TAG_NAME, MovementDirection.class); + if (tag.contains(START_TAG_NAME, NBTTagIds.TAG_COMPOUND)) { + start = NBTUtils.getBlockPos(tag, START_TAG_NAME); + } + if (tag.contains(TARGET_TAG_NAME, NBTTagIds.TAG_COMPOUND)) { + target = NBTUtils.getVector3d(tag, TARGET_TAG_NAME); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementActionType.java b/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementActionType.java new file mode 100644 index 00000000..268e95cf --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RobotMovementActionType.java @@ -0,0 +1,38 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; + +public final class RobotMovementActionType extends AbstractRobotActionType { + public RobotMovementActionType(final int id) { + super(id); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public void registerData(final EntityDataManager dataManager) { + dataManager.register(RobotMovementAction.TARGET_POSITION, BlockPos.ZERO); + } + + @Override + public void initializeData(final RobotEntity robot) { + robot.getDataManager().set(RobotMovementAction.TARGET_POSITION, robot.getPosition()); + } + + @Override + public void performClient(final RobotEntity robot) { + final Vector3d target = RobotMovementAction.getTargetPositionInBlock(robot.getDataManager().get(RobotMovementAction.TARGET_POSITION)); + if (robot.getPositionVec().squareDistanceTo(target) > RobotMovementAction.TARGET_EPSILON) { + RobotMovementAction.moveTowards(robot, target); + } + } + + @Override + public AbstractRobotAction deserialize(final CompoundNBT tag) { + return new RobotMovementAction(tag); + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationAction.java b/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationAction.java new file mode 100644 index 00000000..b106f36a --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationAction.java @@ -0,0 +1,101 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.Constants; +import li.cil.oc2.common.entity.RobotEntity; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.oc2.common.util.NBTUtils; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.Direction; +import net.minecraft.util.math.MathHelper; + +import javax.annotation.Nullable; + +public final class RobotRotationAction extends AbstractRobotAction { + public static final DataParameter TARGET_DIRECTION = EntityDataManager.createKey(RobotEntity.class, DataSerializers.DIRECTION); + public static final float TARGET_EPSILON = 0.0001f; + + /////////////////////////////////////////////////////////////////// + + private static final float ROTATION_SPEED = 45f / Constants.TICK_SECONDS; // In degrees per second. + + private static final String DIRECTION_TAG_NAME = "direction"; + private static final String TARGET_TAG_NAME = "start"; + + /////////////////////////////////////////////////////////////////// + + private RotationDirection direction; + @Nullable private Direction target; + + /////////////////////////////////////////////////////////////////// + + public RobotRotationAction(final RotationDirection direction) { + super(RobotActions.ROTATION); + this.direction = direction; + } + + RobotRotationAction(final CompoundNBT tag) { + super(RobotActions.ROTATION); + deserialize(tag); + } + + /////////////////////////////////////////////////////////////////// + + public static void rotateTowards(final RobotEntity robot, final Direction targetRotation) { + robot.rotationYaw = MathHelper.approachDegrees(robot.rotationYaw, targetRotation.getHorizontalAngle(), ROTATION_SPEED); + } + + @Override + public void initialize(final RobotEntity robot) { + if (target == null) { + target = robot.getHorizontalFacing(); + switch (direction) { + case LEFT: + target = target.rotateYCCW(); + break; + case RIGHT: + target = target.rotateY(); + break; + } + } + + robot.getDataManager().set(TARGET_DIRECTION, target); + } + + @Override + public boolean perform(final RobotEntity robot) { + if (target == null) { + return true; + } + + rotateTowards(robot, target); + + return MathHelper.degreesDifferenceAbs(robot.rotationYaw, target.getHorizontalAngle()) < TARGET_EPSILON; + } + + /////////////////////////////////////////////////////////////////// + + @Override + public CompoundNBT serialize() { + final CompoundNBT tag = super.serialize(); + + NBTUtils.putEnum(tag, DIRECTION_TAG_NAME, direction); + if (target != null) { + NBTUtils.putEnum(tag, TARGET_TAG_NAME, target); + } + + return tag; + } + + @Override + protected void deserialize(final CompoundNBT tag) { + super.deserialize(tag); + + direction = NBTUtils.getEnum(tag, DIRECTION_TAG_NAME, RotationDirection.class); + if (tag.contains(TARGET_TAG_NAME, NBTTagIds.TAG_INT)) { + target = NBTUtils.getEnum(tag, TARGET_TAG_NAME, Direction.class); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationActionType.java b/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationActionType.java new file mode 100644 index 00000000..81a8f513 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RobotRotationActionType.java @@ -0,0 +1,38 @@ +package li.cil.oc2.common.entity.robot; + +import li.cil.oc2.common.entity.RobotEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.util.Direction; +import net.minecraft.util.math.MathHelper; + +public final class RobotRotationActionType extends AbstractRobotActionType { + public RobotRotationActionType(final int id) { + super(id); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public void registerData(final EntityDataManager dataManager) { + dataManager.register(RobotRotationAction.TARGET_DIRECTION, Direction.NORTH); + } + + @Override + public void initializeData(final RobotEntity robot) { + robot.getDataManager().set(RobotRotationAction.TARGET_DIRECTION, robot.getHorizontalFacing()); + } + + @Override + public void performClient(final RobotEntity robot) { + final Direction target = robot.getDataManager().get(RobotRotationAction.TARGET_DIRECTION); + if (MathHelper.degreesDifferenceAbs(robot.rotationYaw, target.getHorizontalAngle()) > RobotRotationAction.TARGET_EPSILON) { + RobotRotationAction.rotateTowards(robot, target); + } + } + + @Override + public AbstractRobotAction deserialize(final CompoundNBT tag) { + return new RobotRotationAction(tag); + } +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/RotationDirection.java b/src/main/java/li/cil/oc2/common/entity/robot/RotationDirection.java new file mode 100644 index 00000000..1635c643 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/RotationDirection.java @@ -0,0 +1,6 @@ +package li.cil.oc2.common.entity.robot; + +public enum RotationDirection { + LEFT, + RIGHT, +} diff --git a/src/main/java/li/cil/oc2/common/entity/robot/package-info.java b/src/main/java/li/cil/oc2/common/entity/robot/package-info.java new file mode 100644 index 00000000..f0afb44f --- /dev/null +++ b/src/main/java/li/cil/oc2/common/entity/robot/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.common.entity.robot; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/li/cil/oc2/common/item/Items.java b/src/main/java/li/cil/oc2/common/item/Items.java index 3ddff545..202bb03f 100644 --- a/src/main/java/li/cil/oc2/common/item/Items.java +++ b/src/main/java/li/cil/oc2/common/item/Items.java @@ -30,6 +30,7 @@ public final class Items { public static final RegistryObject BUS_INTERFACE_ITEM = register(Constants.BUS_INTERFACE_ITEM_NAME, BusInterfaceItem::new); public static final RegistryObject NETWORK_CABLE_ITEM = register(Constants.NETWORK_CABLE_ITEM_NAME, NetworkCableItem::new); + public static final RegistryObject ROBOT_ITEM = register(Constants.ROBOT_ENTITY_NAME, RobotItem::new); public static final RegistryObject MEMORY_ITEM = register(Constants.MEMORY_ITEM_NAME, MemoryItem::new, new Item.Properties()); public static final RegistryObject HARD_DRIVE_ITEM = register(Constants.HARD_DRIVE_ITEM_NAME, HardDriveItem::new, new Item.Properties()); diff --git a/src/main/java/li/cil/oc2/common/item/RobotItem.java b/src/main/java/li/cil/oc2/common/item/RobotItem.java new file mode 100644 index 00000000..c948e8f4 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/item/RobotItem.java @@ -0,0 +1,56 @@ +package li.cil.oc2.common.item; + +import li.cil.oc2.common.entity.Entities; +import li.cil.oc2.common.entity.RobotEntity; +import li.cil.oc2.common.entity.robot.RobotActions; +import li.cil.oc2.common.util.WorldUtils; +import net.minecraft.block.SoundType; +import net.minecraft.item.BlockItemUseContext; +import net.minecraft.item.Item; +import net.minecraft.item.ItemUseContext; +import net.minecraft.stats.Stats; +import net.minecraft.util.ActionResultType; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraft.world.World; + +public final class RobotItem extends Item { + public RobotItem(final Properties properties) { + super(properties); + } + + @Override + public ActionResultType onItemUse(final ItemUseContext context) { + final World world = context.getWorld(); + final BlockPos pos = context.getPos(); + + final Vector3d position; + if (world.getBlockState(pos).isReplaceable(new BlockItemUseContext(context))) { + position = Vector3d.copyCentered(pos); + } else { + position = Vector3d.copyCentered(pos.offset(context.getFace())); + } + + final RobotEntity robot = Entities.ROBOT.get().create(context.getWorld()); + robot.setLocationAndAngles(position.getX(), position.getY() - robot.getHeight() * 0.5f, position.getZ(), + Direction.fromAngle(context.getPlacementYaw()).getOpposite().getHorizontalAngle(), 0); + if (!world.hasNoCollisions(robot)) { + return super.onItemUse(context); + } + + RobotActions.initializeData(robot); + + if (!world.isRemote()) { + WorldUtils.playSound(world, new BlockPos(position), SoundType.METAL, SoundType::getPlaceSound); + world.addEntity(robot); + if (!context.getPlayer().isCreative()) { + context.getItem().shrink(1); + } + } + + context.getPlayer().addStat(Stats.ITEM_USED.get(this)); + + return ActionResultType.SUCCESS; + } +} diff --git a/src/main/java/li/cil/oc2/common/util/NBTUtils.java b/src/main/java/li/cil/oc2/common/util/NBTUtils.java index a4bb25e9..e43a4390 100644 --- a/src/main/java/li/cil/oc2/common/util/NBTUtils.java +++ b/src/main/java/li/cil/oc2/common/util/NBTUtils.java @@ -2,6 +2,8 @@ package li.cil.oc2.common.util; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompoundNBT; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; import net.minecraftforge.items.ItemStackHandler; import javax.annotation.Nullable; @@ -27,6 +29,40 @@ public final class NBTUtils { } } + public static void putBlockPos(final CompoundNBT tag, final String key, final BlockPos value) { + final CompoundNBT valueTag = new CompoundNBT(); + valueTag.putInt("x", value.getX()); + valueTag.putInt("y", value.getY()); + valueTag.putInt("z", value.getZ()); + tag.put(key, valueTag); + } + + public static BlockPos getBlockPos(final CompoundNBT tag, final String key) { + final CompoundNBT valueTag = tag.getCompound(key); + return new BlockPos( + valueTag.getInt("x"), + valueTag.getInt("y"), + valueTag.getInt("z") + ); + } + + public static void putVector3d(final CompoundNBT tag, final String key, final Vector3d value) { + final CompoundNBT valueTag = new CompoundNBT(); + valueTag.putDouble("x", value.getX()); + valueTag.putDouble("y", value.getY()); + valueTag.putDouble("z", value.getZ()); + tag.put(key, valueTag); + } + + public static Vector3d getVector3d(final CompoundNBT tag, final String key) { + final CompoundNBT valueTag = tag.getCompound(key); + return new Vector3d( + valueTag.getDouble("x"), + valueTag.getDouble("y"), + valueTag.getDouble("z") + ); + } + public static CompoundNBT makeInventoryTag(final ItemStack... items) { final ItemStackHandler itemStackHandler = new ItemStackHandler(items.length); for (int i = 0; i < items.length; i++) { diff --git a/src/main/resources/assets/oc2/textures/entity/robot/robot.png b/src/main/resources/assets/oc2/textures/entity/robot/robot.png new file mode 100644 index 00000000..1071b5f2 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/entity/robot/robot.png differ