diff --git a/src/main/java/rearth/oritech/block/base/entity/MachineBlockEntity.java b/src/main/java/rearth/oritech/block/base/entity/MachineBlockEntity.java index b61bb9a10..d6cebc11f 100644 --- a/src/main/java/rearth/oritech/block/base/entity/MachineBlockEntity.java +++ b/src/main/java/rearth/oritech/block/base/entity/MachineBlockEntity.java @@ -87,7 +87,7 @@ public void tick(World world, BlockPos pos, BlockState state, MachineBlockEntity if (recipeCandidate.isEmpty()) currentRecipe = OritechRecipe.DUMMY; // reset recipe when invalid or no input is given - if (recipeCandidate.isPresent() && canOutputRecipe(recipeCandidate.get().value())) { + if (recipeCandidate.isPresent() && canOutputRecipe(recipeCandidate.get().value()) && canProceed(recipeCandidate.get().value())) { // this is separate so that progress is not reset when out of energy if (hasEnoughEnergy()) { var activeRecipe = recipeCandidate.get().value(); @@ -118,6 +118,10 @@ public void tick(World world, BlockPos pos, BlockState state, MachineBlockEntity } } + protected boolean canProceed(OritechRecipe value) { + return true; + } + protected boolean hasEnoughEnergy() { return energyStorage.amount >= calculateEnergyUsage(); } @@ -173,7 +177,7 @@ public List getCraftingResults(OritechRecipe activeRecipe) { return activeRecipe.getResults(); } - private void craftItem(OritechRecipe activeRecipe, List outputInventory, List inputInventory) { + protected void craftItem(OritechRecipe activeRecipe, List outputInventory, List inputInventory) { var results = getCraftingResults(activeRecipe); var inputs = activeRecipe.getInputs(); diff --git a/src/main/java/rearth/oritech/block/entity/machines/processing/CentrifugeBlockEntity.java b/src/main/java/rearth/oritech/block/entity/machines/processing/CentrifugeBlockEntity.java index 444916fd9..a091c629f 100644 --- a/src/main/java/rearth/oritech/block/entity/machines/processing/CentrifugeBlockEntity.java +++ b/src/main/java/rearth/oritech/block/entity/machines/processing/CentrifugeBlockEntity.java @@ -1,35 +1,130 @@ package rearth.oritech.block.entity.machines.processing; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.base.FilteringStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage; +import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; import net.minecraft.block.BlockState; +import net.minecraft.fluid.Fluids; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.Registries; import net.minecraft.screen.ScreenHandlerType; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; import net.minecraft.util.math.Vec3i; +import org.jetbrains.annotations.Nullable; import rearth.oritech.block.base.entity.MultiblockMachineEntity; import rearth.oritech.client.init.ModScreens; +import rearth.oritech.init.BlockContent; import rearth.oritech.init.BlockEntitiesContent; +import rearth.oritech.init.recipes.OritechRecipe; import rearth.oritech.init.recipes.OritechRecipeType; import rearth.oritech.init.recipes.RecipeContent; +import rearth.oritech.network.NetworkContent; +import rearth.oritech.util.FluidProvider; import rearth.oritech.util.InventorySlotAssignment; import java.util.List; -public class CentrifugeBlockEntity extends MultiblockMachineEntity { +public class CentrifugeBlockEntity extends MultiblockMachineEntity implements FluidProvider { - // todo create addon that enables different recipe type - // add fluid processing option, adding 2 tanks for input and output - // add different gui with two tanks + private static final long CAPACITY = 8 * FluidConstants.BUCKET; - // missing addon models: - // block destroyer crop filter - done - // fragment forge yield addon - done - // centrifuge fluid addon - done + public final SingleVariantStorage inputStorage = createBasicTank(); + public final SingleVariantStorage outputStorage = createBasicTank(); + private final Storage exposedInput = FilteringStorage.insertOnlyOf(inputStorage); + private final Storage exposedOutput = FilteringStorage.extractOnlyOf(outputStorage); + private final Storage combinedTanks = new CombinedStorage<>(List.of(exposedInput, exposedOutput)); + + public boolean hasFluidAddon; public CentrifugeBlockEntity(BlockPos pos, BlockState state) { super(BlockEntitiesContent.CENTRIFUGE_ENTITY, pos, state, 32); } + @Override + protected boolean canProceed(OritechRecipe recipe) { + + if (!hasFluidAddon) return super.canProceed(recipe); + + // check if input is available + var input = recipe.getFluidInput(); + if (input == null) return false; + if (!input.variant().equals(inputStorage.variant) || input.amount() > inputStorage.amount) return false; + + // check if output fluid fits + var output = recipe.getFluidOutput(); + if (output.variant().getFluid().equals(Fluids.EMPTY) || outputStorage.amount == 0) return true; // no output + if (outputStorage.amount + output.amount() > outputStorage.getCapacity()) return false; // output full + return outputStorage.variant.equals(output.variant()); // type check + + } + + @Override + protected void craftItem(OritechRecipe activeRecipe, List outputInventory, List inputInventory) { + super.craftItem(activeRecipe, outputInventory, inputInventory); + + if (hasFluidAddon) + craftFluids(activeRecipe); + } + + private void craftFluids(OritechRecipe activeRecipe) { + + var input = activeRecipe.getFluidInput(); + var output = activeRecipe.getFluidOutput(); + + try (var tx = Transaction.openOuter()) { + + inputStorage.extract(input.variant(), input.amount(), tx); + outputStorage.insert(output.variant(), output.amount(), tx); + tx.commit(); + + } + } + + @Override + public void getAdditionalStatFromAddon(AddonBlock addonBlock) { + if (addonBlock.state().getBlock().equals(BlockContent.MACHINE_FLUID_ADDON)) { + hasFluidAddon = true; + } + } + + @Override + public void resetAddons() { + super.resetAddons(); + hasFluidAddon = false; + } + + @Override + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + nbt.putBoolean("fluidAddon", hasFluidAddon); + + nbt.put("fluidVariantIn", inputStorage.variant.toNbt()); + nbt.putLong("fluidAmountIn", inputStorage.amount); + nbt.put("fluidVariantOut", outputStorage.variant.toNbt()); + nbt.putLong("fluidAmountOut", outputStorage.amount); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + + hasFluidAddon = nbt.getBoolean("fluidAddon"); + + inputStorage.variant = FluidVariant.fromNbt(nbt.getCompound("fluidVariantIn")); + inputStorage.amount = nbt.getLong("fluidAmountIn"); + outputStorage.variant = FluidVariant.fromNbt(nbt.getCompound("fluidVariantOut")); + outputStorage.amount = nbt.getLong("fluidAmountOut"); + } + @Override protected OritechRecipeType getOwnRecipeType() { + if (hasFluidAddon) return RecipeContent.CENTRIFUGE_FLUID; return RecipeContent.CENTRIFUGE; } @@ -60,7 +155,7 @@ public int getInventorySize() { @Override public List getCorePositions() { return List.of( - new Vec3i(0, 1,0) + new Vec3i(0, 1, 0) ); } @@ -73,8 +168,59 @@ public boolean inputOptionsEnabled() { public List getAddonSlots() { return List.of( - new Vec3i(0, 0,-1), - new Vec3i(0, 0,1) + new Vec3i(0, 0, -1), + new Vec3i(0, 0, 1) ); } + + // this will allow full access on top and bottom to specific tank kinds (allowing both insertion and extraction) + // sides can access both tanks, but insert only to input tank, and extract only from output tank + @Override + public Storage getFluidStorage(Direction direction) { + if (!hasFluidAddon) return exposedOutput; + if (direction == null) return combinedTanks; + return switch (direction) { + case DOWN -> outputStorage; + case UP -> inputStorage; + default -> combinedTanks; + }; + } + + @Override + public @Nullable SingleVariantStorage getForDirectFluidAccess() { + return outputStorage; + } + + @Override + protected void sendNetworkEntry() { + super.sendNetworkEntry(); + NetworkContent.MACHINE_CHANNEL.serverHandle(this).send( + new NetworkContent.CentrifugeFluidSyncPacket( + pos, + hasFluidAddon, + Registries.FLUID.getId(inputStorage.variant.getFluid()).toString(), + inputStorage.amount, + Registries.FLUID.getId(outputStorage.variant.getFluid()).toString(), + outputStorage.amount)); + } + + private SingleVariantStorage createBasicTank() { + return new SingleVariantStorage<>() { + @Override + protected FluidVariant getBlankVariant() { + return FluidVariant.blank(); + } + + @Override + protected long getCapacity(FluidVariant variant) { + return CAPACITY; + } + + @Override + protected void onFinalCommit() { + super.onFinalCommit(); + CentrifugeBlockEntity.this.markDirty(); + } + }; + } } diff --git a/src/main/java/rearth/oritech/client/init/ModScreens.java b/src/main/java/rearth/oritech/client/init/ModScreens.java index 5f17d92e3..eeec08721 100644 --- a/src/main/java/rearth/oritech/client/init/ModScreens.java +++ b/src/main/java/rearth/oritech/client/init/ModScreens.java @@ -14,7 +14,7 @@ public class ModScreens implements AutoRegistryContainer> { public static final ExtendedScreenHandlerType GRINDER_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); public static final ExtendedScreenHandlerType ASSEMBLER_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); public static final ExtendedScreenHandlerType FOUNDRY_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); - public static final ExtendedScreenHandlerType CENTRIFUGE_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); + public static final ExtendedScreenHandlerType CENTRIFUGE_SCREEN = new ExtendedScreenHandlerType<>(CentrifugeScreenHandler::new); public static final ExtendedScreenHandlerType ATOMIC_FORGE_SCREEN = new ExtendedScreenHandlerType<>(BasicMachineScreenHandler::new); public static final ExtendedScreenHandlerType POWERED_FURNACE_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); public static final ExtendedScreenHandlerType TEST_GENERATOR_SCREEN = new ExtendedScreenHandlerType<>(UpgradableMachineScreenHandler::new); @@ -26,20 +26,20 @@ public class ModScreens implements AutoRegistryContainer> { public static final ExtendedScreenHandlerType ITEM_FILTER_SCREEN = new ExtendedScreenHandlerType<>(ItemFilterScreenHandler::new); public static void assignScreens() { - HandledScreens.register(PULVERIZER_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(GRINDER_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(ASSEMBLER_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(FOUNDRY_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(CENTRIFUGE_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(POWERED_FURNACE_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(TEST_GENERATOR_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(BASIC_GENERATOR_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(PULVERIZER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(GRINDER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(ASSEMBLER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(FOUNDRY_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(CENTRIFUGE_SCREEN, CentrifugeScreen::new); + HandledScreens.register(POWERED_FURNACE_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(TEST_GENERATOR_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(BASIC_GENERATOR_SCREEN, UpgradableMachineScreen::new); HandledScreens.register(ATOMIC_FORGE_SCREEN, BasicMachineScreen::new); HandledScreens.register(INVENTORY_PROXY_SCREEN, InventoryProxyScreen::new); HandledScreens.register(ITEM_FILTER_SCREEN, ItemFilterScreen::new); - HandledScreens.register(DESTROYER_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(PLACER_SCREEN, UpgradableMachineScreen::new); - HandledScreens.register(FERTILIZER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(DESTROYER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(PLACER_SCREEN, UpgradableMachineScreen::new); + HandledScreens.register(FERTILIZER_SCREEN, UpgradableMachineScreen::new); } @Override diff --git a/src/main/java/rearth/oritech/client/ui/BasicMachineScreen.java b/src/main/java/rearth/oritech/client/ui/BasicMachineScreen.java index 44d386bcd..a08069b0f 100644 --- a/src/main/java/rearth/oritech/client/ui/BasicMachineScreen.java +++ b/src/main/java/rearth/oritech/client/ui/BasicMachineScreen.java @@ -1,5 +1,6 @@ package rearth.oritech.client.ui; +import com.mojang.blaze3d.systems.RenderSystem; import io.wispforest.owo.ui.base.BaseOwoHandledScreen; import io.wispforest.owo.ui.component.*; import io.wispforest.owo.ui.container.Containers; @@ -8,14 +9,20 @@ import io.wispforest.owo.ui.util.SpriteUtilInvoker; import net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRendering; import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.minecraft.client.render.*; import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.math.MatrixStack; import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.fluid.Fluids; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; import rearth.oritech.Oritech; import rearth.oritech.client.renderers.LaserArmModel; import rearth.oritech.network.NetworkContent; +import rearth.oritech.util.ScreenProvider; import java.util.List; @@ -181,17 +188,27 @@ public void fillOverlay(FlowLayout overlay) { updateProgressBar(); } - private void updateFluidBar() { - if (fluidBackground == null) return; + protected void updateFluidBar() { + var container = handler.fluidProvider.getForDirectFluidAccess(); var data = handler.screenData.getFluidConfiguration(); + + if (fluidBackground.getSprite() == null && !container.isResourceBlank()) { + var parent = fluidBackground.parent(); + var targetIndex = parent.children().indexOf(fluidBackground); + var newFluid = createFluidRenderer(container.getResource(), container.getAmount(), data); + parent.removeChild(fluidBackground); + ((FlowLayout) parent).child(targetIndex, newFluid); + fluidBackground = newFluid; + } + var fill = 1 - ((float) container.getAmount() / container.getCapacity()); - - var targetFill = LaserArmModel.lerp(lastFluidFill, fill, 0.07f); + + var targetFill = LaserArmModel.lerp(lastFluidFill, fill, 0.15f); lastFluidFill = targetFill; - + fluidFillStatusOverlay.verticalSizing(Sizing.fixed((int) (data.height() * targetFill * 0.98f))); - + var tooltipText = List.of(Text.of(FluidVariantRendering.getTooltip(container.getResource()).get(0)), Text.of((container.getAmount() * 1000 / FluidConstants.BUCKET) + " mb")); fluidBackground.tooltip(tooltipText); } @@ -211,51 +228,58 @@ private void addProgressArrow(FlowLayout panel) { // only supports single variants, for more complex variants override this protected void addFluidBar(FlowLayout panel) { - var storage = handler.fluidProvider.getFluidStorage(null); - var hasContent = false; + var container = handler.fluidProvider.getForDirectFluidAccess(); fluidBackground = null; var config = handler.screenData.getFluidConfiguration(); - - var container = handler.fluidProvider.getForDirectFluidAccess(); lastFluidFill = 1 - ((float) container.getAmount() / container.getCapacity()); - for (var it = storage.nonEmptyIterator(); it.hasNext(); ) { - + for (var it = container.nonEmptyIterator(); it.hasNext(); ) { var fluid = it.next(); - var sprite = FluidVariantRendering.getSprite(fluid.getResource()); - var spriteColor = FluidVariantRendering.getColor(fluid.getResource()); - - //var tooltipText = Text.of(fluid.getResource() + ": " + (fluid.getAmount() / FluidConstants.BUCKET * 1000) + "mb"); - var tooltipText = List.of(Text.of(FluidVariantRendering.getTooltip(fluid.getResource()).get(0)), Text.of(": " + (fluid.getAmount() * 1000 / FluidConstants.BUCKET) + " mb")); - - hasContent = true; - fluidBackground = new ColoredSpriteComponent(sprite); - fluidBackground.color = Color.ofArgb(spriteColor); - fluidBackground.sizing(Sizing.fixed(config.height()), Sizing.fixed(config.width())); - fluidBackground.positioning(Positioning.absolute(config.x(), config.y())); - fluidBackground.tooltip(tooltipText); + fluidBackground = createFluidRenderer(fluid.getResource(), fluid.getAmount(), config); break; } - fluidFillStatusOverlay = Components.box(Sizing.fixed(config.height()), Sizing.fixed((int) (config.width() * lastFluidFill))); + if (fluidBackground == null) { + fluidBackground = createFluidRenderer(FluidVariant.of(Fluids.EMPTY), 0L, config); + } + + fluidFillStatusOverlay = Components.box(Sizing.fixed(config.width()), Sizing.fixed((int) (config.height() * lastFluidFill))); fluidFillStatusOverlay.color(new Color(77.6f / 255f, 77.6f / 255f, 77.6f / 255f)); fluidFillStatusOverlay.fill(true); fluidFillStatusOverlay.positioning(Positioning.absolute(config.x(), config.y())); - var foreGround = Components.texture(GUI_COMPONENTS, 48, 0, 50, 50, 98, 96); - foreGround.sizing(Sizing.fixed(config.height()), Sizing.fixed(config.width())); + var foreGround = Components.texture(GUI_COMPONENTS, 48, 0, 14, 50, 98, 96); + foreGround.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height())); foreGround.positioning(Positioning.absolute(config.x(), config.y())); - if (hasContent) - panel.child(fluidBackground); - + panel.child(fluidBackground); panel.child(fluidFillStatusOverlay); panel.child(foreGround); } + public static ColoredSpriteComponent createFluidRenderer(FluidVariant variant, long amount, ScreenProvider.BarConfiguration config) { + var sprite = FluidVariantRendering.getSprite(variant); + var spriteColor = FluidVariantRendering.getColor(variant); + + return getColoredSpriteComponent(variant, amount, config, sprite, spriteColor); + } + + @NotNull + private static ColoredSpriteComponent getColoredSpriteComponent(FluidVariant variant, long amount, ScreenProvider.BarConfiguration config, Sprite sprite, int spriteColor) { + var tooltipText = List.of(Text.of(FluidVariantRendering.getTooltip(variant).get(0)), Text.of(": " + (amount * 1000 / FluidConstants.BUCKET) + " mb")); + + var result = new ColoredSpriteComponent(sprite); + result.widthMultiplier = config.width() / 60f; + result.color = Color.ofArgb(spriteColor); + result.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height())); + result.positioning(Positioning.absolute(config.x(), config.y())); + result.tooltip(tooltipText); + return result; + } + private void addEnergyBar(FlowLayout panel) { var config = handler.screenData.getEnergyConfiguration(); @@ -283,22 +307,52 @@ private void addEnergyBar(FlowLayout panel) { panel .child(energy_indicator_background) - .child(energy_indicator) - ; + .child(energy_indicator); } - protected static class ColoredSpriteComponent extends SpriteComponent { + public static class ColoredSpriteComponent extends SpriteComponent { public Color color; + public float widthMultiplier = 1f; protected ColoredSpriteComponent(Sprite sprite) { super(sprite); } + public Sprite getSprite() { + return sprite; + } + @Override public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) { + if (sprite == null) return; SpriteUtilInvoker.markSpriteActive(this.sprite); - context.drawSprite(this.x, this.y, 0, this.width, this.height, this.sprite, this.color.red(), this.color.green(), this.color.blue(), this.color.alpha()); + drawSprite(this.x, this.y, 0, this.width, this.height, this.sprite, this.color.red(), this.color.green(), this.color.blue(), this.color.alpha(), context.getMatrices()); + } + + // these 2 methods are copies from drawContext, width slight modifications + public void drawSprite(int x, int y, int z, int width, int height, Sprite sprite, float red, float green, float blue, float alpha, MatrixStack matrices) { + + var uvWidth = sprite.getMaxU() - sprite.getMinU(); + var newMax = sprite.getMinU() + uvWidth * widthMultiplier; + + this.drawTexturedQuad(sprite.getAtlasId(), matrices, x, x + width, y, y + height, z, sprite.getMinU(), newMax, sprite.getMinV(), sprite.getMaxV(), red, green, blue, alpha); + } + + // direct copy of the method in drawContext, because I can't be called from here due to private access + private void drawTexturedQuad(Identifier texture, MatrixStack matrices, int x1, int x2, int y1, int y2, int z, float u1, float u2, float v1, float v2, float red, float green, float blue, float alpha) { + RenderSystem.setShaderTexture(0, texture); + RenderSystem.setShader(GameRenderer::getPositionColorTexProgram); + RenderSystem.enableBlend(); + Matrix4f matrix4f = matrices.peek().getPositionMatrix(); + BufferBuilder bufferBuilder = Tessellator.getInstance().getBuffer(); + bufferBuilder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE); + bufferBuilder.vertex(matrix4f, x1, y1, z).color(red, green, blue, alpha).texture(u1, v1).next(); + bufferBuilder.vertex(matrix4f, x1, y2, z).color(red, green, blue, alpha).texture(u1, v2).next(); + bufferBuilder.vertex(matrix4f, x2, y2, z).color(red, green, blue, alpha).texture(u2, v2).next(); + bufferBuilder.vertex(matrix4f, x2, y1, z).color(red, green, blue, alpha).texture(u2, v1).next(); + BufferRenderer.drawWithGlobalProgram(bufferBuilder.end()); + RenderSystem.disableBlend(); } } diff --git a/src/main/java/rearth/oritech/client/ui/CentrifugeScreen.java b/src/main/java/rearth/oritech/client/ui/CentrifugeScreen.java new file mode 100644 index 000000000..85dd58934 --- /dev/null +++ b/src/main/java/rearth/oritech/client/ui/CentrifugeScreen.java @@ -0,0 +1,90 @@ +package rearth.oritech.client.ui; + +import io.wispforest.owo.ui.component.BoxComponent; +import io.wispforest.owo.ui.component.Components; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.core.Color; +import io.wispforest.owo.ui.core.Positioning; +import io.wispforest.owo.ui.core.Sizing; +import net.fabricmc.fabric.api.transfer.v1.client.fluid.FluidVariantRendering; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.fluid.Fluids; +import net.minecraft.text.Text; +import rearth.oritech.block.entity.machines.processing.CentrifugeBlockEntity; +import rearth.oritech.client.renderers.LaserArmModel; +import rearth.oritech.util.ScreenProvider; + +import java.util.List; + +public class CentrifugeScreen extends UpgradableMachineScreen { + private ColoredSpriteComponent inFluidBackground; + private float inLastFluidFill; + private BoxComponent inFluidFillStatusOverlay; + + private static final ScreenProvider.BarConfiguration inputConfig = new ScreenProvider.BarConfiguration(28, 6, 21, 74); + + public CentrifugeScreen(CentrifugeScreenHandler handler, PlayerInventory inventory, Text title) { + super(handler, inventory, title); + } + + @Override + protected void updateFluidBar() { + + if (!((CentrifugeBlockEntity) handler.blockEntity).hasFluidAddon) return; + + super.updateFluidBar(); + + var container = handler.inputTank; + var fill = 1 - ((float) container.getAmount() / container.getCapacity()); + + var targetFill = LaserArmModel.lerp(inLastFluidFill, fill, 0.15f); + inLastFluidFill = targetFill; + + inFluidFillStatusOverlay.verticalSizing(Sizing.fixed((int) (inputConfig.height() * targetFill * 0.98f))); + + var tooltipText = List.of(Text.of(FluidVariantRendering.getTooltip(container.getResource()).get(0)), Text.of((container.getAmount() * 1000 / FluidConstants.BUCKET) + " mb")); + inFluidBackground.tooltip(tooltipText); + } + + @Override + protected void addFluidBar(FlowLayout panel) { + + if (!((CentrifugeBlockEntity) handler.blockEntity).hasFluidAddon) return; + + super.addFluidBar(panel); + + var storage = handler.inputTank; + inFluidBackground = null; + var config = inputConfig; + inLastFluidFill = 1 - ((float) storage.getAmount() / storage.getCapacity()); + + + for (var it = storage.nonEmptyIterator(); it.hasNext(); ) { + + var fluid = it.next(); + inFluidBackground = BasicMachineScreen.createFluidRenderer(fluid.getResource(), fluid.getAmount(), config); + break; + } + + if (inFluidBackground == null) { + inFluidBackground = createFluidRenderer(FluidVariant.of(Fluids.EMPTY), 0L, config); + } + + inFluidFillStatusOverlay = Components.box(Sizing.fixed(config.width()), Sizing.fixed((int) (config.height() * inLastFluidFill))); + inFluidFillStatusOverlay.color(new Color(77.6f / 255f, 77.6f / 255f, 77.6f / 255f)); + inFluidFillStatusOverlay.fill(true); + inFluidFillStatusOverlay.positioning(Positioning.absolute(config.x(), config.y())); + + + var foreGround = Components.texture(GUI_COMPONENTS, 48, 0, 14, 50, 98, 96); + foreGround.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height())); + foreGround.positioning(Positioning.absolute(config.x(), config.y())); + + panel.child(inFluidBackground); + panel.child(inFluidFillStatusOverlay); + panel.child(foreGround); + + } +} diff --git a/src/main/java/rearth/oritech/client/ui/CentrifugeScreenHandler.java b/src/main/java/rearth/oritech/client/ui/CentrifugeScreenHandler.java new file mode 100644 index 000000000..700dbc995 --- /dev/null +++ b/src/main/java/rearth/oritech/client/ui/CentrifugeScreenHandler.java @@ -0,0 +1,36 @@ +package rearth.oritech.client.ui; + +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleVariantStorage; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.network.PacketByteBuf; +import rearth.oritech.Oritech; +import rearth.oritech.block.entity.machines.processing.CentrifugeBlockEntity; +import rearth.oritech.util.MachineAddonController; + +import java.util.Objects; + +import static rearth.oritech.util.MachineAddonController.ADDON_UI_ENDEC; + +public class CentrifugeScreenHandler extends UpgradableMachineScreenHandler{ + + public final SingleVariantStorage inputTank; + + public CentrifugeScreenHandler(int syncId, PlayerInventory inventory, PacketByteBuf buf) { + this(syncId, inventory, Objects.requireNonNull(inventory.player.getWorld().getBlockEntity(buf.readBlockPos())), buf.read(ADDON_UI_ENDEC), buf.readFloat()); + } + + public CentrifugeScreenHandler(int syncId, PlayerInventory playerInventory, BlockEntity blockEntity, MachineAddonController.AddonUiData addonUiData, float coreQuality) { + super(syncId, playerInventory, blockEntity, addonUiData, coreQuality); + + if (!(blockEntity instanceof CentrifugeBlockEntity centrifugeEntity)) { + inputTank = null; + Oritech.LOGGER.error("Opened centrifuge screen on non-centrifuge block, this should never happen"); + return; + } + + inputTank = centrifugeEntity.inputStorage; + + } +} diff --git a/src/main/java/rearth/oritech/client/ui/UpgradableMachineScreen.java b/src/main/java/rearth/oritech/client/ui/UpgradableMachineScreen.java index dd6dad0c6..cb77f37cc 100644 --- a/src/main/java/rearth/oritech/client/ui/UpgradableMachineScreen.java +++ b/src/main/java/rearth/oritech/client/ui/UpgradableMachineScreen.java @@ -18,7 +18,7 @@ import rearth.oritech.init.BlockContent; -public class UpgradableMachineScreen extends BasicMachineScreen { +public class UpgradableMachineScreen extends BasicMachineScreen { private static final Color SPEED_COLOR = Color.ofRgb(0x219ebc); private static final Color EFFICIENCY_COLOR = Color.ofRgb(0x8ecae6); @@ -29,7 +29,7 @@ public class UpgradableMachineScreen extends BasicMachineScreen FOUNDRY_ENTITY = FabricBlockEntityTypeBuilder.create(FoundryBlockEntity::new, BlockContent.FOUNDRY_BLOCK).build(); + @AssignSidedFluid @AssignSidedInventory @AssignSidedEnergy public static final BlockEntityType CENTRIFUGE_ENTITY = FabricBlockEntityTypeBuilder.create(CentrifugeBlockEntity::new, BlockContent.CENTRIFUGE_BLOCK).build(); diff --git a/src/main/java/rearth/oritech/init/compat/OritechREIPlugin.java b/src/main/java/rearth/oritech/init/compat/OritechREIPlugin.java index a1c74c99a..d442d9055 100644 --- a/src/main/java/rearth/oritech/init/compat/OritechREIPlugin.java +++ b/src/main/java/rearth/oritech/init/compat/OritechREIPlugin.java @@ -26,6 +26,7 @@ public void registerCategories(CategoryRegistry registry) { registerOritechCategory(registry, RecipeContent.ASSEMBLER, BlockContent.ASSEMBLER_BLOCK, BasicMachineScreen::new); registerOritechCategory(registry, RecipeContent.FOUNDRY, BlockContent.FOUNDRY_BLOCK, BasicMachineScreen::new); registerOritechCategory(registry, RecipeContent.CENTRIFUGE, BlockContent.CENTRIFUGE_BLOCK, BasicMachineScreen::new); + registerOritechCategory(registry, RecipeContent.CENTRIFUGE_FLUID, BlockContent.CENTRIFUGE_BLOCK, BasicMachineScreen::new); registerOritechCategory(registry, RecipeContent.ATOMIC_FORGE, BlockContent.ATOMIC_FORGE_BLOCK, BasicMachineScreen::new); // workstations @@ -34,6 +35,7 @@ public void registerCategories(CategoryRegistry registry) { registerOriWorkstation(registry, RecipeContent.ASSEMBLER, BlockContent.ASSEMBLER_BLOCK); registerOriWorkstation(registry, RecipeContent.FOUNDRY, BlockContent.FOUNDRY_BLOCK); registerOriWorkstation(registry, RecipeContent.CENTRIFUGE, BlockContent.CENTRIFUGE_BLOCK); + registerOriWorkstation(registry, RecipeContent.CENTRIFUGE_FLUID, BlockContent.CENTRIFUGE_BLOCK); registerOriWorkstation(registry, RecipeContent.ATOMIC_FORGE, BlockContent.ATOMIC_FORGE_BLOCK); registry.addWorkstations(CategoryIdentifier.of("minecraft", "plugins/smelting"), EntryStacks.of(BlockContent.POWERED_FURNACE_BLOCK)); @@ -49,6 +51,7 @@ public void registerDisplays(DisplayRegistry registry) { registerMachineRecipeType(registry, RecipeContent.GRINDER); registerMachineRecipeType(registry, RecipeContent.FOUNDRY); registerMachineRecipeType(registry, RecipeContent.CENTRIFUGE); + registerMachineRecipeType(registry, RecipeContent.CENTRIFUGE_FLUID); registerMachineRecipeType(registry, RecipeContent.ATOMIC_FORGE); } diff --git a/src/main/java/rearth/oritech/init/recipes/OritechRecipe.java b/src/main/java/rearth/oritech/init/recipes/OritechRecipe.java index aa2837caa..2a8cc6716 100644 --- a/src/main/java/rearth/oritech/init/recipes/OritechRecipe.java +++ b/src/main/java/rearth/oritech/init/recipes/OritechRecipe.java @@ -10,23 +10,31 @@ import net.minecraft.registry.DynamicRegistryManager; import net.minecraft.util.collection.DefaultedList; import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import rearth.oritech.util.FluidStack; import java.util.List; public class OritechRecipe implements Recipe { - + private final OritechRecipeType type; private final List inputs; private final List results; + @Nullable + private final FluidStack fluidInput; + @Nullable + private final FluidStack fluidOutput; private final int time; - public static final OritechRecipe DUMMY = new OritechRecipe(-1, DefaultedList.ofSize(1, Ingredient.ofStacks(Items.IRON_INGOT.getDefaultStack())), DefaultedList.ofSize(1, Items.IRON_BLOCK.getDefaultStack()), RecipeContent.PULVERIZER); + public static final OritechRecipe DUMMY = new OritechRecipe(-1, DefaultedList.ofSize(1, Ingredient.ofStacks(Items.IRON_INGOT.getDefaultStack())), DefaultedList.ofSize(1, Items.IRON_BLOCK.getDefaultStack()), RecipeContent.PULVERIZER, null, null); - public OritechRecipe(int time, List inputs, List results, OritechRecipeType type) { + public OritechRecipe(int time, List inputs, List results, OritechRecipeType type, @Nullable FluidStack fluidInput, @Nullable FluidStack fluidOutput) { this.type = type; this.results = results; this.inputs = inputs; this.time = time; + this.fluidInput = fluidInput; + this.fluidOutput = fluidOutput; } @Override @@ -76,17 +84,19 @@ public boolean isIgnoredInRecipeBook() { public RecipeType getType() { return type; } - + @Override public String toString() { return "OritechRecipe{" + - "type=" + type + - ", inputs=" + inputs + - ", results=" + results + - ", time=" + time + - '}'; + "type=" + type + + ", inputs=" + inputs + + ", results=" + results + + ", fluidInput=" + fluidInput + + ", fluidOutput=" + fluidOutput + + ", time=" + time + + '}'; } - + public int getTime() { return time; } @@ -109,4 +119,11 @@ public OritechRecipeType getOriType() { return type; } + public @Nullable FluidStack getFluidInput() { + return fluidInput; + } + + public @Nullable FluidStack getFluidOutput() { + return fluidOutput; + } } diff --git a/src/main/java/rearth/oritech/init/recipes/OritechRecipeType.java b/src/main/java/rearth/oritech/init/recipes/OritechRecipeType.java index 088e0d13f..1c6ce8f96 100644 --- a/src/main/java/rearth/oritech/init/recipes/OritechRecipeType.java +++ b/src/main/java/rearth/oritech/init/recipes/OritechRecipeType.java @@ -4,19 +4,26 @@ import io.wispforest.owo.serialization.StructEndec; import io.wispforest.owo.serialization.endec.BuiltInEndecs; import io.wispforest.owo.serialization.endec.StructEndecBuilder; +import io.wispforest.owo.serialization.format.nbt.NbtEndec; import io.wispforest.owo.serialization.util.EndecRecipeSerializer; +import net.minecraft.nbt.NbtCompound; import net.minecraft.recipe.Ingredient; import net.minecraft.recipe.RecipeType; import net.minecraft.registry.Registries; import net.minecraft.util.Identifier; +import rearth.oritech.util.FluidStack; public class OritechRecipeType extends EndecRecipeSerializer implements RecipeType { + public static final Endec FLUID_STACK_ENDEC = NbtEndec.COMPOUND.xmap(FluidStack::fromNbt, stack -> stack.toNbt(new NbtCompound())); + public static final Endec ORI_RECIPE_ENDEC = StructEndecBuilder.of( Endec.INT.fieldOf("time", OritechRecipe::getTime), Endec.ofCodec(Ingredient.DISALLOW_EMPTY_CODEC).listOf().fieldOf("ingredients", OritechRecipe::getInputs), BuiltInEndecs.ITEM_STACK.listOf().fieldOf("results", OritechRecipe::getResults), BuiltInEndecs.IDENTIFIER.xmap(identifier1 -> (OritechRecipeType) Registries.RECIPE_TYPE.get(identifier1), OritechRecipeType::getIdentifier).fieldOf("type", OritechRecipe::getOriType), + FLUID_STACK_ENDEC.optionalFieldOf("fluidInput", OritechRecipe::getFluidInput, (FluidStack) null), + FLUID_STACK_ENDEC.optionalFieldOf("fluidOutput", OritechRecipe::getFluidOutput, (FluidStack) null), OritechRecipe::new ); diff --git a/src/main/java/rearth/oritech/init/recipes/RecipeContent.java b/src/main/java/rearth/oritech/init/recipes/RecipeContent.java index 6e386ecb6..881853e18 100644 --- a/src/main/java/rearth/oritech/init/recipes/RecipeContent.java +++ b/src/main/java/rearth/oritech/init/recipes/RecipeContent.java @@ -1,16 +1,9 @@ package rearth.oritech.init.recipes; -import net.minecraft.item.ItemConvertible; import net.minecraft.registry.Registries; import net.minecraft.registry.Registry; import net.minecraft.util.Identifier; import rearth.oritech.Oritech; -import rearth.oritech.init.BlockContent; -import rearth.oritech.init.compat.OritechREIPlugin; -import rearth.oritech.init.compat.Screens.BasicMachineScreen; -import rearth.oritech.init.compat.Screens.PulverizerScreen; - -import java.util.function.BiFunction; public class RecipeContent { @@ -19,6 +12,7 @@ public class RecipeContent { public static final OritechRecipeType ASSEMBLER = register(new Identifier(Oritech.MOD_ID, "assembler")); public static final OritechRecipeType FOUNDRY = register(new Identifier(Oritech.MOD_ID, "foundry")); public static final OritechRecipeType CENTRIFUGE = register(new Identifier(Oritech.MOD_ID, "centrifuge")); + public static final OritechRecipeType CENTRIFUGE_FLUID = register(new Identifier(Oritech.MOD_ID, "centrifuge_fluid")); public static final OritechRecipeType ATOMIC_FORGE = register(new Identifier(Oritech.MOD_ID, "atomic_forge")); public static final OritechRecipeType TEST_GENERATOR = register(new Identifier(Oritech.MOD_ID, "test_generator")); diff --git a/src/main/java/rearth/oritech/network/NetworkContent.java b/src/main/java/rearth/oritech/network/NetworkContent.java index dc8ddb6f0..a559a148c 100644 --- a/src/main/java/rearth/oritech/network/NetworkContent.java +++ b/src/main/java/rearth/oritech/network/NetworkContent.java @@ -15,6 +15,7 @@ import rearth.oritech.block.entity.machines.addons.InventoryProxyAddonBlockEntity; import rearth.oritech.block.entity.machines.generators.BigSolarPanelEntity; import rearth.oritech.block.entity.machines.interaction.LaserArmBlockEntity; +import rearth.oritech.block.entity.machines.processing.CentrifugeBlockEntity; import rearth.oritech.block.entity.pipes.ItemFilterBlockEntity; import rearth.oritech.init.recipes.OritechRecipe; import rearth.oritech.init.recipes.OritechRecipeType; @@ -26,43 +27,67 @@ import java.util.Map; public class NetworkContent { - + public static final OwoNetChannel MACHINE_CHANNEL = OwoNetChannel.create(new Identifier(Oritech.MOD_ID, "machine_data")); public static final OwoNetChannel UI_CHANNEL = OwoNetChannel.create(new Identifier(Oritech.MOD_ID, "ui_interactions")); - + // Server -> Client - public record MachineSyncPacket(BlockPos position, long energy, long maxEnergy, long maxInsert, int progress, OritechRecipe activeRecipe, InventoryInputMode inputMode) {} - + public record MachineSyncPacket(BlockPos position, long energy, long maxEnergy, long maxInsert, int progress, + OritechRecipe activeRecipe, InventoryInputMode inputMode) { + } + // Client -> Server (e.g. from UI interactions - public record InventoryInputModeSelectorPacket(BlockPos position) {} - public record InventoryProxySlotSelectorPacket(BlockPos position, int slot) {} - public record GeneratorUISyncPacket(BlockPos position, int burnTime) {} - public record MachineSetupEventPacket(BlockPos position) {} - public record MachineFrameMovementPacket(BlockPos position, BlockPos currentTarget, BlockPos lastTarget, BlockPos areaMin, BlockPos areaMax) {} // times are in ticks - public record MachineFrameGuiPacket(BlockPos position, long currentEnergy, long maxEnergy, int progress){} - public record ItemFilterSyncPacket(BlockPos position, ItemFilterBlockEntity.FilterData data) {} // this goes both ways + public record InventoryInputModeSelectorPacket(BlockPos position) { + } + + public record InventoryProxySlotSelectorPacket(BlockPos position, int slot) { + } + + public record GeneratorUISyncPacket(BlockPos position, int burnTime) { + } + + public record MachineSetupEventPacket(BlockPos position) { + } - public record LaserArmSyncPacket(BlockPos position, BlockPos target, long lastFiredAt){} - public record SingleVariantFluidSyncPacket(BlockPos position, String fluidType, long amount) {} + public record MachineFrameMovementPacket(BlockPos position, BlockPos currentTarget, BlockPos lastTarget, + BlockPos areaMin, BlockPos areaMax) { + } // times are in ticks - public record InventorySyncPacket(BlockPos position, List heldStacks) {} + public record MachineFrameGuiPacket(BlockPos position, long currentEnergy, long maxEnergy, int progress) { + } + + public record ItemFilterSyncPacket(BlockPos position, ItemFilterBlockEntity.FilterData data) { + } // this goes both ways + + public record LaserArmSyncPacket(BlockPos position, BlockPos target, long lastFiredAt) { + } + + public record SingleVariantFluidSyncPacket(BlockPos position, String fluidType, long amount) { + } + + public record CentrifugeFluidSyncPacket(BlockPos position, boolean fluidAddon, String fluidTypeIn, long amountIn, String fluidTypeOut, + long amountOut) { + } + + public record InventorySyncPacket(BlockPos position, List heldStacks) { + } @SuppressWarnings("unchecked") public static void registerChannels() { - + Oritech.LOGGER.info("Registering oritech channels"); - + ReflectiveEndecBuilder.register(OritechRecipeType.ORI_RECIPE_ENDEC, OritechRecipe.class); ReflectiveEndecBuilder.register(ItemFilterBlockEntity.FILTER_ITEMS_ENDEC, (Class>) (Object) Map.class); // I don't even know what kind of abomination this cast is, but it seems to work - + MACHINE_CHANNEL.registerClientbound(MachineSyncPacket.class, ((message, access) -> { - + var entity = access.player().clientWorld.getBlockEntity(message.position); - + if (entity instanceof MachineBlockEntity machine) { machine.handleNetworkEntry(message); } - + })); MACHINE_CHANNEL.registerClientbound(MachineSetupEventPacket.class, ((message, access) -> { @@ -112,6 +137,22 @@ public static void registerChannels() { })); + MACHINE_CHANNEL.registerClientbound(CentrifugeFluidSyncPacket.class, ((message, access) -> { + + var entity = access.player().clientWorld.getBlockEntity(message.position); + + if (entity instanceof CentrifugeBlockEntity centrifuge) { + centrifuge.hasFluidAddon = message.fluidAddon; + var inStorage = centrifuge.inputStorage; + var outStorage = centrifuge.outputStorage; + inStorage.amount = message.amountIn; + outStorage.amount = message.amountOut; + inStorage.variant = FluidVariant.of(Registries.FLUID.get(new Identifier(message.fluidTypeIn))); + outStorage.variant = FluidVariant.of(Registries.FLUID.get(new Identifier(message.fluidTypeOut))); + } + + })); + MACHINE_CHANNEL.registerClientbound(GeneratorUISyncPacket.class, ((message, access) -> { var entity = access.player().clientWorld.getBlockEntity(message.position); @@ -159,15 +200,15 @@ public static void registerChannels() { } })); - + UI_CHANNEL.registerServerbound(InventoryInputModeSelectorPacket.class, (message, access) -> { - + var entity = access.player().getWorld().getBlockEntity(message.position); - + if (entity instanceof MachineBlockEntity machine) { machine.cycleInputMode(); } - + }); UI_CHANNEL.registerServerbound(InventoryProxySlotSelectorPacket.class, (message, access) -> { @@ -190,7 +231,7 @@ public static void registerChannels() { } })); - + } - + } diff --git a/src/main/java/rearth/oritech/util/FluidStack.java b/src/main/java/rearth/oritech/util/FluidStack.java new file mode 100644 index 000000000..5d0392ad5 --- /dev/null +++ b/src/main/java/rearth/oritech/util/FluidStack.java @@ -0,0 +1,30 @@ +package rearth.oritech.util; + + +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.minecraft.nbt.NbtCompound; + +public record FluidStack(FluidVariant variant, long amount) { + + @Override + public String toString() { + return "FluidStack{" + "variant=" + variant + ", amount=" + amount + '}'; + } + + public static FluidStack fromNbt(NbtCompound nbt) { + var amount = nbt.getLong("amount"); + var variant = FluidVariant.fromNbt(nbt.getCompound("variant")); + return new FluidStack(variant, amount); + } + + public NbtCompound toNbt(NbtCompound nbt) { + nbt.put("variant", variant.toNbt()); + nbt.putLong("amount", amount); + return nbt; + } + + public static void toNbt(NbtCompound nbt, FluidStack stack) { + stack.toNbt(nbt); + } + +} diff --git a/src/main/java/rearth/oritech/util/ScreenProvider.java b/src/main/java/rearth/oritech/util/ScreenProvider.java index 6d1d55be7..0bdb8e102 100644 --- a/src/main/java/rearth/oritech/util/ScreenProvider.java +++ b/src/main/java/rearth/oritech/util/ScreenProvider.java @@ -36,7 +36,7 @@ default BarConfiguration getEnergyConfiguration() { return new BarConfiguration(7, 24, 15, 54); } default BarConfiguration getFluidConfiguration() { - return new BarConfiguration(100, 18, 60, 62); + return new BarConfiguration(146, 6, 21, 74); } default ArrowConfiguration getIndicatorConfiguration() { diff --git a/src/main/resources/assets/oritech/textures/gui/modular/machine_gui_components.png b/src/main/resources/assets/oritech/textures/gui/modular/machine_gui_components.png index 7581e7cfc..4ff27079e 100644 Binary files a/src/main/resources/assets/oritech/textures/gui/modular/machine_gui_components.png and b/src/main/resources/assets/oritech/textures/gui/modular/machine_gui_components.png differ diff --git a/src/main/resources/data/oritech/recipes/diamond_to_copper_iron_centrifuge_fluid.json b/src/main/resources/data/oritech/recipes/diamond_to_copper_iron_centrifuge_fluid.json new file mode 100644 index 000000000..9355d9612 --- /dev/null +++ b/src/main/resources/data/oritech/recipes/diamond_to_copper_iron_centrifuge_fluid.json @@ -0,0 +1,27 @@ +{ + "type": "oritech:centrifuge_fluid", + "ingredients": [ + { + "item": "minecraft:emerald" + } + ], + "results": [ + { + "id": "minecraft:copper_ingot", + "Count": 1 + } + ], + "time": 60, + "fluidInput": { + "amount": 81000, + "variant": { + "fluid": "minecraft:water" + } + }, + "fluidOutput": { + "amount": 81000, + "variant": { + "fluid": "minecraft:lava" + } + } +} \ No newline at end of file