From 4524f0d7677e337ca9c8af3d4890ade0576e7bc0 Mon Sep 17 00:00:00 2001 From: rearth Date: Mon, 11 Mar 2024 23:36:00 +0100 Subject: [PATCH] Begin adding pump block / entity --- .../19c08d24c255c2719fbb8ac01f9dff290b763461 | 5 +- .../oritech/blockstates/pump_block.json | 7 + .../oritech/models/block/pump_block.json | 6 + .../oritech/models/item/pump_block.json | 3 + .../machines/interaction/PumpBlock.java | 51 ++++ .../machines/interaction/PumpBlockEntity.java | 265 ++++++++++++++++++ .../rearth/oritech/init/BlockContent.java | 1 + .../oritech/init/BlockEntitiesContent.java | 18 +- .../oritech/init/datagen/ModelGenerator.java | 1 + .../rearth/oritech/util/FluidProvider.java | 13 + .../oritech/textures/block/pump_block.png | Bin 0 -> 917 bytes 11 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 src/main/generated/assets/oritech/blockstates/pump_block.json create mode 100644 src/main/generated/assets/oritech/models/block/pump_block.json create mode 100644 src/main/generated/assets/oritech/models/item/pump_block.json create mode 100644 src/main/java/rearth/oritech/block/blocks/machines/interaction/PumpBlock.java create mode 100644 src/main/java/rearth/oritech/block/entity/machines/interaction/PumpBlockEntity.java create mode 100644 src/main/java/rearth/oritech/util/FluidProvider.java create mode 100644 src/main/resources/assets/oritech/textures/block/pump_block.png diff --git a/src/main/generated/.cache/19c08d24c255c2719fbb8ac01f9dff290b763461 b/src/main/generated/.cache/19c08d24c255c2719fbb8ac01f9dff290b763461 index 54a292bea..1d4cc1822 100644 --- a/src/main/generated/.cache/19c08d24c255c2719fbb8ac01f9dff290b763461 +++ b/src/main/generated/.cache/19c08d24c255c2719fbb8ac01f9dff290b763461 @@ -1,4 +1,4 @@ -// 1.20.4 2024-03-11T15:36:48.2448916 Oritech/Model Definitions +// 1.20.4 2024-03-11T21:29:26.0904898 Oritech/Model Definitions 3d6f377b938592ac22aca17bd64561a6579ad217 assets\oritech\blockstates\grinder_block.json 8ede0997449fd1c3f65a07df184768a843b473ed assets\oritech\models\item\centrifuge_block.json 90e28b721796ff7ee21c8eefe4525f03a251d2fb assets\oritech\models\block\machine_extender.json @@ -7,11 +7,13 @@ e6f197ee5ac22cd68bc502a86d40b65bfe40884a assets\oritech\models\item\target_desig 53f76976c5fe0593fa89a4b835dde07c96b07067 assets\oritech\blockstates\centrifuge_block.json 95eae9b13fb2835f15057d2fd4d9550192064ca4 assets\oritech\models\item\grinder_block.json 3ceff0ef4325bb269831205626285bc857c60de8 assets\oritech\models\item\destroyer_block.json +829faf2dfb04c9aea2dd6c971037fbfdaa9aa389 assets\oritech\models\item\pump_block.json 964457f56e25b66dfef2160c8618bcaba2b640aa assets\oritech\blockstates\pulverizer_block.json 001d7907def5667f14bbdce07c9d6198efa45e71 assets\oritech\blockstates\laser_arm_block.json e4497bad8afbd4f4b4e830d892284a05718b06dd assets\oritech\models\block\machine_core_basic.json 505d30e3b61ee0ba1f2616e1226689e2cdb7ab58 assets\oritech\models\block\addon_indicator_block.json a5651b8d4a27308b40c404f7cd3abcf9769b64b6 assets\oritech\models\item\machine_core_basic.json +c3174dc9e04055bae724425eb8b2c2894297bf70 assets\oritech\blockstates\pump_block.json e109fcba8a22fe480b127c3f8972d80bf3821f51 assets\oritech\blockstates\basic_generator_block.json 2f7dfa17792770a034a13f117e6b8b1942b459e4 assets\oritech\models\block\placer_block.json ee822d455f99c8c53c789fdb744979f678ca2729 assets\oritech\blockstates\fertilizer_block.json @@ -38,6 +40,7 @@ c641d411ad252487b93d779cee9028b9ee920281 assets\oritech\blockstates\machine_core a5afa0ba8780ec08bd0a9284bc08e0c681fe7158 assets\oritech\blockstates\block_placer_head.json f29468ff950a191567ab35557334666099ccd7cc assets\oritech\blockstates\assembler_block.json 5054284e6d5fe0785ac93bdda5101c47337953cf assets\oritech\models\item\machine_frame_block.json +56c7ceb5cc86d67913ad95cb4ae5ec192aed926f assets\oritech\models\block\pump_block.json 1b9cd3719105dc1e03398de41f2cab37337816fe assets\oritech\blockstates\machine_core_good.json 3810f72943d88c81d3ab6b4329d437911c859621 assets\oritech\blockstates\powered_furnace_block.json 5462a2453fcdd2f8b99129894aec60f282d29ca9 assets\oritech\models\item\pulverizer_block.json diff --git a/src/main/generated/assets/oritech/blockstates/pump_block.json b/src/main/generated/assets/oritech/blockstates/pump_block.json new file mode 100644 index 000000000..1c735f785 --- /dev/null +++ b/src/main/generated/assets/oritech/blockstates/pump_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "oritech:block/pump_block" + } + } +} \ No newline at end of file diff --git a/src/main/generated/assets/oritech/models/block/pump_block.json b/src/main/generated/assets/oritech/models/block/pump_block.json new file mode 100644 index 000000000..45889baf5 --- /dev/null +++ b/src/main/generated/assets/oritech/models/block/pump_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "oritech:block/pump_block" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/oritech/models/item/pump_block.json b/src/main/generated/assets/oritech/models/item/pump_block.json new file mode 100644 index 000000000..ca7f07b9e --- /dev/null +++ b/src/main/generated/assets/oritech/models/item/pump_block.json @@ -0,0 +1,3 @@ +{ + "parent": "oritech:block/pump_block" +} \ No newline at end of file diff --git a/src/main/java/rearth/oritech/block/blocks/machines/interaction/PumpBlock.java b/src/main/java/rearth/oritech/block/blocks/machines/interaction/PumpBlock.java new file mode 100644 index 000000000..724b966c2 --- /dev/null +++ b/src/main/java/rearth/oritech/block/blocks/machines/interaction/PumpBlock.java @@ -0,0 +1,51 @@ +package rearth.oritech.block.blocks.machines.interaction; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.BlockRenderType; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; +import rearth.oritech.block.entity.machines.interaction.LaserArmBlockEntity; +import rearth.oritech.block.entity.machines.interaction.PumpBlockEntity; +import rearth.oritech.network.NetworkContent; +import rearth.oritech.util.MultiblockMachineController; + +public class PumpBlock extends Block implements BlockEntityProvider { + + public PumpBlock(Settings settings) { + super(settings); + } + +// @Override +// public BlockRenderType getRenderType(BlockState state) { +// return BlockRenderType.ENTITYBLOCK_ANIMATED; +// } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new PumpBlockEntity(pos, state); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Nullable + @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + return (world1, pos, state1, blockEntity) -> { + if (blockEntity instanceof BlockEntityTicker ticker) + ticker.tick(world1, pos, state1, blockEntity); + }; + } +} diff --git a/src/main/java/rearth/oritech/block/entity/machines/interaction/PumpBlockEntity.java b/src/main/java/rearth/oritech/block/entity/machines/interaction/PumpBlockEntity.java new file mode 100644 index 000000000..43c7e24a1 --- /dev/null +++ b/src/main/java/rearth/oritech/block/entity/machines/interaction/PumpBlockEntity.java @@ -0,0 +1,265 @@ +package rearth.oritech.block.entity.machines.interaction; + +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidConstants; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +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.block.Blocks; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.fluid.FluidState; +import net.minecraft.fluid.Fluids; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import rearth.oritech.init.BlockContent; +import rearth.oritech.init.BlockEntitiesContent; +import rearth.oritech.util.FluidProvider; + +import java.util.*; +import java.util.stream.Collectors; + +// todo add energy storage / usage +public class PumpBlockEntity extends BlockEntity implements BlockEntityTicker, FluidProvider { + + private static final int MAX_SEARCH_COUNT = 10000; + + private boolean initialized = false; + private boolean toolheadLowered = false; + private boolean searchActive = false; + private BlockPos toolheadPosition; + private FloodFillSearch searchInstance; + private Deque pendingLiquidPositions; + + private final SingleVariantStorage fluidStorage = new SingleVariantStorage<>() { + @Override + protected FluidVariant getBlankVariant() { + return FluidVariant.blank(); + } + + @Override + protected long getCapacity(FluidVariant variant) { + return (16 * FluidConstants.BUCKET); + } + + @Override + protected void onFinalCommit() { + super.onFinalCommit(); + PumpBlockEntity.this.markDirty(); + } + }; + + public PumpBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntitiesContent.PUMP_BLOCK, pos, state); + } + + @Override + protected void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + nbt.putBoolean("initialized", initialized); + nbt.put("fluidVariant", fluidStorage.variant.toNbt()); + nbt.putLong("amount", fluidStorage.amount); + nbt.putLongArray("pendingTargets", pendingLiquidPositions.stream().mapToLong(BlockPos::asLong).toArray()); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + initialized = nbt.getBoolean("initialized"); + fluidStorage.variant = FluidVariant.fromNbt(nbt.getCompound("fluidVariant")); + fluidStorage.amount = nbt.getLong("amount"); + pendingLiquidPositions = Arrays.stream(nbt.getLongArray("pendingTargets")).mapToObj(BlockPos::fromLong).collect(Collectors.toCollection(ArrayDeque::new)); + } + + @Override + public void tick(World world, BlockPos pos, BlockState state, PumpBlockEntity blockEntity) { + if (world.isClient) return; + + if (!initialized) { + progressStartup(); + return; + } + + if (world.getTime() % 10 == 0) { + + if (pendingLiquidPositions.isEmpty() || tankIsFull()) return; + + var targetBlock = pendingLiquidPositions.peekLast(); + + if (!world.getBlockState(targetBlock).isLiquid()) { + pendingLiquidPositions.pollLast(); + return; + } + + var targetState = world.getFluidState(targetBlock); + if (!targetState.getFluid().matchesType(Fluids.WATER)) { + drainSourceBlock(targetBlock); + pendingLiquidPositions.pollLast(); + System.out.println("pumped and removed block: " + targetState.getFluid()); + } + + addLiquidToTank(targetState); + this.markDirty(); + } + + } + + private boolean tankIsFull() { + return fluidStorage.amount > fluidStorage.getCapacity() - FluidConstants.BUCKET; + } + + private void addLiquidToTank(FluidState targetState) { + try (var tx = Transaction.openOuter()) { + var amountInserted = fluidStorage.insert(FluidVariant.of(targetState.getFluid()), FluidConstants.BUCKET, tx); + tx.commit(); + } + } + + private void drainSourceBlock(BlockPos targetBlock) { + world.setBlockState(targetBlock, Blocks.AIR.getDefaultState()); + } + + private void progressStartup() { + + // startup sequence is: + // move down until no longer in air + // check if target is liquid + // if liquid is water, consider as infinite + // if liquid, start flood fill to find all liquid blocks. Add all found blocks to queue so that it can be soaked up in reverse + // search all neighbors per tick + // if more than 10000 blocks are found, consider as infinite and stop search + // mark startup as completed + + if (toolheadPosition == null) { + toolheadPosition = pos; + } + + if (!toolheadLowered) { + + if (world.getTime() % 10 != 0) + moveToolheadDown(); + + return; + } + + if (searchActive) { + if (searchInstance.nextGeneration()) { + finishSearch(); + searchActive = false; + } + } + } + + private void moveToolheadDown() { + toolheadLowered = checkToolheadEnd(toolheadPosition); + if (toolheadLowered) { + startLiquidSearch(toolheadPosition.down()); + return; + } + + toolheadPosition = toolheadPosition.down(); + world.setBlockState(toolheadPosition, BlockContent.BANANA_BLOCK.getDefaultState()); + } + + private boolean checkToolheadEnd(BlockPos newPosition) { + + var posBelow = newPosition.down(); + var stateBelow = world.getBlockState(posBelow); + var blockBelow = stateBelow.getBlock(); + + return !(blockBelow.equals(Blocks.AIR) || blockBelow.equals(BlockContent.BANANA_BLOCK)); + } + + private void startLiquidSearch(BlockPos start) { + + var state = world.getFluidState(start); + if (!state.isStill()) return; + + searchInstance = new FloodFillSearch(start, world); + searchActive = true; + + System.out.println("starting search at: " + start + " " + state.getFluid() + " " + state.isStill()); + } + + private void finishSearch() { + System.out.println("search finished, found: " + searchInstance.foundTargets.size()); + pendingLiquidPositions = searchInstance.foundTargets; + initialized = true; + searchInstance = null; + } + + @Override + public Storage getFluidStorage(Direction direction) { + return fluidStorage; + } + + private static class FloodFillSearch { + + final HashSet checkedPositions = new HashSet<>(); + final HashSet nextTargets = new HashSet<>(); + final Deque foundTargets = new ArrayDeque<>(); + final World world; + + public FloodFillSearch(BlockPos startPosition, World world) { + this.world = world; + nextTargets.add(startPosition); + } + + // returns true when done + @SuppressWarnings("unchecked") + public boolean nextGeneration() { + + var currentGeneration = (HashSet) nextTargets.clone(); + + var earlyStop = false; + + for (var target : currentGeneration) { + if (isValidTarget(target)) { + foundTargets.addLast(target); + addNeighborsToQueue(target); + if (checkForEarlyStop(target)) earlyStop = true; + } + + checkedPositions.add(target); + nextTargets.remove(target); + } + + if (cutoffSearch() || earlyStop) nextTargets.clear(); + + return nextTargets.isEmpty(); + } + + private boolean checkForEarlyStop(BlockPos target) { + return world.getFluidState(target).getFluid().matchesType(Fluids.WATER); + } + + private boolean cutoffSearch() { + return foundTargets.size() >= MAX_SEARCH_COUNT; + } + + private boolean isValidTarget(BlockPos target) { + var state = world.getFluidState(target); + return state.isStill(); + } + + private void addNeighborsToQueue(BlockPos self) { + + for (var neighbor : getNeighbors(self)) { + if (checkedPositions.contains(neighbor)) continue; + nextTargets.add(neighbor); + } + + } + + // returns all neighboring positions except up + private List getNeighbors(BlockPos pos) { + return List.of(pos.down(), pos.north(), pos.east(), pos.south(), pos.west()); + } + + } +} diff --git a/src/main/java/rearth/oritech/init/BlockContent.java b/src/main/java/rearth/oritech/init/BlockContent.java index 88cbe681f..3753ab329 100644 --- a/src/main/java/rearth/oritech/init/BlockContent.java +++ b/src/main/java/rearth/oritech/init/BlockContent.java @@ -69,6 +69,7 @@ public class BlockContent implements BlockRegistryContainer { public static final Block PLACER_BLOCK = new PlacerBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque()); public static final Block DESTROYER_BLOCK = new DestroyerBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque()); public static final Block FERTILIZER_BLOCK = new FertilizerBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque()); + public static final Block PUMP_BLOCK = new PumpBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque()); public static final Block MACHINE_CORE_BASIC = new MachineCoreBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque(), 1); public static final Block MACHINE_CORE_GOOD = new MachineCoreBlock(FabricBlockSettings.copyOf(Blocks.IRON_BLOCK).nonOpaque(), 6); diff --git a/src/main/java/rearth/oritech/init/BlockEntitiesContent.java b/src/main/java/rearth/oritech/init/BlockEntitiesContent.java index e30477ae2..2cdc819ac 100644 --- a/src/main/java/rearth/oritech/init/BlockEntitiesContent.java +++ b/src/main/java/rearth/oritech/init/BlockEntitiesContent.java @@ -2,6 +2,7 @@ import io.wispforest.owo.registration.reflect.AutoRegistryContainer; import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; import net.minecraft.block.entity.BlockEntityType; import net.minecraft.registry.Registries; @@ -12,12 +13,10 @@ import rearth.oritech.block.entity.machines.addons.InventoryProxyAddonBlockEntity; import rearth.oritech.block.entity.machines.generators.BasicGeneratorEntity; import rearth.oritech.block.entity.machines.generators.TestGeneratorEntity; -import rearth.oritech.block.entity.machines.interaction.DestroyerBlockEntity; -import rearth.oritech.block.entity.machines.interaction.FertilizerBlockEntity; -import rearth.oritech.block.entity.machines.interaction.LaserArmBlockEntity; -import rearth.oritech.block.entity.machines.interaction.PlacerBlockEntity; +import rearth.oritech.block.entity.machines.interaction.*; import rearth.oritech.block.entity.machines.processing.*; import rearth.oritech.util.EnergyProvider; +import rearth.oritech.util.FluidProvider; import rearth.oritech.util.InventoryProvider; import team.reborn.energy.api.EnergyStorage; @@ -68,6 +67,10 @@ public class BlockEntitiesContent implements AutoRegistryContainer LASER_ARM_BLOCK = FabricBlockEntityTypeBuilder.create(LaserArmBlockEntity::new, BlockContent.LASER_ARM_BLOCK).build(); +// @AssignSidedEnergy + @AssignSidedFluid + public static final BlockEntityType PUMP_BLOCK = FabricBlockEntityTypeBuilder.create(PumpBlockEntity::new, BlockContent.PUMP_BLOCK).build(); + @AssignSidedEnergy public static final BlockEntityType ENERGY_ACCEPTOR_ADDON_ENTITY = FabricBlockEntityTypeBuilder.create(EnergyAcceptorAddonBlockEntity::new, BlockContent.MACHINE_ACCEPTOR_ADDON).build(); @@ -117,6 +120,9 @@ public void postProcessField(String namespace, BlockEntityType value, String if (field.isAnnotationPresent(AssignSidedEnergy.class)) EnergyStorage.SIDED.registerForBlockEntity((blockEntity, direction) -> ((EnergyProvider) blockEntity).getStorage(), value); + if (field.isAnnotationPresent(AssignSidedFluid.class)) + FluidStorage.SIDED.registerForBlockEntity((blockEntity, direction) -> ((FluidProvider) blockEntity).getFluidStorage(direction), value); + if (field.isAnnotationPresent(AssignSidedInventory.class)) ItemStorage.SIDED.registerForBlockEntity((blockEntity, direction) -> ((InventoryProvider) blockEntity).getInventory(direction), value); @@ -129,4 +135,8 @@ public void postProcessField(String namespace, BlockEntityType value, String @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface AssignSidedInventory {} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD}) + public @interface AssignSidedFluid {} } diff --git a/src/main/java/rearth/oritech/init/datagen/ModelGenerator.java b/src/main/java/rearth/oritech/init/datagen/ModelGenerator.java index 3068cfb5a..393fc13ae 100644 --- a/src/main/java/rearth/oritech/init/datagen/ModelGenerator.java +++ b/src/main/java/rearth/oritech/init/datagen/ModelGenerator.java @@ -21,6 +21,7 @@ public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGen blockStateModelGenerator.registerSimpleCubeAll(BlockContent.PLACER_BLOCK); blockStateModelGenerator.registerSimpleCubeAll(BlockContent.DESTROYER_BLOCK); blockStateModelGenerator.registerSimpleCubeAll(BlockContent.FERTILIZER_BLOCK); + blockStateModelGenerator.registerSimpleCubeAll(BlockContent.PUMP_BLOCK); blockStateModelGenerator.registerSimpleCubeAll(BlockContent.ADDON_INDICATOR_BLOCK); blockStateModelGenerator.registerSimpleCubeAll(BlockContent.BLOCK_DESTROYER_HEAD); diff --git a/src/main/java/rearth/oritech/util/FluidProvider.java b/src/main/java/rearth/oritech/util/FluidProvider.java new file mode 100644 index 000000000..aa9eedb77 --- /dev/null +++ b/src/main/java/rearth/oritech/util/FluidProvider.java @@ -0,0 +1,13 @@ +package rearth.oritech.util; + +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidVariant; +import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; +import net.fabricmc.fabric.api.transfer.v1.storage.Storage; +import net.minecraft.util.math.Direction; + +public interface FluidProvider { + + Storage getFluidStorage(Direction direction); + +} diff --git a/src/main/resources/assets/oritech/textures/block/pump_block.png b/src/main/resources/assets/oritech/textures/block/pump_block.png new file mode 100644 index 0000000000000000000000000000000000000000..24cf0e75c82b8fb76fa2c4a01ea7c0e84bf9372c GIT binary patch literal 917 zcmV;G18V$p0Cu3&WN-ZA)1}2Z?Al7kpts zVn#N-GTDWR5hHPWhsr{-1TvFdc{8J!5aS|=UZEJWXf{CESTsi z%U^xX?zV)cX&9exd}>s`ah-T#oucIs6;B++^$6zHW2E;p_j>B0#A3b9#3MdsWOkgd$ffY++L;@W+_?@5!ZA~vol=(eU7bMj)yiY%{y7XO@yf` zGf-hLu&}pHR#PxsdvD~RdF~cTT|&&OL~7l%J$T4DUk3n}mgljwwUf~_>cu40<&`9x zo9GU90#GI^GV^H>F|VS8LI5P2n^;P$^JU~Vz16h{RvWK$on+JFWmV6hMON_pT96eP z!^QsB4**Dc31vj{P-!pySt1M3UMW?;7yC93wc@_`Y4mLD(8G1T&NG>jl~)1 zd6kT&@&AnhL<|g+20iua0rtV-TOS3bC)ja?{oSvdjQW5hio=O`_ zY$N7XJ~(@xp9(t0_!_jeBUo*mKG{QAgQt#x!65)PJzl)AI0J)2ynkVs`T82fyvkc= z&olA;B&OLJt`3hOSZ$o??Wf9YDMfTST?h_=pQk2xyEa%_LYKIi*`X{DM2oEO*~L#w zLcf@N9MIq3$0TP}>^Jgu^Um z)zSxYR^`OzHUI;ILtOghNABPL^)VoTK1l^&#%TkNF2l{= zZV-6xMN)=Gyt(%ex!wEh?L6SGx>_pJ65I9?DqEGAVzGcpa0me0sdZyb-lMU?jI79b zV{xVnc{V*>z)hF$7~Yp&qi#QqtjMIK6mnKAK?3j`sX?|_s0()D_q7mp2&_&`V2rO( rY