From 7f5fbe87279c192ef70824c9a5fc3fc89cfd62a9 Mon Sep 17 00:00:00 2001 From: FoundationGames <43485105+FoundationGames@users.noreply.github.com> Date: Fri, 21 Jul 2023 15:17:43 -0700 Subject: [PATCH] Add radio broadcasting sounds - Added Radio Transceiver/Loudspeaker - Server emitters can be authoritative over sound networks --- build.gradle | 2 - gradle.properties | 2 +- .../github/foundationgames/phonos/Phonos.java | 13 +- .../foundationgames/phonos/PhonosClient.java | 16 +- .../block/AbstractLoudspeakerBlock.java | 67 +++++++ .../phonos/block/ElectronicJukeboxBlock.java | 14 +- .../phonos/block/ElectronicNoteBlock.java | 1 - .../phonos/block/LoudspeakerBlock.java | 51 +----- .../phonos/block/PhonosBlocks.java | 12 +- .../phonos/block/RadioLoudspeakerBlock.java | 56 ++++++ .../phonos/block/RadioTransceiverBlock.java | 171 ++++++++++++++++++ .../entity/ElectronicJukeboxBlockEntity.java | 18 +- .../entity/RadioLoudspeakerBlockEntity.java | 130 +++++++++++++ .../entity/RadioTransceiverBlockEntity.java | 144 +++++++++++++++ .../RadioLoudspeakerBlockEntityRenderer.java | 52 ++++++ .../RadioTransceiverBlockEntityRenderer.java | 41 +++++ .../phonos/network/ClientPayloadPackets.java | 6 + .../phonos/network/PayloadPackets.java | 6 + .../phonos/radio/RadioDevice.java | 25 +++ .../phonos/radio/RadioStorage.java | 145 +++++++++++++++ .../phonos/sound/ClientSoundStorage.java | 13 +- .../sound/MultiSourceSoundInstance.java | 2 + .../phonos/sound/ServerSoundStorage.java | 6 +- .../phonos/sound/SoundStorage.java | 14 +- .../sound/emitter/SoundEmitterStorage.java | 21 ++- .../sound/emitter/SoundEmitterTree.java | 127 ++++++++++++- .../foundationgames/phonos/util/UniqueId.java | 4 + .../phonos/blockstates/radio_loudspeaker.json | 8 + .../phonos/blockstates/radio_transceiver.json | 8 + .../resources/assets/phonos/lang/en_us.json | 2 + .../models/block/radio_loudspeaker.json | 62 +++++++ .../models/block/radio_transceiver.json | 63 +++++++ .../phonos/models/item/radio_loudspeaker.json | 1 + .../phonos/models/item/radio_transceiver.json | 1 + .../textures/block/loudspeaker_back.png | Bin 6265 -> 6310 bytes .../phonos/textures/block/radio_antenna.png | Bin 0 -> 5721 bytes .../textures/block/radio_loudspeaker_back.png | Bin 0 -> 6127 bytes .../textures/block/radio_loudspeaker_top.png | Bin 0 -> 6427 bytes .../block/radio_transceiver_front.png | Bin 0 -> 6166 bytes .../textures/block/radio_transceiver_side.png | Bin 0 -> 6089 bytes .../textures/block/radio_transceiver_top.png | Bin 0 -> 6277 bytes .../minecraft/tags/blocks/mineable/axe.json | 4 +- 42 files changed, 1231 insertions(+), 77 deletions(-) create mode 100644 src/main/java/io/github/foundationgames/phonos/block/AbstractLoudspeakerBlock.java create mode 100644 src/main/java/io/github/foundationgames/phonos/block/RadioLoudspeakerBlock.java create mode 100644 src/main/java/io/github/foundationgames/phonos/block/RadioTransceiverBlock.java create mode 100644 src/main/java/io/github/foundationgames/phonos/block/entity/RadioLoudspeakerBlockEntity.java create mode 100644 src/main/java/io/github/foundationgames/phonos/block/entity/RadioTransceiverBlockEntity.java create mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/block/RadioLoudspeakerBlockEntityRenderer.java create mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/block/RadioTransceiverBlockEntityRenderer.java create mode 100644 src/main/java/io/github/foundationgames/phonos/radio/RadioDevice.java create mode 100644 src/main/java/io/github/foundationgames/phonos/radio/RadioStorage.java create mode 100644 src/main/resources/assets/phonos/blockstates/radio_loudspeaker.json create mode 100644 src/main/resources/assets/phonos/blockstates/radio_transceiver.json create mode 100644 src/main/resources/assets/phonos/models/block/radio_loudspeaker.json create mode 100644 src/main/resources/assets/phonos/models/block/radio_transceiver.json create mode 100644 src/main/resources/assets/phonos/models/item/radio_loudspeaker.json create mode 100644 src/main/resources/assets/phonos/models/item/radio_transceiver.json create mode 100644 src/main/resources/assets/phonos/textures/block/radio_antenna.png create mode 100644 src/main/resources/assets/phonos/textures/block/radio_loudspeaker_back.png create mode 100644 src/main/resources/assets/phonos/textures/block/radio_loudspeaker_top.png create mode 100644 src/main/resources/assets/phonos/textures/block/radio_transceiver_front.png create mode 100644 src/main/resources/assets/phonos/textures/block/radio_transceiver_side.png create mode 100644 src/main/resources/assets/phonos/textures/block/radio_transceiver_top.png diff --git a/build.gradle b/build.gradle index c2e6d7d..6234dd9 100644 --- a/build.gradle +++ b/build.gradle @@ -11,8 +11,6 @@ version = "${project.mod_version}+${project.minecraft_version}" group = project.maven_group repositories { - maven { url = "https://server.bbkr.space/artifactory/libs-release" } - maven { url = "https://storage.googleapis.com/devan-maven/" } maven { url "https://api.modrinth.com/maven" } } diff --git a/gradle.properties b/gradle.properties index a2891f9..57c8bb2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ loader_version=0.14.21 #Fabric api fabric_version=0.84.0+1.20.1 -mod_version = 1.0.0-beta.1 +mod_version = 1.0.0-beta.2 maven_group = io.github.foundationgames archives_base_name = phonos diff --git a/src/main/java/io/github/foundationgames/phonos/Phonos.java b/src/main/java/io/github/foundationgames/phonos/Phonos.java index fc3854c..970c52b 100644 --- a/src/main/java/io/github/foundationgames/phonos/Phonos.java +++ b/src/main/java/io/github/foundationgames/phonos/Phonos.java @@ -4,6 +4,8 @@ import io.github.foundationgames.phonos.item.ItemGroupQueue; import io.github.foundationgames.phonos.item.PhonosItems; import io.github.foundationgames.phonos.network.PayloadPackets; +import io.github.foundationgames.phonos.radio.RadioDevice; +import io.github.foundationgames.phonos.radio.RadioStorage; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.emitter.SoundEmitter; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterStorage; @@ -41,7 +43,8 @@ public void onInitialize() { SoundDataTypes.init(); InputPlugPoint.init(); - ServerLifecycleEvents.SERVER_STARTED.register(e -> { + ServerLifecycleEvents.SERVER_STARTING.register(e -> { + RadioStorage.serverReset(); SoundStorage.serverReset(); SoundEmitterStorage.serverReset(); }); @@ -51,12 +54,20 @@ public void onInitialize() { if (be instanceof SoundEmitter p) { SoundEmitterStorage.getInstance(world).addEmitter(p); } + if (be instanceof RadioDevice.Receiver rec) { + rec.setAndUpdateChannel(rec.getChannel()); + } }); ServerBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register((be, world) -> { if (be instanceof SoundEmitter p) { SoundEmitterStorage.getInstance(world).removeEmitter(p); } + if (be instanceof RadioDevice.Receiver rec) { + rec.removeReceiver(); + } }); + + RadioStorage.init(); } public static Identifier id(String path) { diff --git a/src/main/java/io/github/foundationgames/phonos/PhonosClient.java b/src/main/java/io/github/foundationgames/phonos/PhonosClient.java index 4002a51..e3a13bf 100644 --- a/src/main/java/io/github/foundationgames/phonos/PhonosClient.java +++ b/src/main/java/io/github/foundationgames/phonos/PhonosClient.java @@ -3,15 +3,18 @@ import io.github.foundationgames.jsonem.JsonEM; import io.github.foundationgames.phonos.block.PhonosBlocks; import io.github.foundationgames.phonos.client.render.block.CableOutputBlockEntityRenderer; +import io.github.foundationgames.phonos.client.render.block.RadioLoudspeakerBlockEntityRenderer; +import io.github.foundationgames.phonos.client.render.block.RadioTransceiverBlockEntityRenderer; import io.github.foundationgames.phonos.item.AudioCableItem; import io.github.foundationgames.phonos.item.PhonosItems; import io.github.foundationgames.phonos.network.ClientPayloadPackets; +import io.github.foundationgames.phonos.radio.RadioDevice; +import io.github.foundationgames.phonos.radio.RadioStorage; import io.github.foundationgames.phonos.sound.ClientSoundStorage; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.emitter.SoundEmitter; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterStorage; import io.github.foundationgames.phonos.util.PhonosUtil; -import io.github.foundationgames.phonos.world.sound.data.SoundDataTypes; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientBlockEntityEvents; @@ -35,10 +38,14 @@ public void onInitializeClient() { JsonEM.registerModelLayer(AUDIO_CABLE_END_LAYER); BlockRenderLayerMap.INSTANCE.putBlock(PhonosBlocks.ELECTRONIC_NOTE_BLOCK, RenderLayer.getCutout()); + BlockRenderLayerMap.INSTANCE.putBlock(PhonosBlocks.RADIO_TRANSCEIVER, RenderLayer.getCutout()); + BlockRenderLayerMap.INSTANCE.putBlock(PhonosBlocks.RADIO_LOUDSPEAKER, RenderLayer.getCutout()); BlockEntityRendererFactories.register(PhonosBlocks.ELECTRONIC_NOTE_BLOCK_ENTITY, CableOutputBlockEntityRenderer::new); BlockEntityRendererFactories.register(PhonosBlocks.ELECTRONIC_JUKEBOX_ENTITY, CableOutputBlockEntityRenderer::new); BlockEntityRendererFactories.register(PhonosBlocks.CONNECTION_HUB_ENTITY, CableOutputBlockEntityRenderer::new); + BlockEntityRendererFactories.register(PhonosBlocks.RADIO_TRANSCEIVER_ENTITY, RadioTransceiverBlockEntityRenderer::new); + BlockEntityRendererFactories.register(PhonosBlocks.RADIO_LOUDSPEAKER_ENTITY, RadioLoudspeakerBlockEntityRenderer::new); ColorProviderRegistry.BLOCK.register((state, world, pos, tintIndex) -> world != null && pos != null && state != null ? @@ -55,6 +62,7 @@ public void onInitializeClient() { ClientEntityEvents.ENTITY_LOAD.register((entity, world) -> { if (entity == MinecraftClient.getInstance().player) { + RadioStorage.clientReset(); SoundStorage.clientReset(); SoundEmitterStorage.clientReset(); } @@ -66,11 +74,17 @@ public void onInitializeClient() { if (be instanceof SoundEmitter p) { SoundEmitterStorage.getInstance(world).addEmitter(p); } + if (be instanceof RadioDevice.Receiver rec) { + rec.setAndUpdateChannel(rec.getChannel()); + } }); ClientBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register((be, world) -> { if (be instanceof SoundEmitter p) { SoundEmitterStorage.getInstance(world).removeEmitter(p); } + if (be instanceof RadioDevice.Receiver rec) { + rec.removeReceiver(); + } }); //ScreenRegistry.register(Phonos.RADIO_JUKEBOX_HANDLER, (gui, inventory, title) -> new RadioJukeboxScreen(gui, inventory.player)); diff --git a/src/main/java/io/github/foundationgames/phonos/block/AbstractLoudspeakerBlock.java b/src/main/java/io/github/foundationgames/phonos/block/AbstractLoudspeakerBlock.java new file mode 100644 index 0000000..ed0b811 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/block/AbstractLoudspeakerBlock.java @@ -0,0 +1,67 @@ +package io.github.foundationgames.phonos.block; + +import io.github.foundationgames.phonos.world.sound.block.SoundDataHandler; +import io.github.foundationgames.phonos.world.sound.data.NoteBlockSoundData; +import io.github.foundationgames.phonos.world.sound.data.SoundData; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.HorizontalFacingBlock; +import net.minecraft.client.MinecraftClient; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.state.StateManager; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class AbstractLoudspeakerBlock extends HorizontalFacingBlock implements SoundDataHandler { + public AbstractLoudspeakerBlock(Settings settings) { + super(settings); + + setDefaultState(getDefaultState().with(FACING, Direction.NORTH)); + } + + @Override + protected void appendProperties(StateManager.Builder builder) { + super.appendProperties(builder); + builder.add(FACING); + } + + @Nullable + @Override + public BlockState getPlacementState(ItemPlacementContext ctx) { + return getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite()); + } + + @Override + public void receiveSound(BlockState state, World world, BlockPos pos, SoundData sound) { + if (!world.isClient()) { + return; + } + + if (MinecraftClient.getInstance().player.getPos().squaredDistanceTo(pos.getX(), pos.getY(), pos.getZ()) > 5000) { + return; + } + + if (sound instanceof NoteBlockSoundData noteData) { + double note = noteData.note / 24D; + if (!world.getBlockState(pos.up()).isSideSolidFullSquare(world, pos.up(), Direction.DOWN)) { + world.addParticle(ParticleTypes.NOTE, + pos.getX() + 0.5, pos.getY() + 1.1, pos.getZ() + 0.5, + note, 0, 0); + } else { + var facing = state.get(FACING); + for (var dir : Direction.Type.HORIZONTAL) if (dir != facing.getOpposite()) { + if (!world.getBlockState(pos.offset(dir)).isSideSolidFullSquare(world, pos.offset(dir), dir.getOpposite())) { + world.addParticle(ParticleTypes.NOTE, + pos.getX() + 0.5 + dir.getVector().getX() * 0.6, + pos.getY() + 0.35, + pos.getZ() + 0.5 + dir.getVector().getZ() * 0.6, + note, 0, 0); + } + } + } + } + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/block/ElectronicJukeboxBlock.java b/src/main/java/io/github/foundationgames/phonos/block/ElectronicJukeboxBlock.java index 8077512..b817c9c 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/ElectronicJukeboxBlock.java +++ b/src/main/java/io/github/foundationgames/phonos/block/ElectronicJukeboxBlock.java @@ -52,18 +52,18 @@ public ActionResult useMusicDisc(World world, BlockPos pos, BlockState state, It @Override public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { var side = hit.getSide(); - if (side == Direction.UP) { - var stack = player.getStackInHand(hand); - if (!state.get(HAS_RECORD) && stack.getItem() instanceof MusicDiscItem) { - return this.useMusicDisc(world, pos, state, stack, player); - } - return super.onUse(state, world, pos, player, hand, hit); - } if (side == Direction.DOWN) { return ActionResult.PASS; } + var stack = player.getStackInHand(hand); + if (!state.get(HAS_RECORD) && stack.getItem() instanceof MusicDiscItem) { + return this.useMusicDisc(world, pos, state, stack, player); + } else if (side == Direction.UP) { + return super.onUse(state, world, pos, player, hand, hit); + } + if (player.canModifyBlocks()) { if (!world.isClient() && world.getBlockEntity(pos) instanceof ElectronicJukeboxBlockEntity be) { if (!PhonosUtil.holdingAudioCable(player) && be.outputs.tryRemoveConnection(world, hit, !player.isCreative())) { diff --git a/src/main/java/io/github/foundationgames/phonos/block/ElectronicNoteBlock.java b/src/main/java/io/github/foundationgames/phonos/block/ElectronicNoteBlock.java index 1cbe98d..687a212 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/ElectronicNoteBlock.java +++ b/src/main/java/io/github/foundationgames/phonos/block/ElectronicNoteBlock.java @@ -1,6 +1,5 @@ package io.github.foundationgames.phonos.block; -import io.github.foundationgames.phonos.block.entity.ElectronicJukeboxBlockEntity; import io.github.foundationgames.phonos.block.entity.ElectronicNoteBlockEntity; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterTree; diff --git a/src/main/java/io/github/foundationgames/phonos/block/LoudspeakerBlock.java b/src/main/java/io/github/foundationgames/phonos/block/LoudspeakerBlock.java index 570bc58..38ab117 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/LoudspeakerBlock.java +++ b/src/main/java/io/github/foundationgames/phonos/block/LoudspeakerBlock.java @@ -3,17 +3,10 @@ import io.github.foundationgames.phonos.util.PhonosUtil; import io.github.foundationgames.phonos.world.sound.block.BlockConnectionLayout; import io.github.foundationgames.phonos.world.sound.block.InputBlock; -import io.github.foundationgames.phonos.world.sound.block.SoundDataHandler; -import io.github.foundationgames.phonos.world.sound.data.NoteBlockSoundData; -import io.github.foundationgames.phonos.world.sound.data.SoundData; import net.minecraft.block.Block; import net.minecraft.block.BlockState; -import net.minecraft.block.HorizontalFacingBlock; -import net.minecraft.client.MinecraftClient; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.ItemPlacementContext; import net.minecraft.item.ItemUsageContext; -import net.minecraft.particle.ParticleTypes; import net.minecraft.state.StateManager; import net.minecraft.state.property.BooleanProperty; import net.minecraft.util.ActionResult; @@ -23,9 +16,8 @@ import net.minecraft.util.math.Direction; import net.minecraft.util.math.MathHelper; import net.minecraft.world.World; -import org.jetbrains.annotations.Nullable; -public class LoudspeakerBlock extends HorizontalFacingBlock implements SoundDataHandler, InputBlock { +public class LoudspeakerBlock extends AbstractLoudspeakerBlock implements InputBlock { public static final BooleanProperty[] INPUTS = BlockProperties.pluggableInputs(4); public final BlockConnectionLayout inputLayout = new BlockConnectionLayout() @@ -37,13 +29,13 @@ public class LoudspeakerBlock extends HorizontalFacingBlock implements SoundData public LoudspeakerBlock(Settings settings) { super(settings); - setDefaultState(BlockProperties.withAll(getDefaultState(), INPUTS, false).with(FACING, Direction.NORTH)); + setDefaultState(BlockProperties.withAll(getDefaultState(), INPUTS, false)); } @Override protected void appendProperties(StateManager.Builder builder) { super.appendProperties(builder); - builder.add(FACING); + builder.add(INPUTS); } @@ -60,12 +52,6 @@ public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEnt return ActionResult.success(hit.getSide().equals(state.get(FACING).getOpposite())); } - @Nullable - @Override - public BlockState getPlacementState(ItemPlacementContext ctx) { - return getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite()); - } - @Override public boolean canInputConnect(ItemUsageContext ctx) { var world = ctx.getWorld(); @@ -92,37 +78,6 @@ public Direction getRotation(BlockState state) { return state.get(FACING); } - @Override - public void receiveSound(BlockState state, World world, BlockPos pos, SoundData sound) { - if (!world.isClient()) { - return; - } - - if (MinecraftClient.getInstance().player.getPos().squaredDistanceTo(pos.getX(), pos.getY(), pos.getZ()) > 5000) { - return; - } - - if (sound instanceof NoteBlockSoundData noteData) { - double note = noteData.note / 24D; - if (!world.getBlockState(pos.up()).isSolidBlock(world, pos.up())) { - world.addParticle(ParticleTypes.NOTE, - pos.getX() + 0.5, pos.getY() + 1.1, pos.getZ() + 0.5, - note, 0, 0); - } else { - var facing = state.get(FACING); - for (var dir : Direction.Type.HORIZONTAL) if (dir != facing.getOpposite()) { - if (!world.getBlockState(pos.offset(dir)).isSolidBlock(world, pos.offset(dir))) { - world.addParticle(ParticleTypes.NOTE, - pos.getX() + 0.5 + dir.getVector().getX() * 0.6, - pos.getY() + 0.35, - pos.getZ() + 0.5 + dir.getVector().getZ() * 0.6, - note, 0, 0); - } - } - } - } - } - @Override public boolean isInputPluggedIn(int inputIndex, BlockState state, World world, BlockPos pos) { inputIndex = MathHelper.clamp(inputIndex, 0, 3); diff --git a/src/main/java/io/github/foundationgames/phonos/block/PhonosBlocks.java b/src/main/java/io/github/foundationgames/phonos/block/PhonosBlocks.java index 02f5cbd..03b8d1f 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/PhonosBlocks.java +++ b/src/main/java/io/github/foundationgames/phonos/block/PhonosBlocks.java @@ -1,9 +1,7 @@ package io.github.foundationgames.phonos.block; import io.github.foundationgames.phonos.Phonos; -import io.github.foundationgames.phonos.block.entity.ConnectionHubBlockEntity; -import io.github.foundationgames.phonos.block.entity.ElectronicJukeboxBlockEntity; -import io.github.foundationgames.phonos.block.entity.ElectronicNoteBlockEntity; +import io.github.foundationgames.phonos.block.entity.*; import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings; import net.minecraft.block.Block; import net.minecraft.block.Blocks; @@ -18,6 +16,8 @@ public class PhonosBlocks { public static final Block ELECTRONIC_NOTE_BLOCK = register(new ElectronicNoteBlock(FabricBlockSettings.copy(Blocks.NOTE_BLOCK)), "electronic_note_block"); public static final Block ELECTRONIC_JUKEBOX = register(new ElectronicJukeboxBlock(FabricBlockSettings.copy(Blocks.JUKEBOX)), "electronic_jukebox"); public static final Block CONNECTION_HUB = register(new ConnectionHubBlock(FabricBlockSettings.copy(Blocks.OAK_PLANKS)), "connection_hub"); + public static final Block RADIO_TRANSCEIVER = register(new RadioTransceiverBlock(FabricBlockSettings.copy(Blocks.OAK_SLAB)), "radio_transceiver"); + public static final Block RADIO_LOUDSPEAKER = register(new RadioLoudspeakerBlock(FabricBlockSettings.copy(Blocks.NOTE_BLOCK)), "radio_loudspeaker"); public static BlockEntityType ELECTRONIC_NOTE_BLOCK_ENTITY = Registry.register( Registries.BLOCK_ENTITY_TYPE, Phonos.id("electronic_note_block"), @@ -28,6 +28,12 @@ public class PhonosBlocks { public static BlockEntityType CONNECTION_HUB_ENTITY = Registry.register( Registries.BLOCK_ENTITY_TYPE, Phonos.id("connection_hub"), BlockEntityType.Builder.create(ConnectionHubBlockEntity::new, CONNECTION_HUB).build(null)); + public static BlockEntityType RADIO_TRANSCEIVER_ENTITY = Registry.register( + Registries.BLOCK_ENTITY_TYPE, Phonos.id("radio_transceiver"), + BlockEntityType.Builder.create(RadioTransceiverBlockEntity::new, RADIO_TRANSCEIVER).build(null)); + public static BlockEntityType RADIO_LOUDSPEAKER_ENTITY = Registry.register( + Registries.BLOCK_ENTITY_TYPE, Phonos.id("radio_loudspeaker"), + BlockEntityType.Builder.create(RadioLoudspeakerBlockEntity::new, RADIO_LOUDSPEAKER).build(null)); private static Block register(Block block, String name) { var item = Registry.register(Registries.ITEM, Phonos.id(name), new BlockItem(block, new Item.Settings())); diff --git a/src/main/java/io/github/foundationgames/phonos/block/RadioLoudspeakerBlock.java b/src/main/java/io/github/foundationgames/phonos/block/RadioLoudspeakerBlock.java new file mode 100644 index 0000000..1fc0ada --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/block/RadioLoudspeakerBlock.java @@ -0,0 +1,56 @@ +package io.github.foundationgames.phonos.block; + +import io.github.foundationgames.phonos.block.entity.RadioLoudspeakerBlockEntity; +import io.github.foundationgames.phonos.util.PhonosUtil; +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class RadioLoudspeakerBlock extends AbstractLoudspeakerBlock implements BlockEntityProvider { + public RadioLoudspeakerBlock(Settings settings) { + super(settings); + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + var side = hit.getSide(); + var facing = state.get(FACING); + + if (side == facing.getOpposite()) { + if (!world.isClient()) { + int inc = player.isSneaking() ? -1 : 1; + if (world.getBlockEntity(pos) instanceof RadioLoudspeakerBlockEntity be) { + be.setAndUpdateChannel(be.getChannel() + inc); + be.markDirty(); + } + + return ActionResult.CONSUME; + } + + return ActionResult.SUCCESS; + } + + return super.onUse(state, world, pos, player, hand, hit); + } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new RadioLoudspeakerBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + return PhonosUtil.blockEntityTicker(type, PhonosBlocks.RADIO_LOUDSPEAKER_ENTITY); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/block/RadioTransceiverBlock.java b/src/main/java/io/github/foundationgames/phonos/block/RadioTransceiverBlock.java new file mode 100644 index 0000000..89ebb2a --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/block/RadioTransceiverBlock.java @@ -0,0 +1,171 @@ +package io.github.foundationgames.phonos.block; + +import io.github.foundationgames.phonos.block.entity.RadioTransceiverBlockEntity; +import io.github.foundationgames.phonos.util.PhonosUtil; +import io.github.foundationgames.phonos.world.sound.block.BlockConnectionLayout; +import io.github.foundationgames.phonos.world.sound.block.InputBlock; +import net.minecraft.block.*; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemUsageContext; +import net.minecraft.state.StateManager; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class RadioTransceiverBlock extends HorizontalFacingBlock implements BlockEntityProvider, InputBlock { + private static final VoxelShape SHAPE = createCuboidShape(0, 0, 0, 16, 7, 16); + + public final BlockConnectionLayout inputLayout = new BlockConnectionLayout() + .addPoint(-4.5, -4.5, -8, Direction.NORTH) + .addPoint(4.5, -4.5, -8, Direction.NORTH); + + public RadioTransceiverBlock(Settings settings) { + super(settings); + setDefaultState(getDefaultState().with(FACING, Direction.NORTH)); + } + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + var side = hit.getSide(); + var facing = state.get(FACING); + + if (side == Direction.DOWN) { + return ActionResult.PASS; + } + + if (side == Direction.UP) { + if (!world.isClient()) { + int inc = player.isSneaking() ? -1 : 1; + if (world.getBlockEntity(pos) instanceof RadioTransceiverBlockEntity be) { + be.setAndUpdateChannel(be.getChannel() + inc); + be.markDirty(); + } + + return ActionResult.CONSUME; + } + + return ActionResult.SUCCESS; + } + + if (player.canModifyBlocks()) { + if (!world.isClient() && world.getBlockEntity(pos) instanceof RadioTransceiverBlockEntity be) { + if (PhonosUtil.holdingAudioCable(player)) { + return ActionResult.PASS; + } + + if (side != facing) { + if (be.outputs.tryRemoveConnection(world, hit, !player.isCreative())) { + be.sync(); + return ActionResult.SUCCESS; + } + } else { + return tryRemoveConnection(state, world, pos, hit); + } + } + } + + return ActionResult.success(side == facing); + } + + @Override + public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { + if (!newState.isOf(this) && world.getBlockEntity(pos) instanceof RadioTransceiverBlockEntity be) { + be.onDestroyed(); + } + + super.onStateReplaced(state, world, pos, newState, moved); + } + + @Override + public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) { + return SHAPE; + } + + @Override + public Direction getRotation(BlockState state) { + return state.get(FACING); + } + + @Nullable + @Override + public BlockState getPlacementState(ItemPlacementContext ctx) { + return getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite()); + } + + @Override + protected void appendProperties(StateManager.Builder builder) { + super.appendProperties(builder); + + builder.add(FACING); + } + + @Override + public boolean canInputConnect(ItemUsageContext ctx) { + var world = ctx.getWorld(); + var pos = ctx.getBlockPos(); + var state = world.getBlockState(pos); + var facing = state.get(FACING); + var side = ctx.getSide(); + + if (side == facing) { + int index = this.getInputLayout().getClosestIndexClicked(ctx.getHitPos(), pos, getRotation(state)); + + return !this.isInputPluggedIn(index, state, world, pos); + } + + return false; + } + + @Override + public boolean playsSound(World world, BlockPos pos) { + return false; + } + + @Override + public boolean isInputPluggedIn(int inputIndex, BlockState state, World world, BlockPos pos) { + if (world.getBlockEntity(pos) instanceof RadioTransceiverBlockEntity be) { + inputIndex = MathHelper.clamp(inputIndex, 0, be.inputs.length - 1); + + return be.inputs[inputIndex]; + } + + return false; + } + + @Override + public void setInputPluggedIn(int inputIndex, boolean pluggedIn, BlockState state, World world, BlockPos pos) { + if (world.getBlockEntity(pos) instanceof RadioTransceiverBlockEntity be) { + inputIndex = MathHelper.clamp(inputIndex, 0, be.inputs.length - 1); + be.inputs[inputIndex] = pluggedIn; + be.sync(); + } + } + + @Override + public BlockConnectionLayout getInputLayout() { + return inputLayout; + } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new RadioTransceiverBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + return PhonosUtil.blockEntityTicker(type, PhonosBlocks.RADIO_TRANSCEIVER_ENTITY); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java index 885b80e..1d06e2f 100644 --- a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java @@ -1,6 +1,7 @@ package io.github.foundationgames.phonos.block.entity; import io.github.foundationgames.phonos.block.PhonosBlocks; +import io.github.foundationgames.phonos.network.PayloadPackets; import io.github.foundationgames.phonos.sound.SoundStorage; import io.github.foundationgames.phonos.sound.emitter.SoundEmitterTree; import io.github.foundationgames.phonos.sound.emitter.SoundSource; @@ -20,6 +21,7 @@ import net.minecraft.network.listener.ClientPlayPacketListener; import net.minecraft.network.packet.Packet; import net.minecraft.registry.Registries; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.DyeColor; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; @@ -44,6 +46,8 @@ public class ElectronicJukeboxBlockEntity extends JukeboxBlockEntity implements private @Nullable NbtCompound pendingNbt = null; private final long emitterId; + private @Nullable SoundEmitterTree playingSound = null; + public ElectronicJukeboxBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { super(pos, state); this.type = type; @@ -68,9 +72,11 @@ public void startPlaying() { this.world.updateNeighborsAlways(this.getPos(), this.getCachedState().getBlock()); if (this.getStack().getItem() instanceof MusicDiscItem disc && !world.isClient()) { + this.playingSound = new SoundEmitterTree(this.emitterId); + SoundStorage.getInstance(world).play(world, SoundEventSoundData.create( emitterId, Registries.SOUND_EVENT.getEntry(disc.getSound()), 2, 1), - new SoundEmitterTree(this.emitterId)); + this.playingSound); sync(); } @@ -84,6 +90,8 @@ protected void stopPlaying() { this.world.updateNeighborsAlways(this.getPos(), this.getCachedState().getBlock()); if (!world.isClient()) { + this.playingSound = null; + SoundStorage.getInstance(world).stop(world, emitterId); sync(); } @@ -100,6 +108,14 @@ public void tick(World world, BlockPos pos, BlockState state) { if (!world.isClient()) { super.tick(world, pos, state); + if (this.playingSound != null) { + var delta = this.playingSound.updateServer(world); + + if (delta.hasChanges() && world instanceof ServerWorld sWorld) for (var player : sWorld.getPlayers()) { + PayloadPackets.sendSoundUpdate(player, delta); + } + } + if (this.outputs.purge(conn -> this.outputs.dropConnectionItem(world, conn, true))) { sync(); } diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/RadioLoudspeakerBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/RadioLoudspeakerBlockEntity.java new file mode 100644 index 0000000..7c67dd0 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/RadioLoudspeakerBlockEntity.java @@ -0,0 +1,130 @@ +package io.github.foundationgames.phonos.block.entity; + +import io.github.foundationgames.phonos.block.PhonosBlocks; +import io.github.foundationgames.phonos.radio.RadioDevice; +import io.github.foundationgames.phonos.radio.RadioStorage; +import io.github.foundationgames.phonos.sound.emitter.SoundSource; +import io.github.foundationgames.phonos.world.sound.block.SoundDataHandler; +import io.github.foundationgames.phonos.world.sound.data.SoundData; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.state.property.Properties; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class RadioLoudspeakerBlockEntity extends BlockEntity implements Syncing, Ticking, RadioDevice.Receiver, SoundSource { + private int channel = 0; + private boolean needsAdd = false; + + public RadioLoudspeakerBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + public RadioLoudspeakerBlockEntity(BlockPos pos, BlockState state) { + this(PhonosBlocks.RADIO_LOUDSPEAKER_ENTITY, pos, state); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + + if (this.world == null) { + this.needsAdd = true; + this.channel = nbt.getInt("channel"); + } else { + this.setAndUpdateChannel(nbt.getInt("channel")); + } + } + + @Override + protected void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + + nbt.putInt("channel", this.getChannel()); + } + + @Override + public NbtCompound toInitialChunkDataNbt() { + NbtCompound nbt = new NbtCompound(); + this.writeNbt(nbt); + return nbt; + } + + @Nullable + @Override + public Packet toUpdatePacket() { + return this.getPacket(); + } + + @Override + public int getChannel() { + return channel; + } + + @Override + public void setAndUpdateChannel(int channel) { + channel = Math.floorMod(channel, RadioStorage.CHANNEL_COUNT); + + var radio = RadioStorage.getInstance(this.world); + + radio.removeReceivingSource(this.channel, this); + this.channel = channel; + radio.addReceivingSource(channel, this); + + sync(); + } + + @Override + public void addReceiver() { + setAndUpdateChannel(getChannel()); + } + + @Override + public void removeReceiver() { + var radio = RadioStorage.getInstance(this.world); + radio.removeReceivingSource(this.getChannel(), this); + } + + @Override + public double x() { + return this.getPos().getX() + 0.5; + } + + @Override + public double y() { + return this.getPos().getY() + 0.5; + } + + @Override + public double z() { + return this.getPos().getZ() + 0.5; + } + + @Override + public void onSoundPlayed(World world, SoundData sound) { + var pos = this.getPos(); + var state = world.getBlockState(pos); + + if (state.getBlock() instanceof SoundDataHandler block) { + block.receiveSound(state, world, pos, sound); + } + } + + @Override + public void tick(World world, BlockPos pos, BlockState state) { + if (this.needsAdd) { + this.addReceiver(); + this.needsAdd = false; + } + } + + public Direction getRotation() { + return this.getCachedState().get(Properties.HORIZONTAL_FACING); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/RadioTransceiverBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/RadioTransceiverBlockEntity.java new file mode 100644 index 0000000..e36fab3 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/block/entity/RadioTransceiverBlockEntity.java @@ -0,0 +1,144 @@ +package io.github.foundationgames.phonos.block.entity; + +import io.github.foundationgames.phonos.block.PhonosBlocks; +import io.github.foundationgames.phonos.block.RadioTransceiverBlock; +import io.github.foundationgames.phonos.radio.RadioDevice; +import io.github.foundationgames.phonos.radio.RadioStorage; +import io.github.foundationgames.phonos.world.sound.InputPlugPoint; +import io.github.foundationgames.phonos.world.sound.block.BlockConnectionLayout; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.item.ItemStack; +import net.minecraft.item.ItemUsageContext; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.state.property.Properties; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.util.function.LongConsumer; + +public class RadioTransceiverBlockEntity extends AbstractConnectionHubBlockEntity implements RadioDevice.Transmitter, RadioDevice.Receiver { + public static final BlockConnectionLayout OUTPUT_LAYOUT = new BlockConnectionLayout() + .addPoint(-8, -5, 0, Direction.WEST) + .addPoint(8, -5, 0, Direction.EAST) + .addPoint(0, -5, 8, Direction.SOUTH); + + private int channel = 0; + private boolean needsAdd = false; + + public RadioTransceiverBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state, OUTPUT_LAYOUT, new boolean[2]); + } + + public RadioTransceiverBlockEntity(BlockPos pos, BlockState state) { + this(PhonosBlocks.RADIO_TRANSCEIVER_ENTITY, pos, state); + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + + if (this.world == null) { + this.needsAdd = true; + this.channel = nbt.getInt("channel"); + } else { + this.setAndUpdateChannel(nbt.getInt("channel")); + } + } + + @Override + protected void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + + nbt.putInt("channel", this.getChannel()); + } + + @Override + public void forEachChild(LongConsumer action) { + RadioDevice.Transmitter.super.forEachChild(action); + + super.forEachChild(action); + } + + @Override + public int getChannel() { + return channel; + } + + @Override + public boolean canConnect(ItemUsageContext ctx) { + var side = ctx.getSide(); + var facing = getRotation(); + + if (side != Direction.UP && side != Direction.DOWN && side != getCachedState().get(Properties.HORIZONTAL_FACING)) { + return !this.outputs.isOutputPluggedIn(OUTPUT_LAYOUT.getClosestIndexClicked(ctx.getHitPos(), this.getPos(), facing)); + } + + return false; + } + + @Override + public boolean addConnection(Vec3d hitPos, @Nullable DyeColor color, InputPlugPoint destInput, ItemStack cable) { + int index = OUTPUT_LAYOUT.getClosestIndexClicked(hitPos, this.getPos(), getRotation()); + + if (this.outputs.tryPlugOutputIn(index, color, destInput, cable)) { + this.markDirty(); + this.sync(); + return true; + } + + return false; + } + + @Override + public boolean forwards() { + return true; + } + + @Override + public Direction getRotation() { + if (this.getCachedState().getBlock() instanceof RadioTransceiverBlock block) { + return block.getRotation(this.getCachedState()); + } + + return Direction.NORTH; + } + + @Override + public void setAndUpdateChannel(int channel) { + channel = Math.floorMod(channel, RadioStorage.CHANNEL_COUNT); + + var radio = RadioStorage.getInstance(this.world); + + radio.removeReceivingEmitter(this.channel, this.emitterId()); + this.channel = channel; + radio.addReceivingEmitter(channel, this); + + sync(); + } + + @Override + public void addReceiver() { + setAndUpdateChannel(getChannel()); + } + + @Override + public void removeReceiver() { + var radio = RadioStorage.getInstance(this.world); + radio.removeReceivingEmitter(this.getChannel(), this.emitterId()); + } + + @Override + public void tick(World world, BlockPos pos, BlockState state) { + super.tick(world, pos, state); + + if (this.needsAdd) { + this.addReceiver(); + this.needsAdd = false; + } + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioLoudspeakerBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioLoudspeakerBlockEntityRenderer.java new file mode 100644 index 0000000..4ad04a8 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioLoudspeakerBlockEntityRenderer.java @@ -0,0 +1,52 @@ +package io.github.foundationgames.phonos.client.render.block; + +import io.github.foundationgames.phonos.block.entity.RadioLoudspeakerBlockEntity; +import io.github.foundationgames.phonos.radio.RadioStorage; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BlockEntityRenderer; +import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.RotationAxis; + +public class RadioLoudspeakerBlockEntityRenderer implements BlockEntityRenderer { + public static final Text[] CHANNEL_TO_TEXT = new Text[RadioStorage.CHANNEL_COUNT]; + public static int TEXT_COLOR = 0xFF2A2A; + public static int OUTLINE_COLOR = 0x4F0000; + + private final TextRenderer font; + + public RadioLoudspeakerBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { + this.font = ctx.getTextRenderer(); + } + + @Override + public void render(RadioLoudspeakerBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { + matrices.push(); + + matrices.translate(0.5, 0.5, 0.5); + matrices.multiply(RotationAxis.NEGATIVE_Y.rotationDegrees(entity.getRotation().asRotation())); + matrices.translate(0, 0, -0.501); + + matrices.scale(0.0268f, 0.0268f, 0.0268f); + matrices.multiply(RotationAxis.POSITIVE_Z.rotation((float) Math.PI)); + matrices.translate(0, 2.25, 0); + + var text = RadioLoudspeakerBlockEntityRenderer.getTextForChannel(entity.getChannel()).asOrderedText(); + + this.font.drawWithOutline(text, -this.font.getWidth(text) * 0.5f, 0, + RadioLoudspeakerBlockEntityRenderer.TEXT_COLOR, RadioLoudspeakerBlockEntityRenderer.OUTLINE_COLOR, + matrices.peek().getPositionMatrix(), vertexConsumers, 15728880); + + matrices.pop(); + } + + public static Text getTextForChannel(int channel) { + if (CHANNEL_TO_TEXT[channel] == null) { + CHANNEL_TO_TEXT[channel] = Text.literal(Integer.toString(channel)); + } + + return CHANNEL_TO_TEXT[channel]; + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioTransceiverBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioTransceiverBlockEntityRenderer.java new file mode 100644 index 0000000..29318f8 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/RadioTransceiverBlockEntityRenderer.java @@ -0,0 +1,41 @@ +package io.github.foundationgames.phonos.client.render.block; + +import io.github.foundationgames.phonos.block.entity.RadioTransceiverBlockEntity; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.RotationAxis; + +public class RadioTransceiverBlockEntityRenderer extends CableOutputBlockEntityRenderer { + private final TextRenderer font; + + public RadioTransceiverBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) { + super(ctx); + + this.font = ctx.getTextRenderer(); + } + + @Override + public void render(RadioTransceiverBlockEntity entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { + super.render(entity, tickDelta, matrices, vertexConsumers, light, overlay); + + matrices.push(); + + matrices.translate(0.5, 0.4378, 0.5); + + matrices.scale(0.0268f, 0.0268f, 0.0268f); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90)); + matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(entity.getRotation().asRotation())); + + matrices.translate(0, 4.75, 0); + + var text = RadioLoudspeakerBlockEntityRenderer.getTextForChannel(entity.getChannel()).asOrderedText(); + + this.font.drawWithOutline(text, -this.font.getWidth(text) * 0.5f, 0, + RadioLoudspeakerBlockEntityRenderer.TEXT_COLOR, RadioLoudspeakerBlockEntityRenderer.OUTLINE_COLOR, + matrices.peek().getPositionMatrix(), vertexConsumers, 15728880); + + matrices.pop(); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java b/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java index a2f5c77..b1de784 100644 --- a/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java +++ b/src/main/java/io/github/foundationgames/phonos/network/ClientPayloadPackets.java @@ -23,5 +23,11 @@ public static void initClient() { client.execute(() -> SoundStorage.getInstance(client.world).stop(client.world, id)); }); + + ClientPlayNetworking.registerGlobalReceiver(Phonos.id("sound_update"), (client, handler, buf, responseSender) -> { + SoundEmitterTree.Delta delta = SoundEmitterTree.Delta.fromPacket(buf); + + client.execute(() -> SoundStorage.getInstance(client.world).update(delta)); + }); } } diff --git a/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java b/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java index 25bd849..06decf6 100644 --- a/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java +++ b/src/main/java/io/github/foundationgames/phonos/network/PayloadPackets.java @@ -24,4 +24,10 @@ public static void sendSoundStop(ServerPlayerEntity player, long sourceId) { buf.writeLong(sourceId); ServerPlayNetworking.send(player, Phonos.id("sound_stop"), buf); } + + public static void sendSoundUpdate(ServerPlayerEntity player, SoundEmitterTree.Delta delta) { + var buf = new PacketByteBuf(Unpooled.buffer()); + SoundEmitterTree.Delta.toPacket(buf, delta); + ServerPlayNetworking.send(player, Phonos.id("sound_update"), buf); + } } diff --git a/src/main/java/io/github/foundationgames/phonos/radio/RadioDevice.java b/src/main/java/io/github/foundationgames/phonos/radio/RadioDevice.java new file mode 100644 index 0000000..441dd22 --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/radio/RadioDevice.java @@ -0,0 +1,25 @@ +package io.github.foundationgames.phonos.radio; + +import io.github.foundationgames.phonos.sound.emitter.SoundEmitter; +import io.github.foundationgames.phonos.util.UniqueId; + +import java.util.function.LongConsumer; + +public interface RadioDevice { + int getChannel(); + + interface Transmitter extends RadioDevice, SoundEmitter { + @Override + default void forEachChild(LongConsumer action) { + action.accept(UniqueId.ofRadioChannel(getChannel())); + } + } + + interface Receiver extends RadioDevice { + void setAndUpdateChannel(int channel); + + void addReceiver(); + + void removeReceiver(); + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/radio/RadioStorage.java b/src/main/java/io/github/foundationgames/phonos/radio/RadioStorage.java new file mode 100644 index 0000000..834f7df --- /dev/null +++ b/src/main/java/io/github/foundationgames/phonos/radio/RadioStorage.java @@ -0,0 +1,145 @@ +package io.github.foundationgames.phonos.radio; + +import io.github.foundationgames.phonos.sound.emitter.SoundEmitter; +import io.github.foundationgames.phonos.sound.emitter.SoundEmitterStorage; +import io.github.foundationgames.phonos.sound.emitter.SoundSource; +import io.github.foundationgames.phonos.util.UniqueId; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.World; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.LongConsumer; + +public class RadioStorage { + public static final int CHANNEL_COUNT = 30; + public static final LongList RADIO_EMITTERS = new LongArrayList(); + + private static RadioStorage CLIENT; + private static final Map, RadioStorage> SERVER = new HashMap<>(); + + private static final RadioStorage INVALID = new RadioStorage() {}; + + private final Channel[] channels; + + public RadioStorage() { + channels = new Channel[CHANNEL_COUNT]; + for (int i = 0; i < CHANNEL_COUNT; i++) { + channels[i] = new Channel(i, new LongArrayList(), new ArrayList<>()); + } + } + + public LongList getReceivingEmitters(int channel) { + return channels[channel].receivingEmitters(); + } + + public void addReceivingEmitter(int channel, E receiver) { + var list = getReceivingEmitters(channel); + long id = receiver.emitterId(); + + if (!list.contains(id)) { + list.add(id); + } + } + + public void removeReceivingEmitter(int channel, long emitterId) { + getReceivingEmitters(channel).rem(emitterId); + } + + public List getReceivingSources(int channel) { + return channels[channel].receivingSources(); + } + + public void addReceivingSource(int channel, SoundSource receiver) { + var list = getReceivingSources(channel); + + if (!list.contains(receiver)) { + list.add(receiver); + } + } + + public void removeReceivingSource(int channel, SoundSource receiver) { + getReceivingSources(channel).remove(receiver); + } + + public static RadioStorage getInstance(World world) { + if (world.isClient()) { + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) { + if (CLIENT == null) { + CLIENT = new RadioStorage(); + } + + return CLIENT; + } + } + + if (world instanceof ServerWorld sWorld) { + return SERVER.computeIfAbsent(sWorld.getRegistryKey(), w -> new RadioStorage()); + } + + return INVALID; + } + + public static void serverReset() { + SERVER.clear(); + } + + public static void clientReset() { + CLIENT = null; + } + + public record Channel(int number, LongList receivingEmitters, List receivingSources) {} + + public static class RadioEmitter implements SoundEmitter { + public final int channel; + + private final World world; + private final long emitterId; + + public RadioEmitter(World world, int channel) { + this.world = world; + this.channel = channel; + this.emitterId = UniqueId.ofRadioChannel(channel); + } + + @Override + public long emitterId() { + return this.emitterId; + } + + @Override + public void forEachSource(Consumer action) { + var radio = RadioStorage.getInstance(world); + + for (var source : radio.getReceivingSources(this.channel)) { + action.accept(source); + } + } + + @Override + public void forEachChild(LongConsumer action) { + var emitters = SoundEmitterStorage.getInstance(world); + var radio = RadioStorage.getInstance(world); + + for (long rec : radio.getReceivingEmitters(this.channel)) if (emitters.isLoaded(rec)) { + action.accept(rec); + } + } + } + + public static void init() { + for (int i = 0; i < CHANNEL_COUNT; i++) { + final int channel = i; + SoundEmitterStorage.DEFAULT_EMITTERS.add(w -> new RadioEmitter(w, channel)); + RADIO_EMITTERS.add(UniqueId.ofRadioChannel(channel)); + } + } +} diff --git a/src/main/java/io/github/foundationgames/phonos/sound/ClientSoundStorage.java b/src/main/java/io/github/foundationgames/phonos/sound/ClientSoundStorage.java index 4bed39c..7eb2cbd 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/ClientSoundStorage.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/ClientSoundStorage.java @@ -32,6 +32,8 @@ public static void registerProvider(SoundData.Type type @Override public void play(World world, SoundData data, SoundEmitterTree tree) { + tree.updateClient(world); + var inst = provideSound(data, tree, world.getRandom()); MinecraftClient.getInstance().getSoundManager().play(inst); @@ -56,6 +58,15 @@ public void stop(World world, long soundUniqueId) { activeEmitterTrees.removeIf(tree -> tree.rootId == soundUniqueId); } + @Override + public void update(SoundEmitterTree.Delta delta) { + for (var tree : activeEmitterTrees) { + if (tree.rootId == delta.rootId()) { + delta.apply(tree); + } + } + } + @Override public void tick(World world) { this.playingSounds.long2ObjectEntrySet().removeIf(e -> { @@ -67,7 +78,7 @@ public void tick(World world) { return false; }); - this.activeEmitterTrees.forEach(tree -> tree.update(world)); + this.activeEmitterTrees.forEach(tree -> tree.updateClient(world)); } public interface SoundInstanceFactory { diff --git a/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java b/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java index 6b4df6d..1516236 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/MultiSourceSoundInstance.java @@ -24,6 +24,8 @@ public MultiSourceSoundInstance(SoundEmitterTree tree, SoundEvent sound, Random this.emitters = new AtomicReference<>(tree); this.volume = volume; this.pitch = pitch; + + this.updatePosition(); } @Override diff --git a/src/main/java/io/github/foundationgames/phonos/sound/ServerSoundStorage.java b/src/main/java/io/github/foundationgames/phonos/sound/ServerSoundStorage.java index b800588..3f5050b 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/ServerSoundStorage.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/ServerSoundStorage.java @@ -9,7 +9,7 @@ public class ServerSoundStorage extends SoundStorage { @Override public void play(World world, SoundData data, SoundEmitterTree tree) { - tree.update(world); + tree.updateServer(world); if (world instanceof ServerWorld sWorld) for (var player : sWorld.getPlayers()) { PayloadPackets.sendSoundPlay(player, data, tree); @@ -25,6 +25,10 @@ public void stop(World world, long soundUniqueId) { } } + @Override + public void update(SoundEmitterTree.Delta delta) { + } + @Override public void tick(World world) { } diff --git a/src/main/java/io/github/foundationgames/phonos/sound/SoundStorage.java b/src/main/java/io/github/foundationgames/phonos/sound/SoundStorage.java index c025782..2251b01 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/SoundStorage.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/SoundStorage.java @@ -5,6 +5,7 @@ import io.github.foundationgames.phonos.world.sound.data.SoundData; import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.registry.RegistryKey; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.World; @@ -13,7 +14,7 @@ public abstract class SoundStorage { private static SoundStorage CLIENT; - private static final Map SERVER = new HashMap<>(); + private static final Map, SoundStorage> SERVER = new HashMap<>(); private static final SoundStorage INVALID = new SoundStorage() { @Override public void play(World world, SoundData data, SoundEmitterTree tree) { @@ -25,6 +26,11 @@ public void stop(World world, long emitterId) { Phonos.LOG.error("Stopping " + Long.toHexString(emitterId) + " in an invalid world"); } + @Override + public void update(SoundEmitterTree.Delta delta) { + Phonos.LOG.error("Updating " + Long.toHexString(delta.rootId()) + " in an invalid world"); + } + @Override public void tick(World world) { } @@ -38,7 +44,9 @@ protected void notifySoundSourcesPlayed(World world, SoundData data, SoundEmitte public abstract void play(World world, SoundData data, SoundEmitterTree tree); - public abstract void stop(World world, long soundUniqueId); + public abstract void stop(World world, long emitterId); + + public abstract void update(SoundEmitterTree.Delta delta); public abstract void tick(World world); @@ -54,7 +62,7 @@ public static SoundStorage getInstance(World world) { } if (world instanceof ServerWorld sWorld) { - return SERVER.computeIfAbsent(sWorld, w -> new ServerSoundStorage()); + return SERVER.computeIfAbsent(sWorld.getRegistryKey(), w -> new ServerSoundStorage()); } return INVALID; diff --git a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterStorage.java b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterStorage.java index 1606c00..7e3481a 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterStorage.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterStorage.java @@ -5,16 +5,23 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.registry.RegistryKey; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.World; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.function.Function; public class SoundEmitterStorage { private static SoundEmitterStorage CLIENT; - private static final Map SERVER = new HashMap<>(); - private static final SoundEmitterStorage INVALID = new SoundEmitterStorage() { + private static final Map, SoundEmitterStorage> SERVER = new HashMap<>(); + + public static final List> DEFAULT_EMITTERS = new ArrayList<>(); + + private static final SoundEmitterStorage INVALID = new SoundEmitterStorage(null) { @Override public boolean isLoaded(long uniqueId) { Phonos.LOG.error("Tried to query emitter " + Long.toHexString(uniqueId) + " in invalid world"); @@ -40,6 +47,12 @@ public void removeEmitter(long uniqueId) { private final Long2ObjectMap emitters = new Long2ObjectOpenHashMap<>(); + protected SoundEmitterStorage(World world) { + if (world != null) for (var factory : DEFAULT_EMITTERS) { + this.addEmitter(factory.apply(world)); + } + } + public boolean isLoaded(long uniqueId) { return emitters.containsKey(uniqueId); } @@ -64,7 +77,7 @@ public static SoundEmitterStorage getInstance(World world) { if (world.isClient()) { if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) { if (CLIENT == null) { - CLIENT = new SoundEmitterStorage(); + CLIENT = new SoundEmitterStorage(world); } return CLIENT; @@ -72,7 +85,7 @@ public static SoundEmitterStorage getInstance(World world) { } if (world instanceof ServerWorld sWorld) { - return SERVER.computeIfAbsent(sWorld, w -> new SoundEmitterStorage()); + return SERVER.computeIfAbsent(sWorld.getRegistryKey(), w -> new SoundEmitterStorage(world)); } return INVALID; diff --git a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java index 8096198..61666da 100644 --- a/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java +++ b/src/main/java/io/github/foundationgames/phonos/sound/emitter/SoundEmitterTree.java @@ -1,6 +1,10 @@ package io.github.foundationgames.phonos.sound.emitter; +import io.github.foundationgames.phonos.radio.RadioStorage; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; import net.minecraft.network.PacketByteBuf; import net.minecraft.world.World; @@ -34,7 +38,61 @@ public boolean contains(long value, int upUntil) { return false; } - public void update(World world) { + // Updates the tree on the server and provides a list of changes to be sent to the client + // More accurate than the updateClient() method, as far as what the server is aware of (loaded chunks) + public Delta updateServer(World world) { + var emitters = SoundEmitterStorage.getInstance(world); + var delta = new Delta(this.rootId, new Int2ObjectOpenHashMap<>()); + + int index = 0; + + while (index < this.levels.size() && !this.levels.get(index).empty()) { + if (index + 1 == this.levels.size()) { + this.levels.add(new Level(new LongArrayList(), new LongArrayList())); + } + + var level = this.levels.get(index); + var nextLevel = this.levels.get(index + 1); + + var nextLevelChanges = new ChangeList(new LongArrayList(), new LongArrayList(nextLevel.active())); + + for (long emId : level.active()) { + if (emitters.isLoaded(emId)) { + var emitter = emitters.getEmitter(emId); + + final int searchUntil = index; + emitter.forEachChild(child -> { + if (this.contains(child, searchUntil)) { + return; + } + + nextLevelChanges.remove().rem(child); + + if (!nextLevel.active().contains(child)) { + nextLevelChanges.add().add(child); + } + }); + } + } + + if (!nextLevelChanges.add().isEmpty() || !nextLevelChanges.remove().isEmpty()) { + nextLevelChanges.apply(nextLevel); + delta.deltas().put(index + 1, new Level( + new LongArrayList(nextLevel.active()), + new LongArrayList(nextLevel.inactive()) + )); + } + + index++; + } + + return delta; + } + + // Updates the tree on the client side + // Not entirely accurate, but enough so that most modifications of sound networks will have + // immediate effects regardless of server speed/latency + public void updateClient(World world) { var emitters = SoundEmitterStorage.getInstance(world); int index = 0; @@ -49,6 +107,10 @@ public void update(World world) { nextLevel.inactive().addAll(nextLevel.active()); nextLevel.active().clear(); + for (long l : nextLevel.inactive()) if (RadioStorage.RADIO_EMITTERS.contains(l)) { + nextLevel.active().add(l); + } + nextLevel.inactive().removeAll(nextLevel.active()); for (long emId : level.active()) { if (emitters.isLoaded(emId)) { @@ -71,6 +133,12 @@ public void update(World world) { index++; } + + if (index < this.levels.size() && this.levels.get(index).empty()) { + while (index < this.levels.size()) { + this.levels.remove(this.levels.size() - 1); + } + } } public void forEachSource(World world, Consumer action) { @@ -85,7 +153,7 @@ public void forEachSource(World world, Consumer action) { } } - public record Level(LongArrayList active, LongArrayList inactive){ + public record Level(LongArrayList active, LongArrayList inactive) { public boolean empty() { return active.isEmpty() && inactive.isEmpty(); } @@ -111,4 +179,59 @@ public void toPacket(PacketByteBuf buf) { public static SoundEmitterTree fromPacket(PacketByteBuf buf) { return new SoundEmitterTree(buf.readLong(), buf.readCollection(ArrayList::new, Level::fromPacket)); } + + public record Delta(long rootId, Int2ObjectMap deltas) { + public static void toPacket(PacketByteBuf buf, Delta delta) { + buf.writeLong(delta.rootId); + buf.writeMap(delta.deltas, PacketByteBuf::writeInt, Level::toPacket); + } + + public static Delta fromPacket(PacketByteBuf buf) { + var id = buf.readLong(); + var deltas = buf.readMap(Int2ObjectOpenHashMap::new, PacketByteBuf::readInt, Level::fromPacket); + + return new Delta(id, deltas); + } + + public boolean hasChanges() { + return this.deltas.size() > 0; + } + + public void apply(SoundEmitterTree tree) { + for (var entry : this.deltas().int2ObjectEntrySet()) { + int idx = entry.getIntKey(); + if (idx < tree.levels.size()) { + tree.levels.set(idx, entry.getValue()); + } else { + tree.levels.add(idx, entry.getValue()); + } + } + } + } + + public record ChangeList(LongList add, LongList remove) { + public static void toPacket(PacketByteBuf buf, ChangeList level) { + buf.writeCollection(level.add, PacketByteBuf::writeLong); + buf.writeCollection(level.remove, PacketByteBuf::writeLong); + } + + public static ChangeList fromPacket(PacketByteBuf buf) { + var add = buf.readCollection(LongArrayList::new, PacketByteBuf::readLong); + var rem = buf.readCollection(LongArrayList::new, PacketByteBuf::readLong); + + return new ChangeList(add, rem); + } + + public void apply(Level level) { + level.active().removeAll(this.remove); + level.inactive().removeAll(this.remove); + + for (long l : this.add) { + if (!level.active().contains(l)) { + level.active().add(l); + } + level.inactive().rem(l); + } + } + } } diff --git a/src/main/java/io/github/foundationgames/phonos/util/UniqueId.java b/src/main/java/io/github/foundationgames/phonos/util/UniqueId.java index 6d9033e..2f2d8c8 100644 --- a/src/main/java/io/github/foundationgames/phonos/util/UniqueId.java +++ b/src/main/java/io/github/foundationgames/phonos/util/UniqueId.java @@ -12,4 +12,8 @@ public static long random() { public static long ofBlock(BlockPos pos) { return new Random(pos.asLong() + 0xABCDEF).nextLong(); } + + public static long ofRadioChannel(int channel) { + return new Random(channel + 0xFADECAB).nextLong(); + } } diff --git a/src/main/resources/assets/phonos/blockstates/radio_loudspeaker.json b/src/main/resources/assets/phonos/blockstates/radio_loudspeaker.json new file mode 100644 index 0000000..bcb9157 --- /dev/null +++ b/src/main/resources/assets/phonos/blockstates/radio_loudspeaker.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=north": {"model": "phonos:block/radio_loudspeaker"}, + "facing=south": {"model": "phonos:block/radio_loudspeaker", "y": 180}, + "facing=east": {"model": "phonos:block/radio_loudspeaker", "y": 90}, + "facing=west": {"model": "phonos:block/radio_loudspeaker", "y": 270} + } +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/blockstates/radio_transceiver.json b/src/main/resources/assets/phonos/blockstates/radio_transceiver.json new file mode 100644 index 0000000..abb59c2 --- /dev/null +++ b/src/main/resources/assets/phonos/blockstates/radio_transceiver.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=east": {"model": "phonos:block/radio_transceiver", "y": 90}, + "facing=north": {"model": "phonos:block/radio_transceiver"}, + "facing=south": {"model": "phonos:block/radio_transceiver", "y": 180}, + "facing=west": {"model": "phonos:block/radio_transceiver", "y": 270} + } +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/lang/en_us.json b/src/main/resources/assets/phonos/lang/en_us.json index 80dde47..634ebdc 100644 --- a/src/main/resources/assets/phonos/lang/en_us.json +++ b/src/main/resources/assets/phonos/lang/en_us.json @@ -5,6 +5,8 @@ "block.phonos.electronic_jukebox": "Electronic Jukebox", "block.phonos.electronic_note_block": "Electronic Note Block", "block.phonos.connection_hub": "Connection Hub", + "block.phonos.radio_transceiver": "Radio Transceiver", + "block.phonos.radio_loudspeaker": "Radio Loudspeaker", "item.phonos.audio_cable": "Audio Cable", "item.phonos.black_audio_cable": "Black Audio Cable", diff --git a/src/main/resources/assets/phonos/models/block/radio_loudspeaker.json b/src/main/resources/assets/phonos/models/block/radio_loudspeaker.json new file mode 100644 index 0000000..12dce80 --- /dev/null +++ b/src/main/resources/assets/phonos/models/block/radio_loudspeaker.json @@ -0,0 +1,62 @@ +{ + "parent": "block/block", + "textures": { + "0": "phonos:block/radio_antenna", + "1": "phonos:block/loudspeaker", + "2": "phonos:block/loudspeaker_east", + "3": "phonos:block/loudspeaker_west", + "4": "phonos:block/stone_base", + "5": "phonos:block/radio_loudspeaker_back", + "6": "phonos:block/radio_loudspeaker_top", + "particle": "phonos:block/radio_antenna" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": {"uv": [0, 0, 16, 16], "texture": "#1", "cullface": "north"}, + "east": {"uv": [0, 0, 16, 16], "texture": "#2", "cullface": "east"}, + "south": {"uv": [0, 0, 16, 16], "texture": "#5", "cullface": "south"}, + "west": {"uv": [0, 0, 16, 16], "texture": "#3", "cullface": "west"}, + "up": {"uv": [0, 0, 16, 16], "texture": "#6", "cullface": "up"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#4", "cullface": "down"} + } + }, + { + "from": [7, 7.99, 8], + "to": [9, 16.99, 8], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 7, 8]}, + "faces": { + "north": {"uv": [7, 1, 9, 10], "texture": "#0"}, + "east": {"uv": [0, 0, 0, 0], "texture": "#0"}, + "south": {"uv": [7, 1, 9, 10], "texture": "#0"}, + "west": {"uv": [0, 0, 0, 0], "texture": "#0"}, + "up": {"uv": [0, 0, 0, 0], "texture": "#0"}, + "down": {"uv": [0, 0, 0, 0], "texture": "#0"} + } + }, + { + "from": [0, 16.99, -0.5], + "to": [16, 16.99, 15.5], + "rotation": {"angle": 22.5, "axis": "y", "origin": [8, 7, 8]}, + "faces": { + "north": {"uv": [0, 0, 0, 0], "texture": "#0", "cullface": "up"}, + "east": {"uv": [0, 0, 0, 0], "texture": "#0", "cullface": "up"}, + "south": {"uv": [0, 0, 0, 0], "texture": "#0", "cullface": "up"}, + "west": {"uv": [0, 0, 0, 0], "texture": "#0", "cullface": "up"}, + "up": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#0", "cullface": "up"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#0", "cullface": "up"} + } + }, + { + "from": [8, 7.99, 7], + "to": [8, 16.99, 9], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 7, 8]}, + "faces": { + "east": {"uv": [7, 1, 9, 10], "texture": "#0", "cullface": "up"}, + "west": {"uv": [7, 1, 9, 10], "texture": "#0", "cullface": "up"} + } + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/models/block/radio_transceiver.json b/src/main/resources/assets/phonos/models/block/radio_transceiver.json new file mode 100644 index 0000000..7cf6026 --- /dev/null +++ b/src/main/resources/assets/phonos/models/block/radio_transceiver.json @@ -0,0 +1,63 @@ +{ + "parent": "block/block", + "textures": { + "0": "phonos:block/radio_antenna", + "1": "phonos:block/radio_transceiver_front", + "2": "phonos:block/radio_transceiver_side", + "3": "phonos:block/radio_transceiver_top", + "4": "phonos:block/stone_base", + "particle": "phonos:block/radio_transceiver_top" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 7, 16], + "faces": { + "north": {"uv": [0, 9, 16, 16], "texture": "#1"}, + "east": {"uv": [0, 9, 16, 16], "texture": "#2"}, + "south": {"uv": [0, 9, 16, 16], "texture": "#2"}, + "west": {"uv": [0, 9, 16, 16], "texture": "#2"}, + "up": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#3"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#4"} + } + }, + { + "from": [8, 6.99, 10], + "to": [8, 15.99, 12], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 6, 11]}, + "faces": { + "east": {"uv": [7, 1, 9, 10], "texture": "#0"}, + "west": {"uv": [7, 1, 9, 10], "texture": "#0"} + } + }, + { + "from": [7, 6.99, 11], + "to": [9, 15.99, 11], + "rotation": {"angle": 45, "axis": "y", "origin": [8, 6, 11]}, + "faces": { + "north": {"uv": [7, 1, 9, 10], "texture": "#0"}, + "south": {"uv": [7, 1, 9, 10], "texture": "#0"} + } + }, + { + "from": [0, 15.99, 2.5], + "to": [16, 15.99, 18.5], + "rotation": {"angle": 22.5, "axis": "y", "origin": [8, 6, 11]}, + "faces": { + "up": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#0"}, + "down": {"uv": [0, 0, 16, 16], "texture": "#0"} + } + }, + { + "from": [6, 7, 9], + "to": [10, 9, 13], + "faces": { + "north": {"uv": [6, 14, 10, 16], "rotation": 180, "texture": "#1"}, + "east": {"uv": [10, 13, 6, 15], "texture": "#1"}, + "south": {"uv": [6, 13, 10, 15], "texture": "#1"}, + "west": {"uv": [10, 14, 6, 16], "rotation": 180, "texture": "#1"}, + "up": {"uv": [6, 3, 10, 7], "rotation": 180, "texture": "#3"} + } + } + ] +} \ No newline at end of file diff --git a/src/main/resources/assets/phonos/models/item/radio_loudspeaker.json b/src/main/resources/assets/phonos/models/item/radio_loudspeaker.json new file mode 100644 index 0000000..baf7759 --- /dev/null +++ b/src/main/resources/assets/phonos/models/item/radio_loudspeaker.json @@ -0,0 +1 @@ +{ "parent": "phonos:block/radio_loudspeaker" } \ No newline at end of file diff --git a/src/main/resources/assets/phonos/models/item/radio_transceiver.json b/src/main/resources/assets/phonos/models/item/radio_transceiver.json new file mode 100644 index 0000000..9c14892 --- /dev/null +++ b/src/main/resources/assets/phonos/models/item/radio_transceiver.json @@ -0,0 +1 @@ +{ "parent": "phonos:block/radio_transceiver" } \ No newline at end of file diff --git a/src/main/resources/assets/phonos/textures/block/loudspeaker_back.png b/src/main/resources/assets/phonos/textures/block/loudspeaker_back.png index ad9c967bd9ea11949135270e36cd39974668413f..f44936cf52ee488701b00a9300139017dbe0ed46 100644 GIT binary patch delta 2163 zcmV-(2#ojnFs3n(Bmu#ZB_My2BPk61=M-}UB(WZcHKK1Y$DhZRwzKT+s+q|QCqf7z z^dx~*`}4E+-@pe= zyC3%=bmWhsYo!FrJ_b_S{QSz#TOoFHQ?xA*S}W_iCu~K++ak02Wm|vbV(>3pt7Wfj zHY+@SMm#&~$QyhOYwukI9atL=f7W4WFC3>LyBvBkPXCB9Y~m&)8eb8!td#K+NoYO3 z=&K=!H;I98!jGrpHK2qEur%(4lD?ez?3;&=K0M#qmc}Bz4dEx$9s%+i9~VU*_&W?P z)Pmas@(QbVSNoPv_9%Zk19Gjy&nt&=t^C5bZg}wVsAsr)uc6OPV;9Ld_`>Z?ovlm? za(Q@h&T@LFbP7=$K#Vuy4La$9n5w!U(=zBt4D34@s+6cxn{A;26Ad@^Xqk&*nalu? zA{WF3ArSCVXD?Bz1O!J}F(iE1LUY+Tmn#%>Rk{*HnV^hjME!ruSAAEo_mN9PI$XYj zT|C1VTmt-~ffx{>zMX2PgU@?^2Cf1G?R0a%29wTesmPWd!6g%DfuKdZvYve13g99v zZD9-v5YUdql7mH_*%QH$0RSbK&SE$N0kW1WjASF?01mPPYh#Y5Daw`?8&y?|5-ScF z9Dq;)SjJeyk2ik-F_f%WW36@8+hC(jPTa^m=e!%`IeGEMTkpL0LHX!YkYIuhF8B~a z3@LIHly4i6r!mABQ^KMNniFOx2&9xbOE%eNmwgU7s+89K6x>r5)*wcmDhV_5T`vcTyqs9}dA?PpE!1ey9 zpixfjzzm47Apq`y8W2NZ56pBRV|XApFvEF-tgs9t?Et6JKnw_)Mr=D>*lpw#y!gN1 zrhJ&V@qa~54d}j!+*jP*p;pH!Q6^$54Tn>;Ap1Jk#36CklJp=#(qCKkY2XiSNRx2d zWMj!##&Lf%mkS?Tv2M;Ork3iLZGaOAIoYU)Q&J64X_J@bn$5Y=SgGjBXD-gU`x=jl zq8^H$*}S-@+@YkU$vu{lR2DCqRzxOKr5cK9oX$ku>fTDJRIL*doyis^x^89436Ktedm?La)eXf=Gu7TGOf7OXt%#mH8)jcP-^@&*n_7f>;B-aY+oVM)~dS^^`q8&0Qc^qbU&0zvf{o;Y~ymc z3MYqJb_61oOgl9Ii8a}gjB6|HG!7p2MvZ?S&M0;(9Z?og3U{qDS6vwYFLnupP+sX} z#^qGXGDNk8PIH&*YOpU(oV>mkZ+4_{;nk}J7vYxcN^6C`T`A!HI=e360M?849#ece zI^4|vy`6+3^QKR2g+J2QczN4r#vfu=5ByKtX-*CdNk7evJI!zl+@42u)McMA7KeX@ z+Hga0TwX(!pkR+>X6G`l=2gzYgbVww<)(xkQ;I1jzowx``bgW3f(^tJuh6+FVyyKw z(pIa?_;dLCvzCrJxo5I_=u(U5_c^Xy*xN&eGiXj^8-JyW+MIYxp3|C>1BO0HEM@AA z66fDU=iM{Jhgfl|-X#AQhSU!dMAtNkx{1USNA8pX3iI4w0}Vqz^cH#ambVq#`B zEjT$iV_`8hV>2>1W|NQ#sSP3|UF*43l{lD1YQh zL_t(I%Z-seZxcZfhMzm%`p#=zWM5(-3&jag#S|z7qHvQ2iI#uDPe7vLS5VQ=AtX8^ zXaFSwlqeF74F?j~vJ>YHn~gX3VG46c4kF>L=6!Z|=A9X%)83r3m zaAmDV({%voPi6optyy;~l4A&vk!3`0}w?KQ55AmyF35R zG|feV!C;VAS4tJ!NGS{aymnppkAr)iORUybxY}&6_p$fi_|C&;IF5sKmG$;$_d5WkQmHV5BuR3XBuU|o z5MnWdt*tGD5QT|n$F^;>*8IIKfFKC4q?8zj@z34?wAO{)XNj^xN+~Sg_Zbd{2q7-C pnK2;*Ns?fiCTW^t7zSyY@(W0tDD?%Zi4y<-002ovPDHLkV1i}M`O5$R delta 2117 zcmV-L2)g&CG5Ii%BmuRNB_MyYB`FB~cNM(^!3me+lo7pyUVb0PyhCMG^^h6wN+Kd~ z0DY?c_2;Gjz{BjUvMz{^-fKKI+Q=l{EWQ#4SdkF zzT?`2w)|Ffc1ob!J(1Gl=XZWy3RylCEej0IopoIcmLkK;B0Im!vdDi&32&~|vUawM zCHJ2-fxYt-EPgHP5PSqJtZhI%Yd7>N9ETz|19}+ydy6t`;%3Nb{2F7Ym8Sn0lQ4$y zOA3NCSZ=VKkr8+eC}9FDjccQ%PcUC&bI0i6`5m`3Ho00CenIU{koWvJ6g}`)8XVNb zuL<)GtM+I7A)nk@4X5EnvL3PU%cEd9bsYZ}7W#*9KV+)OK

UjmsYkgA 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 Tui$3n=?(B^*w6dbH6 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/block/radio_loudspeaker_back.png b/src/main/resources/assets/phonos/textures/block/radio_loudspeaker_back.png new file mode 100644 index 0000000000000000000000000000000000000000..a61e08a4fb772d01d5b4c4cf2c412f357ce3d737 GIT binary patch literal 6127 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 gf4nX;Z&8fOxq@qw+OsoWLp6pm>|N=F%h$&J8k literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/block/radio_loudspeaker_top.png b/src/main/resources/assets/phonos/textures/block/radio_loudspeaker_top.png new file mode 100644 index 0000000000000000000000000000000000000000..05d4b288ce9b4601fcc0894104b0221392417c0e GIT binary patch literal 6427 zcmeHKc{tST+n;2~SaO8+)0mKL_8GG@w(M#gMF>-7`3{rW%nZg-k+S3@TNEl$IteKv z+o5PxN!n1d98ua6Cpvn+L+kaP>w2%=>;1idHFJGuzR&&K_vg9q&wW2zlDnIe26Pb= z0)c3_INPrS|Fz`L9A)r#+1rl{2t-*Y($hz}ju8$MiG>_44}eJ{L;wtsaXAo(tgFU@Gv{b^+4 zo}S=nUS*oLQNg_xZJP?8e|R`|BB%cHod{bkjN`^VX;t;MDK(vKAuBQ2Q^2sybzjY- zgR9@cO@k4O4c-R@IW=)2?uMf7$L|ZX53|f(nzl!KW0hCy7_Zv-g>`Fe{aYiS55Bkb z|4yt)t(x_|WckgF#%^{3!_hfqL#y{{%_CTpyl&-rXHuv3&AS-0iE8HCkKd$`wm<1! zjJ7`P*4d?oThiZOyMCY$Wo6x&6`#=)|7P}?T-eDEB;4Db(H*_{Mq3+uy?xGD5BSxD zWFH<;T4p^uYCb62nl}5{t2H|#&p(XXYen2mHyBA%Yb1v9fgAbRPI^P8dxSSP#-7=J z&f5*)v1ON-jE8GRUf{luadm1AwQRNh;7aBTx11IjEa#B*$a)q?|s!S=G z^}36nl}K*X^|m-ONuW!3qH_sa_B$7(m-~eD8zwqu>X=}=s+0fdFveWW z?8ToGpMR>jGZPEku&8?`xtv~j^>~cc1wHR*RR>q|-js0n_L|vybNFTN z^>QljuT!~03Gb$lTmFHx9Fw|tmv5bzQXo{UElj%YuC79GwmZ9KU%T6bCiTQL0)z@7 zz?7MDuT?Gx=E8{R=7=4|q8+e@kYhQq3$!Y)r;a=hs7~{^-2ZWVE)*M5_;FX>qMQF{ z)wG>$uq!R<9FxKES49Tvam6P+Xa-5x8;dNB(np8ZZh6ZO>y$TaF)(H?1~ljrQ^=Ede3e&q-?4pJItTn* zeX~A1kN4PcI>?W4aMj*ooo-{gTIh)=v!hv?F5hXJ-$J;TWLvyYsZ#)4D&n1pyY<$@ zWnkXcp1x$MpXf-*G2Y6(^EYk1z8-%2rv1(R3z}U$TG}(bHfLP-R&;TP*mdM5){jhX zIdHDHCMBiMyAE-ZY+AS5E@ah{&8yl==98+|rn8DfNUxBwRTs;7nIfn-Ol8u^r!8Pl z)x?2`v*~<`G@VXp67GbD<5(9xnJNuaRcs3mXPcHC3bpj{*wxV;Xj0=x_{+kmqr9=A z*R_dIs~gxt9_yneR%$2s@e`lKHX)B0q~RVarWVc54_Iv?Bo)}C>kU71`SlfDXG#9W z27bbuph#={#prI&$_ENSZ)0HAqhP>SNL8Y9&AsH7w zQW(6sI?}eqP@zF^%sNLjD|5U(PIJp;j|n4cmup1kUkW}ARj)eFJ*Qn-`smV*#v=u9 z?D^7owUG1W`}W(k|Cy=k-ZNQCoa{=8Kb++AG`D5+?o-oi)uA>0^B;VmQv##96L&{; z9DR--9Dn}q%)CDyLbv8mxV+WB#u?|uooK7Gn+lo7UA!)@cChSm^ZGO5Dd6hMsOH?% zwucU3?fnm;Jr4`Lb>7fY+q$zSGDf=UUY8yw#zwC!K4dy|S8HJF;yaCZR_@+=ODiE7 z+$J{t>;AwMNm=s!!P2EwBgS35HXP~{q}Dz}8Kc)nh(0FLbQJnnL!sFSjUooRMnbu; z3{yxwWuj?NubW^`+|6W#p%P^ZCXw`>kFgxhfbf5NUrcL ztr)X>uV$yjH*5~|WjP0^x*UP6a$KtG$s5{+<>al^21D=3UAW^*X( z>>WQrfGtaSkW?z7pitrA;mB|tQYa2YVaQ}M3XMf!u?SEDA&C%388U=GvRn=^gJBOy zSYoh}xk3R%vOL#38*IM@&S>>po5qkVxFNItOu@_~{uL?{drjpFlB-&#nd4q+h3 zCx`ykLgEP?u~6#(i7-^m0vy5sfpqz|5Ny^Ldr_#EHysX}g#vg0A5@iqqhh`r(#eJ9 z{>4I0K_HhenzjPT{!UZM<$NXUJKy9z)8TyU2x$HV_dD&+u}>?5S~MEPUdRH^LISCa zy(L^eK7}o0aoLpVS3Hr41{fqF!h(Y)An<4$13^NQFbF)0%pkI`SO%WS`3A~GAdxZz zEI4cnk(X!r|BmCYy=naM)xE4ihsC#b!|)g=C&q$ACZkCNJeG{Z zp!JPeL%6BnASQ zPuO@Yn@DDp@HmT^q5Mg6yQ&nSGef`YWn$iS5u zTR{BqTfxiAHKcC9%KRE;p_H&RQ;`b+AKk51*27XBSXLbFg>xUTlA?2Ud^?ybe z^y^&_5P%1~aPTIWqE257-XfKmtDWp2ALZZthGQ9^L{;RxK>~pwm&>17vZK~vpio8X zLUT|VQqi5Qpm+CfMky#-D0T3W+6npcD+gry91O5w;aq7DOnwgbj@6tGfhZ`u*xP!N zS`TES}+a7DgE8cTh9WWNw(+^& zwG~}4hN(6oIr@Xakof~^E|jCUgukpAKPY)2Y12&^&oGLMEJ zX`1Xc`wPeQB->kM^b8lCA{8$w*uv;i=Sb9CblTs&6dWDe^A@s%8e#9O{stYPXmecs zpN$DY=5BEK+df_ls(1YG75Mu#iyGPu4!O+Jj;psK)0cl7JxC`nO790CL%#rTKK>S{tyJA&h$Q#nzj?k4$V;L)a%(E($m1~|I%>7Hu!{qw;WDWi~K6u}k z)q8p1^_(M~7p|8Ob9k%A%74Ex;vf3-+T*Onk_DhRe)wHvJ3B4NT=RZ%4*c~q<8$v% zE==>AWe}Az#drm=)p~`{Q>%TvV{?Nevd*dTULVl#Sny)oZQpm>*3}h^Ufr&CW%!EEJ5+3 Yf}+tjO5E@QuxubM4sP~ks{*$F7Zif~asU7T literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/block/radio_transceiver_front.png b/src/main/resources/assets/phonos/textures/block/radio_transceiver_front.png new file mode 100644 index 0000000000000000000000000000000000000000..c43b17bc3123227b243f1e4df6010e18a2f37e24 GIT binary patch literal 6166 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 Yq42weimT3xVAc?9mL0QTt!Ko40pd=9(*OVf literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/block/radio_transceiver_side.png b/src/main/resources/assets/phonos/textures/block/radio_transceiver_side.png new file mode 100644 index 0000000000000000000000000000000000000000..cd7f5dc4020013a0afc3d3c8e38e9dca64db3c9c GIT binary patch literal 6089 zcmeHLcT`hZw+|v!L2v*?P+|ZDB|RYp0xBRxClm=q0VU-IBBYR9AQTm`e1gItDt3_S zNV730!hk*qVnIYukzQ0pL@W&8i1OWlb-h{ZE#G?I`^#h{xp$wve|zuW*=OH#lHl&@ ztfjd?69$86(On$ZLBBeRkGd-KZSr=K1%s(%Ml*fEb*xCZSR&%_LjgD#B?jPtjL(C? zWUWO$eyOHqnjao6<7vzoVN;@knit)uZOMzyfMxkvSKgQ6L-(sI^}I4K zhWEY2s%Ty7?Q6CS(z#LWcC?DyG0}k@U1ImD{%GL2nc0OtPwI!ecjSF2Zd31gb192s zx#LZPKPM`Vih{?aQMB_GE+M6w3XuMM| z&m0-s78ldj0iZ zIU3ErV7-0*Z@qNuw!{;gOSA522Aivn&ZBHTIFMy`eT_bf`qCJS^HF}GhF3Csl2`!` z>&x7m{X2o~l5j=hGCsblsnAt=C{<8v9dGf=LEW^8*PN|hudb{{>OKP5DpfM((u#94 z$FIY%@Kq1B^D~qxGsYUOdc~c}V7aygmddU4x z(=tn+VVih+zxwuHVHVZl2G>>8Q^wl6&209=#O@2vB@y@MS;TA%1--&nSIlIJXRI;| zr1bB%Z!~}GKH7V7%x?VI0iTQ=J3XQn*I-(u$#Vlnur>1!BrK4oK2CL8EI*ihapK6C;=dPcEW%jfHB%gi|501bWW@J;qnRn;^1O%;Lx1SMi1N?M5k685z}3?uF2e$Nwt0|NfEtj zycPQvD!o!~j+;L>CXn|F<+^g&cEdZS%w0j`0X1@xDtpDlTQRn`ZcFXZ^5#~u`E$q6Lp|#G`phmlQhL+BkZZH&;81^KEa`RikvC@X;qoX+ zdYCaoI?(13dn6=3=aSE{aVb)y0K-yZ7#o`8)j*GH$6 zy_hp{-sf>uYE<6F z!$Y%eWFvvzI^_uQQa_IGGsnENh;=R91|{0g*Uf6fUmMmhuO2MP&2Vz?jS(93%Q{)Q z0_PQ_&M5^Yh!$LHEzKmm*vT=#vnyhJ*5TU{z`W}DvpCM3LEVoPro(e$1r*JaT61>F zwcJCa8V@x(Dk&vR%E2;Nu1>_k!QTWqRt@gbE_T%*(k~}-aIJ3L>7c)omPx!j*50_M zId3k-%A;iW25*}Soxi6U8FsXt+lD)aUh?eD%J!=U_r>NPGJ}qF2`_XJ{QBMv6a3zG z;wMWU^*$eqJXUmc=*?+fTND=ko99c<5=q%!>!&J5hBwAEp1fpg)7jmACSKa*)S0_? znU<}7QSyVWr?dY$3ICJ6aA}OMU-|DF7ZYQ~Quf@nQY(nK*JK?~Gx^Jww=acCWWEE% z+#bh?b4j2h--)VR5d5_&U)u2THF1z>cM-;c6VrtFJ-70A3W)byB#mJmv1T#xia(u|OYZ>zV9eXbpYZu4E$CZ1Z z_vSo9n0Fr5+fXCxd2M&6y#H0_mbklxxce^>l=69o1CIW;pZLhHE}!u#b56DTn%m6> zj>@fU2M5!m&%DgCy;VNku)tk&iE=C=Y~R3cr;3%f<@|vYV+qxTR+csWH3w7!QpWcW zUbrPXu~vJQEk2an^tOh@KHn^t%)59Yb8dr1k6yqCiYb^}6kKzB$v&De3M$VQ=Lo)hClq7!Acl$p2D7!7iCLTw0EDxFAij`> z7%s0u!1-Jn!iUJfFvJc(FyAFw0(eHdGC9#9910gMQ1qe1`z0A#^sf>5E9Dx)E$ zaH-I>LX1Yhr&PcY8p4<14tEeq05}0fKw*$hGJXUOVW$bVm2i2~b&jh)K|psjL@)@7 zsc3X$WF#sQj}l3O&{ztEg2v#`I2;nvKuV*8AWMc6N=+3I(-@9`lp}#EnJ*H;6__lx zC>*3A5YRaMb9@3ZgYgAkDE-6�Of&5~Hyw3|b&Se`_HHogyHRPXYa-g_H^HchKtq zsVH2+0h}TLA!zz71ef#0UK}n7ol1wxK?9+H08*7gUa{Y~bfz=hzgQ?J2;vLGQ&te! z-)Vw;-dD1|i%l^ymCm<;K;~a?ztjGl`;;=I#b8h!MVxR&cyvb^LgAmv6><1n>eMBH z#Kr(DG6`wT!w``K44#D~W5`$}fkR=DI5-@Oz~+4eMHfmzmXHG|pdfG*AHpG!0UVJ_ zA|Ww29tla{VF4r?px}^19*Kh`kSH83fc*x-O~Qw&k`?-GR0=3A1VtwE*km?|f+S+` zcqD;<#UjagJQvC4vT-~fmtxIhPeXAz)YT%1fCWt_U%(0i&|+cGln({r)RpdZ8UlyH ze3iI|vOpeW0IdPOkSmImejQ@+1%M~WQt*kjCR4~bk~M}*q)@Qd_^(RdfJ6$_q5>6* zLE#BgBZ`TkLg_%%vJ{mH0ZhrEY^V+rfCY*qOpz#*hEN0rS6F@-Wv=z$-ZaY2cE_^BEKWa)ey{0^Yu<5($ z+fe@0VuHh`mIal?nNC5S70{#Dx9?=?00w{PSj>M)Q2_%v=l1#!=kk-~@9F|MLV692h zRr7_s|8;tJs05H-D}wwvJzKP*A%31rTl9aM?rDW@Iw(ZTv<&LZP@_kG>GYrYf@1wo zzCM-Nf6@gU{?o}1@%xjmpLG2Y13#qvv$}rL^+OE&kn+#!`ah#f^Xpj=5JDTgNa!SZ zKhmQWIz_6o*E&1GCKc~ff1W-7Ni@VR>!mOl%2e@DlI7Y*KteT;&TvxeSJRuh*qFFJ z76*w8K__3(K_pNdIbc)!V1NsccdJocNu)#_6-PwDg%SPnQskw}*(8%}`f0EG%{B&Q-Ce|6;0g^oUu*R=Tn0e8b%A zQ@07rlXwvk25G*%CV|F7Lk8RQ-SY5O9-F$K!)z*ZI^L+atHK&F$}9NwY0J-t7W67# zT1mJqdq4kfQj=t5`4e4Psz)AuX}9&S#U@o-ad{TGWSe&QE3*~E@~}03T4>(0>a#rd zp_8bZt2d8zoG9A$w!mi5#?IGUGRVnFE&iL3%U_se;Bp8qmL6+*$lBTOjj|fBu&DGt z;}fTpg36nlWcIkhReOT|On*{L+qdU1e7Ox77|2KlHQu+V@ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/phonos/textures/block/radio_transceiver_top.png b/src/main/resources/assets/phonos/textures/block/radio_transceiver_top.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f6f45f4522db2f92b93ff5b82282270b1bb3c6 GIT binary patch literal 6277 zcmeHLc{r478y`zVQ3@?OOcSBZK4TU$B!ea<4r575@65cz#4Kip8H%Kiqx4ZIDI=*I zCuQ%HNJW;En(@B(b3ec5zJK@cx#xLjv-dK0 z1O3_hFc{2$xrE^Z{hz9SPt<`vEuM`6FxZ5qSazV?2T;HzQZZi;0m9`m5)ck51$-Dx zdE-dH3aQmX!*`)3722DW$@iaZJ?^mUzL#eH#mHkn4ZgVLdoNLQN2bkln?1vK))%~< z5`U3NaJ1Y^aZB0T=2)G$`P#Fhx63*oS36#FcwH*WHTc!4vAGZHu+k#1DQARIWOU+b zKs)#M)uyY=4nBx|e|(kSzKjqPTg?LQ_2@WflQLh%2O7!MxZ=PR_kI>~?aK74d;5Ag z$(gjFuIrhc#^9XidLITRJ%~VwUGI@cmLG2m=+MY8^NffLeW7br;?$dny|*l^6-uqP~O^=V~!u1~-OiIJyn!dx9K z`)Rsc7G>smFeC>O@-!c$o-A6OuvqdFTosxM*S)TBQ)%698g)hmvi)8pX}R-C6ZUDA z*Gx&?+cN>n*3`9FoIH1e9woCm5?r;_w)koAH0!;Kx7b=?ZGS`9tx9At5$OkcS+njt z@OlJ0b7Z!dpuDQPgPI$B4iyE*84x?!2l>7pF0Tl87N;PIf~p-uRE4E7jUM%|D$7}G zAK~4^d8q_m&(HH03!jeQFOV~`anJ0ggfHzEUkxdBYuSC%6M1H&H0erav3}5fOS_Aw zcRkatn;xjeojA~tZSu4W*=rj5z`c3PT^P#LJMgOmc=2?~sJ?jqx!%_I zr!Hi>-X>+gRUK;!>@V`DY`w;;zt2A3T>CIK(kJ+6@WAdRWv9}5i~i-b)#rs}P)Hoy zV$O|rd+U(YIe*3f_PQ&Udd*aI^s-Mo`&i7Z&1-d8o^_^W#%b3(Tdq86FNN$ z&lAfX8Gj_7UzvC+)LJLJPlKLT6f_TW`MJ#%ugoWFUwF%RqC3+&v>nqdxM*5-!a6$q z`OF4Rn}&`ce&>wJg4#!6=GmT?Ud0^j|tJY|*9;vfAVMfMx04o9wmb#q3kDcTpb>A4@E`yz{LIZlotR4Q(`gIA>Oi zSu<5HDTtn5yRfROWATE65v4JQ=X#gv|6@pxr*J)W@pWkgh8#Kl?@LhMtQ#tFfJl_C@Q4m|N` zQ~vvt!z`n#uQQwKs+_>qQwYm-aDS%DOol>=>y8>NvtSrJ42srnA7$2eq>B7KEQakpN4p=`ub;YP)%58WiQ?BUl{Udco^KSI zD{69Ggkfg#=wRz8EcXs+C{ZvY59;h8f7{Xp5;<4h(&^0e1_|Di zPbl==!zQ)&QD3aAiII#Zvy3(Gar>gOkOl<+rbjFdnsgcdz zs)2k&>cK6prQSF6k5G5jj7-^0u(U;`CWfCmv$x^U#*~U;!{S8vE3LmC{dM30!m2CF zIOweM-mv4v>b@skaS4|W4ln4_D!juxb#9tMs|Z8t-n7v z_V=ND+WG3oZL_`g=WDG)L~ebU()!o={|GK$-<#jPB6+uTTUeI4pa+x5ss*eidE?KR| zJOE$p_LB)4n7g#vZOdkYs)_z?f2xx!Dm&N&Bel!ZF-h;%9o=}Rf|zpISL9=3I8uA< zMH6}IwFyB6H$OQ3G1#ehcfC$=W$LL;?OsQwSYFv|ggI^y9IPP3OgVl_!!1{SZrhx5 zULH248(!ky6wom8Nh2jyvhcm#BO1+r9^NTV*yyr4pK-n4{IOwl+u)tt7w_U>^DCvv z4@!hk?G60|(ARk&%ah6#3sC@1%mGnKp#%~I3`TQON&s#+D2H>vP=Sb!cwBu70T=M- zhyWrB!;-jwVS**GQqVVc8Jim$&ZY1WPLBFCB^3e?f^q<^6h??-R3#lThD(L6)n+sT zKIS41ry~Mc-f$PO6oeB{1QZ78suV=y5RUqAnv}<<`Y_x+LO^$PM3`JIp`y_Wg#x9( zqr}osG?qf4pfNZ!4u^z1kg^z&98e-fvUzHVaSR41<4PeV3&bM08WZ4%qvUi10vd;Z zicctEu|C6#WFJ|8_&_TG2^x#SpoK#8*E3{t*JudjV?cj9L&k?ub`MBnH&&tK{XTvjuJpPJb=RiK|T@518^K9 zfx;yrDSW7|d>+XjLjrL)5)c0sgqKtRsS=3zIx0034}v0-`5ZEbL_rd6huozp<@GT!l};QOgaLG!hErKM*wm@ zGy&QJ0ufKFkbN0q3xuGr98mL#wI@@^cr2ETCF1R&5WYD1gHjo!MKvlGgTfQWM%0C& zLg_%%0&1l~0AqG28>)*G1mt2VTP%*CBh*2`)iXa2v!D&d1LOb$kb@8?28X9&NK`C^ zjl)s#&@~nb-D1A-7xM)CnE&Rj-ac^Jc+r;#WYGFCW2W&v!X9!$W$4u8|xL89?Fz z5*dRbQt(79@mqA6m@iiVQqVaR;t}Es66hFLaGQ^%vi;Uy5eBO3fWcvqSPT+_Wn*zv zj6Ib=K%oEm^tjLt;bVD_%+(4f09YInz~T8w9*IZ>iCBAp3yf>yGkO1WdU!|z$gdRf zDLoom-4H(&lZO6}a*sQF)j=Uz#%)k%h8jKkbEp5v7ZmG1`T8ice{u;p{Kq2S#qSTg ze$e$@41AaK4|V;Z>$@2EF6STW`oGbo|K(W`6hQ|(1@t5s@Ot@e=qXZ%;OcX5zy`w^&<_eHnG9z(`MfHlRdm_gc$1SuZJFxYQeK3u-=Uq`w&>d| zFfltIdZSIV4EK-IDBmz)kBz4BT%UY6v4B}+xrN);RJMHP)^ZPr^A5YKLf!go`n)U( z29mE(3v4QnkPPl#!U~T2)!oNgcQQ2+wq)xS{JhnhM7XX0A;Nhyx^9tMAlqA4?h-r0ViJrvK?)^28>0e#3qvYlwZjPZ|B5ba9icMY7l}KUAnUdxF z(x9eYNoV@HuK4qzk=+GGNplw`uhT5??|ZUAHOu&y#$2I9+Hv;ik>FeX&$s#D%+r|Z zn=q4Y=X=xysfLb~ysBszoe0HVbo*XUVU?A?-wpz+JxXQC4E#N|3(80{;pwAS@VK5G zku9b^OXoCQ7g+buR>S8J{QM*>rdf}j-XAPh;FNJ6Xm}fA{g_C%np@18OY#K;N%=N~ zds>qO#