diff options
Diffstat (limited to 'src')
69 files changed, 2803 insertions, 98 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..87fd144 100644 --- a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt @@ -1,25 +1,59 @@ 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.FmFullThread +import space.autistic.radio.client.fmsim.FmSimulatorMode import space.autistic.radio.client.gui.FmReceiverScreen +import space.autistic.radio.client.sound.PirateRadioSoundInstance +import space.autistic.radio.client.sound.ReceiverAudioStream +import javax.sound.sampled.Mixer +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 + + val minecraftAudioDevice = object : Mixer.Info("", "", "", "") { + } + val openAlAudioDevice = object : Mixer.Info("", "", "", "") { + } + val systemDefaultAudioDevice = object : Mixer.Info("", "", "", "") { + } + var audioDevice: Mixer.Info = if (ReceiverAudioStream.useNativeAudio) minecraftAudioDevice else openAlAudioDevice override fun onInitializeClient() { + Thread.ofPlatform().daemon().name("fm-receiver").start(FmFullThread) + 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 +65,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/AntennaModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt index 74a7c96..c1f3e13 100644 --- a/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt +++ b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt @@ -5,15 +5,15 @@ import org.joml.Vector3d interface AntennaModel { /** * Returns the linear power level/gain to apply for a receiver at the given position. The receiver is assumed to be - * vertically oriented. + * vertically oriented. The gain should scale with distance, as appropriate. * * Note: 1.0f = 0dB, 0.5f = -3dB (approx.), 0.1f = -10dB. */ fun apply(position: Vector3d): Float /** - * Returns whether to process block/material attenuation. Useful for "global" antennas (i.e. those that return a - * constant for all positions given to [apply]). + * Returns whether to process block/material attenuation. Useful (when false) for "global" antennas (i.e. those that + * return a constant for all positions given to [apply]). */ fun shouldAttenuate(): Boolean } \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/ConstAntennaModel.kt index 3c188b6..fc531d2 100644 --- a/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt +++ b/src/client/kotlin/space/autistic/radio/client/antenna/ConstAntennaModel.kt @@ -2,9 +2,9 @@ package space.autistic.radio.client.antenna import org.joml.Vector3d -class NullModel : AntennaModel { +class ConstAntennaModel(private val level: Float) : AntennaModel { override fun apply(position: Vector3d): Float { - return 0f + return level } override fun shouldAttenuate(): Boolean { 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 7181e95..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 } @@ -33,7 +31,7 @@ class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { override fun create(orientation: Quaterniond): AntennaModel { if (failing) { - return NullModel() + return ConstAntennaModel(0f) } try { val instance = instanceBuilder!!.build() @@ -53,11 +51,9 @@ 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 NullModel() + return ConstAntennaModel(0f) } val shouldAttenuate = instance.exports().global("should-attenuate").value != 0L val apply = instance.export("apply") @@ -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,14 +80,13 @@ 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 NullModel() + return ConstAntennaModel(0f) } } 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..2e64d36 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/cli/Funny.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.client.cli + +import space.autistic.radio.client.flite.FliteWrapper + + +fun main() { +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt b/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt index bc16814..c17d622 100644 --- a/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt +++ b/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt @@ -1,10 +1,15 @@ -package space.autistic.radio.cli +package space.autistic.radio.client.cli +import com.github.ooxi.jdatauri.DataUri import org.joml.Vector2f -import space.autistic.radio.complex.cmul -import space.autistic.radio.fmsim.FmFullConstants -import space.autistic.radio.fmsim.FmFullDemodulator -import space.autistic.radio.fmsim.FmFullModulator +import space.autistic.radio.client.complex.cmul +import space.autistic.radio.client.flite.FliteWrapper +import space.autistic.radio.client.fmsim.FmFullConstants +import space.autistic.radio.client.fmsim.FmFullDemodulator +import space.autistic.radio.client.fmsim.FmFullModulator +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStream @@ -126,6 +131,29 @@ fun main(args: Array<String>) { if (inputFile.filename != "file:///dev/zero") { if (inputFile.filename.startsWith("./")) { inputFile.stream = FileInputStream(inputFile.filename) + } else if (inputFile.filename.startsWith("data:")) { + val uri = try { + DataUri.parse(inputFile.filename, Charsets.UTF_8) + } catch (e: IllegalArgumentException) { + println("error parsing data URI") + exitProcess(1) + } + if (!uri.mime.startsWith("text/")) { + println("unsupported data URI format") + exitProcess(1) + } + FliteWrapper.textToWave(uri.data.toString(uri.charset ?: Charsets.UTF_8)) { + // 8k mono -> 48k stereo float + val bbuf = ByteBuffer.allocate(it.capacity() * 2 * 6 * 4) + val fbuf = bbuf.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() + while (it.hasRemaining()) { + val sample = (it.get().toFloat() + 0.5f) / 32767.5f + for (i in 0 until 2 * 6) { + fbuf.put(sample) + } + } + inputFile.stream = ByteArrayInputStream(bbuf.array()) + } } else { inputFile.stream = URI(inputFile.filename).toPath().inputStream() } @@ -203,7 +231,7 @@ fun main(args: Array<String>) { w.y = plus100k.get() z.cmul(w) floatView.put(i, floatView.get(i) + z.x) - floatView.put(i, floatView.get(i) + z.y) + floatView.put(i + 1, floatView.get(i + 1) + z.y) } for (i in 0 until floatBufferLo.capacity() step 2) { z.x = floatBufferLo.get(i) @@ -215,7 +243,7 @@ fun main(args: Array<String>) { w.y = -minus100k.get() z.cmul(w) floatView.put(i, floatView.get(i) + z.x) - floatView.put(i, floatView.get(i) + z.y) + floatView.put(i + 1, floatView.get(i + 1) + z.y) } if (rfOutput) { outputStream.write(outputBuffer.array()) diff --git a/src/main/kotlin/space/autistic/radio/complex/Complex.kt b/src/client/kotlin/space/autistic/radio/client/complex/Complex.kt index 918dac2..7ca6811 100644 --- a/src/main/kotlin/space/autistic/radio/complex/Complex.kt +++ b/src/client/kotlin/space/autistic/radio/client/complex/Complex.kt @@ -1,4 +1,4 @@ -package space.autistic.radio.complex +package space.autistic.radio.client.complex import org.joml.Vector2f import org.joml.Vector2fc diff --git a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt b/src/client/kotlin/space/autistic/radio/client/dsp/Biquad1stOrder.kt index 8f86218..ddf5b7a 100644 --- a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt +++ b/src/client/kotlin/space/autistic/radio/client/dsp/Biquad1stOrder.kt @@ -1,4 +1,4 @@ -package space.autistic.radio.dsp +package space.autistic.radio.client.dsp class Biquad1stOrder(private val b0: Float, private val b1: Float, private val a1: Float) { private var delaySlot = 0f 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/flite/FliteFactory.kt b/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt new file mode 100644 index 0000000..768a040 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt @@ -0,0 +1,63 @@ +package space.autistic.radio.client.flite + +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.Parser +import net.fabricmc.loader.api.FabricLoader +import space.autistic.radio.wasm.Bindings +import space.autistic.radio.wasm.WasmExitException +import java.io.InputStream +import java.lang.invoke.MethodHandles +import kotlin.io.path.inputStream +import kotlin.reflect.jvm.javaMethod + +object FliteFactory : () -> Instance { + private fun fd_read(a: Int, b: Int, c: Int, d: Int): Int { + // EBADF + return 8 + } + + private fun fd_seek(a: Int, b: Long, c: Int, d: Int): Int { + // EBADF + return 8 + } + + private fun fd_close(a: Int): Int { + // EBADF + return 8 + } + + private fun fd_write(a: Int, b: Int, c: Int, d: Int): Int { + // EBADF + return 8 + } + + private fun proc_exit(status: Int): Nothing { + throw WasmExitException(status) + } + + private val lookup = MethodHandles.lookup() + private val defaultImports = ImportValues.builder() + .addFunction( + Bindings.bindFunc("wasi_snapshot_preview1", "fd_close", lookup, ::fd_close.javaMethod!!, this), + Bindings.bindFunc("wasi_snapshot_preview1", "fd_read", lookup, ::fd_read.javaMethod!!, this), + Bindings.bindFunc("wasi_snapshot_preview1", "fd_write", lookup, ::fd_write.javaMethod!!, this), + Bindings.bindFunc("wasi_snapshot_preview1", "fd_seek", lookup, ::fd_seek.javaMethod!!, this), + Bindings.bindFunc("wasi_snapshot_preview1", "proc_exit", lookup, ::proc_exit.javaMethod!!, this), + ).build() + private val module = Parser.parse(getModuleInputStream()) + private val instanceBuilder = + Instance.builder(module) + .withMachineFactory(AotMachineFactory(module)) + .withImportValues(defaultImports) + + override fun invoke(): Instance = instanceBuilder.build() + + private fun getModuleInputStream(): InputStream { + return FabricLoader.getInstance().getModContainer("pirate-radio").flatMap { it.findPath("flite.wasm") } + .map { it.inputStream() }.orElseGet { + this.javaClass.getResourceAsStream("/flite.wasm") + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/flite/FliteWrapper.kt b/src/client/kotlin/space/autistic/radio/client/flite/FliteWrapper.kt new file mode 100644 index 0000000..e73d4d0 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/flite/FliteWrapper.kt @@ -0,0 +1,35 @@ +package space.autistic.radio.client.flite + +import com.dylibso.chicory.runtime.ByteBufferMemory +import space.autistic.radio.reflection.getBuffer +import java.nio.ByteOrder +import java.nio.ShortBuffer +import java.util.function.Consumer + +object FliteWrapper { + // Produces audio at 8kHz mono + fun textToWave(s: String, consumer: Consumer<ShortBuffer>) { + val instance = FliteFactory.invoke() + instance.export("_initialize").apply() + + val voice = instance.export("flite_wrapper_init").apply()[0] + + val textToWaveImpl = instance.export("flite_text_to_wave") + val mallocImpl = instance.export("malloc") + val memory = instance.memory() as ByteBufferMemory + + val bytes = s.toByteArray() + val space = mallocImpl.apply((bytes.size + 1).toLong())[0].toInt() + memory.getBuffer().run { + put(space, bytes) + put(space + bytes.size, 0) + } + val wavedata = textToWaveImpl.apply(space.toLong(), voice)[0].toInt() + val numSamples = memory.readInt(wavedata + 8) + val samplesPtr = memory.readInt(wavedata + 16) + consumer.accept( + memory.getBuffer().slice(samplesPtr, numSamples * 2).order(ByteOrder.LITTLE_ENDIAN) + .asShortBuffer() + ) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt new file mode 100644 index 0000000..9639a38 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt @@ -0,0 +1,86 @@ +package space.autistic.radio.client.fmsim + +import org.joml.Vector2f +import space.autistic.radio.client.complex.cmul +import java.nio.FloatBuffer +import java.util.concurrent.ThreadLocalRandom +import java.util.function.Consumer + +// FIXME use more realistic model +class FastModulatedNoise(which: Which) { + + private val buffer = when (which) { + Which.BASE -> FloatBuffer.wrap(baseNoise) + Which.UPPER -> FloatBuffer.wrap(upperNoise) + Which.LOWER -> FloatBuffer.wrap(upperNoise) + } + private val flipSpectrum = which == Which.LOWER + private val outBuffer = FloatBuffer.allocate(2 * FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K) + + // complex noise, in IQ format + fun generateNoise(power: Float, consumer: Consumer<FloatBuffer>) { + outBuffer.clear() + while (outBuffer.hasRemaining()) { + if (!buffer.hasRemaining()) { + buffer.clear() + } + if (flipSpectrum) { + outBuffer.put(buffer.get() * power) + outBuffer.put(-buffer.get() * power) + } else { + outBuffer.put(buffer.get() * power) + outBuffer.put(buffer.get() * power) + } + } + outBuffer.clear() + consumer.accept(outBuffer) + } + + enum class Which { + LOWER, BASE, UPPER + } + + companion object { + // 1 second + private val baseNoise = FloatArray(300000 * 2) + private val upperNoise = FloatArray(300000 * 2) + + init { + val fmsim = FmFullModulator() + val buffer = FloatBuffer.wrap(baseNoise) + val input = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2) + val random = ThreadLocalRandom.current() + while (buffer.hasRemaining()) { + input.clear() + while (input.hasRemaining()) { + val sample = random.nextFloat(1f) + input.put(sample) + input.put(sample) + } + input.clear() + fmsim.process(input, 1f, false) { + if (buffer.remaining() < it.remaining()) { + it.limit(it.position() + buffer.remaining()) + } + buffer.put(it) + } + } + buffer.clear() + val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) + val z = Vector2f() + val w = Vector2f() + for (i in baseNoise.indices step 2) { + z.x = baseNoise[i] + z.y = baseNoise[i + 1] + if (!plus100k.hasRemaining()) { + plus100k.clear() + } + w.x = plus100k.get() + w.y = plus100k.get() + z.cmul(w) + upperNoise[i] = z.x + upperNoise[i] = z.y + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullConstants.kt index 6b92328..f5a49ce 100644 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullConstants.kt @@ -1,4 +1,4 @@ -package space.autistic.radio.fmsim +package space.autistic.radio.client.fmsim import kotlin.math.PI import kotlin.math.cos diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullDemodulator.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullDemodulator.kt index de44e69..7cf15af 100644 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullDemodulator.kt +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullDemodulator.kt @@ -1,11 +1,11 @@ -package space.autistic.radio.fmsim +package space.autistic.radio.client.fmsim import org.joml.Vector2f import org.jtransforms.fft.FloatFFT_1D -import space.autistic.radio.complex.I -import space.autistic.radio.complex.cmul -import space.autistic.radio.complex.conjugate -import space.autistic.radio.dsp.Biquad1stOrder +import space.autistic.radio.client.complex.I +import space.autistic.radio.client.complex.cmul +import space.autistic.radio.client.complex.conjugate +import space.autistic.radio.client.dsp.Biquad1stOrder import java.nio.FloatBuffer import java.util.function.BiConsumer @@ -145,6 +145,10 @@ class FmFullDemodulator { } } + fun flush(stereo: Boolean, consumer: BiConsumer<Boolean, FloatBuffer>) { + process(FloatBuffer.allocate(inputBuffer.remaining()), stereo, consumer) + } + companion object { private val fft300k = FloatFFT_1D(FmFullConstants.FFT_SIZE_48K_300K.toLong()) private val fft48k = FloatFFT_1D(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1.toLong()) diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullModulator.kt index 1f3849e..65e208a 100644 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullModulator.kt @@ -1,16 +1,15 @@ -package space.autistic.radio.fmsim +package space.autistic.radio.client.fmsim import org.joml.Vector2f -import space.autistic.radio.complex.cmul -import space.autistic.radio.complex.conjugate -import space.autistic.radio.dsp.Biquad1stOrder +import space.autistic.radio.client.complex.cmul +import space.autistic.radio.client.complex.conjugate +import space.autistic.radio.client.dsp.Biquad1stOrder import java.nio.FloatBuffer import java.util.function.Consumer import org.jtransforms.fft.FloatFFT_1D -import space.autistic.radio.complex.I +import space.autistic.radio.client.complex.I import kotlin.math.max import kotlin.math.min -import kotlin.math.sqrt class FmFullModulator { private val leftPlusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt new file mode 100644 index 0000000..bce7a72 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt @@ -0,0 +1,294 @@ +package space.autistic.radio.client.fmsim + +import net.minecraft.client.sound.SoundSystem +import net.minecraft.util.math.MathHelper +import org.joml.Vector2f +import space.autistic.radio.PirateRadio +import space.autistic.radio.client.PirateRadioClient +import space.autistic.radio.client.complex.cmul +import space.autistic.radio.client.sound.PirateRadioSoundInstance +import space.autistic.radio.client.sound.ReceiverAudioStream +import space.autistic.radio.client.util.LevenshteinDistance +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer +import java.util.UUID +import java.util.concurrent.ArrayBlockingQueue +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.LineUnavailableException +import javax.sound.sampled.Mixer +import kotlin.math.max + +object FmFullThread : Runnable { + class FmTask( + val trackedTransmitters: Map<UUID, PirateRadioSoundInstance.TrackedTransmitter>, + val noiseLevels: FloatArray, + val audioOutput: Mixer.Info, + val minecraftSoundDevice: String?, + ) + + // empty task, marker to shut off the thread + val EMPTY_TASK = FmTask(emptyMap(), FloatArray(3), PirateRadioClient.minecraftAudioDevice, null) + + val trackedTransmitterQueue = ArrayBlockingQueue<FmTask>(8) + + private class TtsModulator( + val buffer: FloatBuffer, + val modulator: FmFullModulator, + var power: Float, + var repeatTimeout: Int, + var mixingBuffer: FloatBuffer + ) + + // 3 seconds + private const val REPEAT_TIMEOUT = 8000 * 3 + + // default to 0.05s (1/20) + val bufferSize = System.getProperty("space.autistic.radio.buffer.size", "").toIntOrNull() ?: 2400 + + override fun run() { + var currentTask = EMPTY_TASK + val modulators = HashMap<UUID, TtsModulator>() + val mixingBuffers = Array(3) { FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K * 2) } + + val inputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2) + + val noiseGens = Array(3) { FastModulatedNoise(FastModulatedNoise.Which.entries[it]) } + + // -120dB or so + val noiseFloor = NoiseFloor(1e-12f) + + val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) + val minus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) + + val demodulator = FmFullDemodulator() + + // for native audio only + val outputBytes = ByteBuffer.allocate(bufferSize * 2 * 2).order(ByteOrder.LITTLE_ENDIAN) + var nativeAudio = if (ReceiverAudioStream.useNativeAudio) { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } else { + null + } + var lastOutput = PirateRadioClient.audioDevice + var lastMinecraftSoundDevice: String? = null + + try { + while (!Thread.interrupted()) { + currentTask = trackedTransmitterQueue.poll() ?: currentTask + if (currentTask === EMPTY_TASK) { + Thread.onSpinWait() + continue + } + + val audioOutput = currentTask.audioOutput + if (lastOutput != audioOutput && audioOutput != PirateRadioClient.minecraftAudioDevice) { + nativeAudio = when (audioOutput) { + PirateRadioClient.systemDefaultAudioDevice -> { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } + + PirateRadioClient.openAlAudioDevice -> { + null + } + + else -> { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format, audioOutput) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } + } + } + if (audioOutput == PirateRadioClient.minecraftAudioDevice) { + if (lastMinecraftSoundDevice != currentTask.minecraftSoundDevice || lastOutput != audioOutput) { + lastMinecraftSoundDevice = currentTask.minecraftSoundDevice + nativeAudio = if (lastMinecraftSoundDevice == "") { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } else { + val device = lastMinecraftSoundDevice!!.removePrefix(SoundSystem.OPENAL_SOFT_ON) + try { + AudioSystem.getSourceDataLine( + ReceiverAudioStream.format, + AudioSystem.getMixerInfo().filter { + AudioSystem.getMixer(it).sourceLineInfo.isNotEmpty() + }.map { + it to LevenshteinDistance.calculate(it.description, device) + }.minByOrNull { it.second }?.first + ).apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } + } + } + lastOutput = audioOutput + + modulators.keys.retainAll(currentTask.trackedTransmitters.keys) + currentTask.trackedTransmitters.forEach { (k, v) -> + modulators.compute(k) { _, modulator -> + if (modulator != null) { + modulator.power = v.power + modulator.mixingBuffer = mixingBuffers[v.frequencyOffset + 1] + return@compute modulator + } + val audioData = v.audio.getNow(null) + if (audioData != null) { + val buf = FloatBuffer.wrap(audioData) + var actualSampleOffset = Math.floorMod(v.sampleOffset, (buf.capacity() + REPEAT_TIMEOUT)) + var repeatTimeout = max(0, actualSampleOffset - buf.capacity()) + if (repeatTimeout > 0) { + actualSampleOffset = 0 + repeatTimeout = REPEAT_TIMEOUT - repeatTimeout + } + if (actualSampleOffset == buf.capacity()) { + actualSampleOffset = 0 + repeatTimeout = REPEAT_TIMEOUT + } + buf.position(actualSampleOffset) + TtsModulator( + buf, FmFullModulator(), v.power, repeatTimeout, mixingBuffers[v.frequencyOffset + 1] + ) + } else { + null + } + } + } + + mixingBuffers.forEach { + it.clear() + while (it.hasRemaining()) it.put(0f) + it.clear() + } + + modulators.values.forEach { + inputBuffer.clear() + while (inputBuffer.hasRemaining()) { + val sample = if (it.repeatTimeout > 0) { + it.repeatTimeout-- + 0f + } else { + it.buffer.get() + } + if (!it.buffer.hasRemaining()) { + it.repeatTimeout = REPEAT_TIMEOUT + it.buffer.clear() + } + for (i in 0 until 2 * 6) inputBuffer.put(sample) + } + inputBuffer.clear() + val mixingBuffer = it.mixingBuffer + it.modulator.process(inputBuffer, it.power, false) { outputBuffer -> + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } + } + } + + if (modulators.any { it.value.mixingBuffer === mixingBuffers[2] }) { + val floatBufferHi = mixingBuffers[2] + val floatView = mixingBuffers[1] + val z = Vector2f() + val w = Vector2f() + for (i in 0 until floatBufferHi.capacity() step 2) { + z.x = floatBufferHi.get(i) + z.y = floatBufferHi.get(i + 1) + if (!plus100k.hasRemaining()) { + plus100k.clear() + } + w.x = plus100k.get() + w.y = plus100k.get() + z.cmul(w) + floatView.put(i, floatView.get(i) + z.x) + floatView.put(i + 1, floatView.get(i + 1) + z.y) + } + } + + if (modulators.any { it.value.mixingBuffer === mixingBuffers[0] }) { + val floatBufferLo = mixingBuffers[0] + val floatView = mixingBuffers[1] + val z = Vector2f() + val w = Vector2f() + for (i in 0 until floatBufferLo.capacity() step 2) { + z.x = floatBufferLo.get(i) + z.y = floatBufferLo.get(i + 1) + if (!minus100k.hasRemaining()) { + minus100k.clear() + } + w.x = minus100k.get() + w.y = -minus100k.get() + z.cmul(w) + floatView.put(i, floatView.get(i) + z.x) + floatView.put(i + 1, floatView.get(i + 1) + z.y) + } + } + + noiseGens.forEachIndexed { index, v -> + if (currentTask.noiseLevels[index] != 0f) { + v.generateNoise(currentTask.noiseLevels[index]) { outputBuffer -> + val mixingBuffer = mixingBuffers[1] + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } + } + } + } + + noiseFloor.noiseBlock { outputBuffer -> + val mixingBuffer = mixingBuffers[1] + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } + } + + @Suppress("NAME_SHADOWING") + val nativeAudio = nativeAudio + demodulator.process(mixingBuffers[1], PirateRadioClient.stereo) { _, audioBuffer -> + // TODO stereo pilot + // we *want* backpressure + // FIXME use bigger buffers? + if (nativeAudio != null) { + while (audioBuffer.hasRemaining()) { + if (!outputBytes.hasRemaining()) { + val written = nativeAudio.write(outputBytes.array(), 0, outputBytes.capacity()) + outputBytes.position(written).compact() + if (written == 0) { + nativeAudio.start() + continue + } + } + val volume = PirateRadioClient.volume + outputBytes.putShort( + (MathHelper.clamp( + (audioBuffer.get() * 32767.5f - 0.5f).toInt(), -32768, 32767 + ) * volume * volume / 100).toShort() + ) + } + } else { + ReceiverAudioStream.bufferQueue.put( + FloatBuffer.allocate(audioBuffer.capacity()).put(audioBuffer).clear() + ) + } + } + } + } catch (e: Throwable) { + PirateRadio.logger.error("Quitting FM simulation thread, as something went wrong!", e) + // for some reason it doesn't print stack trace but we still wanna propagate the exception to the thread + // itself + throw e + } + } +} 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/fmsim/NoiseFloor.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt new file mode 100644 index 0000000..92df212 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt @@ -0,0 +1,31 @@ +package space.autistic.radio.client.fmsim + +import java.nio.FloatBuffer +import java.util.concurrent.ThreadLocalRandom +import java.util.function.Consumer + +class NoiseFloor(level: Float) { + private val buffer = FloatBuffer.allocate(300000 * 2) + private val outputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K * 2) + + init { + // FIXME is this how you generate IQ noise? + val random = ThreadLocalRandom.current() + val dLevel = level.toDouble() + while (buffer.hasRemaining()) { + buffer.put(random.nextGaussian(0.0, dLevel).toFloat()) + buffer.put(0f) + } + } + + // complex noise, in IQ format? + fun noiseBlock(consumer: Consumer<FloatBuffer>) { + outputBuffer.clear() + while (outputBuffer.hasRemaining()) { + if (!buffer.hasRemaining()) buffer.clear() + outputBuffer.put(buffer.get()) + } + outputBuffer.clear() + consumer.accept(outputBuffer) + } +} \ No newline at end of file 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..8886498 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,809 @@ 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.gui.widget.CyclingButtonWidget +import net.minecraft.client.gui.widget.SliderWidget +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.util.InputUtil +import net.minecraft.screen.ScreenTexts +import net.minecraft.text.StringVisitable import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.minecraft.util.Identifier +import net.minecraft.util.math.MathHelper +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 java.util.* +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.Mixer +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 var frequencySlider = makeFrequencySlider(0, 0, 320, 20) + private fun makeFrequencySlider(x: Int, y: Int, width: Int, height: Int) = object : SliderWidget( + x, y, width, height, ScreenTexts.EMPTY, (PirateRadioClient.frequency - 768).toDouble() / (1080.0 - 768.0) + ) { + init { + updateMessage() + } + + override fun updateMessage() { + message = Text.translatable( + "pirate-radio.frequency.selected", PirateRadioClient.frequency / 10, PirateRadioClient.frequency % 10 + ) + } + + override fun applyValue() { + PirateRadioClient.frequency = MathHelper.clampedLerp(768.0, 1080.0, value).toInt() + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val oldValue = this.value + val oldFrequency = PirateRadioClient.frequency + val changed = super.keyPressed(keyCode, scanCode, modifiers) + val bl = keyCode == GLFW.GLFW_KEY_LEFT + if (bl || keyCode == GLFW.GLFW_KEY_RIGHT) { + if (changed) { + val delta = if (bl) -1 else 1 + val newValue = (oldFrequency + delta - 768).toDouble() / (1080.0 - 768.0) + this.value = MathHelper.clamp(newValue, 0.0, 1.0) + if (oldValue != this.value) { + this.applyValue() + } + + this.updateMessage() + } + } + return changed + } + } + + private var volumeSlider = makeVolumeSlider(0, 20, 320, 20) + private fun makeVolumeSlider(x: Int, y: Int, width: Int, height: Int) = object : SliderWidget( + x, y, width, height, ScreenTexts.EMPTY, PirateRadioClient.volume.toDouble() / 10.0 + ) { + init { + updateMessage() + } + + override fun updateMessage() { + message = if (PirateRadioClient.volume == 0) { + Text.translatable("pirate-radio.volume.selected", Text.translatable("pirate-radio.volume.off")) + } else { + Text.translatable("pirate-radio.volume.selected", PirateRadioClient.volume) + } + } + + override fun applyValue() { + PirateRadioClient.volume = MathHelper.clampedLerp(0.0, 10.0, value).toInt() + } + + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val oldValue = this.value + val oldVolume = PirateRadioClient.volume + val changed = super.keyPressed(keyCode, scanCode, modifiers) + val bl = keyCode == GLFW.GLFW_KEY_LEFT + if (bl || keyCode == GLFW.GLFW_KEY_RIGHT) { + if (changed) { + val delta = if (bl) -1 else 1 + val newValue = (oldVolume + delta).toDouble() / 10.0 + this.value = MathHelper.clamp(newValue, 0.0, 1.0) + if (oldValue != this.value) { + this.applyValue() + } + + this.updateMessage() + } + } + return changed + } + } + + 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() + private val cycleModes = CyclingButtonWidget.builder { info: FmSimulatorMode -> + when (info) { + FmSimulatorMode.FAST -> Text.translatable("pirate-radio.mode.fast") + FmSimulatorMode.FULL -> Text.translatable("pirate-radio.mode.full") + FmSimulatorMode.DEAF -> Text.translatable("pirate-radio.mode.deaf") + } + }.values( + listOf( + FmSimulatorMode.FULL, FmSimulatorMode.FAST + ) + ).initially(PirateRadioClient.mode).build(0, 40, 320, 20, Text.translatable("pirate-radio.mode")) { _, it -> + PirateRadioClient.mode = it + } + private val audioDevice = CyclingButtonWidget.builder { info: Mixer.Info -> + when (info) { + PirateRadioClient.minecraftAudioDevice -> Text.translatable("pirate-radio.device.system-matching-minecraft") + PirateRadioClient.systemDefaultAudioDevice -> Text.translatable("pirate-radio.device.system") + PirateRadioClient.openAlAudioDevice -> Text.translatable("pirate-radio.device.openal") + else -> Text.literal(info.description) + } + }.values( + listOf( + PirateRadioClient.minecraftAudioDevice, + PirateRadioClient.systemDefaultAudioDevice, + PirateRadioClient.openAlAudioDevice, + ), AudioSystem.getMixerInfo().filter { + AudioSystem.getMixer(it).sourceLineInfo.isNotEmpty() + }).initially(PirateRadioClient.audioDevice) + .build(0, 60, 320, 20, Text.translatable("pirate-radio.device.choose")) { _, it -> + PirateRadioClient.audioDevice = it + } + + private val buttons = arrayListOf<ClickableWidget>( + frequencyPlusWidget, frequencyMinusWidget, volumePlusWidget, volumeMinusWidget, toggleModes, audioDevice + ) + 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 { + setBaseLayout(0) + instance!!.export("init").apply() + } 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 audioDeviceWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + audioDevice.setDimensionsAndPosition(width, height, x, y) + } + + private fun buttonsDimensions(index: Int, x: Int, y: Int, width: Int, height: Int): Boolean { + if (index < 0 || index >= buttons.size) { + return false + } + buttons[index].setDimensionsAndPosition(width, height, x, y) + return true + } + + 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 + + private fun setBaseLayout(layout: Int) { + when (layout) { + 0 -> { + clearChildren() + addDrawableChild(frequencyPlusWidget) + addDrawableChild(frequencyMinusWidget) + addDrawableChild(volumePlusWidget) + addDrawableChild(volumeMinusWidget) + addDrawableChild(toggleModes) + addDrawableChild(audioDevice) + buttons.clear() + buttons.addAll( + listOf( + frequencyPlusWidget, + frequencyMinusWidget, + volumePlusWidget, + volumeMinusWidget, + toggleModes, + audioDevice + ) + ) + } + + 1 -> { + clearChildren() + frequencySlider = makeFrequencySlider( + frequencySlider.x, + frequencySlider.y, + frequencySlider.width, + frequencySlider.height + ) + volumeSlider = makeVolumeSlider(volumeSlider.x, volumeSlider.y, volumeSlider.width, volumeSlider.height) + addDrawableChild(frequencySlider) + addDrawableChild(volumeSlider) + addDrawableChild(cycleModes) + addDrawableChild(audioDevice) + cycleModes.value = PirateRadioClient.mode + buttons.clear() + buttons.addAll(listOf(frequencySlider, volumeSlider, cycleModes, audioDevice)) + } + + else -> throw WasmScreenException("Invalid base layout: $layout", null) + } + } + + // 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( + "audio-device-widget", "set-dimensions", lookup, this::audioDeviceWidgetDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "buttons", "set-dimensions", lookup, this::buttonsDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "buttons", "set-base-layout", lookup, this::setBaseLayout.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..6e5da3a --- /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]|100\\.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/irc/IRC.kt b/src/client/kotlin/space/autistic/radio/client/irc/IRC.kt new file mode 100644 index 0000000..ee9a99c --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/irc/IRC.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.client.irc + +/** + * Internet Radio Client. + */ +class IRC { +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt b/src/client/kotlin/space/autistic/radio/client/opus/OpusDecoder.kt index 56fce2b..98e80d4 100644 --- a/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt +++ b/src/client/kotlin/space/autistic/radio/client/opus/OpusDecoder.kt @@ -1,4 +1,4 @@ -package space.autistic.radio.opus +package space.autistic.radio.client.opus import com.dylibso.chicory.runtime.ByteBufferMemory import space.autistic.radio.reflection.getBuffer diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt b/src/client/kotlin/space/autistic/radio/client/opus/OpusFactory.kt index 70e0c3c..1562a57 100644 --- a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt +++ b/src/client/kotlin/space/autistic/radio/client/opus/OpusFactory.kt @@ -1,4 +1,4 @@ -package space.autistic.radio.opus +package space.autistic.radio.client.opus import com.dylibso.chicory.experimental.aot.AotMachineFactory import com.dylibso.chicory.runtime.ImportValues 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..435a488 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt @@ -0,0 +1,165 @@ +package space.autistic.radio.client.sound + +import com.dylibso.chicory.wasm.ChicoryException +import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance +import net.minecraft.client.MinecraftClient +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.client.flite.FliteWrapper +import space.autistic.radio.client.fmsim.FmFullThread +import space.autistic.radio.client.fmsim.FmFullThread.trackedTransmitterQueue +import space.autistic.radio.client.fmsim.FmSimulatorMode +import space.autistic.radio.entity.DisposableTransmitterEntity +import space.autistic.radio.wasm.WasmExitException +import java.lang.ref.WeakReference +import java.nio.FloatBuffer +import java.util.UUID +import java.util.concurrent.CompletableFuture +import kotlin.math.PI + +class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingSoundInstance( + SoundEvents.INTENTIONALLY_EMPTY, SoundCategory.MUSIC, SoundInstance.createRandom() +) { + private val futuresCache = HashMap<String, WeakReference<CompletableFuture<FloatArray>>>() + + class TrackedTransmitter( + val power: Float, val sampleOffset: Int, val audio: CompletableFuture<FloatArray>, val frequencyOffset: Int + ) { + override fun toString(): String { + return "TrackedTransmitter(power=$power, sampleOffset=$sampleOffset, audio=$audio, frequencyOffset=$frequencyOffset)" + } + } + + 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 + } + // find relevant entities + @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 && it.text.isNotEmpty() } + .sortedBy { player.pos.squaredDistanceTo(it.pos) } as List<DisposableTransmitterEntity> + val main = trackedEntities.filter { it.frequency == PirateRadioClient.frequency } + .take(if (PirateRadioClient.mode == FmSimulatorMode.FAST) 1 else 2) + val lower = trackedEntities.filter { it.frequency == PirateRadioClient.frequency - 1 } + .take(if (PirateRadioClient.mode == FmSimulatorMode.FAST) 0 else 1) + val upper = trackedEntities.filter { it.frequency == PirateRadioClient.frequency + 1 } + .take(if (PirateRadioClient.mode == FmSimulatorMode.FAST) 0 else 1) + val mainNoise = trackedEntities.filter { it.frequency == PirateRadioClient.frequency }.drop(main.size) + .fold(0f) { noise, entity -> + noise + getPowerReceived(entity.pos.squaredDistanceTo(player.pos)) + } + val lowerNoise = trackedEntities.filter { it.frequency == PirateRadioClient.frequency - 1 }.drop(lower.size) + .fold(0f) { noise, entity -> + noise + getPowerReceived(entity.pos.squaredDistanceTo(player.pos)) + } + val upperNoise = trackedEntities.filter { it.frequency == PirateRadioClient.frequency + 1 }.drop(upper.size) + .fold(0f) { noise, entity -> + noise + getPowerReceived(entity.pos.squaredDistanceTo(player.pos)) + } + // updated tracked transmitters + val trackedTransmitters: MutableMap<UUID, TrackedTransmitter> = HashMap() + listOf(lower, main, upper).flatten().associateTo(trackedTransmitters) { + val text = it.text + val audio = futuresCache[text]?.get() ?: CompletableFuture.supplyAsync { + try { + lateinit var buffer: FloatBuffer + FliteWrapper.textToWave(text) { + buffer = FloatBuffer.allocate(it.capacity()) + while (it.hasRemaining()) { + val sample = (it.get().toFloat() + 0.5f) / 32767.5f + buffer.put(sample) + } + } + buffer.array() + } catch (e: ChicoryException) { + floatArrayOf() + } catch (e: WasmExitException) { + floatArrayOf() + } + } + futuresCache[text] = WeakReference(audio) + it.uuid to TrackedTransmitter( + getPowerReceived(it.pos.squaredDistanceTo(player.pos)), + it.age * (8000 / 20), + audio, + it.frequency - PirateRadioClient.frequency + ) + } + + + val audioOutput = PirateRadioClient.audioDevice + val minecraftSoundDevice = if (audioOutput === PirateRadioClient.minecraftAudioDevice) { + MinecraftClient.getInstance().options.soundDevice.value + } else { + null + } + + // this can be empty but it is not EMPTY_TASK + trackedTransmitterQueue.offer( + FmFullThread.FmTask( + trackedTransmitters, floatArrayOf(lowerNoise, mainNoise, upperNoise), audioOutput, minecraftSoundDevice + ) + ) + volume = PirateRadioClient.volume.toFloat() / 10 + volume *= volume + } + + private fun getPowerReceived(rsq: Double): Float { + // https://www.antenna-theory.com/basics/friis.php + // also fudge results if too close to transmitter + if (rsq < 1.0) return (FRIIS_FACTOR * rsq).toFloat() + return (FRIIS_FACTOR / rsq).toFloat() + } + + 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 + ) + } + + companion object { + private const val TRANSMIT_POWER = 0.01 + + // for 100MHz + private const val WAVELENGTH = 3.0 + private const val UNIT_SPHERE = 4.0 * PI + private const val FRIIS_FACTOR = TRANSMIT_POWER * WAVELENGTH / (UNIT_SPHERE * UNIT_SPHERE) + } +} 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..34d5585 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt @@ -0,0 +1,45 @@ +package space.autistic.radio.client.sound + +import it.unimi.dsi.fastutil.floats.FloatConsumer +import net.minecraft.client.sound.BufferedAudioStream +import space.autistic.radio.client.fmsim.FmFullConstants +import space.autistic.radio.client.fmsim.FmFullThread +import space.autistic.radio.client.fmsim.FmFullThread.trackedTransmitterQueue +import java.nio.FloatBuffer +import java.util.Properties +import java.util.concurrent.ArrayBlockingQueue +import javax.sound.sampled.AudioFormat +import kotlin.math.max + +object ReceiverAudioStream : BufferedAudioStream { + private val format = AudioFormat(48000f, 16, 2, true, false) + + val bufferQueue = ArrayBlockingQueue<FloatBuffer>( + max( + 0, + System.getProperty("space.autistic.radio.buffers", "").toIntOrNull() ?: 250 + ) + ) + + val useNativeAudio = System.getProperty("space.autistic.radio.output", "native") == "native" + + private val skipBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2) + get() = field.clear() + + override fun close() { + trackedTransmitterQueue.clear() + trackedTransmitterQueue.offer(FmFullThread.EMPTY_TASK) + } + + override fun getFormat(): AudioFormat { + return format + } + + override fun read(channelList: FloatConsumer): Boolean { + val buffer = bufferQueue.poll() ?: skipBuffer + while (buffer.hasRemaining()) { + channelList.accept(buffer.get()) + } + return true + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt b/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt new file mode 100644 index 0000000..6f04ce1 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt @@ -0,0 +1,54 @@ +//The MIT License (MIT) +// +//Copyright (c) 2024 +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. + +package space.autistic.radio.client.util + +// taken from https://github.com/Volcano-Bay-Studios/pavlovian-dogs/blob/master/src%2Fmain%2Fjava%2Fxyz%2Fvolcanobay%2Fpavloviandogs%2Futil%2FLevenshteinDistance.java +object LevenshteinDistance { + fun calculate(x: String, y: String): Int { + val dp = Array(x.length + 1) { IntArray(y.length + 1) } + + for (i in 0..x.length) { + for (j in 0..y.length) { + if (i == 0) { + dp[i][j] = j + } else if (j == 0) { + dp[i][j] = i + } else { + dp[i][j] = min( + dp[i - 1][j - 1] + costOfSubstitution(x[i - 1], y[j - 1]), dp[i - 1][j] + 1, dp[i][j - 1] + 1 + ) + } + } + } + + return dp[x.length][y.length] + } + + private fun costOfSubstitution(a: Char, b: Char): Int { + return if (a == b) 0 else 1 + } + + private fun min(vararg numbers: Int): Int { + return numbers.minOrNull() ?: Int.MAX_VALUE + } +} \ No newline at end of file diff --git a/src/client/resources/pirate-radio.client-mixins.json b/src/client/resources/pirate-radio.client-mixins.json new file mode 100644 index 0000000..27a5861 --- /dev/null +++ b/src/client/resources/pirate-radio.client-mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "space.autistic.radio.client.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "client": [ + "BlockStatesLoaderMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b index 072c021..d246b4f 100644 --- a/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b +++ b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b @@ -1,3 +1,3 @@ -// 1.21.1 2025-02-09T00:02:42.294183715 Pirate Radio/Recipes -84f8cd2b2d9d1afcf2a5cf000905c264a6d8267c data/pirate-radio/recipe/disposable-transmitter.json +// 1.21.1 2025-03-14T18:16:59.621431904 Pirate Radio/Recipes +368c94ec69c8320836c81014b1cfeab0742cb6e8 data/pirate-radio/recipe/disposable-transmitter.json 86e73a1d034dc407ce65e0e61af19b1db43e1939 data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json diff --git a/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 index cf1f8c7..8e7e12b 100644 --- a/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 +++ b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 @@ -1,5 +1,4 @@ -// 1.21.1 2025-02-09T00:02:42.294917543 Pirate Radio/Model Definitions -3507512497435bf1047ebd71ae1f4881ceb67f44 assets/pirate-radio/models/item/fm-receiver.json +// 1.21.1 2025-03-14T18:16:59.623037325 Pirate Radio/Model Definitions ab60b602066c94b5746065e1b139a383a6c26429 assets/pirate-radio/models/item/powerbank.json fb8af1b0939020c3a89a7736e47d9f688b38a2c9 assets/pirate-radio/models/item/storage-card.json dbc04d664dacd99a76580bcff2c5b944abb0730e assets/pirate-radio/models/item/sbc.json diff --git a/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json b/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json deleted file mode 100644 index 71813c4..0000000 --- a/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parent": "minecraft:item/generated", - "textures": { - "layer0": "pirate-radio:item/fm-receiver" - } -} \ No newline at end of file diff --git a/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json index 2a1d645..87b9be6 100644 --- a/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json +++ b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json @@ -10,9 +10,6 @@ }, { "item": "pirate-radio:powerbank" - }, - { - "item": "pirate-radio:storage-card" } ], "result": { diff --git a/src/main/kotlin/space/autistic/radio/CommonProxy.kt b/src/main/kotlin/space/autistic/radio/CommonProxy.kt new file mode 100644 index 0000000..76a4b22 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/CommonProxy.kt @@ -0,0 +1,10 @@ +package space.autistic.radio + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand + +open class CommonProxy : SidedProxy { + override fun useStorageCard(player: PlayerEntity, item: ItemStack, hand: Hand) { + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadio.kt b/src/main/kotlin/space/autistic/radio/PirateRadio.kt index 54d0b9f..2a57d10 100644 --- a/src/main/kotlin/space/autistic/radio/PirateRadio.kt +++ b/src/main/kotlin/space/autistic/radio/PirateRadio.kt @@ -1,17 +1,58 @@ package space.autistic.radio +import com.mojang.serialization.Codec import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.event.registry.DynamicRegistries +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking +import net.minecraft.item.ItemStack +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.text.Text +import net.minecraft.util.Identifier import org.slf4j.LoggerFactory +import space.autistic.radio.antenna.Antenna +import space.autistic.radio.antenna.PirateRadioAntennaSerializers +import space.autistic.radio.network.StorageCardUpdateC2SPayload +import kotlin.math.max +import kotlin.math.min object PirateRadio : ModInitializer { - const val MOD_ID = "pirate-radio" - private val logger = LoggerFactory.getLogger(MOD_ID) + var proxy: SidedProxy? = null + const val MOD_ID = "pirate-radio" + val logger = LoggerFactory.getLogger(MOD_ID) - override fun onInitialize() { - logger.info("This project is made with love by a queer trans person.\n" + - "The folks of these identities who have contributed to the project would like to make their identities known:\n" + - "Autgender; Not a person; Anticapitalist; Genderqueer; Trans.") - PirateRadioItems.initialize() - PirateRadioEntityTypes.initialize() - } + + override fun onInitialize() { + if (proxy == null) { + proxy = CommonProxy() + } + logger.info( + "This project is made with love by a queer trans person.\n" + + "The folks of these identities who have contributed to the project would like to make their identities known:\n" + + "Autgender; Not a person; Anticapitalist; Genderqueer; Trans." + ) + PirateRadioRegistries.initialize() + PirateRadioAntennaSerializers.initialize() + DynamicRegistries.registerSynced(PirateRadioRegistryKeys.ANTENNA, Antenna.CODEC) + PayloadTypeRegistry.playC2S() + .register(StorageCardUpdateC2SPayload.PAYLOAD_ID, StorageCardUpdateC2SPayload.CODEC) + ServerPlayNetworking.registerGlobalReceiver(StorageCardUpdateC2SPayload.PAYLOAD_ID) { payload, context -> + if (!context.player().isAlive) { + return@registerGlobalReceiver + } + val stack = context.player().inventory.getStack(payload.slot) + if (stack.isOf(PirateRadioItems.STORAGE_CARD)) { + stack.set(PirateRadioComponents.FREQUENCY, min(1000, max(768, payload.frequency))) + var text = payload.text + if (text.length > 16384) { + text = text.substring(0, 16384) + } + stack.set(PirateRadioComponents.MESSAGE, Text.literal(text)) + } + } + PirateRadioComponents.initialize() + PirateRadioItems.initialize() + PirateRadioEntityTypes.initialize() + } } \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioComponents.kt b/src/main/kotlin/space/autistic/radio/PirateRadioComponents.kt new file mode 100644 index 0000000..f79f26a --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioComponents.kt @@ -0,0 +1,30 @@ +package space.autistic.radio + +import net.minecraft.component.ComponentType +import net.minecraft.network.codec.PacketCodecs +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import net.minecraft.util.Identifier +import net.minecraft.util.dynamic.Codecs + +object PirateRadioComponents { + val FREQUENCY = Registry.register( + Registries.DATA_COMPONENT_TYPE, + Identifier.of(PirateRadio.MOD_ID, "frequency"), + ComponentType.builder<Int>().codec( + Codecs.rangedInt(768, 1080) + ).packetCodec(PacketCodecs.VAR_INT).build() + ) + + val MESSAGE = Registry.register( + Registries.DATA_COMPONENT_TYPE, + Identifier.of(PirateRadio.MOD_ID, "message"), + ComponentType.builder<Text>().codec(TextCodecs.STRINGIFIED_CODEC).packetCodec(TextCodecs.REGISTRY_PACKET_CODEC) + .cache().build() + ) + + fun initialize() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt index f147394..3fbb34f 100644 --- a/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt +++ b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt @@ -11,13 +11,31 @@ import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKeys import net.minecraft.util.Identifier import space.autistic.radio.entity.ElectronicsTraderEntity +import space.autistic.radio.entity.DisposableTransmitterEntity object PirateRadioEntityTypes { - val ELECTRONICS_TRADER_KEY = RegistryKey.of(RegistryKeys.ENTITY_TYPE, Identifier.of(PirateRadio.MOD_ID, "electronics-trader")) - val ELECTRONICS_TRADER = register(EntityType.Builder.create(::ElectronicsTraderEntity, SpawnGroup.MISC).dimensions(0.6F, 1.95F).eyeHeight(1.62F).maxTrackingRange(10), ELECTRONICS_TRADER_KEY) + val ELECTRONICS_TRADER_KEY = + RegistryKey.of(RegistryKeys.ENTITY_TYPE, Identifier.of(PirateRadio.MOD_ID, "electronics-trader")) + val ELECTRONICS_TRADER = register( + EntityType.Builder.create(::ElectronicsTraderEntity, SpawnGroup.MISC).dimensions(0.6F, 1.95F).eyeHeight(1.62F) + .maxTrackingRange(10), ELECTRONICS_TRADER_KEY + ) - fun <T : Entity> register(entityTypeBuilder: EntityType.Builder<T>, registryKey: RegistryKey<EntityType<*>>): EntityType<T> { - return Registry.register(Registries.ENTITY_TYPE, registryKey.value, entityTypeBuilder.build(registryKey.value.path)) + val DISPOSABLE_TRANSMITTER_KEY = RegistryKey.of(RegistryKeys.ENTITY_TYPE, Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter")) + val DISPOSABLE_TRANSMITTER = register( + EntityType.Builder.create(::DisposableTransmitterEntity, SpawnGroup.MISC).dimensions(0.5F, 0.5F).eyeHeight(0F) + .maxTrackingRange(10).trackingTickInterval(Integer.MAX_VALUE), DISPOSABLE_TRANSMITTER_KEY + ) + + fun <T : Entity> register( + entityTypeBuilder: EntityType.Builder<T>, + registryKey: RegistryKey<EntityType<*>> + ): EntityType<T> { + return Registry.register( + Registries.ENTITY_TYPE, + registryKey.value, + entityTypeBuilder.build(registryKey.value.path) + ) } fun initialize() { diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt index 490acaf..e00e4e6 100644 --- a/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt +++ b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt @@ -2,12 +2,16 @@ package space.autistic.radio import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents import net.minecraft.item.Item +import net.minecraft.item.ItemFrameItem import net.minecraft.item.ItemGroups import net.minecraft.registry.Registries import net.minecraft.registry.Registry import net.minecraft.registry.RegistryKey import net.minecraft.registry.RegistryKeys +import net.minecraft.text.Text import net.minecraft.util.Identifier +import space.autistic.radio.item.DisposableTransmitterItem +import space.autistic.radio.item.StorageCardItem object PirateRadioItems { val SBC_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "sbc")) @@ -17,11 +21,20 @@ object PirateRadioItems { val POWERBANK_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "powerbank")) val POWERBANK = register(Item(Item.Settings()), POWERBANK_KEY) val STORAGE_CARD_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "storage-card")) - val STORAGE_CARD = register(Item(Item.Settings()), STORAGE_CARD_KEY) - val DISPOSABLE_TRANSMITTER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter")) - val DISPOSABLE_TRANSMITTER = register(Item(Item.Settings()), DISPOSABLE_TRANSMITTER_KEY) - val FM_RECEIVER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "fm-receiver")) - val FM_RECEIVER = register(Item(Item.Settings()), FM_RECEIVER_KEY) + val STORAGE_CARD = register( + StorageCardItem( + Item.Settings().maxCount(1).component(PirateRadioComponents.FREQUENCY, 768) + .component(PirateRadioComponents.MESSAGE, Text.literal("")) + ), STORAGE_CARD_KEY + ) + val DISPOSABLE_TRANSMITTER_KEY = + RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter")) + val DISPOSABLE_TRANSMITTER = register( + DisposableTransmitterItem(PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER, Item.Settings()), + DISPOSABLE_TRANSMITTER_KEY + ) +// val FM_RECEIVER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "fm-receiver")) +// val FM_RECEIVER = register(Item(Item.Settings()), FM_RECEIVER_KEY) fun register(item: Item, registryKey: RegistryKey<Item>): Item { return Registry.register(Registries.ITEM, registryKey.value, item) diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioRegistries.kt b/src/main/kotlin/space/autistic/radio/PirateRadioRegistries.kt new file mode 100644 index 0000000..6010d3c --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioRegistries.kt @@ -0,0 +1,12 @@ +package space.autistic.radio + +import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder +import net.fabricmc.fabric.api.event.registry.RegistryAttribute + +object PirateRadioRegistries { + val ANTENNA_SERIALIZER = FabricRegistryBuilder.createSimple(PirateRadioRegistryKeys.ANTENNA_SERIALIZER) + .attribute(RegistryAttribute.SYNCED).buildAndRegister() + + fun initialize() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioRegistryKeys.kt b/src/main/kotlin/space/autistic/radio/PirateRadioRegistryKeys.kt new file mode 100644 index 0000000..eb5db1f --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioRegistryKeys.kt @@ -0,0 +1,13 @@ +package space.autistic.radio + +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio.MOD_ID +import space.autistic.radio.antenna.Antenna +import space.autistic.radio.antenna.AntennaSerializer + +object PirateRadioRegistryKeys { + val ANTENNA_SERIALIZER = RegistryKey.ofRegistry<AntennaSerializer<*>>(Identifier.of(MOD_ID, "antenna_serializer")) + val ANTENNA = RegistryKey.ofRegistry<Antenna<*>>(Identifier.of(MOD_ID, "antenna")) +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/SidedProxy.kt b/src/main/kotlin/space/autistic/radio/SidedProxy.kt new file mode 100644 index 0000000..0e34ec9 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/SidedProxy.kt @@ -0,0 +1,9 @@ +package space.autistic.radio + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand + +interface SidedProxy { + fun useStorageCard(player: PlayerEntity, item: ItemStack, hand: Hand) +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/antenna/Antenna.kt b/src/main/kotlin/space/autistic/radio/antenna/Antenna.kt new file mode 100644 index 0000000..c403081 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/antenna/Antenna.kt @@ -0,0 +1,9 @@ +package space.autistic.radio.antenna + +import space.autistic.radio.PirateRadioRegistries + +data class Antenna<T>(val type: AntennaSerializer<T>, val data: T) { + companion object { + val CODEC = PirateRadioRegistries.ANTENNA_SERIALIZER.codec.dispatch({ it.type }, AntennaSerializer<*>::codec) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/antenna/AntennaSerializer.kt b/src/main/kotlin/space/autistic/radio/antenna/AntennaSerializer.kt new file mode 100644 index 0000000..11d0234 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/antenna/AntennaSerializer.kt @@ -0,0 +1,8 @@ +package space.autistic.radio.antenna + +import com.mojang.serialization.MapCodec + +interface AntennaSerializer<T> { + val codec: MapCodec<Antenna<T>> + get +} diff --git a/src/main/kotlin/space/autistic/radio/antenna/ConstAntenna.kt b/src/main/kotlin/space/autistic/radio/antenna/ConstAntenna.kt new file mode 100644 index 0000000..401972c --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/antenna/ConstAntenna.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.antenna + +import com.mojang.serialization.Codec + +object ConstAntenna : AntennaSerializer<Float> { + override val codec = Codec.FLOAT.fieldOf("level").xmap({ Antenna(this, it) }, { it.data }) +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/antenna/PirateRadioAntennaSerializers.kt b/src/main/kotlin/space/autistic/radio/antenna/PirateRadioAntennaSerializers.kt new file mode 100644 index 0000000..19cfff8 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/antenna/PirateRadioAntennaSerializers.kt @@ -0,0 +1,18 @@ +package space.autistic.radio.antenna + +import net.minecraft.registry.Registry +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio +import space.autistic.radio.PirateRadioRegistries + +object PirateRadioAntennaSerializers { + val CONST = register(Identifier.of(PirateRadio.MOD_ID, "const"), ConstAntenna) + val WASM = register(Identifier.of(PirateRadio.MOD_ID, "wasm"), WasmAntenna) + + private fun <T> register(id: Identifier, antennaSerializer: AntennaSerializer<T>): AntennaSerializer<T> { + return Registry.register(PirateRadioRegistries.ANTENNA_SERIALIZER, id, antennaSerializer) + } + + fun initialize() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/antenna/WasmAntenna.kt b/src/main/kotlin/space/autistic/radio/antenna/WasmAntenna.kt new file mode 100644 index 0000000..32c96bb --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/antenna/WasmAntenna.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.antenna + +import net.minecraft.util.Identifier + +object WasmAntenna : AntennaSerializer<Identifier> { + override val codec = Identifier.CODEC.fieldOf("model").xmap({ Antenna(this, it) }, { it.data }) +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/entity/DisposableTransmitterEntity.kt b/src/main/kotlin/space/autistic/radio/entity/DisposableTransmitterEntity.kt new file mode 100644 index 0000000..b50849d --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/entity/DisposableTransmitterEntity.kt @@ -0,0 +1,178 @@ +package space.autistic.radio.entity + +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.data.DataTracker +import net.minecraft.entity.data.TrackedDataHandlerRegistry +import net.minecraft.entity.decoration.AbstractDecorationEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.nbt.NbtCompound +import net.minecraft.network.listener.ClientPlayPacketListener +import net.minecraft.network.packet.Packet +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket +import net.minecraft.server.network.EntityTrackerEntry +import net.minecraft.util.ActionResult +import net.minecraft.util.Hand +import net.minecraft.util.math.* +import net.minecraft.world.World +import net.minecraft.world.event.GameEvent +import space.autistic.radio.PirateRadioComponents +import space.autistic.radio.PirateRadioEntityTypes +import space.autistic.radio.PirateRadioItems +import kotlin.math.max +import kotlin.math.min + +class DisposableTransmitterEntity : AbstractDecorationEntity { + var despawnDelay = 60 * 60 * 20 + + constructor(type: EntityType<out DisposableTransmitterEntity>?, world: World?) : super(type, world) + + constructor( + type: EntityType<out DisposableTransmitterEntity>?, + world: World?, + pos: BlockPos, + facing: Direction + ) : super( + type, world, pos + ) { + this.setFacing(facing) + } + + constructor( + world: World, + blockPos2: BlockPos, + direction: Direction + ) : this(PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER, world, blockPos2, direction) + + override fun initDataTracker(builder: DataTracker.Builder) { + builder.add(TEXT, "") + builder.add(FREQUENCY, 768) + } + + override fun tick() { + super.tick() + if (!world.isClient) { + this.tickDespawnDelay(); + } + } + + private fun tickDespawnDelay() { + if (this.despawnDelay > 0 && --this.despawnDelay == 0) { + this.discard() + } + } + + override fun setFacing(facing: Direction) { + this.facing = facing + if (facing.axis.isHorizontal) { + this.pitch = 0.0f + this.yaw = (this.facing.horizontal * 90).toFloat() + } else { + this.pitch = (-90 * facing.direction.offset()).toFloat() + this.yaw = 0.0f + } + + this.prevPitch = this.pitch + this.prevYaw = this.yaw + this.updateAttachmentPosition() + } + + override fun calculateBoundingBox(pos: BlockPos, side: Direction): Box { + val center = Vec3d.ofCenter(pos).offset(side, -(1.0 - DEPTH) / 2.0) + val axis = side.axis + val dx = if (axis === Direction.Axis.X) DEPTH else WIDTH + val dy = if (axis === Direction.Axis.Y) DEPTH else HEIGHT + val dz = if (axis === Direction.Axis.Z) DEPTH else WIDTH + return Box.of(center, dx, dy, dz) + } + + // leave this true for performance + override fun canStayAttached(): Boolean = true + + override fun onBreak(breaker: Entity?) { + // hmm, what to do here... + } + + override fun onPlace() { + // hmm, what to do here... + } + + var text: String + get() { + return this.dataTracker[TEXT] + } + set(value) { + this.dataTracker[TEXT] = value + } + + var frequency: Int + get() { + return this.dataTracker[FREQUENCY] + } + set(value) { + this.dataTracker[FREQUENCY] = value + } + + override fun writeCustomDataToNbt(nbt: NbtCompound) { + super.writeCustomDataToNbt(nbt) + nbt.putByte("Facing", this.facing.id.toByte()) + nbt.putInt("DespawnDelay", this.despawnDelay) + nbt.putString("Text", this.text) + nbt.putBoolean("Invisible", this.isInvisible) + nbt.putInt("Frequency", this.frequency) + } + + override fun readCustomDataFromNbt(nbt: NbtCompound) { + super.readCustomDataFromNbt(nbt) + this.despawnDelay = nbt.getInt("DespawnDelay") + this.setFacing(Direction.byId(nbt.getByte("Facing").toInt())) + this.text = nbt.getString("Text") + this.isInvisible = nbt.getBoolean("Invisible") + this.frequency = min(1080, max(768, nbt.getInt("Frequency"))) + } + + override fun createSpawnPacket(entityTrackerEntry: EntityTrackerEntry): Packet<ClientPlayPacketListener> { + return EntitySpawnS2CPacket(this, facing.id, this.getAttachedBlockPos()) + } + + override fun onSpawnPacket(packet: EntitySpawnS2CPacket) { + super.onSpawnPacket(packet) + this.setFacing(Direction.byId(packet.entityData)) + } + + override fun getBodyYaw(): Float { + val direction = this.horizontalFacing + val i = if (direction.axis.isVertical) 90 * direction.direction.offset() else 0 + return MathHelper.wrapDegrees(180 + direction.horizontal * 90 + i).toFloat() + } + + override fun interact(player: PlayerEntity, hand: Hand): ActionResult { + val itemStack = player.getStackInHand(hand) + val noTextInTransmitter = this.text.isEmpty() + val isCard = itemStack.isOf(PirateRadioItems.STORAGE_CARD) + val holdingMessage = isCard && (itemStack.get(PirateRadioComponents.MESSAGE)?.literalString ?: "").isNotEmpty() + if (!world.isClient) { + if (noTextInTransmitter) { + if (holdingMessage && !this.isRemoved) { + this.frequency = itemStack.getOrDefault(PirateRadioComponents.FREQUENCY, 768) + this.text = itemStack.get(PirateRadioComponents.MESSAGE)?.literalString ?: "" + this.emitGameEvent(GameEvent.BLOCK_CHANGE, player) + itemStack.decrementUnlessCreative(1, player) + } + } + return ActionResult.CONSUME + } else { + return if (noTextInTransmitter && holdingMessage) ActionResult.SUCCESS else ActionResult.PASS + } + } + + companion object { + private val TEXT = + DataTracker.registerData(DisposableTransmitterEntity::class.java, TrackedDataHandlerRegistry.STRING) + private val FREQUENCY = + DataTracker.registerData(DisposableTransmitterEntity::class.java, TrackedDataHandlerRegistry.INTEGER) + const val DEPTH = 0.0625 + private const val WIDTH = 0.75 + private const val HEIGHT = 0.75 + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt index 3aa53b1..1acbeb4 100644 --- a/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt +++ b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt @@ -21,7 +21,7 @@ class ElectronicsTraderEntity(entityType: EntityType<out ElectronicsTraderEntity override fun fillRecipes() { val offers = this.getOffers() offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.POWERBANK), 3, 0, 0f)) - offers.add(TradeOffer(TradedItem(Items.EMERALD, 10), ItemStack(PirateRadioItems.FM_RECEIVER), 3, 0, 0f)) +// offers.add(TradeOffer(TradedItem(Items.EMERALD, 10), ItemStack(PirateRadioItems.FM_RECEIVER), 3, 0, 0f)) offers.add(TradeOffer(TradedItem(Items.EMERALD, 15), ItemStack(PirateRadioItems.SBC), 3, 0, 0f)) offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.STORAGE_CARD), 3, 0, 0f)) offers.add(TradeOffer(TradedItem(Items.EMERALD, 1), ItemStack(PirateRadioItems.WIRE), 3, 0, 0f)) @@ -29,7 +29,7 @@ class ElectronicsTraderEntity(entityType: EntityType<out ElectronicsTraderEntity override fun tickMovement() { if (!this.world.isClient) { - super.setDespawnDelay(1000) + super.setDespawnDelay(-1) } super.tickMovement() } diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt deleted file mode 100644 index 654d50f..0000000 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt +++ /dev/null @@ -1,4 +0,0 @@ -package space.autistic.radio.fmsim - -class FmFullMixer { -} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/item/DisposableTransmitterItem.kt b/src/main/kotlin/space/autistic/radio/item/DisposableTransmitterItem.kt new file mode 100644 index 0000000..c9a53e4 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/item/DisposableTransmitterItem.kt @@ -0,0 +1,56 @@ +package space.autistic.radio.item + +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.NbtComponent +import net.minecraft.entity.EntityType +import net.minecraft.entity.decoration.AbstractDecorationEntity +import net.minecraft.item.ItemFrameItem +import net.minecraft.item.ItemUsageContext +import net.minecraft.util.ActionResult +import net.minecraft.world.event.GameEvent +import space.autistic.radio.PirateRadioEntityTypes +import space.autistic.radio.entity.DisposableTransmitterEntity + +class DisposableTransmitterItem( + private val entityType: EntityType<out AbstractDecorationEntity>?, + settings: Settings? +) : + ItemFrameItem(entityType, settings) { + + override fun useOnBlock(context: ItemUsageContext): ActionResult { + val blockPos = context.blockPos + val direction = context.side + val blockPos2 = blockPos.offset(direction) + val playerEntity = context.player + val itemStack = context.stack + if (playerEntity != null && !this.canPlaceOn(playerEntity, direction, itemStack, blockPos2)) { + return ActionResult.FAIL + } else { + val world = context.world + val abstractDecorationEntity: AbstractDecorationEntity + if (this.entityType === PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER) { + abstractDecorationEntity = DisposableTransmitterEntity(world, blockPos2, direction) + } else { + return ActionResult.success(world.isClient) + } + + val nbtComponent = itemStack.getOrDefault(DataComponentTypes.ENTITY_DATA, NbtComponent.DEFAULT) + if (!nbtComponent.isEmpty) { + EntityType.loadFromEntityNbt(world, playerEntity, abstractDecorationEntity, nbtComponent) + } + + if (abstractDecorationEntity.canStayAttached()) { + if (!world.isClient) { + abstractDecorationEntity.onPlace() + world.emitGameEvent(playerEntity, GameEvent.ENTITY_PLACE, abstractDecorationEntity.pos) + world.spawnEntity(abstractDecorationEntity) + } + + itemStack.decrement(1) + return ActionResult.success(world.isClient) + } else { + return ActionResult.CONSUME + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/item/StorageCardItem.kt b/src/main/kotlin/space/autistic/radio/item/StorageCardItem.kt new file mode 100644 index 0000000..da1b057 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/item/StorageCardItem.kt @@ -0,0 +1,20 @@ +package space.autistic.radio.item + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.stat.Stats +import net.minecraft.util.Hand +import net.minecraft.util.TypedActionResult +import net.minecraft.world.World +import space.autistic.radio.PirateRadio + +class StorageCardItem(settings: Settings) : Item(settings) { + + override fun use(world: World, user: PlayerEntity, hand: Hand): TypedActionResult<ItemStack> { + val itemStack = user.getStackInHand(hand) + PirateRadio.proxy!!.useStorageCard(user, itemStack, hand) + user.incrementStat(Stats.USED.getOrCreateStat(this)) + return TypedActionResult.success(itemStack, world.isClient()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/network/StorageCardUpdateC2SPayload.kt b/src/main/kotlin/space/autistic/radio/network/StorageCardUpdateC2SPayload.kt new file mode 100644 index 0000000..9ffa75a --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/network/StorageCardUpdateC2SPayload.kt @@ -0,0 +1,27 @@ +package space.autistic.radio.network + +import net.minecraft.network.RegistryByteBuf +import net.minecraft.network.codec.PacketCodec +import net.minecraft.network.codec.PacketCodecs +import net.minecraft.network.packet.CustomPayload +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio + +@JvmRecord +data class StorageCardUpdateC2SPayload(val slot: Int, val text: String, val frequency: Int) : CustomPayload { + override fun getId(): CustomPayload.Id<out CustomPayload> = PAYLOAD_ID + + companion object { + val PAYLOAD_IDENTIFIER = Identifier.of(PirateRadio.MOD_ID, "storage-card-update") + val PAYLOAD_ID = CustomPayload.Id<StorageCardUpdateC2SPayload>(PAYLOAD_IDENTIFIER) + val CODEC: PacketCodec<RegistryByteBuf, StorageCardUpdateC2SPayload> = PacketCodec.tuple( + PacketCodecs.VAR_INT, + StorageCardUpdateC2SPayload::slot, + PacketCodecs.STRING, + StorageCardUpdateC2SPayload::text, + PacketCodecs.VAR_INT, + StorageCardUpdateC2SPayload::frequency, + ::StorageCardUpdateC2SPayload + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/wasm/Bindings.kt b/src/main/kotlin/space/autistic/radio/wasm/Bindings.kt new file mode 100644 index 0000000..5ff4102 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/wasm/Bindings.kt @@ -0,0 +1,178 @@ +package space.autistic.radio.wasm + +import com.dylibso.chicory.runtime.HostFunction +import com.dylibso.chicory.runtime.ImportFunction +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.types.Value +import com.dylibso.chicory.wasm.types.ValueType +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType + +class Bindings { + + companion object { + @JvmStatic + fun longToInt(long: Long): Int = long.toInt() + + @JvmStatic + fun stringArg(instance: Instance, address: Long): String { + return instance.memory().readCString(address.toInt()) + } + + @JvmStatic + fun boolArg(bool: Long): Boolean { + return bool != 0L + } + + @JvmStatic + fun intListArg(instance: Instance, argc: Long, argv: Long): List<Int> { + return IntArray(argc.toInt()) { + instance.memory().readInt(argv.toInt() + 4 * it) + }.toList() + } + + private val lookup = MethodHandles.lookup() + fun bindFunc( + module: String, + name: String, + inLookup: MethodHandles.Lookup, + method: Method, + receiver: Any + ): ImportFunction { + val baseHandle = inLookup.unreflect(method).bindTo(receiver) + val wasmParameters = ArrayList<ValueType>() + val filters = method.genericParameterTypes.map { + when (it) { + Int::class.java -> { + wasmParameters.add(ValueType.I32) + lookup.findStatic( + Bindings::class.java, "longToInt", MethodType.methodType(Int::class.java, Long::class.java) + ) + } + + Long::class.java -> { + wasmParameters.add(ValueType.I64) + MethodHandles.identity(Long::class.java) + } + + Float::class.java -> { + wasmParameters.add(ValueType.F32) + lookup.findStatic( + Value::class.java, "longToFloat", MethodType.methodType(Float::class.java, Long::class.java) + ) + } + + Double::class.java -> { + wasmParameters.add(ValueType.F64) + lookup.findStatic( + Value::class.java, + "longToDouble", + MethodType.methodType(Double::class.java, Long::class.java) + ) + } + + String::class.java -> { + wasmParameters.add(ValueType.I32) + lookup.findStatic( + Bindings::class.java, + "stringArg", + MethodType.methodType(String::class.java, Instance::class.java, Long::class.java) + ) + } + + Boolean::class.java -> { + wasmParameters.add(ValueType.I32) + lookup.findStatic( + Bindings::class.java, + "boolArg", + MethodType.methodType(Boolean::class.java, Long::class.java) + ) + } + + is ParameterizedType -> { + if (it.rawType == List::class.java) { + val converter = when (it.actualTypeArguments[0]) { + java.lang.Integer::class.java -> "intListArg" + else -> throw IllegalArgumentException(it.actualTypeArguments[0].toString()) + } + wasmParameters.add(ValueType.I32) + wasmParameters.add(ValueType.I32) + lookup.findStatic( + Bindings::class.java, + converter, + MethodType.methodType( + List::class.java, + Instance::class.java, + Long::class.java, + Long::class.java + ) + ) + } else { + throw IllegalArgumentException(it.toString()) + } + } + + else -> throw IllegalArgumentException(it.toString()) + } + } + val filterTypes = ArrayList<Class<*>>() + filters.forEach { methodHandle -> + filterTypes.addAll(methodHandle.type().parameterList()) + } + var i = 0 + val permutation = IntArray(filterTypes.size) { + if (filterTypes[it] == Instance::class.java) 0 else ++i + } + var handle = baseHandle + var j = 0 + filters.forEach { + handle = MethodHandles.collectArguments(handle, j, it) + j += it.type().parameterCount() + } + val newtype = MethodType.methodType( + baseHandle.type().returnType(), Instance::class.java, *Array(i) { Long::class.java }) + handle = MethodHandles.permuteArguments(handle, newtype, *permutation) + handle = handle.asSpreader(LongArray::class.java, i) + return when (method.genericReturnType) { + Void.TYPE -> HostFunction(module, name, wasmParameters, emptyList()) { instance, args -> + handle.invokeExact(instance, args) + Value.EMPTY_VALUES + } + + Void::class.java -> HostFunction(module, name, wasmParameters, emptyList()) { instance, args -> + handle.invokeExact(instance, args) + throw IllegalStateException("unreachable") + } + + Int::class.java -> HostFunction(module, name, wasmParameters, listOf(ValueType.I32)) { instance, args -> + val result: Int = handle.invokeExact(instance, args) as Int + longArrayOf(result.toLong()) + } + + Boolean::class.java -> HostFunction( + module, + name, + wasmParameters, + listOf(ValueType.I32) + ) { instance, args -> + val result: Boolean = handle.invokeExact(instance, args) as Boolean + longArrayOf(if (result) 1L else 0L) + } + + Float::class.java -> HostFunction( + module, + name, + wasmParameters, + listOf(ValueType.F32) + ) { instance, args -> + val result: Float = handle.invokeExact(instance, args) as Float + longArrayOf(Value.floatToLong(result)) + } + + else -> throw IllegalArgumentException(method.genericReturnType.toString()) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/wasm/WasmExitException.kt b/src/main/kotlin/space/autistic/radio/wasm/WasmExitException.kt new file mode 100644 index 0000000..43c08be --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/wasm/WasmExitException.kt @@ -0,0 +1,4 @@ +package space.autistic.radio.wasm + +class WasmExitException(val status: Int) : Exception() { +} diff --git a/src/main/resources/assets/pirate-radio/blockstates/disposable-transmitter.json b/src/main/resources/assets/pirate-radio/blockstates/disposable-transmitter.json new file mode 100644 index 0000000..854abc8 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/blockstates/disposable-transmitter.json @@ -0,0 +1,10 @@ +{ + "variants": { + "facing=down": { "model": "pirate-radio:block/disposable-transmitter-vertical" }, + "facing=up": { "model": "pirate-radio:block/disposable-transmitter-vertical" }, + "facing=north": { "model": "pirate-radio:block/disposable-transmitter" }, + "facing=south": { "model": "pirate-radio:block/disposable-transmitter" }, + "facing=west": { "model": "pirate-radio:block/disposable-transmitter" }, + "facing=east": { "model": "pirate-radio:block/disposable-transmitter" } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/lang/en_us.json b/src/main/resources/assets/pirate-radio/lang/en_us.json index 9627729..974deb9 100644 --- a/src/main/resources/assets/pirate-radio/lang/en_us.json +++ b/src/main/resources/assets/pirate-radio/lang/en_us.json @@ -6,5 +6,29 @@ "item.pirate-radio.disposable-transmitter": "Disposable Pirate Radio Transmitter", "item.pirate-radio.fm-receiver": "FM Receiver", "entity.pirate-radio.electronics-trader": "Microcenter", - "pirate-radio.fm-receiver": "FM Receiver" + "pirate-radio.fm-receiver": "FM Receiver", + "pirate-radio.skin-pack": "Skin pack: %s", + "pirate-radio.frequency.plus": "+", + "pirate-radio.frequency.minus": "-", + "pirate-radio.volume.plus": "+", + "pirate-radio.volume.minus": "-", + "pirate-radio.mode": "Mode", + "pirate-radio.frequency.plus.narrated": "Increase Frequency", + "pirate-radio.frequency.minus.narrated": "Decrease Frequency", + "pirate-radio.volume.plus.narrated": "Increase Volume", + "pirate-radio.volume.minus.narrated": "Decrease Volume", + "pirate-radio.mode.selected": "Mode: %s", + "pirate-radio.mode.full": "Full", + "pirate-radio.mode.fast": "Fast", + "pirate-radio.mode.deaf": "Deaf", + "pirate-radio.volume.selected": "Volume: %s", + "pirate-radio.volume.off": "Off", + "pirate-radio.frequency.selected": "Frequency: %s.%s MHz", + "pirate-radio.storage-card": "SD Card", + "pirate-radio.message": "Message...", + "pirate-radio.frequency.edit": "Frequency", + "pirate-radio.device.choose": "Audio output", + "pirate-radio.device.system": "Direct (System default)", + "pirate-radio.device.openal": "Minecraft sound system", + "pirate-radio.device.system-matching-minecraft": "Direct (guess from Minecraft)" } \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter-vertical.json b/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter-vertical.json new file mode 100644 index 0000000..39dc38e --- /dev/null +++ b/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter-vertical.json @@ -0,0 +1,29 @@ +{ + "textures": { + "antenna": "minecraft:block/iron_block", + "body": "minecraft:block/coal_block" + }, + "elements": [ + { "from": [ 6.5, 6.9, 15.4 ], + "to": [ 9.5, 9.1, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" } + } + }, + { "from": [ 7, 7, 9 ], + "to": [ 7.1, 7.1, 15.4 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" } + } + } + ] +} diff --git a/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter.json b/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter.json new file mode 100644 index 0000000..f5e26dc --- /dev/null +++ b/src/main/resources/assets/pirate-radio/models/block/disposable-transmitter.json @@ -0,0 +1,30 @@ +{ + "textures": { + "antenna": "minecraft:block/iron_block", + "body": "minecraft:block/coal_block" + }, + "elements": [ + { "from": [ 6.5, 0.9, 15.4 ], + "to": [ 9.5, 3.1, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#body" } + } + }, + { "from": [ 7, 1, 15.3 ], + "to": [ 7.1, 7.5, 15.4 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "west": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" }, + "east": { "uv": [ 0, 0, 16, 16 ], "texture": "#antenna" } + } + } + ] +} diff --git a/src/main/resources/assets/pirate-radio/textures/entity/electronics-trader.png b/src/main/resources/assets/pirate-radio/textures/entity/electronics-trader.png new file mode 100644 index 0000000..b86a4ef --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/entity/electronics-trader.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/gui/radio-receiver.png b/src/main/resources/assets/pirate-radio/textures/gui/radio-receiver.png new file mode 100644 index 0000000..b86a4ef --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/gui/radio-receiver.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/disposable-transmitter.png b/src/main/resources/assets/pirate-radio/textures/item/disposable-transmitter.png new file mode 100644 index 0000000..c815369 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/disposable-transmitter.png Binary files differdiff --git a/src/main/resources/data/pirate-radio/pirate-radio/antenna/const.json b/src/main/resources/data/pirate-radio/pirate-radio/antenna/const.json new file mode 100644 index 0000000..6f7a630 --- /dev/null +++ b/src/main/resources/data/pirate-radio/pirate-radio/antenna/const.json @@ -0,0 +1,4 @@ +{ + "type": "pirate-radio:const", + "level": 1.0 +} \ No newline at end of file diff --git a/src/main/resources/data/pirate-radio/pirate-radio/antenna/null.json b/src/main/resources/data/pirate-radio/pirate-radio/antenna/null.json new file mode 100644 index 0000000..6b7a819 --- /dev/null +++ b/src/main/resources/data/pirate-radio/pirate-radio/antenna/null.json @@ -0,0 +1,4 @@ +{ + "type": "pirate-radio:const", + "level": 0.0 +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 71f2518..ffaf57a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,15 +3,15 @@ "id": "pirate-radio", "version": "${version}", "name": "Pirate Radio", - "description": "This is an example description! Tell everyone what your mod is about!", + "description": "A TTS-based FM pirate radio simulator in Minecraft! Uses real FM simulation, with some practical approximations.", "authors": [ - "Me!" + "SoniEx2" ], "contact": { - "homepage": "https://fabricmc.net/", - "sources": "https://github.com/FabricMC/fabric-example-mod" + "homepage": "https://modrinth.com/mod/pirate-radio", + "sources": "https://soniex2.autistic.space/git-repos/fmsim.git/tree/" }, - "license": "LGPL-2.1-or-later", + "license": "MIT AND CC-BY-4.0", "icon": "assets/pirate-radio/icon.png", "environment": "*", "entrypoints": { @@ -34,6 +34,12 @@ } ] }, + "mixins": [ + { + "config": "pirate-radio.client-mixins.json", + "environment": "client" + } + ], "depends": { "fabricloader": ">=0.16.10", "minecraft": "~1.21.1", @@ -41,4 +47,4 @@ "fabric-api": "*", "fabric-language-kotlin": "*" } -} \ No newline at end of file +} diff --git a/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt index a4dfe91..1a6446b 100644 --- a/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt +++ b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt @@ -2,6 +2,8 @@ package space.autistic.radio.complex import org.joml.Vector2f import org.junit.jupiter.api.Assertions.* +import space.autistic.radio.client.complex.I +import space.autistic.radio.client.complex.cmul import kotlin.test.Test class ComplexKtTest { diff --git a/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt index 8a4862c..aaa6db3 100644 --- a/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt +++ b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt @@ -1,13 +1,25 @@ package space.autistic.radio.fmsim +import space.autistic.radio.client.fmsim.FmFullConstants +import space.autistic.radio.client.fmsim.FmFullDemodulator +import space.autistic.radio.client.fmsim.FmFullModulator +import java.nio.FloatBuffer import kotlin.test.Test class TestAsserts { @Test - fun testFmFullSim() { - // initialize and flush an FM modulator - // if anything asserts, this should catch it + fun testFmFullMod() { + // initialize and flush an FM modulator, in both mono and stereo val fmFullModulator = FmFullModulator() - fmFullModulator.flush(1f) {} + fmFullModulator.flush(1f, FmFullConstants.STEREO) {} + fmFullModulator.flush(1f, FmFullConstants.MONO) {} + } + + @Test + fun testFmFullDemod() { + // initialize and flush an FM demodulator, in both mono and stereo + val fmFullDemodulator = FmFullDemodulator() + fmFullDemodulator.flush(FmFullConstants.STEREO) { _, _ -> } + fmFullDemodulator.flush(FmFullConstants.MONO) { _, _ -> } } } \ No newline at end of file |