diff --git a/build.gradle b/build.gradle index 98eb7510..3933704a 100644 --- a/build.gradle +++ b/build.gradle @@ -181,8 +181,11 @@ repositories { dependencies { minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' - // cri - //annotationProcessor(implementation("com.github.LlamaLad7:MixinExtras:0.1.1")) + + compileOnly annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1") + implementation(jarJar("io.github.llamalad7:mixinextras-forge:0.4.1")) { + jarJar.ranged(it, "[0.4.1,)") + } implementation ("org.valkyrienskies.core:api:${vs_core_version}") implementation ("org.valkyrienskies.core:impl:${vs_core_version}") @@ -190,11 +193,6 @@ dependencies { implementation ("org.valkyrienskies.core:util:${vs_core_version}") implementation fg.deobf("org.valkyrienskies:valkyrienskies-120-forge:${vs2_version}") - compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")) - implementation(jarJar("io.github.llamalad7:mixinextras-forge:0.4.1")) { - jarJar.ranged(it, "[0.4.1,)") - } - //implementation fg.deobf("mekanism:Mekanism:${mekanism_version}") //runtimeOnly fg.deobf("mekanism:Mekanism:${mekanism_version}:generators") //runtimeOnly fg.deobf("mekanism:Mekanism:${mekanism_version}:additions") diff --git a/src/main/java/net/jcm/vsch/VSCHCapabilities.java b/src/main/java/net/jcm/vsch/VSCHCapabilities.java new file mode 100644 index 00000000..02eaef52 --- /dev/null +++ b/src/main/java/net/jcm/vsch/VSCHCapabilities.java @@ -0,0 +1,13 @@ +package net.jcm.vsch; + +import net.jcm.vsch.api.pipe.capability.INodePortProvider; + +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; + +public final class VSCHCapabilities { + private VSCHCapabilities() {} + + public static final Capability PORT_PROVIDER = CapabilityManager.get(new CapabilityToken<>(){}); +} diff --git a/src/main/java/net/jcm/vsch/VSCHEvents.java b/src/main/java/net/jcm/vsch/VSCHEvents.java index c642d964..00f5f98e 100644 --- a/src/main/java/net/jcm/vsch/VSCHEvents.java +++ b/src/main/java/net/jcm/vsch/VSCHEvents.java @@ -1,11 +1,16 @@ package net.jcm.vsch; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; import net.jcm.vsch.event.AtmosphericCollision; import net.jcm.vsch.event.GravityInducer; import net.jcm.vsch.event.PlanetCollision; +import net.jcm.vsch.pipe.level.NodeLevel; import net.jcm.vsch.util.EmptyChunkAccess; +import net.jcm.vsch.util.Pair; import net.lointain.cosmos.network.CosmosModVariables; +import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; @@ -19,41 +24,55 @@ import org.valkyrienskies.core.api.ships.LoadedServerShip; import org.valkyrienskies.mod.common.VSGameUtilsKt; +import java.util.function.Predicate; + @Mod.EventBusSubscriber public class VSCHEvents { @SubscribeEvent(priority = EventPriority.HIGH) - public static void onServerTick(TickEvent.ServerTickEvent event) { - if (event.phase != TickEvent.Phase.END) { + public static void onServerTick(final TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.START) { return; } for (final LoadedServerShip ship : VSGameUtilsKt.getShipObjectWorld(event.getServer()).getLoadedShips()) { GravityInducer.getOrCreate(ship); } - for (ServerLevel level : event.getServer().getAllLevels()) { - if (level.getPlayers(player -> true, 1).isEmpty()) { - // skip if the no player is in the world - // TODO: maybe we'll have automated ships in the future and this need to be removed? - continue; + } + + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onLevelTick(final TickEvent.LevelTickEvent event) { + if (!(event.level instanceof ServerLevel serverLevel)) { + return; + } + switch (event.phase) { + case START -> { + NodeLevel.get(serverLevel).getNetwork().onTick(); + } + case END -> { + if (serverLevel.getPlayers(player -> true, 1).isEmpty()) { + // skip if the no player is in the world + // TODO: maybe we'll have automated ships in the future and this need to be removed? + return; + } + AtmosphericCollision.atmosphericCollisionTick(serverLevel); + PlanetCollision.planetCollisionTick(serverLevel); } - AtmosphericCollision.atmosphericCollisionTick(level); - PlanetCollision.planetCollisionTick(level); } } @SubscribeEvent - public static void onServerStart(ServerStartedEvent event) { + public static void onServerStart(final ServerStartedEvent event) { GravityInducer.gravityDataTag = CosmosModVariables.WorldVariables.get(event.getServer().overworld()).gravity_data; } // For next vs update // @SubscribeEvent // public static void shipLoad(VSEvents.ShipLoadEvent event) { - //// Gravity.setAll(event.getServer().overworld()); + // Gravity.setAll(event.getServer().overworld()); // } @SubscribeEvent - public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) { + public static void onBlockPlace(final BlockEvent.EntityPlaceEvent event) { if (!(event.getLevel() instanceof Level level)) { return; } @@ -62,4 +81,26 @@ public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) { event.setCanceled(true); } } + + @SubscribeEvent + public static void onBlockUpdate(final BlockEvent.NeighborNotifyEvent event) { + if (!(event.getLevel() instanceof Level level)) { + return; + } + onBlockChange(level, event.getPos()); + } + + /** + * Do not use BlockEvent.NeighborNotifyEvent here, since it won't trigger for shape update. + */ + public static void onBlockChange(final Level level, final BlockPos blockPos) { + if (level.isClientSide) { + return; + } + final NodeLevel nodeLevel = NodeLevel.get(level); + nodeLevel.streamNodesOn(blockPos) + .filter(Predicate.not(PipeNode::canAnchor)) + .map(PipeNode::getPos) + .forEach(nodeLevel::breakNode); + } } diff --git a/src/main/java/net/jcm/vsch/VSCHMod.java b/src/main/java/net/jcm/vsch/VSCHMod.java index 2d93b7d9..2b01aa48 100644 --- a/src/main/java/net/jcm/vsch/VSCHMod.java +++ b/src/main/java/net/jcm/vsch/VSCHMod.java @@ -10,7 +10,10 @@ import net.jcm.vsch.config.VSCHConfig; import net.jcm.vsch.entity.VSCHEntities; import net.jcm.vsch.event.GravityInducer; +import net.jcm.vsch.fluid.VSCHFluidTypes; +import net.jcm.vsch.fluid.VSCHFluids; import net.jcm.vsch.items.VSCHItems; +import net.jcm.vsch.network.VSCHNetwork; import net.jcm.vsch.util.assemble.MoveUtil; import net.minecraftforge.client.event.EntityRenderersEvent; @@ -26,6 +29,7 @@ @Mod(VSCHMod.MODID) public class VSCHMod { public static final String MODID = "vsch"; + public static final String VERSION = ModLoadingContext.get().getActiveContainer().getModInfo().getVersion().toString(); public VSCHMod() { IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); @@ -37,6 +41,10 @@ public VSCHMod() { VSCHTab.register(modBus); VSCHEntities.register(modBus); VSCHTags.register(); + VSCHFluidTypes.register(modBus); + VSCHFluids.register(modBus); + VSCHNetwork.register(); + MoveUtil.registerDefaultMovers(); // Register commands (I took this code from another one of my mods, can't be bothered to make it consistent with the rest of this) diff --git a/src/main/java/net/jcm/vsch/VSCHTab.java b/src/main/java/net/jcm/vsch/VSCHTab.java index 3843d393..37016a15 100644 --- a/src/main/java/net/jcm/vsch/VSCHTab.java +++ b/src/main/java/net/jcm/vsch/VSCHTab.java @@ -11,23 +11,18 @@ import net.minecraftforge.registries.RegistryObject; public class VSCHTab { - public static final DeferredRegister REGISTRY = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, VSCHMod.MODID); - - public static final RegistryObject TAB = REGISTRY.register("starlance", - () -> CreativeModeTab.builder().title(Component.translatable("vsch.itemtab")).icon(() -> new ItemStack(VSCHBlocks.THRUSTER_BLOCK.get())).displayItems((parameters, tabData) -> { - - tabData.accept(VSCHBlocks.THRUSTER_BLOCK.get()); - tabData.accept(VSCHBlocks.AIR_THRUSTER_BLOCK.get()); - tabData.accept(VSCHBlocks.POWERFUL_THRUSTER_BLOCK.get()); - tabData.accept(VSCHBlocks.DRAG_INDUCER_BLOCK.get()); - tabData.accept(VSCHBlocks.GYRO_BLOCK.get()); - tabData.accept(VSCHBlocks.ROCKET_ASSEMBLER_BLOCK.get()); - - tabData.accept(VSCHItems.MAGNET_BOOT.get()); - - tabData.accept(VSCHItems.WRENCH.get()); - - }).build()); + private static final DeferredRegister REGISTRY = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, VSCHMod.MODID); + + public static final RegistryObject TAB = REGISTRY.register( + VSCHMod.MODID, + () -> CreativeModeTab.builder() + .title(Component.translatable("vsch.itemtab")) + .icon(() -> new ItemStack(VSCHBlocks.THRUSTER_BLOCK.get())) + .displayItems((parameters, tabData) -> { + VSCHItems.registerTab(tabData::accept); + }) + .build() + ); public static void register(IEventBus eventBus) { REGISTRY.register(eventBus); diff --git a/src/main/java/net/jcm/vsch/accessor/IChunkMapAccessor.java b/src/main/java/net/jcm/vsch/accessor/IChunkMapAccessor.java new file mode 100644 index 00000000..bfddcbd4 --- /dev/null +++ b/src/main/java/net/jcm/vsch/accessor/IChunkMapAccessor.java @@ -0,0 +1,7 @@ +package net.jcm.vsch.accessor; + +import net.minecraft.server.level.ChunkHolder; + +public interface IChunkMapAccessor { + Iterable starlance$getChunks(); +} diff --git a/src/main/java/net/jcm/vsch/accessor/IClientboundLevelChunkWithLightPacketAccessor.java b/src/main/java/net/jcm/vsch/accessor/IClientboundLevelChunkWithLightPacketAccessor.java new file mode 100644 index 00000000..ee82924b --- /dev/null +++ b/src/main/java/net/jcm/vsch/accessor/IClientboundLevelChunkWithLightPacketAccessor.java @@ -0,0 +1,7 @@ +package net.jcm.vsch.accessor; + +import net.jcm.vsch.network.s2c.PipeNodeSyncChunkS2C; + +public interface IClientboundLevelChunkWithLightPacketAccessor { + PipeNodeSyncChunkS2C starlance$getPipeNodeSyncChunkS2C(); +} diff --git a/src/main/java/net/jcm/vsch/accessor/IGuiAccessor.java b/src/main/java/net/jcm/vsch/accessor/IGuiAccessor.java index 6931ce92..c5de5823 100644 --- a/src/main/java/net/jcm/vsch/accessor/IGuiAccessor.java +++ b/src/main/java/net/jcm/vsch/accessor/IGuiAccessor.java @@ -3,5 +3,5 @@ import net.minecraft.network.chat.Component; public interface IGuiAccessor { - void vsch$setOverlayMessageIfNotExist(Component component, int duration); + void starlance$setOverlayMessageIfNotExist(Component component, int duration); } diff --git a/src/main/java/net/jcm/vsch/accessor/ILevelAccessor.java b/src/main/java/net/jcm/vsch/accessor/ILevelAccessor.java new file mode 100644 index 00000000..64acf75f --- /dev/null +++ b/src/main/java/net/jcm/vsch/accessor/ILevelAccessor.java @@ -0,0 +1,7 @@ +package net.jcm.vsch.accessor; + +import net.jcm.vsch.pipe.level.NodeLevel; + +public interface ILevelAccessor { + NodeLevel starlance$getNodeLevel(); +} diff --git a/src/main/java/net/jcm/vsch/accessor/INodeLevelChunkSection.java b/src/main/java/net/jcm/vsch/accessor/INodeLevelChunkSection.java new file mode 100644 index 00000000..3843df83 --- /dev/null +++ b/src/main/java/net/jcm/vsch/accessor/INodeLevelChunkSection.java @@ -0,0 +1,30 @@ +package net.jcm.vsch.accessor; + +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.SectionPos; +import net.minecraft.network.FriendlyByteBuf; + +public interface INodeLevelChunkSection { + /** + * Do NOT modify the returned array. + */ + PipeNode[][] starlance$getAllNodes(); + + PipeNode starlance$getNode(int x, int y, int z, int index); + + /** + * Do NOT modify the returned array. + */ + PipeNode[] starlance$getNodes(int x, int y, int z); + + PipeNode starlance$setNode(int x, int y, int z, int index, PipeNode node); + + boolean starlance$hasAnyNode(); + + void starlance$writeNodes(FriendlyByteBuf buf); + + void starlance$readNodes(NodeLevel level, SectionPos sectionPos, FriendlyByteBuf buf); +} + diff --git a/src/main/java/net/jcm/vsch/api/pipe/AbstractCustomNode.java b/src/main/java/net/jcm/vsch/api/pipe/AbstractCustomNode.java new file mode 100644 index 00000000..2e7a47a6 --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/AbstractCustomNode.java @@ -0,0 +1,14 @@ +package net.jcm.vsch.api.pipe; + +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.DyeColor; + +public abstract class AbstractCustomNode> extends PipeNode { + protected AbstractCustomNode(final NodeLevel level, final NodePos pos) { + super(level, pos, Type.CUSTOM); + } + + public abstract ResourceLocation getId(); +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/CustomNodeRegistry.java b/src/main/java/net/jcm/vsch/api/pipe/CustomNodeRegistry.java new file mode 100644 index 00000000..9d07055d --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/CustomNodeRegistry.java @@ -0,0 +1,47 @@ +package net.jcm.vsch.api.pipe; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.IForgeRegistry; +import net.minecraftforge.registries.RegistryBuilder; +import net.minecraftforge.registries.RegistryObject; + +import java.util.function.Supplier; + +public final class CustomNodeRegistry { + private CustomNodeRegistry() {} + + public static final ResourceKey>> ID = ResourceKey.createRegistryKey(new ResourceLocation(VSCHMod.MODID, "custom_node")); + private static final DeferredRegister> REGISTRY = DeferredRegister.create(ID, VSCHMod.MODID); + private static Supplier>> REGISTERED_REGISTRY = null; + + public static IForgeRegistry> getRegistry() { + return REGISTERED_REGISTRY.get(); + } + + public static AbstractCustomNode createNode(final ResourceLocation id, final NodeLevel level, final NodePos pos) { + final IForgeRegistry> registry = REGISTERED_REGISTRY.get(); + if (registry == null) { + throw new IllegalStateException("Trying to use registry before it get registered"); + } + final PipeNodeProvider provider = registry.getValue(id); + if (provider == null) { + return null; + } + return provider.createNode(level, pos); + } + + /** + * module private + */ + public static void register(final IEventBus bus) { + REGISTERED_REGISTRY = REGISTRY.makeRegistry(RegistryBuilder::new); + REGISTRY.register(bus); + } +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/FlowDirection.java b/src/main/java/net/jcm/vsch/api/pipe/FlowDirection.java new file mode 100644 index 00000000..d22ea17b --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/FlowDirection.java @@ -0,0 +1,24 @@ +package net.jcm.vsch.api.pipe; + +public enum FlowDirection { + NONE(false, false), + BOTH(true, true), + IN(true, false), + OUT(false, true); + + private final boolean in; + private final boolean out; + + private FlowDirection(final boolean in, final boolean out) { + this.in = in; + this.out = out; + } + + public boolean canFlowIn() { + return this.in; + } + + public boolean canFlowOut() { + return this.out; + } +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/NodePos.java b/src/main/java/net/jcm/vsch/api/pipe/NodePos.java new file mode 100644 index 00000000..c4d7b4da --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/NodePos.java @@ -0,0 +1,466 @@ +package net.jcm.vsch.api.pipe; + +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; + +import org.joml.Vector3d; +import org.valkyrienskies.core.api.ships.Ship; +import org.valkyrienskies.mod.common.VSGameUtilsKt; + +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public record NodePos( + // The block position the node relative to + BlockPos blockPos, + // Which axis of the block the node is on (origin is on the lower corner) + Direction.Axis axis, + // The index (aka distance) from the origin, in range of [0, 7] + int index +) implements Comparable { + public static final int INDEX_BOUND = 8; + public static final int UNIQUE_INDEX_BOUND = 1 + 3 * (INDEX_BOUND - 1); + private static final double NODE_SIZE = 1.0 / INDEX_BOUND; + + public int uniqueIndex() { + if (this.index == 0) { + return 0; + } + return this.axis.ordinal() * 7 + this.index; + } + + public static NodePos originOf(final BlockPos blockPos) { + return new NodePos(blockPos, Direction.Axis.X, 0); + } + + public static NodePos fromUniqueIndex(final BlockPos blockPos, final int uniqueIndex) { + if (uniqueIndex == 0) { + return NodePos.originOf(blockPos); + } + final int axisInd = (uniqueIndex - 1) / 7; + final Direction.Axis axis = Direction.Axis.VALUES[axisInd]; + return new NodePos(blockPos, axis, uniqueIndex - axisInd * 7); + } + + public static NodePos fromHitResult(final Level level, final BlockPos blockPos, Vec3 pos, final double size) { + if (!VSGameUtilsKt.isBlockInShipyard(level, pos)) { + final Ship ship = VSGameUtilsKt.getShipManagingPos(level, blockPos); + if (ship != null) { + final Vector3d worldPos = ship.getWorldToShip().transformPosition(new Vector3d(pos.x, pos.y, pos.z)); + pos = new Vec3(worldPos.x, worldPos.y, worldPos.z); + } + } + return fromVec3(pos, size); + } + + public static NodePos fromVec3(final Vec3 pos, final double size) { + final double r = size / 2; + final Vec3 adjusted = pos.add(r, r, r); + final BlockPos blockPos = BlockPos.containing(adjusted); + final double x = adjusted.x - blockPos.getX(), y = adjusted.y - blockPos.getY(), z = adjusted.z - blockPos.getZ(); + final boolean xIn = x <= size, yIn = y <= size, zIn = z <= size; + Direction.Axis axis = null; + if (xIn) { + if (yIn) { + if (zIn) { + return NodePos.originOf(blockPos); + } + axis = Direction.Axis.Z; + } else if (zIn) { + axis = Direction.Axis.Y; + } + } else if (yIn && zIn) { + axis = Direction.Axis.X; + } + if (axis == null) { + return null; + } + return new NodePos(blockPos, axis, (int)((int)(axis.choose(x, y, z) / size) * (size / NODE_SIZE))); + } + + public boolean isOrigin() { + return this.index == 0; + } + + public boolean isOnAxis(final Direction.Axis axis) { + return this.index == 0 || this.axis == axis; + } + + @Override + public boolean equals(final Object otherObj) { + return this == otherObj || otherObj instanceof NodePos other && + this.blockPos.equals(other.blockPos) && + ( + this.isOrigin() && other.isOrigin() || + this.axis == other.axis && this.index == other.index + ); + } + + @Override + public int compareTo(final NodePos other) { + final int blockPosOrder = this.blockPos.compareTo(other.blockPos); + if (blockPosOrder != 0) { + return blockPosOrder; + } + final int indexDiff = this.uniqueIndex() - other.uniqueIndex(); + return indexDiff < 0 ? -1 : indexDiff == 0 ? 0 : 1; + } + + @Override + public int hashCode() { + return this.blockPos.hashCode() + this.uniqueIndex() * 31; + } + + public Vec3 getCenter() { + if (this.isOrigin()) { + return Vec3.atLowerCornerOf(this.blockPos); + } + return new Vec3( + this.blockPos.getX() + this.axis.choose(this.index, 0, 0) / (double)(INDEX_BOUND), + this.blockPos.getY() + this.axis.choose(0, this.index, 0) / (double)(INDEX_BOUND), + this.blockPos.getZ() + this.axis.choose(0, 0, this.index) / (double)(INDEX_BOUND) + ); + } + + public double manhattanDistTo(final NodePos other) { + final Vec3 center = this.getCenter(); + final Vec3 otherCenter = other.getCenter(); + return Math.abs(center.x - otherCenter.x) + Math.abs(center.y - otherCenter.y) + Math.abs(center.z - otherCenter.z); + } + + public AABB getAABB(final double size) { + final double r = size / 2; + final Vec3 center = this.getCenter(); + return new AABB(center.x - r, center.y - r, center.z - r, center.x + r, center.y + r, center.z + r); + } + + public boolean canAnchoredIn(final Level level, final double size) { + final AABB box = this.getAABB(size); + for (final VoxelShape shape : level.getBlockCollisions(null, box)) { + if (!shape.isEmpty()) { + return true; + } + } + return false; + } + + public void writeTo(final FriendlyByteBuf buf) { + buf.writeBlockPos(this.blockPos); + buf.writeByte(this.uniqueIndex()); + } + + public static NodePos readFrom(final FriendlyByteBuf buf) { + final BlockPos blockPos = buf.readBlockPos(); + final int index = buf.readByte(); + return NodePos.fromUniqueIndex(blockPos, index); + } + + public static Stream streamNodePosOn(final BlockPos pos) { + return Stream.concat( + Stream.of( + NodePos.originOf(pos), + NodePos.originOf(pos.offset(0, 0, 1)), + NodePos.originOf(pos.offset(0, 1, 0)), + NodePos.originOf(pos.offset(0, 1, 1)), + NodePos.originOf(pos.offset(1, 0, 0)), + NodePos.originOf(pos.offset(1, 0, 1)), + NodePos.originOf(pos.offset(1, 1, 0)), + NodePos.originOf(pos.offset(1, 1, 1)) + ), + Stream.of(Direction.Axis.values()).flatMap((axis) -> IntStream.range(1, INDEX_BOUND) + .filter((i) -> i % 2 == 0) + .mapToObj((i) -> Stream.of( + new NodePos(pos, axis, i), + new NodePos(pos.offset(axis.choose(0, 0, 0), axis.choose(0, 0, 1), axis.choose(1, 1, 0)), axis, i), + new NodePos(pos.offset(axis.choose(0, 1, 1), axis.choose(1, 0, 0), axis.choose(0, 0, 0)), axis, i), + new NodePos(pos.offset(axis.choose(0, 1, 1), axis.choose(1, 0, 1), axis.choose(1, 1, 0)), axis, i) + )) + .flatMap(Function.identity()) + ) + ); + } + + public static Stream streamPlaceHint(final NodeLevel level, final BlockPos pos) { + return streamNodePosOn(pos) + .filter((p) -> level.getNode(p) == null) + .filter((p) -> p.canAnchoredIn(level.getLevel(), 4.0 / 16)); + } + + public Stream streamPossibleToConnect() { + if (this.isOrigin()) { + return Direction.stream() + .flatMap((dir) -> { + final Direction.Axis dirAxis = dir.getAxis(); + final BlockPos relative = this.blockPos.relative(dir); + return streamAxisesExclude(dirAxis) + .flatMap((axis) -> { + final BlockPos relNeg = relative.relative(axis, -1); + return IntStream.range(1, INDEX_BOUND) + .mapToObj((i) -> Stream.of(new NodePos(relative, axis, i), new NodePos(relNeg, axis, i))) + .flatMap(Function.identity()); + }); + }); + } + + final Direction.Axis thisAxis = this.axis; + final BlockPos posRel = this.blockPos.relative(thisAxis, 1); + + return Stream.concat( + streamAxisesExclude(thisAxis) + .flatMap((axis) -> Stream.concat( + IntStream.range(1, INDEX_BOUND).mapToObj((i) -> new NodePos(this.blockPos, axis, i)), + IntStream.range(1, INDEX_BOUND).mapToObj((i) -> new NodePos(posRel, axis, i)) + )), + Direction.stream() + .filter((dir) -> dir.getAxis() != thisAxis) + .flatMap((dir) -> { + final Direction.Axis dirAxis = dir.getAxis(); + final BlockPos relative = this.blockPos.relative(dir); + final BlockPos rel2 = relative.relative(thisAxis, 1); + final Stream stream = Stream.concat( + Stream.of(NodePos.originOf(relative), NodePos.originOf(rel2)), + IntStream.range(1, INDEX_BOUND).mapToObj((i) -> new NodePos(relative, thisAxis, i)) + ); + if (dir.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + return stream; + } + return Stream.concat( + stream, + Stream.concat( + IntStream.range(1, INDEX_BOUND).mapToObj((i) -> new NodePos(relative, dirAxis, i)), + IntStream.range(1, INDEX_BOUND).mapToObj((i) -> new NodePos(rel2, dirAxis, i)) + ) + ); + }) + ); + } + + public Direction[] connectPathTo(final NodePos other) { + final boolean selfIsOrigin = this.isOrigin(); + final boolean otherIsOrigin = other.isOrigin(); + + final int xDiff = (other.blockPos.getX() - this.blockPos.getX()) * INDEX_BOUND + + (other.axis.choose(other.index, 0, 0) - this.axis.choose(this.index, 0, 0)); + final int yDiff = (other.blockPos.getY() - this.blockPos.getY()) * INDEX_BOUND + + (other.axis.choose(0, other.index, 0) - this.axis.choose(0, this.index, 0)); + final int zDiff = (other.blockPos.getZ() - this.blockPos.getZ()) * INDEX_BOUND + + (other.axis.choose(0, 0, other.index) - this.axis.choose(0, 0, this.index)); + final int alignCount = (xDiff == 0 ? 1 : 0) + (yDiff == 0 ? 1 : 0) + (zDiff == 0 ? 1 : 0); + if (alignCount == 0) { + throw new IllegalArgumentException("Impossible connection: node positions must align on at least one plane"); + } + if (alignCount == 3) { + throw new IllegalArgumentException("Cannot connect to self"); + } + final Direction xDir = Direction.fromAxisAndDirection( + Direction.Axis.X, + xDiff > 0 ? Direction.AxisDirection.POSITIVE : Direction.AxisDirection.NEGATIVE + ); + final Direction yDir = Direction.fromAxisAndDirection( + Direction.Axis.Y, + yDiff > 0 ? Direction.AxisDirection.POSITIVE : Direction.AxisDirection.NEGATIVE + ); + final Direction zDir = Direction.fromAxisAndDirection( + Direction.Axis.Z, + zDiff > 0 ? Direction.AxisDirection.POSITIVE : Direction.AxisDirection.NEGATIVE + ); + if (selfIsOrigin) { + if (otherIsOrigin) { + throw new IllegalArgumentException("Impossible connection: origin node cannot connect to another origin node"); + } + if (alignCount == 2) { + throw new IllegalArgumentException("Impossible connection: origin node cannot connect to another node that on the same line"); + } + if (xDiff == 0) { + switch (other.axis) { + case Y: + return new Direction[]{yDir, zDir}; + case Z: + return new Direction[]{zDir, yDir}; + } + } else if (yDiff == 0) { + switch (other.axis) { + case X: + return new Direction[]{xDir, zDir}; + case Z: + return new Direction[]{zDir, xDir}; + } + } else if (zDiff == 0) { + switch (other.axis) { + case X: + return new Direction[]{xDir, yDir}; + case Y: + return new Direction[]{yDir, xDir}; + } + } + throw new IllegalStateException("unreachable"); + } + if (otherIsOrigin) { + if (alignCount == 2) { + throw new IllegalArgumentException("Impossible connection: origin node cannot connect to another node that on the same line"); + } + if (xDiff == 0) { + switch (this.axis) { + case Y: + return new Direction[]{zDir, yDir}; + case Z: + return new Direction[]{yDir, zDir}; + } + } else if (yDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{zDir, xDir}; + case Z: + return new Direction[]{xDir, zDir}; + } + } else if (zDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{yDir, xDir}; + case Y: + return new Direction[]{xDir, yDir}; + } + } + throw new IllegalStateException("unreachable"); + } + if (alignCount == 2) { + if (xDiff != 0) { + return new Direction[]{xDir}; + } + if (yDiff != 0) { + return new Direction[]{yDir}; + } + if (zDiff != 0) { + return new Direction[]{zDir}; + } + throw new IllegalStateException("unreachable"); + } + if (alignCount == 1) { + if (this.axis == other.axis) { + if (xDiff == 0) { + switch (this.axis) { + case Y: + return new Direction[]{zDir, yDir, zDir}; + case Z: + return new Direction[]{yDir, zDir, yDir}; + } + } else if (yDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{zDir, xDir, zDir}; + case Z: + return new Direction[]{xDir, zDir, xDir}; + } + } else if (zDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{yDir, xDir, yDir}; + case Y: + return new Direction[]{xDir, yDir, xDir}; + } + } + throw new IllegalStateException("unreachable"); + } + if (xDiff == 0) { + switch (this.axis) { + case Y: + return new Direction[]{zDir, yDir}; + case Z: + return new Direction[]{yDir, zDir}; + } + } else if (yDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{zDir, xDir}; + case Z: + return new Direction[]{xDir, zDir}; + } + } else if (zDiff == 0) { + switch (this.axis) { + case X: + return new Direction[]{yDir, xDir}; + case Y: + return new Direction[]{xDir, yDir}; + } + } + throw new IllegalStateException("unreachable"); + } + throw new IllegalStateException("unreachable"); + } + + private static Stream streamAxisesExclude(final Direction.Axis exclude) { + final Direction.Axis[] axises = new Direction.Axis[2]; + int index = 0; + for (final Direction.Axis axis : Direction.Axis.VALUES) { + if (axis != exclude) { + axises[index++] = axis; + } + } + return Stream.of(axises); + } + + public RelativeNodePos asRelative(final BlockPos blockPos) { + if (this.blockPos.equals(blockPos)) { + return new RelativeNodePos( + this.axis.choose(this.index, 0, 0), + this.axis.choose(0, this.index, 0), + this.axis.choose(0, 0, this.index) + ); + } + final int xIndex = this.blockPos.getX() == blockPos.getX() ? 0 : this.blockPos.getX() == blockPos.getX() + 1 ? INDEX_BOUND : -1; + final int yIndex = this.blockPos.getY() == blockPos.getY() ? 0 : this.blockPos.getY() == blockPos.getY() + 1 ? INDEX_BOUND : -1; + final int zIndex = this.blockPos.getZ() == blockPos.getZ() ? 0 : this.blockPos.getZ() == blockPos.getZ() + 1 ? INDEX_BOUND : -1; + if (xIndex != -1 && yIndex != -1 && zIndex != -1) { + if (this.isOrigin()) { + return new RelativeNodePos(xIndex, yIndex, zIndex); + } + if (this.axis.choose(xIndex, yIndex, zIndex) == 0) { + return new RelativeNodePos( + this.axis.choose(this.index, xIndex, xIndex), + this.axis.choose(yIndex, this.index, yIndex), + this.axis.choose(zIndex, zIndex, this.index) + ); + } + } + throw new IllegalArgumentException("Invalid base block pos " + blockPos + " to find relative of " + this.toString()); + } + + public Stream streamTouchingBlocks(final Level level) { + final double size = 4.0 / 16; + final double r = size / 2; + final Vec3 centerPos = this.getCenter(); + return Stream.of( + new Vec3(centerPos.x + r, centerPos.y + r, centerPos.z + r), + new Vec3(centerPos.x - r, centerPos.y + r, centerPos.z + r), + new Vec3(centerPos.x + r, centerPos.y + r, centerPos.z - r), + new Vec3(centerPos.x - r, centerPos.y + r, centerPos.z - r), + new Vec3(centerPos.x + r, centerPos.y - r, centerPos.z + r), + new Vec3(centerPos.x - r, centerPos.y - r, centerPos.z + r), + new Vec3(centerPos.x + r, centerPos.y - r, centerPos.z - r), + new Vec3(centerPos.x - r, centerPos.y - r, centerPos.z - r) + ) + .filter((corner) -> { + final double EPSILON = 1e-7; + final double x = centerPos.x + (corner.x > centerPos.x ? EPSILON : -EPSILON); + final double y = centerPos.y + (corner.y > centerPos.y ? EPSILON : -EPSILON); + final double z = centerPos.z + (corner.z > centerPos.z ? EPSILON : -EPSILON); + final AABB box = new AABB(x, y, z, corner.x, corner.y, corner.z); + for (final VoxelShape shape : level.getBlockCollisions(null, box)) { + if (!shape.isEmpty()) { + return true; + } + } + return false; + }) + .map(BlockPos::containing) + .distinct(); + } +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/PipeNode.java b/src/main/java/net/jcm/vsch/api/pipe/PipeNode.java new file mode 100644 index 00000000..6109dad7 --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/PipeNode.java @@ -0,0 +1,202 @@ +package net.jcm.vsch.api.pipe; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.api.resource.ModelTextures; +import net.jcm.vsch.pipe.OmniNode; +import net.jcm.vsch.pipe.PipeNetworkOperator; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.common.ForgeMod; +import net.minecraftforge.fluids.FluidType; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public abstract class PipeNode> { + private static final Logger LOGGER = LogManager.getLogger(VSCHMod.MODID); + + /** + * module private + */ + public final PipeNetworkOperator.RelationHolder relation = new PipeNetworkOperator.RelationHolder(); + private final NodeLevel level; + private final NodePos pos; + private final Type type; + private DyeColor color = DyeColor.WHITE; + + protected PipeNode(final NodeLevel level, final NodePos pos, final Type type) { + this.level = level; + this.pos = pos; + this.type = type; + } + + public final NodeLevel getLevel() { + return this.level; + } + + public final NodePos getPos() { + return this.pos; + } + + public final Type getType() { + return this.type; + } + + public DyeColor getColor() { + return this.color; + } + + public void setColor(final DyeColor color) { + this.color = color; + } + + public int getSize() { + return 4; + } + + public abstract ItemStack asItemStack(); + + public abstract ModelTextures getModel(); + + public abstract ModelTextures getPipeModel(final Direction direction); + + /** + * @param dir Direction of another pipe node + * @return if pipes can connect from the direction + */ + public abstract boolean canConnect(Direction dir); + + public boolean canConnect(final Direction dir, final PipeNode other) { + return this.getColor() == other.getColor() && this.canConnect(dir); + } + + /** + * @param dir Block direction contents tring to interact with + * @return {@link FlowDirection} + */ + public abstract FlowDirection getAccessFlowDirection(Direction dir); + + /** + * @param dir Pipe direction contents tring to interact with + * @return {@link FlowDirection} + */ + public abstract FlowDirection getFlowDirection(Direction dir); + + /** + * Water flow rate used to calculate other fluids flow rate based on their viscosity. + * + * @return How fast can water transfer in mB/tick + * @see FluidType#getViscosity + * @see fluidFlowAmount + */ + protected abstract int getWaterFlowRate(); + + /** + * @param dir Direction the fluid flowing towards to + * @param fluid The fluid + * @return How fast can the fluid transfer in mB/tick + * + * @see getWaterFlowRate + * @see getFlowDirection + */ + public int fluidFlowAmount(final Direction dir, final Fluid fluid) { + if (!this.getFlowDirection(dir).canFlowOut()) { + return 0; + } + return this.getWaterFlowRate() * ForgeMod.WATER_TYPE.get().getViscosity() / fluid.getFluidType().getViscosity(); + } + + public abstract int energyFlowAmount(Direction dir); + + public void writeAdditional(final FriendlyByteBuf buf) {} + + public void readAdditional(final FriendlyByteBuf buf) {} + + public final void writeTo(final FriendlyByteBuf buf) { + buf.writeByte((this.type.getCode() << 4) | this.color.getId()); + if (this.type == Type.CUSTOM) { + buf.writeResourceLocation(((AbstractCustomNode) (this)).getId()); + } + this.writeAdditional(buf); + } + + public static PipeNode readFrom(final NodeLevel level, final NodePos pos, final FriendlyByteBuf buf) { + final int typeAndColor = buf.readByte(); + final DyeColor color = DyeColor.byId(typeAndColor & 0xf); + final int typeCode = (typeAndColor >> 4) & 0xf; + final Type type = Type.CODE_MAP[typeCode]; + final PipeNode node = switch (type) { + case OMNI -> new OmniNode(level, pos); + case CUSTOM -> { + final ResourceLocation id = buf.readResourceLocation(); + final AbstractCustomNode n = CustomNodeRegistry.createNode(id, level, pos); + if (n == null) { + LOGGER.error("[starlance]: custom node with ID " + id + " is not found"); + yield null; + } + yield n; + } + default -> { + LOGGER.error("[starlance]: unexpected node type: " + Integer.toString(typeCode, 16)); + yield null; + } + }; + if (node == null) { + return null; + } + node.setColor(color); + node.readAdditional(buf); + return node; + } + + public String toString() { + return String.format("<%s level=%s pos=%s>", this.getClass().getName(), this.level.getLevel().dimension().location(), this.pos); + } + + public boolean canAnchor() { + return this.pos.canAnchoredIn(this.level.getLevel(), this.getSize() / 16.0); + } + + public enum Type { + // Can connect to all directions; push/pull for all directions + OMNI(0x0), + // Can only connect on two opposite directions; push/pull for all directions + STRAIGHT(0x1), + // Can only connect on two perpendicular directions; push/pull for all directions + CORNER(0x2), + // Can only connect & transfer from one direction to its opposite direction; no interact with machine + ONEWAY(0x3), + // Can connect on all directions; no interact with machine + LIMITED_OMNI(0x4), + // Can connect on all directions; only one side can push to machine + LIMITED_PUSH(0x5), + // Can connect on all directions; only one side can pull from machine + LIMITED_PULL(0x6), + // Custom node + CUSTOM(0xf); + + private final int code; + + static final Type[] CODE_MAP = new Type[16]; + + static { + for (final Type t : Type.values()) { + CODE_MAP[t.getCode()] = t; + } + } + + private Type(final int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } + } +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/PipeNodeProvider.java b/src/main/java/net/jcm/vsch/api/pipe/PipeNodeProvider.java new file mode 100644 index 00000000..50006f9a --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/PipeNodeProvider.java @@ -0,0 +1,8 @@ +package net.jcm.vsch.api.pipe; + +import net.jcm.vsch.pipe.level.NodeLevel; + +@FunctionalInterface +public interface PipeNodeProvider> { + T createNode(NodeLevel level, NodePos pos); +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/RelativeNodePos.java b/src/main/java/net/jcm/vsch/api/pipe/RelativeNodePos.java new file mode 100644 index 00000000..e0821d2c --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/RelativeNodePos.java @@ -0,0 +1,36 @@ +package net.jcm.vsch.api.pipe; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +public record RelativeNodePos( + int x, + int y, + int z +) { + public NodePos asAbsolute(BlockPos blockPos) { + int x = this.x, y = this.y, z = this.z; + if (x == NodePos.INDEX_BOUND) { + x = 0; + blockPos = blockPos.offset(1, 0, 0); + } + if (y == NodePos.INDEX_BOUND) { + y = 0; + blockPos = blockPos.offset(0, 1, 0); + } + if (z == NodePos.INDEX_BOUND) { + z = 0; + blockPos = blockPos.offset(0, 0, 1); + } + if (x == 0) { + if (y == 0) { + return new NodePos(blockPos, Direction.Axis.Z, z); + } else if (z == 0) { + return new NodePos(blockPos, Direction.Axis.Y, y); + } + } else if (y == 0 && z == 0) { + return new NodePos(blockPos, Direction.Axis.X, x); + } + throw new IllegalStateException("Invalid relative node pos: " + this.toString()); + } +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/capability/INodePortProvider.java b/src/main/java/net/jcm/vsch/api/pipe/capability/INodePortProvider.java new file mode 100644 index 00000000..7aca009c --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/capability/INodePortProvider.java @@ -0,0 +1,8 @@ +package net.jcm.vsch.api.pipe.capability; + +import net.jcm.vsch.api.pipe.RelativeNodePos; + +@FunctionalInterface +public interface INodePortProvider { + NodePort getNodePort(RelativeNodePos pos); +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/capability/NodeEnergyPort.java b/src/main/java/net/jcm/vsch/api/pipe/capability/NodeEnergyPort.java new file mode 100644 index 00000000..ec21adee --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/capability/NodeEnergyPort.java @@ -0,0 +1,17 @@ +package net.jcm.vsch.api.pipe.capability; + +public interface NodeEnergyPort extends NodePort { + /** + * @param amount The amount of the energy pushing + * @param simulate If this is a simulate action + * @return The actual amount of energy pushed + */ + int pushEnergy(int amount, boolean simulate); + + /** + * @param amount The amount of the energy pulling + * @param simulate If this is a simulate action + * @return The actual amount of energy pulled + */ + int pullEnergy(int amount, boolean simulate); +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/capability/NodeFluidPort.java b/src/main/java/net/jcm/vsch/api/pipe/capability/NodeFluidPort.java new file mode 100644 index 00000000..8369df37 --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/capability/NodeFluidPort.java @@ -0,0 +1,31 @@ +package net.jcm.vsch.api.pipe.capability; + +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.fluids.FluidStack; + +public interface NodeFluidPort extends NodePort { + /** + * Check the fluid the port may interact with. + * If the raw type of the fluid stack is {@link Fluids#EMPTY}, any fluids may push to the port and no fluids can be pulled. + * Otherwise, the port may only accept that type of fluid. + * The returned FluidStack must not be modified. + * + * @return The fluid the port may interact with. + */ + FluidStack peekFluid(); + + /** + * @param stack The fluid pushing + * @param simulate If this is a simulate action + * @return The actual amount of fluid pushed + */ + int pushFluid(FluidStack stack, boolean simulate); + + /** + * @param amount The maximum amount of the fluid pulling + * @param simulate If this is a simulate action + * @return The actual fluid pulled + */ + FluidStack pullFluid(int amount, boolean simulate); +} diff --git a/src/main/java/net/jcm/vsch/api/pipe/capability/NodePort.java b/src/main/java/net/jcm/vsch/api/pipe/capability/NodePort.java new file mode 100644 index 00000000..ddb2c454 --- /dev/null +++ b/src/main/java/net/jcm/vsch/api/pipe/capability/NodePort.java @@ -0,0 +1,15 @@ +package net.jcm.vsch.api.pipe.capability; + +import net.jcm.vsch.api.pipe.FlowDirection; + +public interface NodePort { + /** + * @return pressure on the port, cannot be less than {@code 0} + */ + double getPressure(); + + /** + * @return {@link FlowDirection} relative to the block + */ + FlowDirection getFlowDirection(); +} diff --git a/src/main/java/net/jcm/vsch/api/resource/TextureLocation.java b/src/main/java/net/jcm/vsch/api/resource/TextureLocation.java index 02cd41e0..e2192c70 100644 --- a/src/main/java/net/jcm/vsch/api/resource/TextureLocation.java +++ b/src/main/java/net/jcm/vsch/api/resource/TextureLocation.java @@ -7,6 +7,10 @@ public TextureLocation(ResourceLocation location, int offsetX, int offsetY) { this(location, offsetX, offsetY, 1f); } + public static TextureLocation fromEnd(ResourceLocation location, int offsetX, int offsetY) { + return new TextureLocation(location, -offsetX, -offsetY, -1f); + } + public static TextureLocation fromNonStandardSize(ResourceLocation location, int offsetX, int offsetY, int size) { return new TextureLocation(location, offsetX, offsetY, 16f / size); } diff --git a/src/main/java/net/jcm/vsch/blocks/VSCHBlocks.java b/src/main/java/net/jcm/vsch/blocks/VSCHBlocks.java index b6546838..79ea2973 100644 --- a/src/main/java/net/jcm/vsch/blocks/VSCHBlocks.java +++ b/src/main/java/net/jcm/vsch/blocks/VSCHBlocks.java @@ -7,6 +7,7 @@ import net.jcm.vsch.blocks.entity.PowerfulThrusterBlockEntity; import net.jcm.vsch.blocks.entity.ThrusterBlockEntity; import net.jcm.vsch.blocks.rocketassembler.RocketAssemblerBlock; +import net.jcm.vsch.fluid.VSCHFluids; import net.jcm.vsch.items.VSCHItems; import net.jcm.vsch.util.rot.DirectionalShape; import net.jcm.vsch.util.rot.RotShapes; @@ -15,6 +16,7 @@ import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.material.MapColor; @@ -147,6 +149,30 @@ public class VSCHBlocks { ) );*/ + public static final RegistryObject HYDROGEN_BLOCK = BLOCKS.register( + "hydrogen", + () -> new LiquidBlock( + VSCHFluids.HYDROGEN.get(), + BlockBehaviour.Properties.copy(Blocks.WATER) + ) + ); + + public static final RegistryObject HYDROGEN_PEROXIDE_BLOCK = BLOCKS.register( + "hydrogen_peroxide", + () -> new LiquidBlock( + VSCHFluids.HYDROGEN_PEROXIDE.get(), + BlockBehaviour.Properties.copy(Blocks.WATER) + ) + ); + + public static final RegistryObject OXYGEN_BLOCK = BLOCKS.register( + "oxygen", + () -> new LiquidBlock( + VSCHFluids.OXYGEN.get(), + BlockBehaviour.Properties.copy(Blocks.WATER) + ) + ); + private static RegistryObject registerBlock(String name, Supplier block) { RegistryObject toReturn = BLOCKS.register(name, block); registerBlockItem(name, toReturn); @@ -154,7 +180,7 @@ private static RegistryObject registerBlock(String name, Su } private static RegistryObject registerBlockItem(String name, RegistryObject block) { - return VSCHItems.ITEMS.register(name, () -> new BlockItem(block.get(), new Item.Properties())); + return VSCHItems.registerTabItem(name, () -> new BlockItem(block.get(), new Item.Properties())); } public static void register(IEventBus eventBus) { diff --git a/src/main/java/net/jcm/vsch/blocks/thruster/AbstractThrusterBlockEntity.java b/src/main/java/net/jcm/vsch/blocks/thruster/AbstractThrusterBlockEntity.java index d3122ce4..f942dcfc 100644 --- a/src/main/java/net/jcm/vsch/blocks/thruster/AbstractThrusterBlockEntity.java +++ b/src/main/java/net/jcm/vsch/blocks/thruster/AbstractThrusterBlockEntity.java @@ -259,7 +259,7 @@ public void onFocusWithWrench(final ItemStack stack, final Level level, final Pl if (!level.isClientSide) { return; } - ((IGuiAccessor) (Minecraft.getInstance().gui)).vsch$setOverlayMessageIfNotExist( + ((IGuiAccessor) (Minecraft.getInstance().gui)).starlance$setOverlayMessageIfNotExist( Component.translatable("vsch.message.mode") .append(Component.translatable("vsch." + this.getThrusterMode().toString().toLowerCase())), 25 diff --git a/src/main/java/net/jcm/vsch/client/RenderUtil.java b/src/main/java/net/jcm/vsch/client/RenderUtil.java index 3004ace2..bb9dfa1e 100644 --- a/src/main/java/net/jcm/vsch/client/RenderUtil.java +++ b/src/main/java/net/jcm/vsch/client/RenderUtil.java @@ -29,12 +29,16 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.phys.AABB; public final class RenderUtil { private RenderUtil() {} @@ -45,15 +49,12 @@ public static void drawBox(PoseStack poseStack, VertexConsumer buffer, BoxLightM Vector3f offset = new Vector3f(offseti).div(16); Vector3f size = new Vector3f(sizei).div(16); - poseStack.translate(0.5f, 0.5f, 0.5f); poseStack.mulPose(rot); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.UP, offset, size); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.DOWN, offset, size); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.EAST, offset, size); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.WEST, offset, size); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.NORTH, offset, size); - drawPlane(poseStack, buffer, lightMap, rgba, Direction.SOUTH, offset, size); + for (final Direction dir : Direction.values()) { + drawPlane(poseStack, buffer, lightMap, rgba, dir, offset, size); + } + poseStack.popPose(); } @@ -119,11 +120,13 @@ public static void drawBoxWithTexture(PoseStack poseStack, VertexConsumer buffer public static void drawBoxWithTexture(PoseStack poseStack, VertexConsumer buffer, BoxLightMap lightMap, ModelTextures model, Vector4f rgba, Vector3f offset, Quaternionf rot, Vector3i size, float scale) { poseStack.pushPose(); - poseStack.translate(0.5f, 0.5f, 0.5f); poseStack.mulPose(rot); for (final Direction dir : Direction.values()) { - drawPlaneWithTexture(poseStack, buffer, lightMap, model.getTexture(dir), rgba, dir, offset, size, scale); + final TextureLocation texture = model.getTexture(dir); + if (texture != null) { + drawPlaneWithTexture(poseStack, buffer, lightMap, texture, rgba, dir, offset, size, scale); + } } poseStack.popPose(); } @@ -154,16 +157,16 @@ public static void drawPlaneWithTexture(PoseStack poseStack, VertexConsumer buff switch (perspective) { case UP -> { - final float u2 = stillTexture.getU(pUOffset + size.z * textureScale); - final float v2 = stillTexture.getV(pVOffset + size.x * textureScale); + final float u2 = stillTexture.getU(pUOffset + size.x * textureScale); + final float v2 = stillTexture.getV(pVOffset + size.z * textureScale); buffer.vertex(matrix4f, -sX, sY, -sZ).color(r, g, b, a).uv(u1, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.unw).normal(0f, 1f, 0f).endVertex(); buffer.vertex(matrix4f, -sX, sY, sZ).color(r, g, b, a).uv(u1, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.usw).normal(0f, 1f, 0f).endVertex(); buffer.vertex(matrix4f, sX, sY, sZ).color(r, g, b, a).uv(u2, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.use).normal(0f, 1f, 0f).endVertex(); buffer.vertex(matrix4f, sX, sY, -sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.une).normal(0f, 1f, 0f).endVertex(); } case DOWN -> { - final float u2 = stillTexture.getU(pUOffset + size.z * textureScale); - final float v2 = stillTexture.getV(pVOffset + size.x * textureScale); + final float u2 = stillTexture.getU(pUOffset + size.x * textureScale); + final float v2 = stillTexture.getV(pVOffset + size.z * textureScale); buffer.vertex(matrix4f, -sX, -sY, -sZ).color(r, g, b, a).uv(u1, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.dnw).normal(0f, -1f, 0f).endVertex(); buffer.vertex(matrix4f, sX, -sY, -sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.dne).normal(0f, -1f, 0f).endVertex(); buffer.vertex(matrix4f, sX, -sY, sZ).color(r, g, b, a).uv(u2, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.dse).normal(0f, -1f, 0f).endVertex(); @@ -186,20 +189,20 @@ public static void drawPlaneWithTexture(PoseStack poseStack, VertexConsumer buff buffer.vertex(matrix4f, sX, -sY, -sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.nde).normal(0f, 0f, -1f).endVertex(); } case EAST -> { - final float u2 = stillTexture.getU(pUOffset + size.y * textureScale); - final float v2 = stillTexture.getV(pVOffset + size.z * textureScale); + final float u2 = stillTexture.getU(pUOffset + size.z * textureScale); + final float v2 = stillTexture.getV(pVOffset + size.y * textureScale); buffer.vertex(matrix4f, sX, -sY, -sZ).color(r, g, b, a).uv(u1, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.edn).normal(1f, 0f, 0f).endVertex(); - buffer.vertex(matrix4f, sX, sY, -sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.eun).normal(1f, 0f, 0f).endVertex(); + buffer.vertex(matrix4f, sX, sY, -sZ).color(r, g, b, a).uv(u1, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.eds).normal(1f, 0f, 0f).endVertex(); buffer.vertex(matrix4f, sX, sY, sZ).color(r, g, b, a).uv(u2, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.eus).normal(1f, 0f, 0f).endVertex(); - buffer.vertex(matrix4f, sX, -sY, sZ).color(r, g, b, a).uv(u1, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.eds).normal(1f, 0f, 0f).endVertex(); + buffer.vertex(matrix4f, sX, -sY, sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.eun).normal(1f, 0f, 0f).endVertex(); } case WEST -> { - final float u2 = stillTexture.getU(pUOffset + size.y * textureScale); - final float v2 = stillTexture.getV(pVOffset + size.z * textureScale); + final float u2 = stillTexture.getU(pUOffset + size.z * textureScale); + final float v2 = stillTexture.getV(pVOffset + size.y * textureScale); buffer.vertex(matrix4f, -sX, -sY, -sZ).color(r, g, b, a).uv(u1, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wdn).normal(-1f, 0f, 0f).endVertex(); - buffer.vertex(matrix4f, -sX, -sY, sZ).color(r, g, b, a).uv(u1, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wds).normal(-1f, 0f, 0f).endVertex(); + buffer.vertex(matrix4f, -sX, -sY, sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wun).normal(-1f, 0f, 0f).endVertex(); buffer.vertex(matrix4f, -sX, sY, sZ).color(r, g, b, a).uv(u2, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wus).normal(-1f, 0f, 0f).endVertex(); - buffer.vertex(matrix4f, -sX, sY, -sZ).color(r, g, b, a).uv(u2, v1).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wun).normal(-1f, 0f, 0f).endVertex(); + buffer.vertex(matrix4f, -sX, sY, -sZ).color(r, g, b, a).uv(u1, v2).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(lightMap.wds).normal(-1f, 0f, 0f).endVertex(); } } poseStack.popPose(); @@ -377,5 +380,34 @@ public BoxLightMap getSkyLightMap() { sky.wdn = LightTexture.sky(this.wdn); return sky; } + + public BoxLightMap fillFromLevel(final BlockAndTintGetter level, final AABB box) { + this.setUSE(LevelRenderer.getLightColor(level, BlockPos.containing(box.maxX, box.maxY, box.maxY))); + this.setUSW(LevelRenderer.getLightColor(level, BlockPos.containing(box.minX, box.maxY, box.maxY))); + this.setUNE(LevelRenderer.getLightColor(level, BlockPos.containing(box.maxX, box.maxY, box.minY))); + this.setUNW(LevelRenderer.getLightColor(level, BlockPos.containing(box.minX, box.maxY, box.minY))); + this.setDSE(LevelRenderer.getLightColor(level, BlockPos.containing(box.maxX, box.minY, box.maxY))); + this.setDSW(LevelRenderer.getLightColor(level, BlockPos.containing(box.minX, box.minY, box.maxY))); + this.setDNE(LevelRenderer.getLightColor(level, BlockPos.containing(box.maxX, box.minY, box.minY))); + this.setDNW(LevelRenderer.getLightColor(level, BlockPos.containing(box.minX, box.minY, box.minY))); + + final BoxLightMap blockLightMap = this.getBlockLightMap(); + final BoxLightMap skyLightMap = this.getSkyLightMap(); + + for (final BoxLightMap lights : new BoxLightMap[]{blockLightMap, skyLightMap}) { + for (int i = 0; i < 2; i++) { + lights.setUSE(Math.max(lights.use, Math.max(Math.max(lights.usw, lights.une), lights.dse) - 2)); + lights.setUSW(Math.max(lights.usw, Math.max(Math.max(lights.use, lights.unw), lights.dsw) - 2)); + lights.setUNE(Math.max(lights.une, Math.max(Math.max(lights.unw, lights.use), lights.dne) - 2)); + lights.setUNW(Math.max(lights.unw, Math.max(Math.max(lights.une, lights.usw), lights.dnw) - 2)); + lights.setDSE(Math.max(lights.dse, Math.max(Math.max(lights.dsw, lights.dne), lights.use) - 2)); + lights.setDSW(Math.max(lights.dsw, Math.max(Math.max(lights.dse, lights.dnw), lights.usw) - 2)); + lights.setDNE(Math.max(lights.dne, Math.max(Math.max(lights.dnw, lights.dse), lights.une) - 2)); + lights.setDNW(Math.max(lights.dnw, Math.max(Math.max(lights.dne, lights.dsw), lights.unw) - 2)); + } + } + + return this.packLightMaps(blockLightMap, skyLightMap); + } } } diff --git a/src/main/java/net/jcm/vsch/client/pipe/PipeLevelRenderer.java b/src/main/java/net/jcm/vsch/client/pipe/PipeLevelRenderer.java new file mode 100644 index 00000000..eb7f5a1a --- /dev/null +++ b/src/main/java/net/jcm/vsch/client/pipe/PipeLevelRenderer.java @@ -0,0 +1,420 @@ +package net.jcm.vsch.client.pipe; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.accessor.INodeLevelChunkSection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.api.resource.ModelTextures; +import net.jcm.vsch.api.resource.TextureLocation; +import net.jcm.vsch.client.RenderUtil; +import net.jcm.vsch.items.custom.WrenchItem; +import net.jcm.vsch.pipe.PipeNetworkOperator; +import net.jcm.vsch.pipe.level.NodeGetter; +import net.jcm.vsch.pipe.level.NodeLevel; + +import org.joml.Matrix4d; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.joml.Vector4f; +import org.joml.primitives.AABBi; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientChunkCache; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import org.valkyrienskies.core.api.ships.Ship; +import org.valkyrienskies.mod.common.VSGameUtilsKt; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@Mod.EventBusSubscriber(modid = VSCHMod.MODID, value = Dist.CLIENT) +public class PipeLevelRenderer { + private static final int PIPE_VIEW_RANGE = 8; + private static final Vector3f ZERO_VEC3F = new Vector3f(); + + private static final Vector4f HINT_COLOR = new Vector4f(0.25f, 0.70f, 0.25f, 0.6f); + private static final float HINT_SCALE = 0.7f; + private static final Vector4f HINT_SELECTING_COLOR = new Vector4f(0.25f, 0.92f, 0.25f, 0.8f); + private static final float HINT_SELECTING_SCALE = 0.85f; + private static final ModelTextures HINT_MODEL; + + static { + final ResourceLocation resource = new ResourceLocation(VSCHMod.MODID, "block/pipe/omni_node"); + final TextureLocation texture1 = new TextureLocation(resource, 8, 4); + final TextureLocation texture2 = new TextureLocation(resource, 8, 8); + HINT_MODEL = new ModelTextures(texture1, texture2, texture1, texture2, texture1, texture2); + } + + @SubscribeEvent + public static void renderLevelState(final RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_ENTITIES) { + return; + } + final Minecraft minecraft = Minecraft.getInstance(); + final ClientLevel level = minecraft.level; + if (level == null) { + return; + } + + final PoseStack poseStack = event.getPoseStack(); + final float partialTick = event.getPartialTick(); + final Vec3 view = event.getCamera().getPosition(); + // final Frustum frustum = event.getFrustum(); // TODO: see if frustum filter can improve performance + + final MultiBufferSource.BufferSource bufferSource = minecraft.renderBuffers().bufferSource(); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA); + // TODO: make our own atlas + RenderSystem.setShaderTexture(0, InventoryMenu.BLOCK_ATLAS); + RenderSystem.setShader(GameRenderer::getRendertypeTranslucentShader); + + final VertexConsumer vertexBuilder = bufferSource.getBuffer(RenderType.translucent()); + + poseStack.pushPose(); + poseStack.translate(-view.x, -view.y, -view.z); + + renderPlaceHint(level, poseStack, vertexBuilder, partialTick); + renderNodes(level, poseStack, vertexBuilder, partialTick, view); + + bufferSource.endBatch(); + poseStack.popPose(); + RenderSystem.disableBlend(); + } + + private static Stream streamRenderingChunks(final ClientLevel level, final Vec3 view) { + final Vector3d viewBoxMin = new Vector3d(view.x - PIPE_VIEW_RANGE * 16, view.y - PIPE_VIEW_RANGE * 16, view.z - PIPE_VIEW_RANGE * 16); + final Vector3d viewBoxMax = new Vector3d(view.x + PIPE_VIEW_RANGE * 16, view.y + PIPE_VIEW_RANGE * 16, view.z + PIPE_VIEW_RANGE * 16); + final AABB viewBox = new AABB(viewBoxMin.x, viewBoxMin.y, viewBoxMin.z, viewBoxMax.x, viewBoxMax.y, viewBoxMax.z); + final ClientChunkCache chunkSource = level.getChunkSource(); + final ChunkPos center = new ChunkPos(BlockPos.containing(view)); + + final List> chunkPosStreams = new ArrayList<>(); + chunkPosStreams.add(ChunkPos.rangeClosed(center, PIPE_VIEW_RANGE)); + + final Vector3d viewBoxMinOut = new Vector3d(), viewBoxMaxOut = new Vector3d(); + final AABBi viewBoxOut = new AABBi(); + for (final Ship ship : VSGameUtilsKt.getShipsIntersecting(level, viewBox)) { + ship.getTransform().getWorldToShip().transformAab(viewBoxMin, viewBoxMax, viewBoxMinOut, viewBoxMaxOut); + viewBoxOut + .setMin((int)(viewBoxMinOut.x), (int)(viewBoxMinOut.y), (int)(viewBoxMinOut.z)) + .setMax((int)(viewBoxMaxOut.x), (int)(viewBoxMaxOut.y), (int)(viewBoxMaxOut.z)); + ship.getShipAABB().intersection(viewBoxOut, viewBoxOut); + if (!viewBoxOut.isValid()) { + continue; + } + final int minX = SectionPos.blockToSectionCoord(viewBoxOut.minX), minZ = SectionPos.blockToSectionCoord(viewBoxOut.minZ); + final int maxX = SectionPos.blockToSectionCoord(viewBoxOut.maxX) + 1, maxZ = SectionPos.blockToSectionCoord(viewBoxOut.maxZ) + 1; + chunkPosStreams.add(StreamSupport.stream( + new Spliterators.AbstractSpliterator<>( + (maxX - minX) * (maxZ - minZ), + Spliterator.DISTINCT | Spliterator.SIZED | Spliterator.NONNULL + ) { + private ChunkPos pos = null; + + @Override + public boolean tryAdvance(final Consumer consumer) { + if (this.pos == null) { + this.pos = new ChunkPos(minX, minZ); + } else { + int x = this.pos.x, z = this.pos.z; + final boolean xIn = x < maxX, zIn = z < maxZ; + if (!xIn && !zIn) { + return false; + } + if (xIn) { + x++; + } else { + x = 0; + z++; + } + this.pos = new ChunkPos(x, z); + } + consumer.accept(this.pos); + return true; + } + }, + false + )); + } + + return chunkPosStreams.stream() + .flatMap(Function.identity()) + .map((chunkPos) -> chunkSource.getChunkNow(chunkPos.x, chunkPos.z)) + .filter(Objects::nonNull); + } + + private static void renderPlaceHint(final ClientLevel level, final PoseStack poseStack, final VertexConsumer vertexBuilder, final float partialTick) { + final Minecraft minecraft = Minecraft.getInstance(); + final LocalPlayer player = minecraft.player; + if (player == null || player.isSpectator()) { + return; + } + if (!(player.getItemInHand(InteractionHand.OFF_HAND).getItem() instanceof WrenchItem)) { + return; + } + final HitResult hit = player.pick(4.5, partialTick, false); + if (hit.getType() != HitResult.Type.BLOCK) { + return; + } + final BlockHitResult blockHit = (BlockHitResult) (hit); + final NodePos lookingPos = NodePos.fromHitResult(level, blockHit.getBlockPos(), blockHit.getLocation(), 4.0 / 16); + NodePos.streamPlaceHint(NodeLevel.get(level), blockHit.getBlockPos()).forEach((pos) -> { + renderNodePlaceHint( + level, poseStack, vertexBuilder, partialTick, pos, + pos.equals(lookingPos) ? HINT_SELECTING_COLOR : HINT_COLOR, + pos.equals(lookingPos) ? HINT_SELECTING_SCALE : HINT_SCALE + ); + }); + } + + private static void renderNodes(final ClientLevel level, final PoseStack poseStack, final VertexConsumer vertexBuilder, final float partialTick, final Vec3 view) { + streamRenderingChunks(level, view).forEach((chunk) -> renderNodesInChunk(level, chunk, poseStack, vertexBuilder, partialTick)); + } + + private static void renderNodesInChunk(final ClientLevel level, final ChunkAccess chunk, final PoseStack poseStack, final VertexConsumer vertexBuilder, final float partialTick) { + if (!(chunk instanceof NodeGetter nodeGetter)) { + return; + } + if (!nodeGetter.hasAnyNode()) { + return; + } + nodeGetter.streamNodes().forEach((node) -> renderNode(level, poseStack, vertexBuilder, partialTick, node)); + } + + private static void renderNode(final ClientLevel level, final PoseStack poseStack, final VertexConsumer vertexBuilder, final float partialTick, final PipeNode node) { + final NodeLevel nodeLevel = NodeLevel.get(level); + final PipeNetworkOperator network = nodeLevel.getNetwork(); + final int size = node.getSize(); + final NodePos pos = node.getPos(); + final Vec3 nodeCenter = pos.getCenter(); + final Vector3f color = new Vector3f(node.getColor().getTextureDiffuseColors()); + final RenderUtil.BoxLightMap lightMap = new RenderUtil.BoxLightMap(); + final double r = size / 16.0 / 2; + lightMap.fillFromLevel( + level, + new AABB( + nodeCenter.x - r, nodeCenter.y - r, nodeCenter.y - r, + nodeCenter.x + r, nodeCenter.y + r, nodeCenter.y + r + ) + ); + + final Vec3 nodeCenterRender = toWorldCoordinatesLerp(level, partialTick, nodeCenter); + final Quaternionf rotation = new Quaternionf(); + final Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos.blockPos()); + if (ship != null) { + rotation.setFromNormalized(ship.getPrevTickTransform().getShipToWorld().lerp(ship.getTransform().getShipToWorld(), partialTick, new Matrix4d())); + } + + poseStack.pushPose(); + poseStack.translate(nodeCenterRender.x, nodeCenterRender.y, nodeCenterRender.z); + + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap, + node.getModel(), color, + ZERO_VEC3F, rotation, new Vector3i(size, size, size), + 1f + ); + + poseStack.popPose(); + + for (final NodePos otherPos : network.getConnections(node)) { + if (pos.compareTo(otherPos) >= 0) { + continue; + } + final Vec3 otherCenter = otherPos.getCenter(); + final PipeNode other = nodeLevel.getNode(otherPos); + final Direction[] path = pos.connectPathTo(otherPos); + final Direction path0 = path[0]; + final Direction.Axis pathAxis0 = path0.getAxis(); + switch (path.length) { + case 1 -> { + final Vec3 center1 = nodeCenter.add(path0.getStepX() * r, path0.getStepY() * r, path0.getStepZ() * r); + final Vec3 center3 = otherCenter.add(path0.getStepX() * -r, path0.getStepY() * -r, path0.getStepZ() * -r); + final Vec3 center2 = center1.add(center3).scale(0.5); + final double + x2 = center2.x + pathAxis0.choose(0, r, r), + y2 = center2.y + pathAxis0.choose(r, 0, r), + z2 = center2.z + pathAxis0.choose(r, r, 0); + final AABB box1 = new AABB( + x2, y2, z2, + center1.x - pathAxis0.choose(0, r, r), center1.y - pathAxis0.choose(r, 0, r), center1.z - pathAxis0.choose(r, r, 0) + ); + final AABB box2 = new AABB( + x2, y2, z2, + center3.x - pathAxis0.choose(0, r, r), center3.y - pathAxis0.choose(r, 0, r), center3.z - pathAxis0.choose(r, r, 0) + ); + + poseStack.pushPose(); + final Vec3 box1Center = toWorldCoordinatesLerp(level, partialTick, box1.getCenter()); + poseStack.translate(box1Center.x, box1Center.y, box1Center.z); + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap.fillFromLevel(level, box1), + node.getPipeModel(path0.getOpposite()), color, + ZERO_VEC3F, rotation, + new Vector3i((int) (Math.round(box1.getXsize() * 16)), (int) (Math.round(box1.getYsize() * 16)), (int) (Math.round(box1.getZsize() * 16))), + 1f + ); + poseStack.popPose(); + + poseStack.pushPose(); + final Vec3 box2Center = toWorldCoordinatesLerp(level, partialTick, box2.getCenter()); + poseStack.translate(box2Center.x, box2Center.y, box2Center.z); + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap.fillFromLevel(level, box2), + other.getPipeModel(path0), color, + ZERO_VEC3F, + rotation.rotateXYZ( + (float) (pathAxis0.choose(Math.PI, Math.PI, 0)), + (float) (pathAxis0.choose(0, Math.PI, Math.PI)), + (float) (pathAxis0.choose(Math.PI, 0, Math.PI)), + new Quaternionf() + ), + new Vector3i((int) (Math.round(box2.getXsize() * 16)), (int) (Math.round(box2.getYsize() * 16)), (int) (Math.round(box2.getZsize() * 16))), + 1f + ); + poseStack.popPose(); + } + case 2 -> { + final Direction path1 = path[1].getOpposite(); + final Direction.Axis pathAxis1 = path1.getAxis(); + final Vec3 center1 = nodeCenter.add(path0.getStepX() * r, path0.getStepY() * r, path0.getStepZ() * r); + final Vec3 center3 = otherCenter.add(path1.getStepX() * r, path1.getStepY() * r, path1.getStepZ() * r); + final Vec3 edge2 = new Vec3( + pathAxis0.choose(center3.x, center1.x, center1.x) + (path0.getStepX() + path1.getStepX()) * r, + pathAxis0.choose(center1.y, center3.y, center1.y) + (path0.getStepY() + path1.getStepY()) * r, + pathAxis0.choose(center1.z, center1.z, center3.z) + (path0.getStepZ() + path1.getStepZ()) * r + ); + final AABB box1 = new AABB( + Math.min(center1.x - pathAxis0.choose(0, r, r), edge2.x) - 1e-6, + Math.min(center1.y - pathAxis0.choose(r, 0, r), edge2.y) - 1e-6, + Math.min(center1.z - pathAxis0.choose(r, r, 0), edge2.z) - 1e-6, + Math.max(center1.x + pathAxis0.choose(0, r, r), edge2.x) + 1e-6, + Math.max(center1.y + pathAxis0.choose(r, 0, r), edge2.y) + 1e-6, + Math.max(center1.z + pathAxis0.choose(r, r, 0), edge2.z) + 1e-6 + ); + final AABB box2 = new AABB( + Math.min(center3.x - pathAxis1.choose(0, r, r), edge2.x), + Math.min(center3.y - pathAxis1.choose(r, 0, r), edge2.y), + Math.min(center3.z - pathAxis1.choose(r, r, 0), edge2.z), + Math.max(center3.x + pathAxis1.choose(0, r, r), edge2.x), + Math.max(center3.y + pathAxis1.choose(r, 0, r), edge2.y), + Math.max(center3.z + pathAxis1.choose(r, r, 0), edge2.z) + ); + + poseStack.pushPose(); + final Vec3 box1Center = toWorldCoordinatesLerp(level, partialTick, box1.getCenter()); + poseStack.translate(box1Center.x, box1Center.y, box1Center.z); + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap.fillFromLevel(level, box1), + node.getPipeModel(path0.getOpposite()), color, + ZERO_VEC3F, rotation, + new Vector3i((int) (Math.round(box1.getXsize() * 16)), (int) (Math.round(box1.getYsize() * 16)), (int) (Math.round(box1.getZsize() * 16))), + 1f + ); + poseStack.popPose(); + + poseStack.pushPose(); + final Vec3 box2Center = toWorldCoordinatesLerp(level, partialTick, box2.getCenter()); + poseStack.translate(box2Center.x, box2Center.y, box2Center.z); + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap.fillFromLevel(level, box2), + other.getPipeModel(path1), color, + ZERO_VEC3F, + rotation.rotateXYZ( + (float) (pathAxis0.choose(Math.PI, Math.PI, 0)), + (float) (pathAxis0.choose(0, Math.PI, Math.PI)), + (float) (pathAxis0.choose(Math.PI, 0, Math.PI)), + new Quaternionf() + ), + new Vector3i((int) (Math.round(box2.getXsize() * 16)), (int) (Math.round(box2.getYsize() * 16)), (int) (Math.round(box2.getZsize() * 16))), + 1f + ); + poseStack.popPose(); + } + } + } + } + + private static void renderNodePlaceHint(final ClientLevel level, final PoseStack poseStack, final VertexConsumer vertexBuilder, final float partialTick, final NodePos pos, final Vector4f color, final float scale) { + final int size = 4; + final Vec3 nodeCenter = toWorldCoordinatesLerp(level, partialTick, pos.getCenter()); + final Quaternionf rotation = new Quaternionf(); + final RenderUtil.BoxLightMap lightMap = new RenderUtil.BoxLightMap().setAll(0xf000f0); + + final Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos.blockPos()); + if (ship != null) { + rotation.setFromNormalized(ship.getPrevTickTransform().getShipToWorld().lerp(ship.getTransform().getShipToWorld(), partialTick, new Matrix4d())); + } + + poseStack.pushPose(); + + poseStack.translate(nodeCenter.x, nodeCenter.y, nodeCenter.z); + RenderUtil.drawBoxWithTexture( + poseStack, vertexBuilder, + lightMap, + HINT_MODEL, color, + ZERO_VEC3F, rotation, new Vector3i(size, size, size), + scale + ); + + poseStack.popPose(); + } + + private static Vec3 toWorldCoordinatesLerp(final Level level, final float partialTick, final Vec3 pos) { + final Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos); + if (ship == null) { + return pos; + } + final Vector3d worldPos = ship.getPrevTickTransform().getShipToWorld().lerp( + ship.getTransform().getShipToWorld(), + partialTick, + new Matrix4d() + ) + .transformPosition(pos.x, pos.y, pos.z, new Vector3d()); + return new Vec3(worldPos.x, worldPos.y, worldPos.z); + } +} diff --git a/src/main/java/net/jcm/vsch/client/renderer/GyroRenderer.java b/src/main/java/net/jcm/vsch/client/renderer/GyroRenderer.java index 11dad6e2..eab7abda 100644 --- a/src/main/java/net/jcm/vsch/client/renderer/GyroRenderer.java +++ b/src/main/java/net/jcm/vsch/client/renderer/GyroRenderer.java @@ -25,8 +25,8 @@ import org.valkyrienskies.mod.common.VSGameUtilsKt; public class GyroRenderer implements BlockEntityRenderer { + private static final Vector3f ZERO3 = new Vector3f(); private static final Vector4f ONE4 = new Vector4f(1, 1, 1, 1); - private static final Vector3f HALF3 = new Vector3f(0.5f, 0.5f, 0.5f); private static final Vector3i CORE_SIZE = new Vector3i(6, 6, 6); private static final ModelTextures CORE_MODEL; @@ -66,6 +66,9 @@ public void render( } final VertexConsumer buffer = bufferSource.getBuffer(RenderType.translucent()); - RenderUtil.drawBoxWithTexture(poseStack, buffer, lightMap, CORE_MODEL, ONE4, new Vector3f(), rot, CORE_SIZE, 1f); + poseStack.pushPose(); + poseStack.translate(0.5f, 0.5f, 0.5f); + RenderUtil.drawBoxWithTexture(poseStack, buffer, lightMap, CORE_MODEL, ONE4, ZERO3, rot, CORE_SIZE, 1f); + poseStack.popPose(); } } diff --git a/src/main/java/net/jcm/vsch/config/VSCHConfig.java b/src/main/java/net/jcm/vsch/config/VSCHConfig.java index 55a8fb97..88adff23 100644 --- a/src/main/java/net/jcm/vsch/config/VSCHConfig.java +++ b/src/main/java/net/jcm/vsch/config/VSCHConfig.java @@ -148,7 +148,7 @@ public static void register(ModLoadingContext context){ private static String getDefaultThrusterFuelConsumeRates() { Map rates = new HashMap<>(); - // rates.put("minecraft:lava", 32); + rates.put("vsch:hydrogen_peroxide", 32); return GSON.toJson(rates); } diff --git a/src/main/java/net/jcm/vsch/fluid/GasFluidType.java b/src/main/java/net/jcm/vsch/fluid/GasFluidType.java new file mode 100644 index 00000000..e1fa34df --- /dev/null +++ b/src/main/java/net/jcm/vsch/fluid/GasFluidType.java @@ -0,0 +1,57 @@ +package net.jcm.vsch.fluid; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidType; + +import java.util.function.Consumer; +import org.jetbrains.annotations.Nullable; + +public class GasFluidType extends FluidType { + private final ResourceLocation stillTexture; + private final ResourceLocation flowTexture; + private final int tintColor; + + public GasFluidType(final ResourceLocation stillTexture, final ResourceLocation flowTexture, final int tintColor, final Properties properties) { + super(properties); + this.stillTexture = stillTexture; + this.flowTexture = flowTexture; + this.tintColor = tintColor; + } + + @Override + public void initializeClient(final Consumer consumer) { + consumer.accept(new IClientFluidTypeExtensions() { + @Override + public ResourceLocation getStillTexture() { + return GasFluidType.this.stillTexture; + } + + @Override + public ResourceLocation getFlowingTexture() { + return GasFluidType.this.flowTexture; + } + + @Override + public int getTintColor() { + return GasFluidType.this.tintColor; + } + }); + } + + @Override + public boolean isVaporizedOnPlacement(final Level level, final BlockPos pos, final FluidStack stack) { + final int temperature = level.dimensionType().ultraWarm() ? 1000 : 273; + return temperature > this.getTemperature(); + } + + @Override + public void onVaporize(final @Nullable Player player, final Level level, final BlockPos pos, final FluidStack stack) { + // Otherwise it will vaporize like water + // we can add particles and other fancy stuff here later + } +} diff --git a/src/main/java/net/jcm/vsch/fluid/VSCHFluidTypes.java b/src/main/java/net/jcm/vsch/fluid/VSCHFluidTypes.java new file mode 100644 index 00000000..e7d25f19 --- /dev/null +++ b/src/main/java/net/jcm/vsch/fluid/VSCHFluidTypes.java @@ -0,0 +1,59 @@ +package net.jcm.vsch.fluid; + +import net.jcm.vsch.VSCHMod; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fluids.FluidType; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegistryObject; + +public class VSCHFluidTypes { + private static final ResourceLocation WATER_STILL_RL = new ResourceLocation("minecraft", "block/water_still"); + private static final ResourceLocation WATER_FLOW_RL = new ResourceLocation("minecraft", "block/water_flow"); + + private static final DeferredRegister REGISTERY = DeferredRegister.create(ForgeRegistries.Keys.FLUID_TYPES, VSCHMod.MODID); + + public static final RegistryObject HYDROGEN_FLUID_TYPE = REGISTERY.register( + "hydrogen", + () -> new GasFluidType( + WATER_STILL_RL, + WATER_FLOW_RL, + 0xe0ffffff, + FluidType.Properties.create() + .density(71) + .temperature(20) + .viscosity(182) + ) + ); + + public static final RegistryObject HYDROGEN_PEROXIDE_FLUID_TYPE = REGISTERY.register( + "hydrogen_peroxide", + () -> new GasFluidType( + WATER_STILL_RL, + WATER_FLOW_RL, + 0xd8b4f0ff, + FluidType.Properties.create() + .density(1450) + .temperature(300) + .viscosity(1249) + ) + ); + + public static final RegistryObject OXYGEN_FLUID_TYPE = REGISTERY.register( + "oxygen", + () -> new GasFluidType( + WATER_STILL_RL, + WATER_FLOW_RL, + 0xe029d9ff, + FluidType.Properties.create() + .density(1141) + .temperature(90) + .viscosity(1870) + ) + ); + + public static void register(IEventBus eventBus) { + REGISTERY.register(eventBus); + } +} diff --git a/src/main/java/net/jcm/vsch/fluid/VSCHFluids.java b/src/main/java/net/jcm/vsch/fluid/VSCHFluids.java new file mode 100644 index 00000000..20fd4bfd --- /dev/null +++ b/src/main/java/net/jcm/vsch/fluid/VSCHFluids.java @@ -0,0 +1,68 @@ +package net.jcm.vsch.fluid; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.blocks.VSCHBlocks; +import net.jcm.vsch.items.VSCHItems; + +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fluids.ForgeFlowingFluid; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegistryObject; + +public class VSCHFluids { + + public static final DeferredRegister FLUIDS = DeferredRegister.create(ForgeRegistries.FLUIDS, VSCHMod.MODID); + + public static final RegistryObject HYDROGEN = FLUIDS.register( + "hydrogen", + () -> new ForgeFlowingFluid.Source(VSCHFluids.HYDROGEN_PROPERTIES) + ); + + public static final RegistryObject HYDROGEN_FLOWING = FLUIDS.register( + "hydrogen_flowing", + () -> new ForgeFlowingFluid.Flowing(VSCHFluids.HYDROGEN_PROPERTIES) + ); + + public static final RegistryObject HYDROGEN_PEROXIDE = FLUIDS.register( + "hydrogen_peroxide", + () -> new ForgeFlowingFluid.Source(VSCHFluids.HYDROGEN_PEROXIDE_PROPERTIES) + ); + + public static final RegistryObject HYDROGEN_PEROXIDE_FLOWING = FLUIDS.register( + "hydrogen_peroxide_flowing", + () -> new ForgeFlowingFluid.Flowing(VSCHFluids.HYDROGEN_PEROXIDE_PROPERTIES) + ); + + public static final RegistryObject OXYGEN = FLUIDS.register( + "oxygen", + () -> new ForgeFlowingFluid.Source(VSCHFluids.OXYGEN_PROPERTIES) + ); + + public static final RegistryObject OXYGEN_FLOWING = FLUIDS.register( + "oxygen_flowing", + () -> new ForgeFlowingFluid.Flowing(VSCHFluids.OXYGEN_PROPERTIES) + ); + + public static final ForgeFlowingFluid.Properties HYDROGEN_PROPERTIES = + new ForgeFlowingFluid.Properties(VSCHFluidTypes.HYDROGEN_FLUID_TYPE, HYDROGEN, HYDROGEN_FLOWING) + .bucket(VSCHItems.HYDROGEN_BUCKET) + .block(VSCHBlocks.HYDROGEN_BLOCK); + + public static final ForgeFlowingFluid.Properties HYDROGEN_PEROXIDE_PROPERTIES = + new ForgeFlowingFluid.Properties(VSCHFluidTypes.HYDROGEN_PEROXIDE_FLUID_TYPE, HYDROGEN_PEROXIDE, HYDROGEN_PEROXIDE_FLOWING) + .bucket(VSCHItems.HYDROGEN_PEROXIDE_BUCKET) + .block(VSCHBlocks.HYDROGEN_PEROXIDE_BLOCK); + + public static final ForgeFlowingFluid.Properties OXYGEN_PROPERTIES = + new ForgeFlowingFluid.Properties(VSCHFluidTypes.OXYGEN_FLUID_TYPE, OXYGEN, OXYGEN_FLOWING) + .bucket(VSCHItems.OXYGEN_BUCKET) + .block(VSCHBlocks.OXYGEN_BLOCK); + + public static void register(IEventBus eventBus) { + FLUIDS.register(eventBus); + } +} diff --git a/src/main/java/net/jcm/vsch/items/VSCHItems.java b/src/main/java/net/jcm/vsch/items/VSCHItems.java index 5b81cf9f..e6ff6f2c 100644 --- a/src/main/java/net/jcm/vsch/items/VSCHItems.java +++ b/src/main/java/net/jcm/vsch/items/VSCHItems.java @@ -1,31 +1,176 @@ package net.jcm.vsch.items; +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.fluid.VSCHFluids; import net.jcm.vsch.items.custom.MagnetBootItem; +import net.jcm.vsch.items.custom.WrenchItem; +import net.jcm.vsch.items.pipe.OmniNodeItem; +import net.jcm.vsch.items.pipe.PipeNodeItem; + +import net.minecraft.world.item.ArmorItem; import net.minecraft.world.item.ArmorMaterials; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.DyeColor; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraftforge.client.event.RegisterColorHandlersEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.registries.DeferredRegister; -import net.jcm.vsch.VSCHMod; -import net.jcm.vsch.VSCHTab; -import net.minecraft.world.item.ArmorItem.Type; -import net.jcm.vsch.items.custom.WrenchItem; import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegistryObject; +import java.util.ArrayList; +import java.util.function.Consumer; +import java.util.function.Supplier; + public class VSCHItems { - public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, VSCHMod.MODID); + private static final DeferredRegister REGISTRY = DeferredRegister.create(ForgeRegistries.ITEMS, VSCHMod.MODID); + private static final ArrayList> TAB_ITEMS = new ArrayList<>(); + private static final ArrayList> COLORED_ITEMS = new ArrayList<>(); - public static final RegistryObject WRENCH = ITEMS.register( + public static final RegistryObject WRENCH = registerTabItem( "wrench", () -> new WrenchItem(new Item.Properties()) ); - public static final RegistryObject MAGNET_BOOT = ITEMS.register( + public static final RegistryObject MAGNET_BOOT = registerTabItem( "magnet_boot", - () -> new MagnetBootItem(ArmorMaterials.IRON, Type.BOOTS, new Item.Properties()) + () -> new MagnetBootItem(ArmorMaterials.IRON, ArmorItem.Type.BOOTS, new Item.Properties()) + ); + + // register buckets + + public static final RegistryObject HYDROGEN_BUCKET = registerTabItem( + "hydrogen_bucket", + () -> new BucketItem(VSCHFluids.HYDROGEN.get(), new Item.Properties().craftRemainder(Items.BUCKET).stacksTo(1)) + ); + + public static final RegistryObject HYDROGEN_PEROXIDE_BUCKET = registerTabItem( + "hydrogen_peroxide_bucket", + () -> new BucketItem(VSCHFluids.HYDROGEN_PEROXIDE.get(), new Item.Properties().craftRemainder(Items.BUCKET).stacksTo(1)) + ); + + public static final RegistryObject OXYGEN_BUCKET = registerTabItem( + "oxygen_bucket", + () -> new BucketItem(VSCHFluids.OXYGEN.get(), new Item.Properties().craftRemainder(Items.BUCKET).stacksTo(1)) + ); + + // register nodes + + public static final RegistryObject WHITE_OMNI_NODE = registerNodeItem( + "white_omni_node", + () -> new OmniNodeItem(DyeColor.WHITE, new Item.Properties()) + ); + + public static final RegistryObject ORANGE_OMNI_NODE = registerNodeItem( + "orange_omni_node", + () -> new OmniNodeItem(DyeColor.ORANGE, new Item.Properties()) + ); + + public static final RegistryObject MAGENTA_OMNI_NODE = registerNodeItem( + "magenta_omni_node", + () -> new OmniNodeItem(DyeColor.MAGENTA, new Item.Properties()) + ); + + public static final RegistryObject LIGHT_BLUE_OMNI_NODE = registerNodeItem( + "light_blue_omni_node", + () -> new OmniNodeItem(DyeColor.LIGHT_BLUE, new Item.Properties()) + ); + + public static final RegistryObject YELLOW_OMNI_NODE = registerNodeItem( + "yellow_omni_node", + () -> new OmniNodeItem(DyeColor.YELLOW, new Item.Properties()) + ); + + public static final RegistryObject LIME_OMNI_NODE = registerNodeItem( + "lime_omni_node", + () -> new OmniNodeItem(DyeColor.LIME, new Item.Properties()) + ); + + public static final RegistryObject PINK_OMNI_NODE = registerNodeItem( + "pink_omni_node", + () -> new OmniNodeItem(DyeColor.PINK, new Item.Properties()) ); - - public static void register(IEventBus eventBus) { - ITEMS.register(eventBus); + + public static final RegistryObject GRAY_OMNI_NODE = registerNodeItem( + "gray_omni_node", + () -> new OmniNodeItem(DyeColor.GRAY, new Item.Properties()) + ); + + public static final RegistryObject LIGHT_GRAY_OMNI_NODE = registerNodeItem( + "light_gray_omni_node", + () -> new OmniNodeItem(DyeColor.LIGHT_GRAY, new Item.Properties()) + ); + + public static final RegistryObject CYAN_OMNI_NODE = registerNodeItem( + "cyan_omni_node", + () -> new OmniNodeItem(DyeColor.CYAN, new Item.Properties()) + ); + + public static final RegistryObject PURPLE_OMNI_NODE = registerNodeItem( + "purple_omni_node", + () -> new OmniNodeItem(DyeColor.PURPLE, new Item.Properties()) + ); + + public static final RegistryObject BLUE_OMNI_NODE = registerNodeItem( + "blue_omni_node", + () -> new OmniNodeItem(DyeColor.BLUE, new Item.Properties()) + ); + + public static final RegistryObject BROWN_OMNI_NODE = registerNodeItem( + "brown_omni_node", + () -> new OmniNodeItem(DyeColor.BROWN, new Item.Properties()) + ); + + public static final RegistryObject GREEN_OMNI_NODE = registerNodeItem( + "green_omni_node", + () -> new OmniNodeItem(DyeColor.GREEN, new Item.Properties()) + ); + + public static final RegistryObject RED_OMNI_NODE = registerNodeItem( + "red_omni_node", + () -> new OmniNodeItem(DyeColor.RED, new Item.Properties()) + ); + + public static final RegistryObject BLACK_OMNI_NODE = registerNodeItem( + "black_omni_node", + () -> new OmniNodeItem(DyeColor.BLACK, new Item.Properties()) + ); + + public static void register(final IEventBus eventBus) { + REGISTRY.register(eventBus); + eventBus.addListener(VSCHItems::onItemColorRegister); + } + + public static RegistryObject registerNodeItem(final String name, final Supplier getter) { + final RegistryObject object = registerTabItem(name, getter); + COLORED_ITEMS.add(object); + return object; + } + + public static RegistryObject registerTabItem(final String name, final Supplier getter) { + final RegistryObject object = REGISTRY.register(name, getter); + TAB_ITEMS.add(object); + return object; + } + + public static void registerTab(final Consumer register) { + TAB_ITEMS.stream().map(RegistryObject::get).forEach(register); + } + + private static void onItemColorRegister(final RegisterColorHandlersEvent.Item event) { + event.register( + (stack, tintIndex) -> { + if (!(stack.getItem() instanceof PipeNodeItem nodeItem)) { + return 0xffffff; + } + if (tintIndex != 0) { + return 0xffffff; + } + final float[] textureColor = nodeItem.getColor().getTextureDiffuseColors(); + return (int)(textureColor[0] * 255) << 16 | (int)(textureColor[1] * 255) << 8 | (int)(textureColor[2] * 255); + }, + COLORED_ITEMS.stream().map(RegistryObject::get).toArray(Item[]::new) + ); } } diff --git a/src/main/java/net/jcm/vsch/items/custom/WrenchItem.java b/src/main/java/net/jcm/vsch/items/custom/WrenchItem.java index dbb14333..bd3c0ab7 100644 --- a/src/main/java/net/jcm/vsch/items/custom/WrenchItem.java +++ b/src/main/java/net/jcm/vsch/items/custom/WrenchItem.java @@ -1,13 +1,18 @@ package net.jcm.vsch.items.custom; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; import net.jcm.vsch.blocks.custom.template.WrenchableBlock; import net.jcm.vsch.blocks.thruster.AbstractThrusterBlockEntity; +import net.jcm.vsch.pipe.level.NodeLevel; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -17,6 +22,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; public class WrenchItem extends Item { @@ -47,12 +53,42 @@ public void inventoryTick(ItemStack stack, Level level, Entity entity, int slotI } } + @Override + public InteractionResult onItemUseFirst(final ItemStack stack, final UseOnContext ctx) { + final Level level = ctx.getLevel(); + final Player player = ctx.getPlayer(); + final BlockPos blockPos = ctx.getClickedPos(); + final Vec3 pos = ctx.getClickLocation(); + if (ctx.getHand() != InteractionHand.OFF_HAND) { + return InteractionResult.PASS; + } + if (player == null || !ctx.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + final NodeLevel nodeLevel = NodeLevel.get(level); + final NodePos nodePos = NodePos.fromHitResult(level, blockPos, pos, 4.0 / 16); + if (nodePos == null) { + return InteractionResult.PASS; + } + final PipeNode node = nodeLevel.getNode(nodePos); + if (node == null) { + return InteractionResult.FAIL; + } + nodeLevel.setNode(nodePos, null); + if (!level.isClientSide && !player.getAbilities().instabuild) { + final ItemStack nodeStack = node.asItemStack(); + player.getInventory().placeItemBackInInventory(nodeStack); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + @Override public InteractionResult useOn(final UseOnContext ctx) { - if (ctx.getLevel() instanceof ServerLevel level) { - final BlockPos pos = ctx.getClickedPos(); - final BlockState block = level.getBlockState(ctx.getClickedPos()); - final BlockEntity blockEntity = level.getBlockEntity(pos); + final Level level = ctx.getLevel(); + final BlockPos blockPos = ctx.getClickedPos(); + if (level instanceof ServerLevel serverLevel) { + final BlockState block = serverLevel.getBlockState(blockPos); + final BlockEntity blockEntity = serverLevel.getBlockEntity(blockPos); if (blockEntity instanceof WrenchableBlock wrenchable) { return wrenchable.onUseWrench(ctx); } diff --git a/src/main/java/net/jcm/vsch/items/pipe/OmniNodeItem.java b/src/main/java/net/jcm/vsch/items/pipe/OmniNodeItem.java new file mode 100644 index 00000000..6f2e5773 --- /dev/null +++ b/src/main/java/net/jcm/vsch/items/pipe/OmniNodeItem.java @@ -0,0 +1,37 @@ +package net.jcm.vsch.items.pipe; + +import net.jcm.vsch.api.pipe.PipeNodeProvider; +import net.jcm.vsch.pipe.OmniNode; + +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import java.util.EnumMap; + +public final class OmniNodeItem extends PipeNodeItem { + private static final EnumMap COLOR_MAP = new EnumMap<>(DyeColor.class); + + public OmniNodeItem(final DyeColor color, final Item.Properties props) { + super(color, props); + COLOR_MAP.put(color, this); + } + + public static OmniNodeItem getByColor(final DyeColor color) { + return COLOR_MAP.get(color); + } + + @Override + protected String getDescriptionName() { + return "omni_node"; + } + + @Override + public PipeNodeProvider getPipeNodeProvider(final ItemStack stack) { + return (level, pos) -> { + final OmniNode node = new OmniNode(level, pos); + node.setColor(this.getColor()); + return node; + }; + } +} diff --git a/src/main/java/net/jcm/vsch/items/pipe/PipeNodeItem.java b/src/main/java/net/jcm/vsch/items/pipe/PipeNodeItem.java new file mode 100644 index 00000000..17373022 --- /dev/null +++ b/src/main/java/net/jcm/vsch/items/pipe/PipeNodeItem.java @@ -0,0 +1,91 @@ +package net.jcm.vsch.items.pipe; + +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.api.pipe.PipeNodeProvider; +import net.jcm.vsch.pipe.level.NodeLevel; +import net.jcm.vsch.items.custom.WrenchItem; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +public abstract class PipeNodeItem> extends Item { + private final DyeColor color; + private String descriptionId = null; + + protected PipeNodeItem(final DyeColor color, final Item.Properties props) { + super(props); + this.color = color; + } + + public DyeColor getColor() { + return this.color; + } + + protected abstract String getDescriptionName(); + + @Override + protected String getOrCreateDescriptionId() { + if (this.descriptionId == null) { + this.descriptionId = "pipe." + BuiltInRegistries.ITEM.getKey(this).getNamespace() + "." + this.getDescriptionName(); + } + return this.descriptionId; + } + + @Override + public Component getDescription() { + return Component.translatable("color.minecraft." + this.color).append(" ").append(Component.translatable(this.getDescriptionId())); + } + + @Override + public Component getName(final ItemStack stack) { + return Component.translatable("color.minecraft." + this.color).append(" ").append(Component.translatable(this.getDescriptionId(stack))); + } + + public abstract PipeNodeProvider getPipeNodeProvider(final ItemStack stack); + + @Override + public InteractionResult onItemUseFirst(final ItemStack stack, final UseOnContext context) { + final Player player = context.getPlayer(); + if (player == null || context.getHand() != InteractionHand.MAIN_HAND || context.isSecondaryUseActive()) { + return InteractionResult.PASS; + } + if (!(player.getItemInHand(InteractionHand.OFF_HAND).getItem() instanceof WrenchItem)) { + return InteractionResult.PASS; + } + + final Level level = context.getLevel(); + final NodeLevel nodeLevel = NodeLevel.get(level); + + final NodePos nodePos = NodePos.fromHitResult(level, context.getClickedPos(), context.getClickLocation(), 4.0 / 16); + if (nodePos == null) { + return InteractionResult.PASS; + } + + final PipeNodeProvider provider = this.getPipeNodeProvider(stack); + if (provider == null) { + return InteractionResult.FAIL; + } + if (nodeLevel.getNode(nodePos) != null) { + return InteractionResult.FAIL; + } + final PipeNode node = provider.createNode(nodeLevel, nodePos); + if (!node.canAnchor()) { + return InteractionResult.FAIL; + } + nodeLevel.setNode(nodePos, node); + if (!player.getAbilities().instabuild) { + stack.shrink(1); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/client/MixinGui.java b/src/main/java/net/jcm/vsch/mixin/client/MixinGui.java index 5cfee830..5fb0e1f3 100644 --- a/src/main/java/net/jcm/vsch/mixin/client/MixinGui.java +++ b/src/main/java/net/jcm/vsch/mixin/client/MixinGui.java @@ -23,7 +23,7 @@ public abstract class MixinGui implements IGuiAccessor { public abstract void setChatDisabledByPlayerShown(boolean value); @Override - public void vsch$setOverlayMessageIfNotExist(final Component component, final int duration) { + public void starlance$setOverlayMessageIfNotExist(final Component component, final int duration) { if (this.overlayMessageString != null && this.overlayMessageTime > 21) { return; } diff --git a/src/main/java/net/jcm/vsch/mixin/cosmos/MixinShipspawnspaceProcedure.java b/src/main/java/net/jcm/vsch/mixin/cosmos/MixinShipspawnspaceProcedure.java index 00d9b883..7987a727 100644 --- a/src/main/java/net/jcm/vsch/mixin/cosmos/MixinShipspawnspaceProcedure.java +++ b/src/main/java/net/jcm/vsch/mixin/cosmos/MixinShipspawnspaceProcedure.java @@ -1,30 +1,38 @@ package net.jcm.vsch.mixin.cosmos; -import com.google.gson.JsonObject; -import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.jcm.vsch.VSCHMod; + import net.lointain.cosmos.procedures.ShipspawnspaceProcedure; + import net.minecraft.world.entity.Entity; import net.minecraft.world.level.LevelAccessor; import net.minecraftforge.eventbus.api.Event; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + @Mixin(ShipspawnspaceProcedure.class) public class MixinShipspawnspaceProcedure { + @Unique + private static final Logger LOGGER = LogManager.getLogger(VSCHMod.MODID); - // This does mean we now depend on mixin extras - @WrapMethod( - method = "execute(Lnet/minecraftforge/eventbus/api/Event;Lnet/minecraft/world/level/LevelAccessor;DDDLnet/minecraft/world/entity/Entity;)V", - remap = false - ) - private static void wrapExecute(Event event, LevelAccessor world, double x, double y, double z, Entity entity, Operation original) { - try { - original.call(event, world, x, y, z, entity); - } catch (Exception cancel) { - // Seems goofy but it really does stop a crash - return; - } - } -} \ No newline at end of file + @WrapMethod( + method = "execute(Lnet/minecraftforge/eventbus/api/Event;Lnet/minecraft/world/level/LevelAccessor;DDDLnet/minecraft/world/entity/Entity;)V", + remap = false + ) + private static void wrapExecute(Event event, LevelAccessor world, double x, double y, double z, Entity entity, Operation original) { + try { + original.call(event, world, x, y, z, entity); + } catch (Exception e) { + // Seems goofy but it really does stop a crash + LOGGER.error("Caught exception in ShipspawnspaceProcedure:", e); + } + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkMap.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkMap.java index 1dacbd0a..8a730d1a 100644 --- a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkMap.java +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkMap.java @@ -1,5 +1,6 @@ package net.jcm.vsch.mixin.minecraft; +import net.jcm.vsch.accessor.IChunkMapAccessor; import net.jcm.vsch.util.EmptyChunkAccess; import com.mojang.datafixers.util.Either; @@ -20,11 +21,19 @@ import java.util.concurrent.CompletableFuture; @Mixin(ChunkMap.class) -public class MixinChunkMap { +public abstract class MixinChunkMap implements IChunkMapAccessor { @Shadow @Final ServerLevel level; + @Shadow + protected abstract Iterable getChunks(); + + @Override + public Iterable starlance$getChunks() { + return this.getChunks(); + } + @Inject(method = "schedule", at = @At("HEAD"), cancellable = true) private void schedule(final ChunkHolder holder, final ChunkStatus status, final CallbackInfoReturnable>> cir) { final ServerLevel level = this.level; diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkSerializer.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkSerializer.java new file mode 100644 index 00000000..515f5f49 --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinChunkSerializer.java @@ -0,0 +1,118 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.accessor.INodeLevelChunkSection; +import net.jcm.vsch.pipe.level.NodeLevel; + +import io.netty.buffer.Unpooled; +import net.minecraft.core.Holder; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.chunk.storage.ChunkSerializer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Slice; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ChunkSerializer.class) +public abstract class MixinChunkSerializer { + @Unique + private static final Logger LOGGER = LogManager.getLogger(VSCHMod.MODID); + + @Unique + private static final String SECTION_NODES_KEY = "vsch:nodes"; + + @WrapOperation( + method = "read", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;checkConsistencyWithBlocks(Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/LevelChunkSection;)V" + ) + ) + private static void readLevelChunkSection( + final PoiManager poiManager, + final SectionPos sectionPos, + final LevelChunkSection section, + final Operation operation, + final @Local(argsOnly = true) ServerLevel level, + final @Local(ordinal = 1) CompoundTag sectionData + ) { + operation.call(poiManager, sectionPos, section); + if (!(section instanceof INodeLevelChunkSection nodeSection)) { + return; + } + if (!sectionData.contains(SECTION_NODES_KEY)) { + return; + } + final byte[] sectionNodesData = sectionData.getByteArray(SECTION_NODES_KEY); + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.wrappedBuffer(sectionNodesData)); + try { + nodeSection.starlance$readNodes(NodeLevel.get(level), sectionPos, buf); + } catch (RuntimeException e) { + LOGGER.error("[starlance]: Error when parsing pipe nodes", e); + throw e; + } + } + + @WrapOperation( + method = "write", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/nbt/CompoundTag;put(Ljava/lang/String;Lnet/minecraft/nbt/Tag;)Lnet/minecraft/nbt/Tag;", + ordinal = 0 + ), + slice = @Slice( + from = @At( + value = "CONSTANT", + args = "stringValue=block_states" + ) + ) + ) + private static Tag writeBlockStates( + final CompoundTag sectionData, + final String key, + final Tag value, + final Operation operation, + final @Local LevelChunkSection section + ) { + if (!key.equals("block_states")) { + throw new AssertionError("Incorrect injection point, expect block_states, got " + key); + } + final Tag oldTag = operation.call(sectionData, key, value); + + if (!(section instanceof INodeLevelChunkSection nodeSection) || !nodeSection.starlance$hasAnyNode()) { + return oldTag; + } + // TODO: investigate if Pooled buffer can give more performance + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(128)); + try { + nodeSection.starlance$writeNodes(buf); + } catch (RuntimeException e) { + LOGGER.error("[starlance]: Error when encoding pipe nodes", e); + throw e; + } + final byte[] bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + sectionData.putByteArray(SECTION_NODES_KEY, bytes); + return oldTag; + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinClientboundLevelChunkWithLightPacket.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinClientboundLevelChunkWithLightPacket.java new file mode 100644 index 00000000..5f3e4990 --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinClientboundLevelChunkWithLightPacket.java @@ -0,0 +1,53 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.accessor.IClientboundLevelChunkWithLightPacketAccessor; +import net.jcm.vsch.network.s2c.PipeNodeSyncChunkS2C; + +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.lighting.LevelLightEngine; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.BitSet; + +@Mixin(ClientboundLevelChunkWithLightPacket.class) +public class MixinClientboundLevelChunkWithLightPacket implements IClientboundLevelChunkWithLightPacketAccessor { + @Shadow + @Final + private int x; + + @Shadow + @Final + private int z; + + @Unique + private LevelChunk levelChunk = null; + + @Unique + private volatile PipeNodeSyncChunkS2C pipeNodeSyncChunkPacket = null; + + @Inject( + method = "(Lnet/minecraft/world/level/chunk/LevelChunk;Lnet/minecraft/world/level/lighting/LevelLightEngine;Ljava/util/BitSet;Ljava/util/BitSet;)V", + at = @At("RETURN") + ) + private void init(final LevelChunk levelChunk, final LevelLightEngine lightEngine, final BitSet skyLights, final BitSet blockLights, final CallbackInfo ci) { + this.levelChunk = levelChunk; + } + + @Override + public PipeNodeSyncChunkS2C starlance$getPipeNodeSyncChunkS2C() { + final LevelChunk levelChunk = this.levelChunk; + if (this.pipeNodeSyncChunkPacket == null && levelChunk != null) { + this.pipeNodeSyncChunkPacket = PipeNodeSyncChunkS2C.fromChunk(levelChunk); + this.levelChunk = null; + } + return this.pipeNodeSyncChunkPacket; + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevel.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevel.java new file mode 100644 index 00000000..8382d47b --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevel.java @@ -0,0 +1,39 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.VSCHEvents; +import net.jcm.vsch.accessor.ILevelAccessor; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Level.class) +public abstract class MixinLevel implements ILevelAccessor { + @Unique + private NodeLevel nodeLevel; + + @Inject(method = "*", at = @At("RETURN")) + private void init(final CallbackInfo ci) { + this.nodeLevel = new NodeLevel((Level)((Object)(this))); + } + + @Override + public NodeLevel starlance$getNodeLevel() { + return this.nodeLevel; + } + + @Inject(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", at = @At("RETURN")) + public void setBlock(final BlockPos pos, final BlockState newState, final int flags, final int maxUpdates, final CallbackInfoReturnable cir) { + if (cir.getReturnValueZ()) { + VSCHEvents.onBlockChange((Level)((Object)(this)), pos); + } + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunk.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunk.java new file mode 100644 index 00000000..aac1be00 --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunk.java @@ -0,0 +1,126 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.accessor.INodeLevelChunkSection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.pipe.level.NodeGetter; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.SectionPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +@Mixin(LevelChunk.class) +public abstract class MixinLevelChunk extends ChunkAccess implements BlockGetter, NodeGetter { + @Unique + private static final PipeNode[] EMPTY_NODES = new PipeNode[NodePos.UNIQUE_INDEX_BOUND]; + + protected MixinLevelChunk() { + super(null, null, null, null, 0, null, null); + } + + @Shadow + public abstract Level getLevel(); + + @Unique + private INodeLevelChunkSection getNodeSectionAtBlock(final int y) { + if (this.isOutsideBuildHeight(y)) { + return null; + } + return this.getSection(this.getSectionIndex(y)) instanceof INodeLevelChunkSection nodeSection ? nodeSection : null; + } + + @Override + public PipeNode getNode(final int x, final int y, final int z, final int index) { + final INodeLevelChunkSection nodeSection = this.getNodeSectionAtBlock(y); + if (nodeSection == null) { + return null; + } + return nodeSection.starlance$getNode(x, SectionPos.sectionRelative(y), z, index); + } + + @Override + public PipeNode[] getNodes(final int x, final int y, final int z) { + final INodeLevelChunkSection nodeSection = this.getNodeSectionAtBlock(y); + if (nodeSection == null) { + return null; + } + final PipeNode[] nodes = nodeSection.starlance$getNodes(x, SectionPos.sectionRelative(y), z); + return nodes == null ? EMPTY_NODES : nodes; + } + + @Override + public PipeNode setNode(final int x, final int y, final int z, final int index, final PipeNode node) { + final INodeLevelChunkSection nodeSection = this.getNodeSectionAtBlock(y); + if (nodeSection == null) { + return null; + } + final PipeNode oldNode = nodeSection.starlance$setNode(x, SectionPos.sectionRelative(y), z, index, node); + this.setNodesUnsaved(); + return oldNode; + } + + @Override + public Stream streamNodes() { + return Arrays.stream(this.getSections()) + .filter(INodeLevelChunkSection.class::isInstance) + .map(INodeLevelChunkSection.class::cast) + .filter(INodeLevelChunkSection::starlance$hasAnyNode) + .flatMap((section) -> Arrays.stream(section.starlance$getAllNodes()) + .filter(Objects::nonNull) + .flatMap((nodes) -> Arrays.stream(nodes).filter(Objects::nonNull))); + } + + @Override + public boolean hasAnyNode() { + for (final LevelChunkSection section : this.getSections()) { + if (section instanceof INodeLevelChunkSection nodeSection && nodeSection.starlance$hasAnyNode()) { + return true; + } + } + return false; + } + + @Override + public void writeNodes(final FriendlyByteBuf buf) { + for (final LevelChunkSection section : this.getSections()) { + ((INodeLevelChunkSection) (section)).starlance$writeNodes(buf); + } + } + + @Override + public void readNodes(final FriendlyByteBuf buf) { + final NodeLevel level = NodeLevel.get(this.getLevel()); + final ChunkPos chunkPos = this.getPos(); + int i = 0; + for (final LevelChunkSection section : this.getSections()) { + ((INodeLevelChunkSection) (section)).starlance$readNodes(level, SectionPos.of(chunkPos, this.getSectionYFromSectionIndex(i)), buf); + i++; + } + } + + @Override + public boolean isNodesUnsaved() { + return this.isUnsaved(); + } + + @Override + public void setNodesUnsaved() { + this.setUnsaved(true); + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunkSection.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunkSection.java new file mode 100644 index 00000000..dc9dd774 --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinLevelChunkSection.java @@ -0,0 +1,173 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.accessor.INodeLevelChunkSection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.pipe.level.NodeLevel; +import net.jcm.vsch.util.EncodeHelper; + +import net.minecraft.core.SectionPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.chunk.LevelChunkSection; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LevelChunkSection.class) +public class MixinLevelChunkSection implements INodeLevelChunkSection { + @Unique + private PipeNode[][] nodes = null; + + @Unique + private int nodeCount = 0; + + @Unique + private static short getBlockIndex(final int x, final int y, final int z) { + return (short)(x << 8 | z << 4 | y); + } + + @Override + public PipeNode[][] starlance$getAllNodes() { + return this.nodes; + } + + @Override + public PipeNode starlance$getNode(final int x, final int y, final int z, final int index) { + if (this.nodes == null) { + return null; + } + final PipeNode[] nodes = this.nodes[getBlockIndex(x, y, z)]; + return nodes == null ? null : nodes[index]; + } + + @Override + public PipeNode[] starlance$getNodes(final int x, final int y, final int z) { + if (this.nodes == null) { + return null; + } + final PipeNode[] nodes = this.nodes[getBlockIndex(x, y, z)]; + return nodes == null ? null : nodes; + } + + @Override + public PipeNode starlance$setNode(final int x, final int y, final int z, final int index, final PipeNode node) { + if (this.nodes == null) { + this.nodes = new PipeNode[LevelChunkSection.SECTION_SIZE][]; + } + final int blockIndex = getBlockIndex(x, y, z); + PipeNode[] nodes = this.nodes[blockIndex]; + if (nodes == null) { + nodes = new PipeNode[NodePos.UNIQUE_INDEX_BOUND]; + this.nodes[blockIndex] = nodes; + } + final PipeNode oldNode = nodes[index]; + nodes[index] = node; + final boolean hasOld = oldNode != null; + final boolean hasNew = node != null; + if (hasOld) { + if (!hasNew) { + this.nodeCount--; + } + } else if (hasNew) { + this.nodeCount++; + } + return oldNode; + } + + @Override + public boolean starlance$hasAnyNode() { + return this.nodeCount > 0; + } + + @Override + public void starlance$writeNodes(final FriendlyByteBuf buf) { + final int maxWritten = this.nodeCount; + buf.writeVarInt(maxWritten); + if (maxWritten == 0) { + return; + } + int written = 0; + for (int blockIndex = 0; blockIndex < LevelChunkSection.SECTION_SIZE && written < maxWritten; blockIndex++) { + final PipeNode[] nodes = this.nodes[blockIndex]; + if (nodes == null) { + EncodeHelper.writeVarInt22(buf, 0); + continue; + } + int bitset = 0; + for (int i = 0; i < NodePos.UNIQUE_INDEX_BOUND; i++) { + if (nodes[i] != null) { + bitset |= 1 << i; + } + } + EncodeHelper.writeVarInt22(buf, bitset); + if (bitset == 0) { + continue; + } + for (int i = 0; i < NodePos.UNIQUE_INDEX_BOUND; i++) { + final PipeNode node = nodes[i]; + if (node != null) { + written++; + node.writeTo(buf); + } + } + } + if (written != maxWritten) { + throw new RuntimeException("Incorrect node count, expect " + maxWritten + ", got" + written); + } + } + + @Override + public void starlance$readNodes(final NodeLevel level, final SectionPos sectionPos, final FriendlyByteBuf buf) { + if (this.nodeCount > 0) { + final int maxCount = this.nodeCount; + int count = 0; + for (int blockIndex = 0; blockIndex < LevelChunkSection.SECTION_SIZE && count < maxCount; blockIndex++) { + final PipeNode[] nodes = this.nodes[blockIndex]; + if (nodes == null) { + continue; + } + for (final PipeNode node : nodes) { + if (node != null) { + count++; + level.getNetwork().onNodeRemove(node); + } + } + } + } + final int maxRead = buf.readVarInt(); + this.nodeCount = maxRead; + if (maxRead == 0) { + this.nodes = null; + return; + } + this.nodes = new PipeNode[LevelChunkSection.SECTION_SIZE][]; + int read = 0; + for (short blockIndex = 0; blockIndex < LevelChunkSection.SECTION_SIZE && read < maxRead; blockIndex++) { + int bitset = EncodeHelper.readVarInt22(buf); + if (bitset == 0) { + continue; + } + final PipeNode[] nodes = new PipeNode[NodePos.UNIQUE_INDEX_BOUND]; + this.nodes[blockIndex] = nodes; + for (int i = 0; bitset != 0; i++) { + if ((bitset & 1) != 0) { + read++; + final PipeNode node = PipeNode.readFrom(level, NodePos.fromUniqueIndex(sectionPos.relativeToBlockPos(blockIndex), i), buf); + if (node == null) { + this.nodeCount--; + } else { + nodes[i] = node; + level.getNetwork().onNodeJoin(node); + } + } + bitset >>>= 1; + } + } + if (read != maxRead) { + throw new RuntimeException("Incorrect node count, expect " + maxRead + ", got" + read); + } + } +} diff --git a/src/main/java/net/jcm/vsch/mixin/minecraft/MixinServerPlayer.java b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinServerPlayer.java new file mode 100644 index 00000000..581b6e75 --- /dev/null +++ b/src/main/java/net/jcm/vsch/mixin/minecraft/MixinServerPlayer.java @@ -0,0 +1,34 @@ +package net.jcm.vsch.mixin.minecraft; + +import net.jcm.vsch.accessor.IClientboundLevelChunkWithLightPacketAccessor; +import net.jcm.vsch.network.VSCHNetwork; +import net.jcm.vsch.network.s2c.PipeNodeSyncChunkS2C; + +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.level.ChunkPos; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayer.class) +public class MixinServerPlayer { + @Shadow + public ServerGamePacketListenerImpl connection; + + @Inject( + method = "trackChunk", + at = @At("RETURN") + ) + public void trackChunk(final ChunkPos chunkPos, final Packet packet, final CallbackInfo ci) { + if (!(packet instanceof IClientboundLevelChunkWithLightPacketAccessor chunkPacket)) { + return; + } + final PipeNodeSyncChunkS2C pipePacket = chunkPacket.starlance$getPipeNodeSyncChunkS2C(); + VSCHNetwork.sendToPlayer(pipePacket, (ServerPlayer)((Object)(this))); + } +} diff --git a/src/main/java/net/jcm/vsch/network/INetworkPacket.java b/src/main/java/net/jcm/vsch/network/INetworkPacket.java new file mode 100644 index 00000000..219b1d66 --- /dev/null +++ b/src/main/java/net/jcm/vsch/network/INetworkPacket.java @@ -0,0 +1,10 @@ +package net.jcm.vsch.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +public interface INetworkPacket { + void encode(FriendlyByteBuf buf); + + void handle(NetworkEvent.Context ctx); +} diff --git a/src/main/java/net/jcm/vsch/network/VSCHNetwork.java b/src/main/java/net/jcm/vsch/network/VSCHNetwork.java new file mode 100644 index 00000000..5f1af13a --- /dev/null +++ b/src/main/java/net/jcm/vsch/network/VSCHNetwork.java @@ -0,0 +1,59 @@ +package net.jcm.vsch.network; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.PacketDistributor; +import net.minecraftforge.network.simple.SimpleChannel; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.network.s2c.PipeNodeSyncChunkS2C; +import net.jcm.vsch.network.s2c.PipeNodeUpdateS2C; + +import java.util.Optional; +import java.util.function.Function; + +public final class VSCHNetwork { + private VSCHNetwork() {} + + private static final String PROTOCOL_VERSION = VSCHMod.VERSION; + public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( + new ResourceLocation(VSCHMod.MODID, "main"), + () -> PROTOCOL_VERSION, + PROTOCOL_VERSION::equals, PROTOCOL_VERSION::equals + ); + private static int id = 0; + + public static void register() { + registerS2C(PipeNodeSyncChunkS2C.class, PipeNodeSyncChunkS2C::decode); + registerS2C(PipeNodeUpdateS2C.class, PipeNodeUpdateS2C::decode); + } + + public static void registerS2C(Class clazz, Function decoder) { + CHANNEL.registerMessage( + id++, + clazz, + INetworkPacket::encode, + decoder, + (packet, ctx) -> packet.handle(ctx.get()), + Optional.of(NetworkDirection.PLAY_TO_CLIENT) + ); + } + + public static void sendToServer(INetworkPacket packet) { + CHANNEL.sendToServer(packet); + } + + public static void sendToPlayer(INetworkPacket packet, ServerPlayer player) { + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), packet); + } + + public static void sendToTracking(INetworkPacket packet, ServerLevel level, BlockPos pos) { + level.getChunkSource().chunkMap.getPlayers(new ChunkPos(pos), false).forEach(p -> sendToPlayer(packet, (ServerPlayer) (p))); + } +} diff --git a/src/main/java/net/jcm/vsch/network/s2c/PipeNodeSyncChunkS2C.java b/src/main/java/net/jcm/vsch/network/s2c/PipeNodeSyncChunkS2C.java new file mode 100644 index 00000000..1491a29f --- /dev/null +++ b/src/main/java/net/jcm/vsch/network/s2c/PipeNodeSyncChunkS2C.java @@ -0,0 +1,61 @@ +package net.jcm.vsch.network.s2c; + +import net.jcm.vsch.network.INetworkPacket; +import net.jcm.vsch.pipe.level.NodeGetter; + +import io.netty.buffer.Unpooled; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraftforge.network.NetworkEvent; + +public class PipeNodeSyncChunkS2C implements INetworkPacket { + private final ChunkPos chunkPos; + private final byte[] data; + + public PipeNodeSyncChunkS2C(final ChunkPos chunkPos, final byte[] data) { + this.chunkPos = chunkPos; + this.data = data; + } + + @Override + public void encode(final FriendlyByteBuf buf) { + buf.writeChunkPos(this.chunkPos); + buf.writeByteArray(this.data); + } + + public static PipeNodeSyncChunkS2C fromChunk(final ChunkAccess chunk) { + if (!(chunk instanceof NodeGetter nodeGetter)) { + return null; + } + // TODO: investigate if Pooled buffer can give more performance + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(128)); + nodeGetter.writeNodes(buf); + final byte[] data = new byte[buf.readableBytes()]; + buf.readBytes(data); + return new PipeNodeSyncChunkS2C(chunk.getPos(), data); + } + + public static PipeNodeSyncChunkS2C decode(final FriendlyByteBuf buf) { + final ChunkPos chunkPos = buf.readChunkPos(); + final byte[] data = buf.readByteArray(); + return new PipeNodeSyncChunkS2C(chunkPos, data); + } + + @Override + public void handle(final NetworkEvent.Context ctx) { + ctx.setPacketHandled(true); + final ChunkPos chunkPos = this.chunkPos; + final byte[] data = this.data; + ctx.enqueueWork(() -> { + final ClientLevel level = Minecraft.getInstance().level; + final ChunkAccess chunk = level.getChunkSource().getChunkNow(chunkPos.x, chunkPos.z); + if (!(chunk instanceof NodeGetter nodeGetter)) { + return; + } + nodeGetter.readNodes(new FriendlyByteBuf(Unpooled.wrappedBuffer(data))); + }); + } +} diff --git a/src/main/java/net/jcm/vsch/network/s2c/PipeNodeUpdateS2C.java b/src/main/java/net/jcm/vsch/network/s2c/PipeNodeUpdateS2C.java new file mode 100644 index 00000000..58422a68 --- /dev/null +++ b/src/main/java/net/jcm/vsch/network/s2c/PipeNodeUpdateS2C.java @@ -0,0 +1,62 @@ +package net.jcm.vsch.network.s2c; + +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.network.INetworkPacket; +import net.jcm.vsch.pipe.level.NodeLevel; + +import io.netty.buffer.Unpooled; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.SectionPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraftforge.network.NetworkEvent; + +public class PipeNodeUpdateS2C implements INetworkPacket { + private static final byte[] EMPTY_BYTES = new byte[0]; + + private final NodePos pos; + private final byte[] data; + + public PipeNodeUpdateS2C(final NodePos pos, final byte[] data) { + this.pos = pos; + this.data = data; + } + + @Override + public void encode(final FriendlyByteBuf buf) { + this.pos.writeTo(buf); + buf.writeByteArray(this.data); + } + + public static PipeNodeUpdateS2C fromNode(final NodePos pos, final PipeNode node) { + if (node == null) { + return new PipeNodeUpdateS2C(pos, EMPTY_BYTES); + } + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(4)); + node.writeTo(buf); + final byte[] data = new byte[buf.readableBytes()]; + buf.readBytes(data); + return new PipeNodeUpdateS2C(pos, data); + } + + public static PipeNodeUpdateS2C decode(final FriendlyByteBuf buf) { + final NodePos pos = NodePos.readFrom(buf); + final byte[] data = buf.readByteArray(); + return new PipeNodeUpdateS2C(pos, data); + } + + @Override + public void handle(final NetworkEvent.Context ctx) { + ctx.setPacketHandled(true); + final NodePos pos = this.pos; + final byte[] data = this.data; + ctx.enqueueWork(() -> { + final ClientLevel level = Minecraft.getInstance().level; + final NodeLevel nodeLevel = NodeLevel.get(level); + final PipeNode node = data.length > 0 ? PipeNode.readFrom(nodeLevel, pos, new FriendlyByteBuf(Unpooled.wrappedBuffer(data))) : null; + nodeLevel.setNode(pos, node); + }); + } +} diff --git a/src/main/java/net/jcm/vsch/pipe/OmniNode.java b/src/main/java/net/jcm/vsch/pipe/OmniNode.java new file mode 100644 index 00000000..fc20008b --- /dev/null +++ b/src/main/java/net/jcm/vsch/pipe/OmniNode.java @@ -0,0 +1,103 @@ +package net.jcm.vsch.pipe; + +import net.jcm.vsch.VSCHMod; +import net.jcm.vsch.api.pipe.FlowDirection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.api.resource.ModelTextures; +import net.jcm.vsch.api.resource.TextureLocation; +import net.jcm.vsch.items.pipe.OmniNodeItem; +import net.jcm.vsch.pipe.level.NodeLevel; + +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; + +import java.util.EnumMap; + +public class OmniNode extends PipeNode { + private static final ModelTextures MODEL; + private static final ModelTextures PIPE_MODEL_XN; + private static final ModelTextures PIPE_MODEL_YN; + private static final ModelTextures PIPE_MODEL_ZN; + private static final ModelTextures PIPE_MODEL_XP; + private static final ModelTextures PIPE_MODEL_YP; + private static final ModelTextures PIPE_MODEL_ZP; + + static { + final ResourceLocation resource = new ResourceLocation(VSCHMod.MODID, "block/pipe/omni_node"); + final TextureLocation node1 = new TextureLocation(resource, 8, 4); + final TextureLocation node2 = new TextureLocation(resource, 8, 8); + final TextureLocation pipeH1 = new TextureLocation(resource, 0, 0); + final TextureLocation pipeH2 = new TextureLocation(resource, 4, 12); + final TextureLocation pipeH1R = TextureLocation.fromEnd(resource, 12, 4); + final TextureLocation pipeH2R = TextureLocation.fromEnd(resource, 16, 16); + final TextureLocation pipeV1 = new TextureLocation(resource, 0, 4); + final TextureLocation pipeV2 = new TextureLocation(resource, 12, 0); + final TextureLocation pipeV1R = TextureLocation.fromEnd(resource, 4, 16); + final TextureLocation pipeV2R = TextureLocation.fromEnd(resource, 16, 12); + final TextureLocation end1 = new TextureLocation(resource, 4, 4); + final TextureLocation end2 = new TextureLocation(resource, 4, 8); + MODEL = new ModelTextures(node1, node2, node1, node2, node1, node2); + PIPE_MODEL_XN = new ModelTextures(pipeH1, pipeH2, pipeH1, pipeH2, end1, end2); + PIPE_MODEL_YN = new ModelTextures(end1, end2, pipeV1R, pipeV2R, pipeV1R, pipeV2R); + PIPE_MODEL_ZN = new ModelTextures(pipeV1R, pipeV2R, end1, end2, pipeH2, pipeH1); + PIPE_MODEL_XP = new ModelTextures(pipeH1R, pipeH2R, pipeH1R, pipeH2R, end2, end1); + PIPE_MODEL_YP = new ModelTextures(end2, end1, pipeV1, pipeV2, pipeV1, pipeV2); + PIPE_MODEL_ZP = new ModelTextures(pipeV1, pipeV2, end2, end1, pipeH2R, pipeH1R); + } + + public OmniNode(final NodeLevel level, final NodePos pos) { + super(level, pos, Type.OMNI); + } + + @Override + public ItemStack asItemStack() { + return new ItemStack(OmniNodeItem.getByColor(this.getColor()), 1); + } + + @Override + public ModelTextures getModel() { + return MODEL; + } + + @Override + public ModelTextures getPipeModel(final Direction direction) { + return switch (direction) { + case DOWN -> PIPE_MODEL_YN; + case UP -> PIPE_MODEL_YP; + case NORTH -> PIPE_MODEL_ZN; + case SOUTH -> PIPE_MODEL_ZP; + case WEST -> PIPE_MODEL_XN; + case EAST -> PIPE_MODEL_XP; + }; + } + + @Override + public boolean canConnect(final Direction dir) { + return true; + } + + @Override + public FlowDirection getAccessFlowDirection(final Direction dir) { + return FlowDirection.BOTH; + } + + @Override + public FlowDirection getFlowDirection(final Direction dir) { + return FlowDirection.BOTH; + } + + @Override + protected int getWaterFlowRate() { + // TODO: adjust the value + return 6400; + } + + @Override + public int energyFlowAmount(final Direction dir) { + return 8 * 1024; + } +} diff --git a/src/main/java/net/jcm/vsch/pipe/PipeNetworkOperator.java b/src/main/java/net/jcm/vsch/pipe/PipeNetworkOperator.java new file mode 100644 index 00000000..f0c07f0c --- /dev/null +++ b/src/main/java/net/jcm/vsch/pipe/PipeNetworkOperator.java @@ -0,0 +1,343 @@ +package net.jcm.vsch.pipe; + +import net.jcm.vsch.accessor.IChunkMapAccessor; +import net.jcm.vsch.api.pipe.FlowDirection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.api.pipe.capability.NodeEnergyPort; +import net.jcm.vsch.api.pipe.capability.NodeFluidPort; +import net.jcm.vsch.api.pipe.capability.NodePort; +import net.jcm.vsch.pipe.level.NodeGetter; +import net.jcm.vsch.pipe.level.NodeLevel; +import net.jcm.vsch.util.Pair; + +import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.DyeColor; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fluids.FluidStack; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class PipeNetworkOperator { + private static final Object ENERGY_FLOW_ID = new Object(); + + private final NodeLevel level; + private final Set conflicted = new HashSet<>(); + private int tick = 0; + + public PipeNetworkOperator(final NodeLevel level) { + this.level = level; + } + + public void onNodeRemove(final PipeNode node) { + final NodePos nodePos = node.getPos(); + for (final NodePos otherPos : node.relation.connections.keySet()) { + final PipeNode other = this.level.getNode(otherPos); + if (other != null) { + other.relation.connections.remove(nodePos); + } + } + } + + public void onNodeJoin(final PipeNode node) { + final NodePos pos = node.getPos(); + pos.streamPossibleToConnect() + .map(this.level::getNode) + .filter(Objects::nonNull) + .forEach((other) -> { + final NodePos otherPos = other.getPos(); + final Direction[] connectPath = pos.connectPathTo(otherPos); + final Direction nodeOutDir = connectPath[0]; + final Direction otherOutDir = connectPath[connectPath.length - 1].getOpposite(); + if (!node.canConnect(nodeOutDir, other) || !other.canConnect(otherOutDir, node)) { + return; + } + this.connectNodes(node, nodeOutDir, other, otherOutDir); + }); + } + + public void onTick() { + final int iter = 3; + for (int i = 0; i < iter; i++) { + this.tick(); + } + } + + public void tick() { + this.tick++; + final int tick = this.tick; + final Object2DoubleOpenHashMap freezedPressures = new Object2DoubleOpenHashMap<>(); + final Object2ObjectOpenHashMap freezedFluids = new Object2ObjectOpenHashMap<>(); + final List nodes = this.streamServerNodes() + .filter((node) -> node.relation.checkAndUpdateTick(tick)) + .filter((node) -> !node.relation.getPorts(this.level, node.getPos()).isEmpty()) + .toList(); + nodes.stream() + .map((node) -> node.relation.portsCache) + .flatMap(List::stream) + .forEach((port) -> { + freezedPressures.put(port, port.getPressure()); + if (port instanceof NodeFluidPort fluidPort) { + freezedFluids.put(fluidPort, fluidPort.peekFluid()); + } + }); + freezedPressures.trim(); + freezedFluids.trim(); + for (final PipeNode node : nodes) { + final NodePos nodePos = node.getPos(); + for (final NodePort port : node.relation.portsCache) { + if (!port.getFlowDirection().canFlowOut()) { + continue; + } + if (port instanceof NodeFluidPort fluidPort) { + this.streamFluidFrom(nodePos, fluidPort, freezedPressures, freezedFluids); + } else if (port instanceof NodeEnergyPort energyPort) { + this.streamEnergyFrom(nodePos, energyPort, freezedPressures); + } + } + } + this.conflicted.forEach(this.level::breakNode); + this.conflicted.clear(); + } + + public Stream streamServerNodes() { + if (!(this.level.getLevel() instanceof ServerLevel serverLevel)) { + return Stream.empty(); + } + return StreamSupport.stream(((IChunkMapAccessor)(serverLevel.getChunkSource().chunkMap)).starlance$getChunks().spliterator(), false) + .map(ChunkHolder::getTickingChunk) + .filter(Objects::nonNull) + .map(NodeGetter.class::cast) + .flatMap(NodeGetter::streamNodes); + } + + public Set getConnections(final PipeNode node) { + return Collections.unmodifiableSet(node.relation.connections.keySet()); + } + + public boolean connectNodes(final PipeNode node1, final Direction dirToNode2, final PipeNode node2, final Direction dirToNode1) { + final boolean updated1 = node1.relation.connections.put(node2.getPos(), dirToNode2) != dirToNode2; + final boolean updated2 = node2.relation.connections.put(node1.getPos(), dirToNode1) != dirToNode1; + return updated1 || updated2; + } + + public boolean disconnectNodes(final PipeNode node1, final PipeNode node2) { + final boolean removed1 = node1.relation.connections.remove(node2.getPos()) != null; + final boolean removed2 = node2.relation.connections.remove(node1.getPos()) != null; + return removed1 || removed2; + } + + private void streamFluidFrom( + final NodePos from, + final NodeFluidPort fromPort, + final Object2DoubleOpenHashMap freezedPressures, + final Object2ObjectOpenHashMap freezedFluids + ) { + final double constQ = 1000; + + final double fromPressure = freezedPressures.getDouble(fromPort); + if (fromPressure <= 0) { + return; + } + final FluidStack stack = freezedFluids.get(fromPort); + if (stack.isEmpty()) { + return; + } + final int maxAmount = stack.getAmount(); + final int viscosity = stack.getFluid().getFluidType().getViscosity(); + + double totalFlowRate = 0; + final List> pendingOut = new ArrayList<>(); + final Set streamed = new HashSet<>(); + final Queue> queue = new ArrayDeque<>(); + streamed.add(from); + queue.add(new Pair.RefDouble<>(from, 0)); + while (true) { + final Pair.RefDouble posDist = queue.poll(); + if (posDist == null) { + break; + } + final NodePos pos = posDist.left(); + final double dist = posDist.right(); + final PipeNode node = this.level.getNode(pos); + if (!node.relation.setStreaming(stack.getFluid())) { + this.conflicted.add(pos); + continue; + } + + for (final NodePort port : node.relation.getPorts(this.level, pos)) { + if (!(port instanceof NodeFluidPort fluidPort)) { + continue; + } + if (!fluidPort.getFlowDirection().canFlowIn() || fluidPort.pushFluid(stack, true) <= 0) { + continue; + } + final double pressureDiff = fromPressure - freezedPressures.getDouble(fluidPort); + if (pressureDiff <= 0) { + continue; + } + final double vd = viscosity * dist; + final double flowRate = vd == 0 ? maxAmount : Math.min(maxAmount, constQ * pressureDiff / vd); + totalFlowRate += flowRate; + pendingOut.add(new Pair.RefDouble<>(fluidPort, flowRate)); + } + + for (final Map.Entry entry : node.relation.connections.entrySet()) { + final NodePos flowing = entry.getKey(); + if (streamed.contains(flowing)) { + continue; + } + final PipeNode flowingNode = this.level.getNode(flowing); + final Direction outDir = entry.getValue(); + if (flowingNode == null || !flowingNode.getFlowDirection(outDir).canFlowOut()) { + continue; + } + streamed.add(flowing); + queue.add(new Pair.RefDouble<>(flowing, dist + from.manhattanDistTo(flowing))); + } + } + + final double flowRateScale = (totalFlowRate > maxAmount ? maxAmount / totalFlowRate : 1) * 0.1; + + for (final Pair.RefDouble portRate : pendingOut) { + final NodeFluidPort fluidPort = portRate.left(); + final int flowRate = (int) (portRate.right() * flowRateScale); + final FluidStack pulled = fromPort.pullFluid(flowRate, true); + final int pushed = fluidPort.pushFluid(pulled, false); + fromPort.pullFluid(pushed, false); + } + } + + private void streamEnergyFrom( + final NodePos from, + final NodeEnergyPort fromPort, + final Object2DoubleOpenHashMap freezedPressures + ) { + final double constQ = 100; + final double fromPressure = freezedPressures.getDouble(fromPort); + if (fromPressure <= 0) { + return; + } + final double rateLimit = 1e8; + + double totalFlowRate = 0; + final List> pendingOut = new ArrayList<>(); + final Set streamed = new HashSet<>(); + final Queue> queue = new ArrayDeque<>(); + streamed.add(from); + queue.add(new Pair.RefDouble<>(from, 0)); + while (true) { + final Pair.RefDouble posDist = queue.poll(); + if (posDist == null) { + break; + } + final NodePos pos = posDist.left(); + final double dist = posDist.right(); + final PipeNode node = this.level.getNode(pos); + if (!node.relation.setStreaming(ENERGY_FLOW_ID)) { + this.conflicted.add(pos); + continue; + } + + for (final NodePort port : node.relation.getPorts(this.level, pos)) { + if (!(port instanceof NodeEnergyPort energyPort)) { + continue; + } + if (!energyPort.getFlowDirection().canFlowIn() || energyPort.pushEnergy(1, true) <= 0) { + continue; + } + final double pressureDiff = fromPressure - freezedPressures.getDouble(energyPort); + if (pressureDiff <= 0) { + continue; + } + final double flowRate = dist == 0 ? rateLimit : Math.min(rateLimit, constQ * pressureDiff / dist); + totalFlowRate += flowRate; + pendingOut.add(new Pair.RefDouble<>(energyPort, flowRate)); + } + + for (final Map.Entry entry : node.relation.connections.entrySet()) { + final NodePos flowing = entry.getKey(); + if (streamed.contains(flowing)) { + continue; + } + final PipeNode flowingNode = this.level.getNode(flowing); + final Direction outDir = entry.getValue(); + if (flowingNode == null || !flowingNode.getFlowDirection(outDir).canFlowOut()) { + continue; + } + streamed.add(flowing); + queue.add(new Pair.RefDouble<>(flowing, dist + from.manhattanDistTo(flowing))); + } + } + + final double flowRateScale = (totalFlowRate > rateLimit ? rateLimit / totalFlowRate : 1) * 0.1; + + for (final Pair.RefDouble portRate : pendingOut) { + final NodeEnergyPort energyPort = portRate.left(); + final int flowRate = (int) (portRate.right() * flowRateScale); + final int pulled = fromPort.pullEnergy(flowRate, true); + final int pushed = energyPort.pushEnergy(pulled, false); + fromPort.pullEnergy(pushed, false); + } + } + + public static final class RelationHolder { + private final Map connections = new HashMap<>(); + private int lastTick = 0; + private List portsCache = null; + private Object recentlyStreamed = null; + + private boolean checkAndUpdateTick(final int tick) { + if (this.lastTick == tick) { + return false; + } + this.lastTick = tick; + this.portsCache = null; + this.recentlyStreamed = null; + return true; + } + + private void resetCache() { + this.portsCache = null; + } + + private List getPorts(final NodeLevel level, final NodePos pos) { + if (this.portsCache == null) { + this.portsCache = pos.streamTouchingBlocks(level.getLevel()) + .map((blockPos) -> level.getNodePort(blockPos, pos.asRelative(blockPos))) + .filter(LazyOptional::isPresent) + .map((lazyPort) -> lazyPort.orElseThrow(IllegalStateException::new)) + .filter((port) -> port.getFlowDirection() != FlowDirection.NONE) + .toList(); + if (this.portsCache.isEmpty()) { + this.portsCache = List.of(); + } + } + return this.portsCache; + } + + private boolean setStreaming(final Object id) { + if (this.recentlyStreamed != null && !this.recentlyStreamed.equals(id)) { + return false; + } + this.recentlyStreamed = id; + return true; + } + } +} diff --git a/src/main/java/net/jcm/vsch/pipe/level/NodeGetter.java b/src/main/java/net/jcm/vsch/pipe/level/NodeGetter.java new file mode 100644 index 00000000..cd61547e --- /dev/null +++ b/src/main/java/net/jcm/vsch/pipe/level/NodeGetter.java @@ -0,0 +1,27 @@ +package net.jcm.vsch.pipe.level; + +import net.jcm.vsch.api.pipe.PipeNode; + +import net.minecraft.network.FriendlyByteBuf; + +import java.util.stream.Stream; + +public interface NodeGetter { + PipeNode getNode(int x, int y, int z, int index); + + PipeNode[] getNodes(int x, int y, int z); + + PipeNode setNode(int x, int y, int z, int index, PipeNode node); + + Stream streamNodes(); + + boolean hasAnyNode(); + + void writeNodes(FriendlyByteBuf buf); + + void readNodes(FriendlyByteBuf buf); + + boolean isNodesUnsaved(); + + void setNodesUnsaved(); +} diff --git a/src/main/java/net/jcm/vsch/pipe/level/NodeLevel.java b/src/main/java/net/jcm/vsch/pipe/level/NodeLevel.java new file mode 100644 index 00000000..c5f9dbc3 --- /dev/null +++ b/src/main/java/net/jcm/vsch/pipe/level/NodeLevel.java @@ -0,0 +1,245 @@ +package net.jcm.vsch.pipe.level; + +import net.jcm.vsch.VSCHCapabilities; +import net.jcm.vsch.accessor.ILevelAccessor; +import net.jcm.vsch.api.pipe.FlowDirection; +import net.jcm.vsch.api.pipe.NodePos; +import net.jcm.vsch.api.pipe.PipeNode; +import net.jcm.vsch.api.pipe.RelativeNodePos; +import net.jcm.vsch.api.pipe.capability.INodePortProvider; +import net.jcm.vsch.api.pipe.capability.NodeEnergyPort; +import net.jcm.vsch.api.pipe.capability.NodeFluidPort; +import net.jcm.vsch.api.pipe.capability.NodePort; +import net.jcm.vsch.network.VSCHNetwork; +import net.jcm.vsch.network.s2c.PipeNodeUpdateS2C; +import net.jcm.vsch.pipe.PipeNetworkOperator; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.energy.IEnergyStorage; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidType; +import net.minecraftforge.fluids.capability.IFluidHandler; + +import java.util.Objects; +import java.util.stream.Stream; + +public class NodeLevel { + private final Level level; + private final PipeNetworkOperator network = new PipeNetworkOperator(this); + + /** + * DO NOT initialize, use {@link get} instead. + * + * @see get + */ + public NodeLevel(final Level level) { + this.level = level; + } + + public static NodeLevel get(final Level level) { + return ((ILevelAccessor)(level)).starlance$getNodeLevel(); + } + + public final Level getLevel() { + return this.level; + } + + public final PipeNetworkOperator getNetwork() { + return this.network; + } + + @Override + public String toString() { + return ""; + } + + protected NodeGetter getNodeChunk(final int x, final int z) { + final LevelChunk chunk = this.level.getChunkSource().getChunkNow(x, z); + return chunk instanceof NodeGetter getter ? getter : null; + } + + public PipeNode getNode(final NodePos pos) { + final BlockPos blockPos = pos.blockPos(); + final int x = blockPos.getX(), y = blockPos.getY(), z = blockPos.getZ(); + final NodeGetter getter = this.getNodeChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)); + if (getter == null) { + return null; + } + return getter.getNode( + SectionPos.sectionRelative(x), + y, + SectionPos.sectionRelative(z), + pos.uniqueIndex() + ); + } + + public Stream streamNodesOn(final BlockPos blockPos) { + return NodePos.streamNodePosOn(blockPos).map(this::getNode).filter(Objects::nonNull); + } + + public PipeNode setNode(final NodePos pos, final PipeNode node) { + if (node != null && !node.getPos().equals(pos)) { + throw new IllegalArgumentException("Node position not match"); + } + final BlockPos blockPos = pos.blockPos(); + final int x = blockPos.getX(), y = blockPos.getY(), z = blockPos.getZ(); + final NodeGetter getter = this.getNodeChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)); + if (getter == null) { + return null; + } + final PipeNode oldNode = getter.setNode( + SectionPos.sectionRelative(x), + y, + SectionPos.sectionRelative(z), + pos.uniqueIndex(), + node + ); + if (oldNode != null) { + this.network.onNodeRemove(oldNode); + } + if (node != null) { + this.network.onNodeJoin(node); + } + if (this.level instanceof ServerLevel serverLevel) { + VSCHNetwork.sendToTracking(PipeNodeUpdateS2C.fromNode(pos, node), serverLevel, blockPos); + } + return oldNode; + } + + public void breakNode(final NodePos pos) { + this.breakNode(pos, true); + } + + public void breakNode(final NodePos pos, final boolean drop) { + final PipeNode node = this.setNode(pos, null); + if (node == null) { + return; + } + // TODO: play sound + if (!drop) { + return; + } + final ItemStack stack = node.asItemStack(); + if (stack.isEmpty()) { + return; + } + final Vec3 center = pos.getCenter(); + this.level.addFreshEntity(new ItemEntity(level, center.x, center.y, center.z, stack)); + } + + public LazyOptional getNodePort(final BlockPos blockPos, final RelativeNodePos pos) { + final BlockEntity be = this.level.getBlockEntity(blockPos); + if (be == null) { + return LazyOptional.empty(); + } + final LazyOptional lazyProvider = be.getCapability(VSCHCapabilities.PORT_PROVIDER); + if (!lazyProvider.isPresent()) { + return this.getNodePortFromBasicCapability(be); + } + final LazyOptional lazyPort = lazyProvider.lazyMap((provider) -> provider.getNodePort(pos)); + lazyProvider.addListener((lazyProviderAccess) -> lazyPort.invalidate()); + return lazyPort; + } + + private LazyOptional getNodePortFromBasicCapability(final BlockEntity be) { + final LazyOptional fluidCap = be.getCapability(ForgeCapabilities.FLUID_HANDLER); + final IFluidHandler fluidHandler = fluidCap.orElse(null); + if (fluidHandler != null && fluidHandler.getTanks() == 1) { + final LazyOptional lazyPort = fluidCap.lazyMap(NodeFluidPortImpl::new); + fluidCap.addListener((fluidCapAccess) -> lazyPort.invalidate()); + return lazyPort.cast(); + } + final LazyOptional energyCap = be.getCapability(ForgeCapabilities.ENERGY); + final IEnergyStorage energyStorage = energyCap.orElse(null); + if (energyStorage != null) { + final LazyOptional lazyPort = energyCap.lazyMap(NodeEnergyPortImpl::new); + energyCap.addListener((energyCapAccess) -> lazyPort.invalidate()); + return lazyPort.cast(); + } + return LazyOptional.empty(); + } + + private final class NodeFluidPortImpl implements NodeFluidPort { + private final IFluidHandler fluidHandler; + + private NodeFluidPortImpl(final IFluidHandler fluidHandler) { + this.fluidHandler = fluidHandler; + } + + @Override + public double getPressure() { + final FluidStack stack = this.fluidHandler.getFluidInTank(0); + if (stack.isEmpty()) { + return 0; + } + final FluidType fluid = stack.getFluid().getFluidType(); + final double amount = stack.getAmount(); + final double cap = this.fluidHandler.getTankCapacity(0); + // TODO: more realistic pressure + return amount / 2.0; + } + + @Override + public FlowDirection getFlowDirection() { + return this.fluidHandler.getFluidInTank(0).getAmount() < this.fluidHandler.getTankCapacity(0) ? FlowDirection.BOTH : FlowDirection.OUT; + } + + @Override + public FluidStack peekFluid() { + return this.fluidHandler.getFluidInTank(0); + } + + @Override + public int pushFluid(final FluidStack stack, final boolean simulate) { + return this.fluidHandler.fill(stack, simulate ? IFluidHandler.FluidAction.SIMULATE : IFluidHandler.FluidAction.EXECUTE); + } + + @Override + public FluidStack pullFluid(final int amount, final boolean simulate) { + return this.fluidHandler.drain(amount, simulate ? IFluidHandler.FluidAction.SIMULATE : IFluidHandler.FluidAction.EXECUTE); + } + } + + private final class NodeEnergyPortImpl implements NodeEnergyPort { + private final IEnergyStorage energyStorage; + + private NodeEnergyPortImpl(final IEnergyStorage energyStorage) { + this.energyStorage = energyStorage; + } + + @Override + public double getPressure() { + final double energy = this.energyStorage.getEnergyStored(); + if (energy == 0) { + return 0; + } + final double maxEnergy = this.energyStorage.getMaxEnergyStored(); + return energy * energy / maxEnergy; + } + + @Override + public FlowDirection getFlowDirection() { + return this.energyStorage.getEnergyStored() < this.energyStorage.getMaxEnergyStored() ? FlowDirection.BOTH : FlowDirection.OUT; + } + + @Override + public int pushEnergy(int amount, boolean simulate) { + return this.energyStorage.receiveEnergy(amount, simulate); + } + + @Override + public int pullEnergy(int amount, boolean simulate) { + return this.energyStorage.extractEnergy(amount, simulate); + } + } +} diff --git a/src/main/java/net/jcm/vsch/ship/IVSCHForceApplier.java b/src/main/java/net/jcm/vsch/ship/IVSCHForceApplier.java index a2495108..bdb85723 100644 --- a/src/main/java/net/jcm/vsch/ship/IVSCHForceApplier.java +++ b/src/main/java/net/jcm/vsch/ship/IVSCHForceApplier.java @@ -5,5 +5,5 @@ import org.valkyrienskies.core.impl.game.ships.PhysShipImpl; public interface IVSCHForceApplier { - void applyForces(BlockPos pos, PhysShipImpl physShip); + void applyForces(BlockPos pos, PhysShipImpl physShip); } diff --git a/src/main/java/net/jcm/vsch/util/EncodeHelper.java b/src/main/java/net/jcm/vsch/util/EncodeHelper.java new file mode 100644 index 00000000..337f5d8b --- /dev/null +++ b/src/main/java/net/jcm/vsch/util/EncodeHelper.java @@ -0,0 +1,31 @@ +package net.jcm.vsch.util; + +import net.minecraft.network.FriendlyByteBuf; + +public final class EncodeHelper { + private EncodeHelper() {} + + public static void writeVarInt22(final FriendlyByteBuf buf, int num) { + for (int i = 0; i < 2 && num > 0xff; i++) { + buf.writeByte((num & 0x7f) | 0x80); + num >>>= 7; + } + buf.writeByte(num); + } + + public static int readVarInt22(final FriendlyByteBuf buf) { + int num = 0; + for (int i = 0; true; i++) { + final byte b = buf.readByte(); + num |= (b & 0x7f) << (7 * i); + if ((b & 0x80) == 0) { + break; + } + if (i == 2) { + num |= 0x80 << (7 * 2); + break; + } + } + return num; + } +} diff --git a/src/main/java/net/jcm/vsch/util/Pair.java b/src/main/java/net/jcm/vsch/util/Pair.java index 49d06094..90f06d35 100644 --- a/src/main/java/net/jcm/vsch/util/Pair.java +++ b/src/main/java/net/jcm/vsch/util/Pair.java @@ -1,3 +1,11 @@ package net.jcm.vsch.util; -public record Pair(T left, U right) {} +public record Pair(T left, U right) { + public record RefInt(T left, int right) {} + + public record RefLong(T left, long right) {} + + public record RefFloat(T left, float right) {} + + public record RefDouble(T left, double right) {} +} diff --git a/src/main/resources/assets/vsch/lang/en_us.json b/src/main/resources/assets/vsch/lang/en_us.json index 4ba9d604..ab40a7a0 100644 --- a/src/main/resources/assets/vsch/lang/en_us.json +++ b/src/main/resources/assets/vsch/lang/en_us.json @@ -15,8 +15,18 @@ "block.vsch.powerful_thruster_block": "Powerful thruster", "block.vsch.rocket_assembler": "Rocket Assembler", "block.vsch.thruster_block": "Thruster", + "fluid_type.vsch.hydrogen": "Liquid Hydrogen", + "fluid_type.vsch.hydrogen_peroxide": "Hydrogen Peroxide", + "fluid_type.vsch.oxygen": "Liquid Oxygen", "item.vsch.magnet_boot": "Magnet Boots", "item.vsch.wrench": "Wrench", + "pipe.vsch.corner_node": "Corner Node", + "pipe.vsch.limited_omni_node": "Limited Omni Node", + "pipe.vsch.limited_pull_node": "Limited Pull Node", + "pipe.vsch.limited_push_node": "Limited Push Node", + "pipe.vsch.omni_node": "Omni Node", + "pipe.vsch.oneway_node": "Check Valve", + "pipe.vsch.straight_node": "Straight Node", "config.jade.plugin_vsch.gyro_component_config": "Gyro Mode Details", "config.jade.plugin_vsch.thruster_component_config": "Thruster Mode Details", diff --git a/src/main/resources/assets/vsch/lang/zh_cn.json b/src/main/resources/assets/vsch/lang/zh_cn.json index d986dafb..e41a26a9 100644 --- a/src/main/resources/assets/vsch/lang/zh_cn.json +++ b/src/main/resources/assets/vsch/lang/zh_cn.json @@ -15,8 +15,18 @@ "block.vsch.powerful_thruster_block": "强力推进器", "block.vsch.rocket_assembler": "火箭组装器", "block.vsch.thruster_block": "推进器", + "fluid_type.vsch.hydrogen": "液氢", + "fluid_type.vsch.hydrogen_peroxide": "过氧化氢", + "fluid_type.vsch.oxygen": "液氧", "item.vsch.magnet_boot": "磁力靴", "item.vsch.wrench": "扳手", + "pipe.vsch.corner_node": "转角节点", + "pipe.vsch.limited_omni_node": "受限的全向节点", + "pipe.vsch.limited_pull_node": "受限的拉取节点", + "pipe.vsch.limited_push_node": "受限的推送节点", + "pipe.vsch.omni_node": "全向节点", + "pipe.vsch.oneway_node": "单向阀", + "pipe.vsch.straight_node": "直流节点", "config.jade.plugin_vsch.gyro_component_config": "陀螺仪配置信息", "config.jade.plugin_vsch.thruster_component_config": "推进器配置信息", diff --git a/src/main/resources/assets/vsch/models/block/pipe/omni_node.json b/src/main/resources/assets/vsch/models/block/pipe/omni_node.json new file mode 100644 index 00000000..1f6d2d47 --- /dev/null +++ b/src/main/resources/assets/vsch/models/block/pipe/omni_node.json @@ -0,0 +1,57 @@ +{ + "gui_light": "side", + "textures": { + "main": "vsch:block/pipe/omni_node" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [4, 4, 4], + "faces": { + "down": { "texture": "#main", "tintindex": 0, "uv": [8, 8, 12, 12] }, + "up": { "texture": "#main", "tintindex": 0, "uv": [8, 4, 12, 8] }, + "north": { "texture": "#main", "tintindex": 0, "uv": [8, 8, 12, 12] }, + "south": { "texture": "#main", "tintindex": 0, "uv": [8, 4, 12, 8] }, + "west": { "texture": "#main", "tintindex": 0, "uv": [8, 8, 12, 12] }, + "east": { "texture": "#main", "tintindex": 0, "uv": [8, 4, 12, 8] } + } + } + ], + "display": { + "gui": { + "rotation": [30, -135, 0], + "translation": [-12, 7, -9], + "scale":[1.4, 1.4, 1.4] + }, + "ground": { + "rotation": [0, 0, 0], + "translation": [3, 3, 3], + "scale":[0.5, 0.5, 0.5] + }, + "fixed": { + "rotation": [0, 0, 0], + "translation": [6, 6, 6], + "scale":[1, 1, 1] + }, + "thirdperson_righthand": { + "rotation": [75, 45, 0], + "translation": [4, 2, 4], + "scale": [0.5, 0.5, 0.5] + }, + "thirdperson_lefthand": { + "rotation": [75, -45, 0], + "translation": [-4, 2, 4], + "scale": [0.5, 0.5, 0.5] + }, + "firstperson_righthand": { + "rotation": [0, 45, 0], + "translation": [1, 8, 6], + "scale": [0.6, 0.6, 0.6] + }, + "firstperson_lefthand": { + "rotation": [0, -135, 0], + "translation": [-4.5, 8.25, 1.25], + "scale": [0.6, 0.6, 0.6] + } + } +} diff --git a/src/main/resources/assets/vsch/models/item/black_omni_node.json b/src/main/resources/assets/vsch/models/item/black_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/black_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/blue_omni_node.json b/src/main/resources/assets/vsch/models/item/blue_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/blue_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/brown_omni_node.json b/src/main/resources/assets/vsch/models/item/brown_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/brown_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/cyan_omni_node.json b/src/main/resources/assets/vsch/models/item/cyan_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/cyan_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/gray_omni_node.json b/src/main/resources/assets/vsch/models/item/gray_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/gray_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/green_omni_node.json b/src/main/resources/assets/vsch/models/item/green_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/green_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/light_blue_omni_node.json b/src/main/resources/assets/vsch/models/item/light_blue_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/light_blue_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/light_gray_omni_node.json b/src/main/resources/assets/vsch/models/item/light_gray_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/light_gray_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/lime_omni_node.json b/src/main/resources/assets/vsch/models/item/lime_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/lime_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/magenta_omni_node.json b/src/main/resources/assets/vsch/models/item/magenta_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/magenta_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/orange_omni_node.json b/src/main/resources/assets/vsch/models/item/orange_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/orange_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/pink_omni_node.json b/src/main/resources/assets/vsch/models/item/pink_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/pink_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/purple_omni_node.json b/src/main/resources/assets/vsch/models/item/purple_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/purple_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/red_omni_node.json b/src/main/resources/assets/vsch/models/item/red_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/red_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/white_omni_node.json b/src/main/resources/assets/vsch/models/item/white_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/white_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/models/item/yellow_omni_node.json b/src/main/resources/assets/vsch/models/item/yellow_omni_node.json new file mode 100644 index 00000000..5f52ff5d --- /dev/null +++ b/src/main/resources/assets/vsch/models/item/yellow_omni_node.json @@ -0,0 +1,3 @@ +{ + "parent": "vsch:block/pipe/omni_node" +} diff --git a/src/main/resources/assets/vsch/textures/block/pipe/omni_node.png b/src/main/resources/assets/vsch/textures/block/pipe/omni_node.png new file mode 100644 index 00000000..bce850a3 Binary files /dev/null and b/src/main/resources/assets/vsch/textures/block/pipe/omni_node.png differ diff --git a/src/main/resources/data/vsch/tags/fluids/liquid_hydrogen.json b/src/main/resources/data/vsch/tags/fluids/liquid_hydrogen.json index ba3bc6f7..587d39a7 100644 --- a/src/main/resources/data/vsch/tags/fluids/liquid_hydrogen.json +++ b/src/main/resources/data/vsch/tags/fluids/liquid_hydrogen.json @@ -1,5 +1,6 @@ { "values": [ - "mekanism:hydrogen" + "mekanism:hydrogen", + "vsch:hydrogen" ] } diff --git a/src/main/resources/data/vsch/tags/fluids/liquid_oxygen.json b/src/main/resources/data/vsch/tags/fluids/liquid_oxygen.json index 052c132b..ce62ebce 100644 --- a/src/main/resources/data/vsch/tags/fluids/liquid_oxygen.json +++ b/src/main/resources/data/vsch/tags/fluids/liquid_oxygen.json @@ -1,5 +1,6 @@ { "values": [ - "mekanism:oxygen" + "mekanism:oxygen", + "vsch:oxygen" ] } diff --git a/src/main/resources/vsch.mixins.json b/src/main/resources/vsch.mixins.json index f2f9c426..d7e252de 100644 --- a/src/main/resources/vsch.mixins.json +++ b/src/main/resources/vsch.mixins.json @@ -17,7 +17,13 @@ "create.MixinMechanicalBearingBlockEntity", "create.MixinOrientedContraptionEntity", "minecraft.MixinChunkMap", + "minecraft.MixinChunkSerializer", + "minecraft.MixinClientboundLevelChunkWithLightPacket", + "minecraft.MixinLevel", + "minecraft.MixinLevelChunk", + "minecraft.MixinLevelChunkSection", "minecraft.MixinMob", + "minecraft.MixinServerPlayer", "valkyrienskies.MixinShipAssemblyKt", "valkyrienskies.MixinVSGameUtilsKt", "valkyrienskies.accessor.ServerShipObjectWorldAccessor"