diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2025-03-15 18:57:24 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2025-03-15 18:57:24 -0300 |
commit | 2aa1dea5126290ee6dadc0884a3d8e2791be04ef (patch) | |
tree | 0e488cfbf8bd6337fd194b1b6a467e2172e5ac54 /src/client | |
parent | fee7157d84c3ce887a540be82dc7a7d2e0c8e368 (diff) |
add everything so far
Diffstat (limited to 'src/client')
14 files changed, 1058 insertions, 19 deletions
diff --git a/src/client/java/space/autistic/radio/client/mixin/BlockStatesLoaderMixin.java b/src/client/java/space/autistic/radio/client/mixin/BlockStatesLoaderMixin.java new file mode 100644 index 0000000..0e9b05e --- /dev/null +++ b/src/client/java/space/autistic/radio/client/mixin/BlockStatesLoaderMixin.java @@ -0,0 +1,43 @@ +package space.autistic.radio.client.mixin; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.render.model.BlockStatesLoader; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.BooleanProperty; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.state.property.Property; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockStatesLoader.class) +public abstract class BlockStatesLoaderMixin { + + @Shadow + abstract void loadBlockStates(Identifier id, StateManager<Block, BlockState> stateManager); + + @Shadow + private Profiler profiler; + + @Unique + private static final StateManager<Block, BlockState> STATE_MANAGER = new StateManager.Builder<Block, BlockState>(Blocks.AIR) + .add(DirectionProperty.of("facing")) + .build(Block::getDefaultState, BlockState::new); + + @Inject( + method = {"load()V"}, + at = {@At("HEAD")} + ) + void onLoad(CallbackInfo callbackInfo) { + profiler.push("pirate_radio_static_definitions"); + loadBlockStates(Identifier.of("pirate-radio", "disposable-transmitter"), STATE_MANAGER); + profiler.pop(); + } +} diff --git a/src/client/kotlin/space/autistic/radio/client/ClientProxy.kt b/src/client/kotlin/space/autistic/radio/client/ClientProxy.kt new file mode 100644 index 0000000..3c29cd3 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/ClientProxy.kt @@ -0,0 +1,20 @@ +package space.autistic.radio.client + +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand +import space.autistic.radio.CommonProxy +import space.autistic.radio.PirateRadioItems.STORAGE_CARD +import space.autistic.radio.client.gui.StorageCardEditScreen + +class ClientProxy : CommonProxy() { + override fun useStorageCard(player: PlayerEntity, item: ItemStack, hand: Hand) { + if (player is ClientPlayerEntity) { + if (item.isOf(STORAGE_CARD)) { + MinecraftClient.getInstance().setScreen(StorageCardEditScreen(player, item, hand)) + } + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt index 54b7640..1a68c21 100644 --- a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt @@ -1,25 +1,47 @@ package space.autistic.radio.client -import com.mojang.brigadier.CommandDispatcher import net.fabricmc.api.ClientModInitializer import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry import net.minecraft.client.MinecraftClient -import net.minecraft.command.CommandRegistryAccess +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand import org.slf4j.LoggerFactory import space.autistic.radio.PirateRadio import space.autistic.radio.PirateRadio.MOD_ID import space.autistic.radio.PirateRadioEntityTypes import space.autistic.radio.client.entity.ElectronicsTraderEntityRenderer +import space.autistic.radio.client.entity.DisposableTransmitterEntityRenderer +import space.autistic.radio.client.fmsim.FmSimulatorMode import space.autistic.radio.client.gui.FmReceiverScreen +import space.autistic.radio.client.sound.PirateRadioSoundInstance +import kotlin.math.max +import kotlin.math.min object PirateRadioClient : ClientModInitializer { - private val logger = LoggerFactory.getLogger(MOD_ID) + private var soundInstance: SoundInstance? = null + var volume: Int = 0 + set(value) { + field = min(10, max(0, value)) + } + var stereo: Boolean = false + var frequency = 768 + set(value) { + field = min(1080, max(768, value)) + } + var mode = FmSimulatorMode.FULL override fun onInitializeClient() { + PirateRadio.proxy = ClientProxy() EntityRendererRegistry.register(PirateRadioEntityTypes.ELECTRONICS_TRADER, ::ElectronicsTraderEntityRenderer) + EntityRendererRegistry.register( + PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER, + ::DisposableTransmitterEntityRenderer + ) PirateRadioEntityModelLayers.initialize() ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> dispatcher.register( @@ -31,5 +53,21 @@ object PirateRadioClient : ClientModInitializer { } ) } + ClientTickEvents.END_WORLD_TICK.register { world -> + if (volume > 0 && MinecraftClient.getInstance().player?.isRemoved == false) { + if (soundInstance == null) { + soundInstance = PirateRadioSoundInstance(MinecraftClient.getInstance().player!!) + } + val soundManager = MinecraftClient.getInstance().soundManager + if (!soundManager.isPlaying(soundInstance)) { + soundManager.play(soundInstance) + } + } else { + if (soundInstance != null) { + MinecraftClient.getInstance().soundManager.stop(soundInstance) + soundInstance = null + } + } + } } } \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt index b5130a1..65e9677 100644 --- a/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt @@ -32,7 +32,7 @@ class PirateRadioItemModelGenerator(output: FabricDataOutput) : FabricModelProvi modelGenderator.register(PirateRadioItems.SBC, Models.GENERATED) modelGenderator.register(PirateRadioItems.WIRE, Models.GENERATED) modelGenderator.register(PirateRadioItems.POWERBANK, Models.GENERATED) - modelGenderator.register(PirateRadioItems.FM_RECEIVER, Models.GENERATED) + //modelGenderator.register(PirateRadioItems.FM_RECEIVER, Models.GENERATED) modelGenderator.register(PirateRadioItems.STORAGE_CARD, Models.GENERATED) modelGenderator.register(PirateRadioItems.DISPOSABLE_TRANSMITTER, Models.GENERATED) } @@ -46,7 +46,7 @@ class PirateRadioRecipeGenerator( override fun generate(exporter: RecipeExporter) { ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, PirateRadioItems.DISPOSABLE_TRANSMITTER) .input(PirateRadioItems.SBC).input(PirateRadioItems.WIRE).input(PirateRadioItems.POWERBANK) - .input(PirateRadioItems.STORAGE_CARD) + //.input(PirateRadioItems.STORAGE_CARD) .criterion("has_sbc", RecipeProvider.conditionsFromItem(PirateRadioItems.SBC)).offerTo(exporter) } diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt index 765912d..604fdfd 100644 --- a/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt @@ -9,6 +9,7 @@ import space.autistic.radio.PirateRadio object PirateRadioEntityModelLayers { val ELECTRONICS_TRADER = EntityModelLayer(Identifier.of(PirateRadio.MOD_ID, "electronics-trader"), "main") + val PIRATE_RADIO = EntityModelLayer(Identifier.of(PirateRadio.MOD_ID, "electronics-trader"), "main") fun initialize() { EntityModelLayerRegistry.registerModelLayer(ELECTRONICS_TRADER) { diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt index 51743dd..38d0c97 100644 --- a/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt +++ b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt @@ -12,9 +12,7 @@ import com.dylibso.chicory.wasm.types.Value import com.dylibso.chicory.wasm.types.ValueType import org.joml.Quaterniond import org.joml.Vector3d -import space.autistic.radio.PirateRadio -import java.util.logging.Level -import java.util.logging.Logger +import space.autistic.radio.PirateRadio.logger class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { var failing = false @@ -25,7 +23,7 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { // capped at 1MB per antenna .withMemoryLimits(MemoryLimits(0, 16)) } catch (e: ChicoryException) { - logger.log(Level.SEVERE, "Error while trying to parse antenna model.", e) + logger.error("Error while trying to parse antenna model.", e) failing = true null } @@ -53,9 +51,7 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { orientation.w.toRawBits() ) if (instance.exports().global("should-attenuate").type != ValueType.I32) { - logger.log( - Level.SEVERE, "Error while trying to initialize antenna model: missing 'should-attenuate'" - ) + logger.error("Error while trying to initialize antenna model: missing 'should-attenuate'") failing = true return ConstAntennaModel(0f) } @@ -73,7 +69,7 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { )[0] ) } catch (e: ChicoryException) { - logger.log(Level.SEVERE, "Error while trying to evaluate antenna model.", e) + logger.error("Error while trying to evaluate antenna model.", e) failing = true return 0f } @@ -84,7 +80,7 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { } } } catch (e: ChicoryException) { - logger.log(Level.SEVERE, "Error while trying to initialize antenna model.", e) + logger.error("Error while trying to initialize antenna model.", e) failing = true return ConstAntennaModel(0f) } @@ -92,6 +88,5 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { companion object { private val defaultImports = ImportValues.builder().build() - private val logger = Logger.getLogger(PirateRadio.MOD_ID) } } \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/cli/Funny.kt b/src/client/kotlin/space/autistic/radio/client/cli/Funny.kt new file mode 100644 index 0000000..ebf6b06 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/cli/Funny.kt @@ -0,0 +1,16 @@ +package space.autistic.radio.client.cli + +import space.autistic.radio.wasm.Bindings +import java.lang.invoke.MethodHandles +import kotlin.reflect.jvm.javaMethod + +object Funny { + val lookup = MethodHandles.lookup() + + fun test() { + } +} + +fun main() { + Bindings.bindFunc("", "", Funny.lookup, Funny::test.javaMethod!!, Funny) +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/entity/DisposableTransmitterEntityRenderer.kt b/src/client/kotlin/space/autistic/radio/client/entity/DisposableTransmitterEntityRenderer.kt new file mode 100644 index 0000000..61b1e19 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/entity/DisposableTransmitterEntityRenderer.kt @@ -0,0 +1,79 @@ +package space.autistic.radio.client.entity + +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.TexturedRenderLayers +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.EntityRenderer +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.model.BakedModelManager +import net.minecraft.client.util.ModelIdentifier +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.screen.PlayerScreenHandler +import net.minecraft.util.Identifier +import net.minecraft.util.math.Direction +import net.minecraft.util.math.RotationAxis +import space.autistic.radio.PirateRadio +import space.autistic.radio.entity.DisposableTransmitterEntity + +class DisposableTransmitterEntityRenderer(ctx: EntityRendererFactory.Context) : + EntityRenderer<DisposableTransmitterEntity>(ctx) { + + private val blockRenderManager = ctx.blockRenderManager + + override fun getTexture(entity: DisposableTransmitterEntity): Identifier { + return PlayerScreenHandler.BLOCK_ATLAS_TEXTURE + } + + override fun render( + entity: DisposableTransmitterEntity, + yaw: Float, + tickDelta: Float, + matrices: MatrixStack, + vertexConsumers: VertexConsumerProvider, + light: Int + ) { + super.render(entity, yaw, tickDelta, matrices, vertexConsumers, light) + + matrices.push() + val facing: Direction = entity.horizontalFacing + val vec3d = this.getPositionOffset(entity, tickDelta) + matrices.translate(-vec3d.getX(), -vec3d.getY(), -vec3d.getZ()) + val d = (1.0 - DisposableTransmitterEntity.DEPTH) / 2.0 + matrices.translate( + facing.offsetX.toDouble() * d, facing.offsetY.toDouble() * d, facing.offsetZ.toDouble() * d + ) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(entity.pitch)) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180.0f - entity.yaw)) + if (!entity.isInvisible) { + val bakedModelManager: BakedModelManager = this.blockRenderManager.models.modelManager + matrices.push() + matrices.translate(-0.5f, -0.5f, -0.5f) + this.blockRenderManager.modelRenderer.render( + matrices.peek(), + vertexConsumers.getBuffer(TexturedRenderLayers.getEntitySolid()), + null, + bakedModelManager.getModel(MODEL_ID[facing]), + 1.0f, + 1.0f, + 1.0f, + light, + OverlayTexture.DEFAULT_UV + ) + matrices.pop() + } + + matrices.pop() + } + + companion object { + private val STATES_ID = Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter") + private val MODEL_ID = mapOf( + Direction.DOWN to ModelIdentifier(STATES_ID, "facing=down"), + Direction.UP to ModelIdentifier(STATES_ID, "facing=up"), + Direction.NORTH to ModelIdentifier(STATES_ID, "facing=north"), + Direction.SOUTH to ModelIdentifier(STATES_ID, "facing=south"), + Direction.WEST to ModelIdentifier(STATES_ID, "facing=west"), + Direction.EAST to ModelIdentifier(STATES_ID, "facing=east"), + ) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt index 91c29db..5da8e17 100644 --- a/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt +++ b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt @@ -16,7 +16,7 @@ class ElectronicsTraderEntityRenderer(context: EntityRendererFactory.Context) : ) { companion object { - val TEXTURE = Identifier.of(PirateRadio.MOD_ID, "electronics-trader") + val TEXTURE = Identifier.of(PirateRadio.MOD_ID, "textures/entity/electronics-trader.png") } override fun getTexture(entity: ElectronicsTraderEntity?): Identifier = TEXTURE diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FmSimulatorMode.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmSimulatorMode.kt new file mode 100644 index 0000000..a8bc9fd --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmSimulatorMode.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.client.fmsim + +enum class FmSimulatorMode { + FULL, + FAST, + DEAF +} diff --git a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt index 4bd4db2..f5fd729 100644 --- a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt +++ b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt @@ -1,11 +1,634 @@ package space.autistic.radio.client.gui +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.* +import com.dylibso.chicory.wasm.ChicoryException +import com.dylibso.chicory.wasm.InvalidException +import com.dylibso.chicory.wasm.Parser +import com.dylibso.chicory.wasm.types.FunctionType +import com.dylibso.chicory.wasm.types.Value +import com.dylibso.chicory.wasm.types.ValueType +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.ClickableWidget +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.util.InputUtil +import net.minecraft.text.StringVisitable import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.minecraft.util.Identifier +import org.lwjgl.glfw.GLFW +import org.slf4j.LoggerFactory +import org.slf4j.event.Level +import space.autistic.radio.PirateRadio +import space.autistic.radio.PirateRadio.logger +import space.autistic.radio.client.PirateRadioClient +import space.autistic.radio.client.fmsim.FmSimulatorMode +import space.autistic.radio.wasm.Bindings.Companion.bindFunc +import java.lang.invoke.MethodHandles +import kotlin.math.max +import kotlin.reflect.jvm.javaMethod + class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { + private var loggingEventBuilder = wasmLogger.makeLoggingEventBuilder(Level.INFO) + private var instance: Instance? = null + private var failure: Exception? = null + set(value) { + field = value + clearChildren() + } + private var packTitle: Text? = null + + private var backgroundWidth = 0 + private var backgroundHeight = 0 + private var backgroundTextureWidth = 256 + private var backgroundTextureHeight = 256 + + private var drawContext: DrawContext? = null + + private var textObjects = ArrayList<Text?>() + private var textObjectsFree = ArrayList<Int>() + + private val frequencyPlusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.frequency.plus")) { + if (InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) + || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) + ) { + PirateRadioClient.frequency += 10 + } else { + PirateRadioClient.frequency++ + } + }.dimensions(0, 0, 20, 20) + .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.frequency.plus.narrated")) } + .build() + private val frequencyMinusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.frequency.minus")) { + if (InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) + || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) + ) { + PirateRadioClient.frequency -= 10 + } else { + PirateRadioClient.frequency-- + } + }.dimensions(20, 0, 20, 20) + .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.frequency.plus.narrated")) } + .build() + + private val volumePlusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.volume.plus")) { + PirateRadioClient.volume++ + }.dimensions(0, 20, 20, 20) + .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.volume.plus.narrated")) } + .build() + private val volumeMinusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.volume.minus")) { + PirateRadioClient.volume-- + }.dimensions(20, 20, 20, 20) + .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.volume.minus.narrated")) } + .build() + private val toggleModes = ButtonWidget.builder(Text.translatable("pirate-radio.mode")) { + PirateRadioClient.mode = when (PirateRadioClient.mode) { + FmSimulatorMode.FULL -> FmSimulatorMode.FAST + else -> FmSimulatorMode.FULL + } + }.position(0, 40).build() + override fun init() { - // TODO + if (failure == null && instance == null) { + try { + setupWasm() + } catch (e: WasmScreenException) { + logger.error("Failed to setup wasm.", e) + failure = e + } + } + if (failure == null) { + try { + instance!!.export("init").apply() + addDrawableChild(frequencyPlusWidget) + addDrawableChild(frequencyMinusWidget) + addDrawableChild(volumePlusWidget) + addDrawableChild(volumeMinusWidget) + addDrawableChild(toggleModes) + volumePlusWidget.visible + } catch (e: ChicoryException) { + failure = WasmScreenException("Skin failed to initialize", e) + logger.error("Failed to initialize.", failure) + } catch (e: WasmScreenException) { + failure = e + logger.error("Failed to initialize.", failure) + } + } + } + + override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.renderInGameBackground(context) + + if (failure == null) { + if (backgroundWidth or backgroundHeight != 0) { + RenderSystem.setShader(GameRenderer::getPositionTexProgram) + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) + RenderSystem.setShaderTexture(0, TEXTURE) + val x = (width - backgroundWidth) / 2 + val y = (height - backgroundHeight) / 2 + context.drawTexture( + TEXTURE, + x, + y, + 0f, + 0f, + backgroundWidth, + backgroundHeight, + backgroundTextureWidth, + backgroundTextureHeight + ) + } + } + + + if (client!!.debugHud.shouldShowDebugHud()) { + context.drawText( + textRenderer, + Text.translatable("pirate-radio.skin-pack", packTitle), + 0, + height - textRenderer.fontHeight, + Colors.WHITE, + true + ) + } + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + + if (failure != null) { + context.drawTextWrapped( + textRenderer, StringVisitable.plain(failure!!.message), (width - 320) / 2, 0, 320, Colors.WHITE + ) + } else { + try { + drawContext = context + instance!!.export("render").apply(mouseX.toLong(), mouseY.toLong(), Value.floatToLong(delta)) + } catch (e: ChicoryException) { + failure = WasmScreenException("Skin failed to initialize", e) + logger.error("Failed to initialize.", failure) + } catch (e: WasmScreenException) { + failure = e + logger.error("Failed to initialize.", failure) + } finally { + drawContext = null + } + } + } + + override fun shouldPause() = false + + private fun loggerLog() { + loggingEventBuilder.log() + } + + private fun loggerLogMessage(message: String) { + loggingEventBuilder.log(message) + } + + private fun loggerBegin(level: Int) { + loggingEventBuilder = wasmLogger.makeLoggingEventBuilder( + try { + Level.intToLevel(level) + } catch (e: IllegalArgumentException) { + Level.INFO + } + ) + } + + private fun loggerSetMessage(message: String) { + loggingEventBuilder.setMessage(message) + } + + private fun loggerAddArgumentString(arg: String) { + loggingEventBuilder.addArgument(arg) + } + + private fun loggerAddArgumentInt(arg: Int) { + loggingEventBuilder.addArgument(arg) + } + + private fun loggerAddArgumentLong(arg: Long) { + loggingEventBuilder.addArgument(arg) + } + + private fun loggerAddArgumentFloat(arg: Float) { + loggingEventBuilder.addArgument(arg) + } + + private fun loggerAddArgumentDouble(arg: Double) { + loggingEventBuilder.addArgument(arg) + } + + private fun setBackgroundSize(width: Int, height: Int) { + this.backgroundWidth = max(width, 0) + this.backgroundHeight = max(height, 0) + } + + private fun setBackgroundTextureSize(width: Int, height: Int) { + this.backgroundTextureWidth = max(width, 0) + this.backgroundTextureHeight = max(height, 0) + } + + private fun renderTextTranslatable(text: String, x: Int, y: Int, color: Int, shadow: Boolean) { + drawContext?.drawText(textRenderer, Text.translatable(text), x, y, color, shadow) + } + + private fun renderTextLiteral(text: String, x: Int, y: Int, color: Int, shadow: Boolean) { + drawContext?.drawText(textRenderer, Text.literal(text), x, y, color, shadow) + } + + private fun renderTextObject(handle: Int, x: Int, y: Int, color: Int, shadow: Boolean) { + if (handle >= 0 && handle < textObjects.size && textObjects[handle] != null) { + drawContext?.drawText(textRenderer, textObjects[handle], x, y, color, shadow) + } else { + throw WasmScreenException("Invalid text handle: $handle", null) + } + } + + private fun frequencyPlusWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + frequencyPlusWidget.x = x + frequencyPlusWidget.y = y + frequencyPlusWidget.width = width + frequencyPlusWidget.height = height + } + + private fun frequencyMinusWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + frequencyMinusWidget.x = x + frequencyMinusWidget.y = y + frequencyMinusWidget.width = width + frequencyMinusWidget.height = height + } + + private fun volumePlusWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + volumePlusWidget.x = x + volumePlusWidget.y = y + volumePlusWidget.width = width + volumePlusWidget.height = height + } + + private fun volumeMinusWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + volumeMinusWidget.x = x + volumeMinusWidget.y = y + volumeMinusWidget.width = width + volumeMinusWidget.height = height + } + + private fun toggleWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + toggleModes.x = x + toggleModes.y = y + toggleModes.width = width + toggleModes.height = height + } + + private fun getFrequency(): Int = PirateRadioClient.frequency + + private fun getMode(): Int = PirateRadioClient.mode.ordinal + + private fun getStereo(): Boolean = PirateRadioClient.stereo + + private fun getVolume(): Int = PirateRadioClient.volume + + private fun getStereoPilot(): Float = 0f + + private fun getWidth(): Int = width + + private fun getHeight(): Int = height + + // useful for drawing the stereo pilot, not that it's implemented + private fun drawImage( + red: Float, + green: Float, + blue: Float, + alpha: Float, + x: Int, + y: Int, + u: Float, + v: Float, + w: Int, + h: Int + ) { + val drawContext = drawContext ?: throw WasmScreenException("Inappropriate draw call", null) + RenderSystem.setShader(GameRenderer::getPositionTexProgram) + RenderSystem.setShaderColor(red, green, blue, alpha) + RenderSystem.setShaderTexture(0, TEXTURE) + drawContext.drawTexture( + TEXTURE, + x, + y, + u, + v, + w, + h, + backgroundTextureWidth, + backgroundTextureHeight + ) + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + } + + private fun textFree(handle: Int) { + if (handle >= 0 && handle < textObjects.size && textObjects[handle] != null) { + textObjects[handle] = null + textObjectsFree.add(handle) + } else { + throw WasmScreenException("Invalid text handle: $handle", null) + } + } + + private fun textLiteral(text: String): Int { + val index = if (textObjectsFree.isNotEmpty()) { + textObjectsFree.removeLast() + } else { + textObjects.add(null) + textObjects.size - 1 + } + textObjects[index] = Text.literal(text) + return index + } + + private fun textTranslatable(text: String): Int { + val index = if (textObjectsFree.isNotEmpty()) { + textObjectsFree.removeLast() + } else { + textObjects.add(null) + textObjects.size - 1 + } + textObjects[index] = Text.translatable(text) + return index + } + + private fun textTranslatableArguments(text: String, args: List<Int>): Int { + args.forEach { handle -> + if (handle < 0 || handle >= textObjects.size || textObjects[handle] == null) { + throw WasmScreenException("Invalid text handle: $handle", null) + } + } + val index = if (textObjectsFree.isNotEmpty()) { + textObjectsFree.removeLast() + } else { + textObjects.add(null) + textObjects.size - 1 + } + textObjects[index] = Text.translatable(text, *Array(args.size) { textObjects[args[it]] }) + return index + } + + private fun setupWasm() { + // this should never throw + val resource = this.client!!.resourceManager.getResourceOrThrow(WASM_GUI) + packTitle = resource.pack.info.title + val module = try { + Parser.parse(resource.inputStream) + } catch (e: ChicoryException) { + throw WasmScreenException("Skin failed to load: error parsing module", e) + } + val importValues = ImportValues.builder() + importValues.addFunction( + bindFunc( + "text", "free", lookup, this::textFree.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "text", "literal", lookup, this::textLiteral.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "text", "translatable", lookup, this::textTranslatable.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "text", "translatable-arguments", lookup, this::textTranslatableArguments.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "frequency-plus-widget", + "set-dimensions", + lookup, + this::frequencyPlusWidgetDimensions.javaMethod!!, + this + ) + ) + importValues.addFunction( + bindFunc( + "frequency-minus-widget", + "set-dimensions", + lookup, + this::frequencyMinusWidgetDimensions.javaMethod!!, + this + ) + ) + importValues.addFunction( + bindFunc( + "volume-plus-widget", "set-dimensions", lookup, this::volumePlusWidgetDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "volume-minus-widget", "set-dimensions", lookup, this::volumeMinusWidgetDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "toggle-widget", "set-dimensions", lookup, this::toggleWidgetDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "simulator", "get-frequency", lookup, this::getFrequency.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "simulator", "get-volume", lookup, this::getVolume.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "simulator", "get-mode", lookup, this::getMode.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "simulator", "get-stereo", lookup, this::getStereo.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "simulator", "get-stereo-pilot", lookup, this::getStereoPilot.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "get-width", lookup, this::getWidth.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "get-height", lookup, this::getHeight.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "draw-image", lookup, this::drawImage.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "set-background-size", lookup, this::setBackgroundSize.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "set-background-texture-size", lookup, this::setBackgroundTextureSize.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "render-text-translatable", lookup, this::renderTextTranslatable.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "render-text-literal", lookup, this::renderTextLiteral.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "render-text-object", lookup, this::renderTextObject.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "screen", "set-background-size", lookup, this::setBackgroundSize.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "log", lookup, this::loggerLog.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "log-message", lookup, this::loggerLogMessage.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "begin", lookup, this::loggerBegin.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "log-message", lookup, this::loggerSetMessage.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "add-argument-string", lookup, this::loggerAddArgumentString.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "add-argument-int", lookup, this::loggerAddArgumentInt.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "add-argument-long", lookup, this::loggerAddArgumentLong.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "add-argument-float", lookup, this::loggerAddArgumentFloat.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "logger", "add-argument-double", lookup, this::loggerAddArgumentDouble.javaMethod!!, this + ) + ) + val builder = Instance.builder(module) + builder.withMachineFactory(AotMachineFactory(module)) + builder.withImportValues(importValues.build()) + val instance = try { + builder.build() + } catch (e: ChicoryException) { + throw WasmScreenException("Skin failed to load: error constructing module", e) + } + var initialize: ExportFunction? = null + try { + initialize = instance.export("_initialize") + } catch (_: InvalidException) { + // export may not exist, it's fine + } catch (e: ChicoryException) { + throw WasmScreenException("Skin failed to load: error initializing module", e) + } + try { + initialize?.apply() + } catch (e: ChicoryException) { + throw WasmScreenException("Skin failed to load: error initializing module", e) + } + try { + checkFuncExports( + instance, + "init" to FunctionType.empty(), + "render" to FunctionType.of(arrayOf(ValueType.I32, ValueType.I32, ValueType.F32), emptyArray()) + ) + } catch (e: ChicoryException) { + throw WasmScreenException("Skin failed to load: error checking exports", e) + } + this.instance = instance + } + + private class WasmScreenException(message: String?, cause: Throwable?) : Exception(message, cause) + + @Throws(WasmScreenException::class) + private fun checkFuncExports(instance: Instance, vararg exports: Pair<String, FunctionType>) { + val sb = StringBuilder() + exports.forEach { (name, type) -> + when (exportStatus(instance, name, type)) { + ExportStatus.MISSING -> sb.append("missing: ", name, type, "\n") + ExportStatus.TYPE_MISMATCH -> sb.append("type mismatch: ", name, type, "\n") + ExportStatus.OK -> {} + } + } + if (sb.isNotEmpty()) { + sb.setLength(sb.length - 1) + throw WasmScreenException("Skin failed to load: error checking exports:\n$sb", null) + } + } + + private enum class ExportStatus { + OK, MISSING, TYPE_MISMATCH, + } + + private fun exportStatus(instance: Instance, name: String, type: FunctionType): ExportStatus { + try { + // because instance.exportType doesn't check if it's actually a function + instance.export(name) + return if (instance.exportType(name) == type) ExportStatus.OK else ExportStatus.TYPE_MISMATCH + } catch (_: InvalidException) { + return ExportStatus.MISSING + } + } + + companion object { + private val WASM_GUI = Identifier.of(PirateRadio.MOD_ID, "guis/radio-receiver.wasm") + + // the texture is at a fixed location but the respack can replace it too + private val TEXTURE = Identifier.of(PirateRadio.MOD_ID, "textures/gui/radio-receiver.png") + + private val wasmLogger = LoggerFactory.getLogger(PirateRadio.MOD_ID + "/wasm/radio-receiver") + private val lookup = MethodHandles.lookup() } -} \ No newline at end of file +} diff --git a/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt b/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt new file mode 100644 index 0000000..a1dd5d5 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt @@ -0,0 +1,115 @@ +package space.autistic.radio.client.gui + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.EditBoxWidget +import net.minecraft.client.gui.widget.TextFieldWidget +import net.minecraft.client.util.NarratorManager +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.screen.ScreenTexts +import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.minecraft.util.Hand +import space.autistic.radio.PirateRadioComponents +import space.autistic.radio.network.StorageCardUpdateC2SPayload + +class StorageCardEditScreen( + private val player: PlayerEntity, private val storageCard: ItemStack, private val hand: Hand +) : Screen(NarratorManager.EMPTY) { + private var dirty = false + private var frequency = 768 + private lateinit var editBox: EditBoxWidget + private lateinit var frequencyBox: TextFieldWidget + private lateinit var doneButton: ButtonWidget + + override fun init() { + if (!this::editBox.isInitialized) { + editBox = EditBoxWidget( + textRenderer, + (width - WIDTH) / 2, + (height - HEIGHT) / 2, + WIDTH, + HEIGHT, + Text.translatable("pirate-radio.message"), + Text.translatable("pirate-radio.message") + ) + editBox.text = storageCard.get(PirateRadioComponents.MESSAGE)?.literalString ?: "" + editBox.setMaxLength(16384) + editBox.setChangeListener { + dirty = true + } + } + frequency = storageCard.getOrDefault(PirateRadioComponents.FREQUENCY, 768) + if (!this::frequencyBox.isInitialized) { + frequencyBox = + TextFieldWidget(textRenderer, FREQ_WIDTH, FREQ_HEIGHT, Text.translatable("pirate-radio.frequency.edit")) + frequencyBox.setMaxLength(5) + frequencyBox.text = (frequency / 10).toString() + "." + (frequency % 10).toString() + frequencyBox.setTextPredicate { + FREQ_REGEX_CHARACTERS.matches(it) + } + frequencyBox.setChangedListener { + if (FREQ_REGEX.matches(it)) { + frequency = it.replace(".", "").toInt() + frequencyBox.setEditableColor(Colors.WHITE) + dirty = true + } else { + frequencyBox.setEditableColor(Colors.RED) + } + } + } + editBox.x = (width - WIDTH) / 2 + editBox.y = (height - HEIGHT) / 2 + frequencyBox.x = editBox.x + frequencyBox.y = editBox.y - FREQ_HEIGHT + addDrawableChild(frequencyBox) + addDrawableChild(editBox) + doneButton = this.addDrawableChild( + ButtonWidget.builder(ScreenTexts.DONE) { + client!!.setScreen(null) + this.saveChanges() + }.dimensions(editBox.x + editBox.width - 98, frequencyBox.y, 98, 20).build() + ) + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + + context.drawText( + textRenderer, + Text.translatable("pirate-radio.frequency.edit"), + frequencyBox.x, + frequencyBox.y - textRenderer.fontHeight, + Colors.WHITE, + true + ) + } + + private fun saveChanges() { + if (this.dirty) { + this.writeNbtData() + val slot = if (this.hand == Hand.MAIN_HAND) player.inventory.selectedSlot else 40 + ClientPlayNetworking.send(StorageCardUpdateC2SPayload(slot, this.editBox.text, this.frequency)) + } + } + + private fun writeNbtData() { + this.storageCard.set(PirateRadioComponents.MESSAGE, Text.literal(this.editBox.text)) + this.storageCard.set(PirateRadioComponents.FREQUENCY, this.frequency) + } + + companion object { + const val WIDTH = 192 + const val HEIGHT = 144 + + const val FREQ_WIDTH = 40 + const val FREQ_HEIGHT = 20 + + val FREQ_REGEX = Regex("^76\\.[8-9]|7[7-9]\\.[0-9]|[8-9][0-9]\\.[0-9]|10[0-7]\\.[0-9]|108\\.0$") + val FREQ_REGEX_CHARACTERS = Regex("^[0-9]*\\.?[0-9]?$") + + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt new file mode 100644 index 0000000..b69cc42 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt @@ -0,0 +1,68 @@ +package space.autistic.radio.client.sound + +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.sound.* +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadioEntityTypes +import space.autistic.radio.client.PirateRadioClient +import space.autistic.radio.entity.DisposableTransmitterEntity +import java.util.concurrent.CompletableFuture + +class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingSoundInstance( + SoundEvents.INTENTIONALLY_EMPTY, SoundCategory.MUSIC, SoundInstance.createRandom() +) { + + init { + this.repeat = false + this.repeatDelay = 0 + this.volume = 1f + this.pitch = 1f + this.relative = true + } + + override fun tick() { + if (player.isRemoved) { + this.setDone() + return + } + @Suppress("UNCHECKED_CAST") val trackedEntities: List<DisposableTransmitterEntity> = + player.clientWorld.entities.filter { it.type == PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER } + .filter { (it as DisposableTransmitterEntity).frequency <= PirateRadioClient.frequency + 1 && it.frequency >= PirateRadioClient.frequency - 1 } + .sortedByDescending { player.pos.squaredDistanceTo(it.pos) } as List<DisposableTransmitterEntity> + val main = trackedEntities.filter { it.frequency == PirateRadioClient.frequency }.take(2) + val lower = trackedEntities.find { it.frequency == PirateRadioClient.frequency - 1 } + val upper = trackedEntities.find { it.frequency == PirateRadioClient.frequency + 1 } + // TODO implement + } + + override fun getAudioStream( + loader: SoundLoader?, id: Identifier?, repeatInstantly: Boolean + ): CompletableFuture<AudioStream> { + // TODO setup thread + return CompletableFuture.completedFuture(ReceiverAudioStream) + } + + override fun getVolume(): Float { + return this.volume + } + + override fun getPitch(): Float { + return this.pitch + } + + override fun getSound(): Sound { + return Sound( + FabricSoundInstance.EMPTY_SOUND, + { 1f }, + { 1f }, + 1, + Sound.RegistrationType.SOUND_EVENT, + true, + false, + 16 + ) + } +} diff --git a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt new file mode 100644 index 0000000..5ca802b --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt @@ -0,0 +1,34 @@ +package space.autistic.radio.client.sound + +import it.unimi.dsi.fastutil.floats.FloatConsumer +import net.minecraft.client.sound.AudioStream +import net.minecraft.client.sound.ChannelList +import java.nio.ByteBuffer +import javax.sound.sampled.AudioFormat + +object ReceiverAudioStream : AudioStream { + private val format = AudioFormat(48000f, 16, 2, true, false) + + override fun close() { + // TODO, nop for now, should stop the processing + } + + override fun getFormat(): AudioFormat { + return format + } + + override fun read(size: Int): ByteBuffer { + val channelList = ChannelList(size + 8192) + + while (this.read(channelList) && channelList.currentBufferSize < size) { + } + + return channelList.buffer + } + + private fun read(channelList: FloatConsumer): Boolean { + channelList.accept(0f) + channelList.accept(0f) + return true + } +} \ No newline at end of file |