From 54bac6077c7e5829378fe652a75fe43bb9826d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Wed, 13 Jan 2021 01:02:09 +0100 Subject: [PATCH] Started working on the robbit. --- .../java/li/cil/oc2/client/ClientSetup.java | 5 + .../renderer/entity/RobotEntityRenderer.java | 110 ++++++ .../client/renderer/entity/package-info.java | 7 + .../java/li/cil/oc2/common/Constants.java | 4 + src/main/java/li/cil/oc2/common/Main.java | 2 + .../li/cil/oc2/common/entity/Entities.java | 33 ++ .../li/cil/oc2/common/entity/RobotEntity.java | 323 ++++++++++++++++++ .../cil/oc2/common/entity/package-info.java | 7 + .../entity/robot/AbstractRobotAction.java | 39 +++ .../entity/robot/AbstractRobotActionType.java | 32 ++ .../entity/robot/MovementDirection.java | 8 + .../oc2/common/entity/robot/RobotActions.java | 65 ++++ .../entity/robot/RobotMovementAction.java | 142 ++++++++ .../entity/robot/RobotMovementActionType.java | 38 +++ .../entity/robot/RobotRotationAction.java | 101 ++++++ .../entity/robot/RobotRotationActionType.java | 38 +++ .../entity/robot/RotationDirection.java | 6 + .../oc2/common/entity/robot/package-info.java | 7 + .../java/li/cil/oc2/common/item/Items.java | 1 + .../li/cil/oc2/common/item/RobotItem.java | 56 +++ .../java/li/cil/oc2/common/util/NBTUtils.java | 36 ++ .../oc2/textures/entity/robot/robot.png | Bin 0 -> 4797 bytes 22 files changed, 1060 insertions(+) create mode 100644 src/main/java/li/cil/oc2/client/renderer/entity/RobotEntityRenderer.java create mode 100644 src/main/java/li/cil/oc2/client/renderer/entity/package-info.java create mode 100644 src/main/java/li/cil/oc2/common/entity/Entities.java create mode 100644 src/main/java/li/cil/oc2/common/entity/RobotEntity.java create mode 100644 src/main/java/li/cil/oc2/common/entity/package-info.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotAction.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/AbstractRobotActionType.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/MovementDirection.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RobotActions.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RobotMovementAction.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RobotMovementActionType.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RobotRotationAction.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RobotRotationActionType.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/RotationDirection.java create mode 100644 src/main/java/li/cil/oc2/common/entity/robot/package-info.java create mode 100644 src/main/java/li/cil/oc2/common/item/RobotItem.java create mode 100644 src/main/resources/assets/oc2/textures/entity/robot/robot.png 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 0000000000000000000000000000000000000000..1071b5f274ca23cb2057e5321bcb04edd05acae9 GIT binary patch literal 4797 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 yMvxzYR