diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index 75c739613..c648baf93 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -59,6 +59,7 @@ repositories { includeGroup("org.squiddev") includeGroup("cc.tweaked") // Things we mirror + includeGroup("alexiil.mc.lib") includeGroup("dev.architectury") includeGroup("maven.modrinth") includeGroup("me.shedaniel") diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt index 41f97276b..5656bc840 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Extensions.kt @@ -49,6 +49,7 @@ * Copy additional [BaseExecSpec] options which aren't handled by [ProcessForkOptions.copyTo]. */ fun BaseExecSpec.copyToExec(spec: BaseExecSpec) { + spec.workingDir = workingDir spec.isIgnoreExitValue = isIgnoreExitValue if (standardInput != null) spec.standardInput = standardInput if (standardOutput != null) spec.standardOutput = standardOutput diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9152cadaa..ce5a78e8b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,7 @@ slf4j = "1.7.36" # Minecraft mods iris = "1.5.2+1.19.4" jei = "13.1.0.11" +libmultipart = "0.10.0" modmenu = "6.1.0-rc.1" oculus = "1.2.5" rei = "10.0.578" @@ -96,6 +97,7 @@ iris = { module = "maven.modrinth:iris", version.ref = "iris" } jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" } jei-fabric = { module = "mezz.jei:jei-1.19.4-fabric", version.ref = "jei" } jei-forge = { module = "mezz.jei:jei-1.19.4-forge", version.ref = "jei" } +libmultipart = { module = "alexiil.mc.lib:libmultipart-all", version.ref = "libmultipart" } mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" } modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu" } oculus = { module = "maven.modrinth:oculus", version.ref = "oculus" } @@ -152,7 +154,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"] externalMods-forge-compile = ["oculus", "jei-api"] externalMods-forge-runtime = ["jei-forge"] externalMods-fabric = ["nightConfig-core", "nightConfig-toml"] -externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"] +externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin", "libmultipart"] externalMods-fabric-runtime = ["jei-fabric", "modmenu"] # Testing diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java index d1e9031b1..8e14b4cd7 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemState.java @@ -11,7 +11,7 @@ import javax.annotation.Nullable; import java.util.concurrent.atomic.AtomicBoolean; -public class ModemState { +public final class ModemState { private final @Nullable Runnable onChanged; private final AtomicBoolean changed = new AtomicBoolean(true); @@ -69,4 +69,30 @@ public void closeAll() { setOpen(false); } } + + /** + * Copy this modem state, returning a new instance. The new instance will have the same set of open channels but no + * on-change listener. + * + * @return The new modem state. + */ + public ModemState copy() { + return copy(null); + } + + /** + * Copy this modem state, returning a new instance. The new instance will have the same set of open channels and a + * different on-change listener. + * + * @param onChanged The on-change listener. + * @return The new modem state. + */ + public ModemState copy(@Nullable Runnable onChanged) { + synchronized (channels) { + var clone = onChanged == null ? new ModemState() : new ModemState(onChanged); + clone.channels.addAll(channels); + clone.open = open; + return clone; + } + } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlockEntity.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlockEntity.java index 10fe36371..d3ab4098a 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlockEntity.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemBlockEntity.java @@ -18,7 +18,7 @@ import javax.annotation.Nullable; -public class WirelessModemBlockEntity extends BlockEntity { +public final class WirelessModemBlockEntity extends BlockEntity { private static class Peripheral extends WirelessModemPeripheral { private final WirelessModemBlockEntity entity; @@ -96,6 +96,10 @@ private void updateBlockState() { } } + public ModemState getModemState() { + return modem.getModemState(); + } + @Nullable public IPeripheral getPeripheral(@Nullable Direction direction) { return direction == null || getDirection() == direction ? modem : null; diff --git a/projects/fabric/build.gradle.kts b/projects/fabric/build.gradle.kts index bd1d4981f..858e547ca 100644 --- a/projects/fabric/build.gradle.kts +++ b/projects/fabric/build.gradle.kts @@ -28,6 +28,7 @@ fun addRemappedConfiguration(name: String) { val ourSourceSet = sourceSets.register(name) { // Try to make this source set as much of a non-entity as possible. listOf(allSource, java, resources, kotlin).forEach { it.setSrcDirs(emptyList()) } + runtimeClasspath += sourceSets["client"].runtimeClasspath } val capitalName = name.replaceFirstChar { it.titlecase(Locale.ROOT) } loom.addRemapConfiguration("mod$capitalName") { @@ -45,6 +46,7 @@ fun addRemappedConfiguration(name: String) { addRemappedConfiguration("testWithSodium") addRemappedConfiguration("testWithIris") +addRemappedConfiguration("integrations") dependencies { modImplementation(libs.bundles.externalMods.fabric) @@ -60,6 +62,7 @@ dependencies { "modTestWithSodium"(libs.sodium) "modTestWithIris"(libs.iris) "modTestWithIris"(libs.sodium) + "modIntegrations"(libs.libmultipart) include(libs.cobalt) include(libs.jzlib) @@ -166,6 +169,14 @@ loom { property("fabric-api.gametest.report-file", project.buildDir.resolve("test-results/runGametest.xml").absolutePath) runDir("run/gametest") } + + register("clientWithIntegrations") { + configName = "Client (+integrations)" + runDir("run/integration") + client() + + source(sourceSets["integrations"]) + } } } diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java index 170034086..6c57c6d69 100644 --- a/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java +++ b/projects/fabric/src/client/java/dan200/computercraft/client/ComputerCraftClient.java @@ -4,11 +4,16 @@ package dan200.computercraft.client; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.client.integration.libmultipart.LibMultiPartIntegrationClient; import dan200.computercraft.client.model.EmissiveComputerModel; import dan200.computercraft.client.model.turtle.TurtleModelLoader; import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.config.ConfigSpec; +import dan200.computercraft.shared.integration.LoadedMods; import dan200.computercraft.shared.network.client.ClientNetworkContext; import dan200.computercraft.shared.peripheral.modem.wired.CableBlock; +import dan200.computercraft.shared.platform.FabricConfigFile; import dan200.computercraft.shared.platform.NetworkHandler; import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; @@ -17,6 +22,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.fabric.api.event.client.player.ClientPickBlockGatherCallback; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.RenderType; import net.minecraft.world.item.ItemStack; @@ -68,5 +74,10 @@ public static void init() { return cable.getCloneItemStack(state, hit, level, pos, player); }); + + // Load config file + ((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml")); + + if (LoadedMods.LIB_MULTI_PART) LibMultiPartIntegrationClient.init(); } } diff --git a/projects/fabric/src/client/java/dan200/computercraft/client/integration/libmultipart/LibMultiPartIntegrationClient.java b/projects/fabric/src/client/java/dan200/computercraft/client/integration/libmultipart/LibMultiPartIntegrationClient.java new file mode 100644 index 000000000..3a3b6668d --- /dev/null +++ b/projects/fabric/src/client/java/dan200/computercraft/client/integration/libmultipart/LibMultiPartIntegrationClient.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.client.integration.libmultipart; + +import alexiil.mc.lib.multipart.api.render.PartStaticModelRegisterEvent; +import dan200.computercraft.shared.integration.libmultipart.BlockStateModelKey; +import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration; +import net.minecraft.client.Minecraft; + +/** + * Client-side support for LibMultiPart. + * + * @see LibMultiPartIntegration + */ +public class LibMultiPartIntegrationClient { + public static void init() { + PartStaticModelRegisterEvent.EVENT.register(renderer -> { + var baker = Minecraft.getInstance().getBlockRenderer(); + renderer.register(BlockStateModelKey.class, (key, ctx) -> + ctx.bakedModelConsumer().accept(baker.getBlockModel(key.state()), key.state())); + }); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/mixin/BlockItemMixin.java b/projects/fabric/src/main/java/dan200/computercraft/mixin/BlockItemMixin.java new file mode 100644 index 000000000..9adc46b49 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/mixin/BlockItemMixin.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.mixin; + +import dan200.computercraft.shared.integration.LoadedMods; +import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.context.BlockPlaceContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * Adds multipart support to {@link BlockItem}. + */ +@Mixin(BlockItem.class) +class BlockItemMixin extends Item { + BlockItemMixin(Properties properties) { + super(properties); + } + + @Inject(method = "place", at = @At(value = "HEAD"), cancellable = true) + private void placeMultipart(BlockPlaceContext context, CallbackInfoReturnable cir) { + if (!LoadedMods.LIB_MULTI_PART) return; + + // If we have custom handling for this item, and the default logic would not work, then we run our libmultipart + // hook. + var factory = LibMultiPartIntegration.getCreatorForItem(this); + if (factory != null && !context.canPlace()) cir.setReturnValue(factory.placePart(context)); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java index d4587a7a1..764963e7d 100644 --- a/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/ComputerCraft.java @@ -12,6 +12,8 @@ import dan200.computercraft.shared.config.Config; import dan200.computercraft.shared.config.ConfigSpec; import dan200.computercraft.shared.details.FluidDetails; +import dan200.computercraft.shared.integration.LoadedMods; +import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration; import dan200.computercraft.shared.network.client.UpgradesLoadedMessage; import dan200.computercraft.shared.peripheral.commandblock.CommandBlockPeripheral; import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods; @@ -30,7 +32,6 @@ import net.fabricmc.fabric.api.loot.v2.LootTableEvents; import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; import net.fabricmc.fabric.api.resource.ResourceManagerHelper; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.resources.PreparableReloadListener; @@ -100,11 +101,11 @@ public static void init() { CommonHooks.onDatapackReload((name, listener) -> ResourceManagerHelper.get(PackType.SERVER_DATA).registerReloadListener(new ReloadListener(name, listener))); - ((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml")); - FabricDetailRegistries.FLUID_VARIANT.addProvider(FluidDetails::fill); ComputerCraftAPI.registerGenericSource(new InventoryMethods()); + + if (LoadedMods.LIB_MULTI_PART) LibMultiPartIntegration.init(); } private record ReloadListener(String name, PreparableReloadListener listener) diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/LoadedMods.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/LoadedMods.java new file mode 100644 index 000000000..57578538d --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/LoadedMods.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration; +import net.fabricmc.loader.api.FabricLoader; + +/** + * Constants indicating whether various mods are loaded or not. These are stored as static final fields, to avoid + * repeated lookups and allow the JIT to inline them to constants. + */ +public final class LoadedMods { + /** + * Whether LibMultiPart is loaded. + * + * @see LibMultiPartIntegration + */ + public static final boolean LIB_MULTI_PART = FabricLoader.getInstance().isModLoaded(LibMultiPartIntegration.MOD_ID); + + private LoadedMods() { + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/BlockStateModelKey.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/BlockStateModelKey.java new file mode 100644 index 000000000..5878707c7 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/BlockStateModelKey.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration.libmultipart; + +import alexiil.mc.lib.multipart.api.render.PartModelKey; +import net.minecraft.world.level.block.state.BlockState; + +/** + * A {@link PartModelKey} which just renders a basic {@link BlockState}. + */ +public final class BlockStateModelKey extends PartModelKey { + private final BlockState state; + + public BlockStateModelKey(BlockState state) { + this.state = state; + } + + public BlockState state() { + return state; + } + + @Override + public boolean equals(Object o) { + return o instanceof BlockStateModelKey other && state == other.state; + } + + @Override + public int hashCode() { + return state.hashCode(); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/LibMultiPartIntegration.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/LibMultiPartIntegration.java new file mode 100644 index 000000000..89e887e9c --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/LibMultiPartIntegration.java @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration.libmultipart; + +import alexiil.mc.lib.attributes.*; +import alexiil.mc.lib.multipart.api.NativeMultipart; +import dan200.computercraft.api.network.wired.WiredElement; +import dan200.computercraft.api.node.wired.WiredElementLookup; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.api.peripheral.PeripheralLookup; +import dan200.computercraft.shared.ModRegistry; +import dan200.computercraft.shared.integration.libmultipart.parts.WirelessModemPart; +import net.minecraft.world.item.Item; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Integration for LibMultiPart. + *

+ * This adds multipart versions of modems and cables. + */ +public final class LibMultiPartIntegration { + public static final String MOD_ID = "libmultipart"; + + public static final Attribute PERIPHERAL = Attributes.create(IPeripheral.class); + public static final Attribute WIRED_ELEMENT = Attributes.create(WiredElement.class); + + private static final Map itemPlacers = new HashMap<>(); + + private LibMultiPartIntegration() { + } + + public static void init() { + // Register an adapter from Fabric block lookup to attributes. This would be very inefficient by default, so + // we only do it for blocks which explicitly implement the attribute interfaces. + PeripheralLookup.get().registerFallback((world, pos, state, blockEntity, context) -> + state.getBlock() instanceof AttributeProvider || blockEntity instanceof AttributeProviderBlockEntity + ? PERIPHERAL.getFirstOrNull(world, pos, SearchOptions.inDirection(context.getOpposite())) + : null); + + WiredElementLookup.get().registerFallback((world, pos, state, blockEntity, context) -> + state.getBlock() instanceof AttributeProvider || blockEntity instanceof AttributeProviderBlockEntity + ? WIRED_ELEMENT.getFirstOrNull(world, pos, SearchOptions.inDirection(context.getOpposite())) + : null); + + registerWirelessModem(WirelessModemPart.makeDefinition(ModRegistry.Blocks.WIRELESS_MODEM_NORMAL, false)); + registerWirelessModem(WirelessModemPart.makeDefinition(ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED, true)); + } + + private static void registerWirelessModem(WirelessModemPart.Definition definition) { + definition.register(); + + NativeMultipart.LOOKUP.registerForBlocks((world, pos, state, blockEntity, context) -> (level, blockPos, blockState) -> + List.of(holder -> definition.convert(holder, state, blockEntity)), definition.block()); + + itemPlacers.put(definition.block().asItem(), definition); + } + + /** + * Get the corresponding {@link PlacementMultipartCreator} for an item. + * + * @param item The item we're trying to place. + * @return The placement-aware multipart creator, or {@code null}. + */ + public static @Nullable PlacementMultipartCreator getCreatorForItem(Item item) { + return itemPlacers.get(item); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/PlacementMultipartCreator.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/PlacementMultipartCreator.java new file mode 100644 index 000000000..103142571 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/PlacementMultipartCreator.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration.libmultipart; + +import alexiil.mc.lib.multipart.api.AbstractPart; +import alexiil.mc.lib.multipart.api.MultipartContainer.MultipartCreator; +import alexiil.mc.lib.multipart.api.MultipartHolder; +import alexiil.mc.lib.multipart.api.MultipartUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEvent; + +/** + * Creates a {@linkplain AbstractPart multipart} based on a {@link BlockPlaceContext}. + * + * @see MultipartCreator + */ +public interface PlacementMultipartCreator { + /** + * Create a new part. + * + * @param holder The holder which is creating this part. + * @param context The current block placement context. + * @return The newly created part. + */ + AbstractPart create(MultipartHolder holder, BlockPlaceContext context); + + /** + * Attempt to place this part into the world. + *

+ * This largely mirrors the logic in {@link BlockItem#place(BlockPlaceContext)}, but using + * {@link MultipartUtil#offerNewPart(Level, BlockPos, MultipartCreator)} to place the new part instead. + * + * @param context The current placement context. + * @return Whether the part was placed or not. + */ + default InteractionResult placePart(BlockPlaceContext context) { + var level = context.getLevel(); + var position = context.getClickedPos(); + + var offer = MultipartUtil.offerNewPart(level, position, holder -> create(holder, context)); + if (offer == null) return InteractionResult.PASS; + + // Be careful to only apply this server-side. Multiparts send a bunch of network packets when created, which we + // obviously don't want to do on the server! + if (!level.isClientSide) offer.apply(); + + // Approximate the block state from the placed part, and then fire all the appropriate events. + var stack = context.getItemInHand(); + var blockState = ((BlockItem) stack.getItem()).getBlock().defaultBlockState(); + var player = context.getPlayer(); + var sound = blockState.getSoundType(); + level.playSound(player, position, sound.getPlaceSound(), SoundSource.BLOCKS, (sound.getVolume() + 1f) / 2f, sound.getPitch() * 0.8f); + level.gameEvent(GameEvent.BLOCK_PLACE, position, GameEvent.Context.of(player, blockState)); + if (player == null || !player.getAbilities().instabuild) stack.shrink(1); + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/parts/WirelessModemPart.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/parts/WirelessModemPart.java new file mode 100644 index 000000000..c62f2c6fe --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/libmultipart/parts/WirelessModemPart.java @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration.libmultipart.parts; + +import alexiil.mc.lib.attributes.AttributeList; +import alexiil.mc.lib.multipart.api.AbstractPart; +import alexiil.mc.lib.multipart.api.MultipartEventBus; +import alexiil.mc.lib.multipart.api.MultipartHolder; +import alexiil.mc.lib.multipart.api.PartDefinition; +import alexiil.mc.lib.multipart.api.event.PartTickEvent; +import alexiil.mc.lib.multipart.api.render.PartModelKey; +import alexiil.mc.lib.net.IMsgReadCtx; +import alexiil.mc.lib.net.IMsgWriteCtx; +import alexiil.mc.lib.net.InvalidInputDataException; +import alexiil.mc.lib.net.NetByteBuf; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.integration.libmultipart.BlockStateModelKey; +import dan200.computercraft.shared.integration.libmultipart.LibMultiPartIntegration; +import dan200.computercraft.shared.integration.libmultipart.PlacementMultipartCreator; +import dan200.computercraft.shared.peripheral.modem.ModemShapes; +import dan200.computercraft.shared.peripheral.modem.ModemState; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlockEntity; +import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral; +import dan200.computercraft.shared.platform.RegistryEntry; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; + +import javax.annotation.Nullable; + +/** + * A {@linkplain AbstractPart multipart} for wireless modems. + * + * @see WirelessModemBlock + * @see WirelessModemBlockEntity + */ +public final class WirelessModemPart extends AbstractPart { + private final WirelessModemBlock modemBlock; + private final boolean advanced; + private final Direction direction; + + private final Peripheral modem; + private boolean on; + + private WirelessModemPart( + PartDefinition definition, MultipartHolder holder, WirelessModemBlock modemBlock, boolean advanced, + Direction direction, @Nullable ModemState state + ) { + super(definition, holder); + this.modemBlock = modemBlock; + this.advanced = advanced; + this.direction = direction; + + modem = new Peripheral(this, state); + } + + public static Definition makeDefinition(RegistryEntry modem, boolean advanced) { + return new Definition(modem, advanced); + } + + @Override + public void onAdded(MultipartEventBus bus) { + if (container.getMultipartWorld().isClientSide) return; + bus.addListener(this, PartTickEvent.class, event -> { + if (modem.getModemState().pollChanged()) sendNetworkUpdate(this, NET_RENDER_DATA); + }); + } + + @Override + public void addAllAttributes(AttributeList list) { + super.addAllAttributes(list); + if (list.attribute == LibMultiPartIntegration.PERIPHERAL && list.getSearchDirection() == direction.getOpposite()) { + list.offer(modem); + } + } + + @Override + public void writeCreationData(NetByteBuf buffer, IMsgWriteCtx ctx) { + super.writeCreationData(buffer, ctx); + buffer.writeEnum(direction); + } + + @Override + public CompoundTag toTag() { + var tag = super.toTag(); + tag.putString("direction", direction.getSerializedName()); + return tag; + } + + @Override + public void writeRenderData(NetByteBuf buffer, IMsgWriteCtx ctx) { + super.writeRenderData(buffer, ctx); + buffer.writeBoolean(on = modem.getModemState().isOpen()); + } + + @Override + public void readRenderData(NetByteBuf buffer, IMsgReadCtx ctx) throws InvalidInputDataException { + super.readRenderData(buffer, ctx); + on = buffer.readBoolean(); + redrawIfChanged(); + } + + @Override + public VoxelShape getShape() { + return ModemShapes.getBounds(direction); + } + + @Nullable + @Override + public PartModelKey getModelKey() { + return new BlockStateModelKey( + modemBlock.defaultBlockState() + .setValue(WirelessModemBlock.FACING, direction) + .setValue(WirelessModemBlock.ON, on) + ); + } + + private static class Peripheral extends WirelessModemPeripheral { + private final WirelessModemPart part; + + Peripheral(WirelessModemPart part, @Nullable ModemState state) { + // state will be non-null when converting an existing modem. This allows us to preserve the open channels. + super(state == null ? new ModemState() : state.copy(), part.advanced); + this.part = part; + } + + @Override + public Level getLevel() { + return part.container.getMultipartWorld(); + } + + @Override + public Vec3 getPosition() { + return Vec3.atLowerCornerOf(part.container.getMultipartPos().relative(part.direction)); + } + + @Override + public boolean equals(@Nullable IPeripheral other) { + return this == other || (other instanceof Peripheral && part == ((Peripheral) other).part); + } + + @Override + public Object getTarget() { + return part; + } + } + + public static final class Definition extends PartDefinition implements PlacementMultipartCreator { + private final RegistryEntry modem; + private final boolean advanced; + + private Definition(RegistryEntry modem, boolean advanced) { + super( + modem.id(), + (def, holder, tag) -> { + var direction = Direction.CODEC.byName(tag.getString("direction"), Direction.NORTH); + return new WirelessModemPart(def, holder, modem.get(), advanced, direction, null); + }, + (def, holder, buffer, context) -> { + var direction = buffer.readEnum(Direction.class); + return new WirelessModemPart(def, holder, modem.get(), advanced, direction, null); + } + ); + this.modem = modem; + this.advanced = advanced; + } + + public Block block() { + return modem.get(); + } + + public AbstractPart convert(MultipartHolder holder, BlockState state, @Nullable BlockEntity blockEntity) { + return new WirelessModemPart( + this, holder, modem.get(), advanced, + state.getValue(WirelessModemBlock.FACING), + blockEntity instanceof WirelessModemBlockEntity modemBlockEntity ? modemBlockEntity.getModemState() : null + ); + } + + @Override + public AbstractPart create(MultipartHolder holder, BlockPlaceContext context) { + return new WirelessModemPart(this, holder, modem.get(), advanced, context.getClickedFace().getOpposite(), null); + } + } +} diff --git a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json index 0b6d01a0c..c1e84c21a 100644 --- a/projects/fabric/src/main/resources/computercraft.fabric.mixins.json +++ b/projects/fabric/src/main/resources/computercraft.fabric.mixins.json @@ -8,6 +8,7 @@ }, "mixins": [ "ArgumentTypeInfosAccessor", + "BlockItemMixin", "ChunkMapMixin", "EntityMixin", "ExplosionDamageCalculatorMixin",