diff options
Diffstat (limited to 'src/main/kotlin/space/autistic')
29 files changed, 678 insertions, 850 deletions
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/cli/OfflineSimulator.kt b/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt deleted file mode 100644 index bc16814..0000000 --- a/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt +++ /dev/null @@ -1,237 +0,0 @@ -package space.autistic.radio.cli - -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 java.io.FileInputStream -import java.io.FileOutputStream -import java.io.InputStream -import java.net.URI -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.nio.FloatBuffer -import kotlin.io.path.inputStream -import kotlin.io.path.toPath -import kotlin.math.min -import kotlin.system.exitProcess - -fun printUsage() { - println("Usage: OfflineSimulator <-o|-O> OUTFILE.raw {[-p POWER] [-l|-h] [-m] file:///FILE.raw} [-m]") - println(" file:///FILE.raw (or ./FILE.raw - the ./ is required)") - println(" The raw input file. two-channel (even with -m), 48kHz 32-bit float.") - println(" -o OUTFILE.raw") - println(" The raw RF stream to output, 2x300kHz 32-bit float.") - println(" -O OUTFILE.raw") - println(" The raw audio stream to output, 2x48kHz 32-bit float.") - println(" -p POWER") - println(" The signal amplitude (power level), e.g. 1.0.") - println(" -l") - println(" Simulate a partial overlap on the lower half of the tuned-into frequency.") - println(" -h") - println(" Simulate a partial overlap on the upper half of the tuned-into frequency.") - println(" -m") - println(" Downconvert to mono. As the last option, demodulate as mono.") -} - -class SimFile(val power: Float, val band: Int, val filename: String, val stereo: Boolean) { - var closed: Boolean = false - val buffer: FloatBuffer = FloatBuffer.allocate(8192) - val modulator = FmFullModulator() - var stream: InputStream? = null -} - -fun main(args: Array<String>) { - if (args.isEmpty()) { - printUsage() - exitProcess(1) - } - var hasOutput = false - var inArg = "" - var output = "" - var rfOutput = true - var power = 1.0f - var band = 2 - var stereo = FmFullConstants.STEREO - val files: ArrayList<SimFile> = ArrayList() - for (arg in args) { - if (!hasOutput) { - if (arg == "-o" || arg == "-O") { - hasOutput = true - inArg = arg - } else { - printUsage() - exitProcess(1) - } - } else { - when (inArg) { - "-o" -> { - output = arg - rfOutput = true - inArg = "" - } - - "-O" -> { - output = arg - rfOutput = false - inArg = "" - } - - "-p" -> { - power = arg.toFloatOrNull() ?: run { - println("Error processing -p argument: not a valid float") - printUsage() - exitProcess(1) - } - inArg = "" - } - - "" -> { - if (!arg.startsWith("-")) { - files.add(SimFile(power, band, arg, stereo)) - inArg = "" - band = 2 - power = 1.0f - stereo = FmFullConstants.STEREO - } else { - when (arg) { - "-p" -> inArg = "-p" - "-l" -> band = 1 - "-h" -> band = 3 - "-m" -> stereo = FmFullConstants.MONO - else -> { - println("Unknown option") - printUsage() - exitProcess(1) - } - } - } - } - - else -> throw NotImplementedError(inArg) - } - } - } - - if (files.isEmpty()) { - printUsage() - exitProcess(1) - } - - println(ProcessHandle.current().pid()) - - FileOutputStream(output).buffered().use { outputStream -> - for (inputFile in files) { - if (inputFile.filename != "file:///dev/zero") { - if (inputFile.filename.startsWith("./")) { - inputFile.stream = FileInputStream(inputFile.filename) - } else { - inputFile.stream = URI(inputFile.filename).toPath().inputStream() - } - } - } - - val buffer = ByteBuffer.allocate(2 * 4 * FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) - val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) - val minus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K) - val demodulator = FmFullDemodulator() - var lastStereoPilot = false - while (true) { - // initialized to maximum buffer size, trimmed down later - var minBuffer = 8192 - for (inputFile in files) { - val stream = inputFile.stream - if (stream == null) { - if (inputFile.buffer.remaining() > 2 * FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K) { - inputFile.modulator.flush(inputFile.power, inputFile.stereo) { - inputFile.buffer.put(it) - } - } - } else { - val bytes = stream.read(buffer.array()) - if (bytes <= 0) { - stream.close() - inputFile.stream = null - inputFile.closed = true - inputFile.modulator.flush(inputFile.power, inputFile.stereo) { - inputFile.buffer.put(it) - } - } else { - val floats = buffer.slice(0, bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() - var shouldFlush = true - inputFile.modulator.process(floats, inputFile.power, inputFile.stereo) { - inputFile.buffer.put(it) - shouldFlush = false - } - if (shouldFlush) { - inputFile.modulator.flush(inputFile.power, inputFile.stereo) { - inputFile.buffer.put(it) - } - } - } - } - minBuffer = min(minBuffer, inputFile.buffer.position()) - } - - val outputBuffer = ByteBuffer.allocate(minBuffer * 4) - val floatView = outputBuffer.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() - val floatBufferLo = FloatBuffer.allocate(minBuffer) - val floatBufferHi = FloatBuffer.allocate(minBuffer) - for (inputFile in files) { - inputFile.buffer.flip() - val floatBuffer = when (inputFile.band) { - 1 -> floatBufferLo - 2 -> floatView - 3 -> floatBufferHi - else -> throw IllegalStateException() - } - for (i in 0 until floatBuffer.capacity()) { - floatBuffer.put(i, floatBuffer.get(i) + inputFile.buffer.get()) - } - inputFile.buffer.compact() - } - 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, floatView.get(i) + z.y) - } - 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, floatView.get(i) + z.y) - } - if (rfOutput) { - outputStream.write(outputBuffer.array()) - } else { - demodulator.process(floatView, stereo) { stereoPilot, it -> - if (stereoPilot != lastStereoPilot) { - println(if (stereoPilot) "stereo" else "mono") - } - lastStereoPilot = stereoPilot - buffer.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(0, it.array()) - outputStream.write(buffer.array()) - } - } - if (files.all { it.closed }) { - break - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/complex/Complex.kt b/src/main/kotlin/space/autistic/radio/complex/Complex.kt deleted file mode 100644 index 918dac2..0000000 --- a/src/main/kotlin/space/autistic/radio/complex/Complex.kt +++ /dev/null @@ -1,32 +0,0 @@ -package space.autistic.radio.complex - -import org.joml.Vector2f -import org.joml.Vector2fc - -fun Vector2f.cmul(v: Vector2fc): Vector2f { - return this.cmul(v, this) -} - -fun Vector2f.cmul(v: Vector2fc, dest: Vector2f): Vector2f { - val a = this.x * v.x() - val b = this.y * v.y() - val c = (this.x() + this.y()) * (v.x() + v.y()) - val x = a - b - val y = c - a - b - dest.x = x - dest.y = y - return dest -} - -fun Vector2f.conjugate(): Vector2f { - return this.conjugate(this) -} - -fun Vector2f.conjugate(dest: Vector2f): Vector2f { - dest.x = this.x() - dest.y = -this.y() - return dest -} - -val I - get() = Vector2f(0f, 1f) \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt b/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt deleted file mode 100644 index 8f86218..0000000 --- a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt +++ /dev/null @@ -1,11 +0,0 @@ -package space.autistic.radio.dsp - -class Biquad1stOrder(private val b0: Float, private val b1: Float, private val a1: Float) { - private var delaySlot = 0f - - fun process(samp: Float): Float { - val out = samp * b0 + delaySlot - delaySlot = samp * b1 - out * a1 - return out - } -} \ 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/FmFullConstants.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt deleted file mode 100644 index 6b92328..0000000 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt +++ /dev/null @@ -1,114 +0,0 @@ -package space.autistic.radio.fmsim - -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.sin - -object FmFullConstants { - // tau = 75us, fh = 20396.25Hz - const val FM_PREEMPAHSIS_B0_48K = 6.7639647f - const val FM_PREEMPHASIS_B1_48K = -4.975628f - - /* const val FM_PREEMPHASIS_A0_48K = 1f */ - const val FM_PREEMPHASIS_A1_48K = 0.78833646f - - const val FM_DEEMPAHSIS_B0_48K = 1f / FM_PREEMPAHSIS_B0_48K - const val FM_DEEMPHASIS_B1_48K = FM_PREEMPHASIS_A1_48K / FM_PREEMPAHSIS_B0_48K - - /* const val FM_DEEMPHASIS_A0_48K = 1f */ - const val FM_DEEMPHASIS_A1_48K = FM_PREEMPHASIS_B1_48K / FM_PREEMPAHSIS_B0_48K - - val FIR_LPF_48K_15K_3K1 = floatArrayOf( - -0.0010006913216784596f, - 0.001505308784544468f, - -2.625857350794219e-18f, - -0.002777613466605544f, - 0.0030173989944159985f, - 0.002290070755407214f, - -0.008225799538195133f, - 0.004239063244313002f, - 0.010359899140894413f, - -0.017650796100497246f, - 1.510757873119297e-17f, - 0.029305754229426384f, - -0.02889496460556984f, - -0.020366130396723747f, - 0.07103750854730606f, - -0.03811456635594368f, - -0.10945471376180649f, - 0.29212409257888794f, - 0.6252123713493347f, - 0.29212409257888794f, - -0.10945471376180649f, - -0.03811456635594368f, - 0.07103750854730606f, - -0.020366130396723747f, - -0.02889496460556984f, - 0.029305754229426384f, - 1.510757873119297e-17f, - -0.017650796100497246f, - 0.010359899140894413f, - 0.004239063244313002f, - -0.008225799538195133f, - 0.002290070755407214f, - 0.0030173989944159985f, - -0.002777613466605544f, - -2.625857350794219e-18f, - 0.001505308784544468f, - -0.0010006913216784596f, - ) - - // chosen such that we can easily do 38kHz mixing in frequency (1500*38k/300k = shift of 95 bins, where 1500 comes - // from the 4/25 ratio 48k/300k i.e. 240*25/4) - // (the theoretical optimum, as per above, would be around 180) - // (we could have fudged the carrier frequency a bit but we chose not to) - // NOTE: latency = (data block size / 48000) seconds (84 -> 1.75 ms) - const val FFT_SIZE_LPF_48K_15K_3K1 = 2 * 120 - const val FFT_OVERLAP_LPF_48K_15K_3K1 = 36 - const val FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 = FFT_SIZE_LPF_48K_15K_3K1 - FFT_OVERLAP_LPF_48K_15K_3K1 - - init { - assert(FFT_OVERLAP_LPF_48K_15K_3K1 >= FIR_LPF_48K_15K_3K1.size - 1) - } - - const val DECIMATION_48K_300K = 4 - const val INTERPOLATION_48K_300K = 25 - - const val FFT_SIZE_48K_300K = FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K - const val FFT_OVERLAP_48K_300K = FFT_OVERLAP_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K - const val FFT_DATA_BLOCK_SIZE_48K_300K = FFT_SIZE_48K_300K - FFT_OVERLAP_48K_300K - - // how many bins to shift for 38kHz mixing - // assuming FFT_SIZE_LPF_48K_15K_3K1 *bins* (complex) - // 19 / 150 is the ratio between 38k/300k - const val FREQUENCY_MIXING_BINS_38K = - FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K * 19 / 150 - - // a single cycle of a 19kHz signal takes (1/19k)/(1/300k) or 300k/19k samples. - // since that number isn't exact, buffer an entire 19 cycles. - const val BUFFER_SIZE_19K_300K = 300 - - // using cosine is nicer - val BUFFER_19K_300K = FloatArray(BUFFER_SIZE_19K_300K) { - 0.1f * cos(2 * PI * 19000.0 * it.toDouble() / 300000.0).toFloat() - } - - // we want a carrier deviation of +-75kHz, at a sampling rate of 300kHz - const val CORRECTION_FACTOR = (75000.0 / (300000.0 / (2.0 * PI))).toFloat() - const val INVERSE_CORRECTION_FACTOR = 1 / CORRECTION_FACTOR - - // these are used for "low/high" mixing - const val CBUFFER_SIZE_100K_300K = 3 - - val CBUFFER_100K_300K = FloatArray(2 * CBUFFER_SIZE_100K_300K) { - val index = it / 2 - if (it and 1 == 0) { - 1f * sin(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() - } else { - 1f * cos(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() - } - } - - const val STEREO = true - const val MONO = false -} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullDemodulator.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullDemodulator.kt deleted file mode 100644 index de44e69..0000000 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullDemodulator.kt +++ /dev/null @@ -1,158 +0,0 @@ -package space.autistic.radio.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 java.nio.FloatBuffer -import java.util.function.BiConsumer - -class FmFullDemodulator { - private val inputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_48K_300K) - private val fft300kBuf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_48K_300K) - private val fft48kBuf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - private val outputBuffer = FloatBuffer.allocate(2 * FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) - - init { - inputBuffer.position(2 * FmFullConstants.FFT_OVERLAP_48K_300K) - } - - // yep. - private val boxcarI = Biquad1stOrder(1f, 1f, 0f) - private val boxcarQ = Biquad1stOrder(1f, 1f, 0f) - private val delayI = Biquad1stOrder(0f, 1f, 0f) - private val delayQ = Biquad1stOrder(0f, 1f, 0f) - - private val deemphasisLeft = Biquad1stOrder( - FmFullConstants.FM_DEEMPAHSIS_B0_48K, - FmFullConstants.FM_DEEMPHASIS_B1_48K, - FmFullConstants.FM_DEEMPHASIS_A1_48K - ) - private val deemphasisRight = Biquad1stOrder( - FmFullConstants.FM_DEEMPAHSIS_B0_48K, - FmFullConstants.FM_DEEMPHASIS_B1_48K, - FmFullConstants.FM_DEEMPHASIS_A1_48K - ) - - private val lastStereoPilot = Vector2f() - private val lastStereoPilotPolarDiscriminator = Vector2f() - - /** - * Takes in samples at 300kHz, in I/Q format, and processes them for output. - * - * Calls consumer with processed samples at 48kHz, stereo. - */ - fun process(input: FloatBuffer, stereo: Boolean, consumer: BiConsumer<Boolean, FloatBuffer>) { - while (input.remaining() >= 2) { - val z = Vector2f() - val w = Vector2f() - while (input.remaining() >= 2 && inputBuffer.hasRemaining()) { - z.x = boxcarI.process(input.get()) - z.y = boxcarQ.process(input.get()) - // quadrature demodulation = FM demodulation - // see https://wiki.gnuradio.org/index.php/Quadrature_Demod and such - w.x = delayI.process(z.x) - w.y = -delayQ.process(z.y) - z.cmul(w) - inputBuffer.put(org.joml.Math.atan2(z.y, z.x) * FmFullConstants.INVERSE_CORRECTION_FACTOR) - } - if (!inputBuffer.hasRemaining()) { - var stereoPilot = false - fft300kBuf.put(0, inputBuffer.array()) - fft300k.realForward(fft300kBuf.array()) - for (i in 0 until fft48kBuf.capacity()) { - fft48kBuf.put(i, 0f) - } - for (i in 2 until (FmFullConstants.FREQUENCY_MIXING_BINS_38K - 2 and 1.inv()) step 2) { - z.x = fft300kBuf.get(i) - z.y = fft300kBuf.get(i + 1) - w.x = fir48kLpf.get(i) - w.y = fir48kLpf.get(i + 1) - z.cmul(w) - fft48kBuf.put(i, z.x) - fft48kBuf.put(i + 1, z.y) - } - fft48kBuf.put(0, fft300kBuf.get(0) * fir48kLpf.get(0)) - fft48k.realInverse(fft48kBuf.array(), false) - outputBuffer.clear() - fft48kBuf.position(FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) - for (i in 0 until FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) { - val sample = fft48kBuf.get() * (1f / FmFullConstants.FFT_SIZE_48K_300K) - outputBuffer.put(sample) - outputBuffer.put(sample) - } - outputBuffer.clear() - if (stereo) { - z.x = fft300kBuf.get(FmFullConstants.FREQUENCY_MIXING_BINS_38K) - z.y = fft300kBuf.get(FmFullConstants.FREQUENCY_MIXING_BINS_38K + 1) - z.conjugate(w).cmul(lastStereoPilot).conjugate().normalize() - if (lastStereoPilotPolarDiscriminator.distanceSquared(w) < 0.5f && z.lengthSquared() >= FmFullConstants.FFT_SIZE_48K_300K) { - stereoPilot = true - } - lastStereoPilot.set(z) - lastStereoPilotPolarDiscriminator.set(w) - if (stereoPilot) { - // w is our phase offset - // TODO check if this is mathematically sound - z.normalize().cmul(z).cmul(w.conjugate()).conjugate() - // z is our recovered 38kHz carrier, including phase offset - for (i in 0 until fft48kBuf.capacity()) { - fft48kBuf.put(i, 0f) - } - val base = FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 - val sz = Vector2f() - val sw = Vector2f() - for (i in 2 until (FmFullConstants.FREQUENCY_MIXING_BINS_38K - 2 and 1.inv()) step 2) { - sz.x = fft300kBuf.get(base + i) - sz.y = fft300kBuf.get(base + i + 1) - sw.x = fft300kBuf.get(base - i) - sw.y = fft300kBuf.get(base - i + 1) - sz.cmul(z).add(sw.cmul(z).conjugate()) - sw.x = fir48kLpf.get(i) - sw.y = fir48kLpf.get(i + 1) - sz.cmul(sw) - fft48kBuf.put(i, sz.x) - fft48kBuf.put(i + 1, sz.y) - } - sz.x = fft300kBuf.get(base) - sz.y = fft300kBuf.get(base + 1) - sz.cmul(z) - fft48kBuf.put(0, sz.x * fir48kLpf.get(0)) - fft48k.realInverse(fft48kBuf.array(), false) - outputBuffer.clear() - fft48kBuf.position(FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) - for (i in 0 until FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) { - val lmr = fft48kBuf.get() * (1f / FmFullConstants.FFT_SIZE_48K_300K) - val lpr = outputBuffer.get(outputBuffer.position()) - outputBuffer.put((lpr + lmr) * 0.5f) - outputBuffer.put((lpr - lmr) * 0.5f) - } - outputBuffer.clear() - } - } - inputBuffer.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K) - inputBuffer.compact() - for (i in 0 until outputBuffer.capacity() step 2) { - outputBuffer.put(i, deemphasisLeft.process(outputBuffer.get(i))) - } - for (i in 1 until outputBuffer.capacity() step 2) { - outputBuffer.put(i, deemphasisRight.process(outputBuffer.get(i))) - } - consumer.accept(stereoPilot, outputBuffer) - } - } - } - - 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()) - private val fir48kLpf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - - init { - fir48kLpf.put(0, FmFullConstants.FIR_LPF_48K_15K_3K1) - fft48k.realForward(fir48kLpf.array()) - } - } -} \ No newline at end of file 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/fmsim/FmFullModulator.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt deleted file mode 100644 index 1f3849e..0000000 --- a/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt +++ /dev/null @@ -1,171 +0,0 @@ -package space.autistic.radio.fmsim - -import org.joml.Vector2f -import space.autistic.radio.complex.cmul -import space.autistic.radio.complex.conjugate -import space.autistic.radio.dsp.Biquad1stOrder -import java.nio.FloatBuffer -import java.util.function.Consumer -import org.jtransforms.fft.FloatFFT_1D -import space.autistic.radio.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) - private val leftMinusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - private val biquadLeft = Biquad1stOrder( - FmFullConstants.FM_PREEMPAHSIS_B0_48K, - FmFullConstants.FM_PREEMPHASIS_B1_48K, - FmFullConstants.FM_PREEMPHASIS_A1_48K - ) - private val biquadRight = Biquad1stOrder( - FmFullConstants.FM_PREEMPAHSIS_B0_48K, - FmFullConstants.FM_PREEMPHASIS_B1_48K, - FmFullConstants.FM_PREEMPHASIS_A1_48K - ) - private val fft48kBuffer = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - private val mixingBuffer = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_48K_300K) - private val outputBuffer = FloatBuffer.allocate(2 * FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K) - private val stereoPilot = FloatBuffer.wrap(FmFullConstants.BUFFER_19K_300K) - - private val cycle19k = Vector2f(0f, 1f) - private var lastSum = 0f - - init { - // pre-pad the buffers - leftPlusRight.position(FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) - leftMinusRight.position(FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) - } - - /** - * Takes in samples at 48kHz, interleaved stereo (even when set to MONO), and processes them for output. - * - * Calls consumer with processed samples at 300kHz in I/Q format. - */ - fun process(input: FloatBuffer, power: Float, stereo: Boolean, consumer: Consumer<FloatBuffer>) { - while (input.remaining() >= 2) { - while (input.remaining() >= 2 && leftPlusRight.hasRemaining()) { - // FIXME AGC (currently clamping/clipping) - val left = min(max(biquadLeft.process(input.get()), -1f), 1f) - val right = min(max(biquadRight.process(input.get()), -1f), 1f) - leftPlusRight.put(left + right) - leftMinusRight.put(left - right) - } - if (!leftPlusRight.hasRemaining()) { - // zero the mixing buffer - for (i in 0 until mixingBuffer.capacity()) { - mixingBuffer.put(i, 0f) - } - fft48kBuffer.put(0, leftPlusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - fft48k.realForward(fft48kBuffer.array()) - fft48kBuffer.array().forEachIndexed { index, fl -> - fft48kBuffer.put( - index, - 0.4f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl - ) - } - val z = Vector2f() - val w = Vector2f() - for (i in 2 until (FmFullConstants.FREQUENCY_MIXING_BINS_38K - 2 and 1.inv()) step 2) { - z.x = fft48kBuffer.get(i) - z.y = fft48kBuffer.get(i + 1) - w.x = fir48kLpf.get(i) - w.y = fir48kLpf.get(i + 1) - z.cmul(w) - mixingBuffer.put(i, z.x) - mixingBuffer.put(i + 1, z.y) - } - mixingBuffer.put(0, fft48kBuffer.get(0) * fir48kLpf.get(0)) - if (stereo) { - fft48kBuffer.put(0, leftMinusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - fft48k.realForward(fft48kBuffer.array()) - fft48kBuffer.array().forEachIndexed { index, fl -> - fft48kBuffer.put( - index, - 0.2f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl - ) - } - val base = FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 - for (i in 2 until (FmFullConstants.FREQUENCY_MIXING_BINS_38K - 2 and 1.inv()) step 2) { - z.x = fft48kBuffer.get(i) - z.y = fft48kBuffer.get(i + 1) - w.x = fir48kLpf.get(i) - w.y = fir48kLpf.get(i + 1) - z.cmul(w) - mixingBuffer.put(base + i, z.x) - mixingBuffer.put(base + i + 1, z.y) - } - mixingBuffer.put(base, fft48kBuffer.get(0) * fir48kLpf.get(0)) - // cycle (phase offset) is frequency-doubled the 19k carrier - // but we need to add a 90deg rotation because ??? - // TODO check if this is mathematically sound - val cycle = cycle19k.cmul(cycle19k, Vector2f()).cmul(I) - // bandwidth we care about is about half of 38k, so just, well, half it - for (i in 2 until (FmFullConstants.FREQUENCY_MIXING_BINS_38K - 2 and 1.inv()) step 2) { - z.x = mixingBuffer.get(base + i) - z.y = mixingBuffer.get(base + i + 1) - // we also need the conjugate - z.conjugate(w) - z.cmul(cycle) - w.cmul(cycle) - mixingBuffer.put(base + i, z.x) - mixingBuffer.put(base + i + 1, z.y) - mixingBuffer.put(base - i, mixingBuffer.get(base - i) + w.x) - mixingBuffer.put(base - i + 1, mixingBuffer.get(base - i + 1) + w.y) - } - // handle 38kHz itself - z.x = mixingBuffer.get(base) - z.y = mixingBuffer.get(base + 1) - z.cmul(cycle) - mixingBuffer.put(base, z.x) - mixingBuffer.put(base + 1, z.y) - // add pilot - mixingBuffer.put( - FmFullConstants.FREQUENCY_MIXING_BINS_38K, - 75f / FmFullConstants.FFT_SIZE_48K_300K * cycle19k.x - ) - mixingBuffer.put( - FmFullConstants.FREQUENCY_MIXING_BINS_38K + 1, - 75f / FmFullConstants.FFT_SIZE_48K_300K * cycle19k.y - ) - // phase correction factors (due to dropping 225 bins) - cycle19k.cmul(I.conjugate()) - } - // mark data block as processed - leftPlusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) - leftMinusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) - leftPlusRight.compact() - leftMinusRight.compact() - fft300k.realInverse(mixingBuffer.array(), false) - outputBuffer.clear() - var sum = lastSum - for (i in FmFullConstants.FFT_OVERLAP_48K_300K until FmFullConstants.FFT_SIZE_48K_300K) { - sum += mixingBuffer.get(i) * FmFullConstants.CORRECTION_FACTOR - outputBuffer.put(org.joml.Math.cos(sum) * power) - outputBuffer.put(org.joml.Math.sin(sum) * power) - } - lastSum = sum % (2 * Math.PI).toFloat() - outputBuffer.clear() - consumer.accept(outputBuffer) - } - } - input.compact() - } - - fun flush(power: Float, stereo: Boolean, consumer: Consumer<FloatBuffer>) { - process(FloatBuffer.allocate(2 * leftPlusRight.remaining()), power, stereo, consumer) - } - - companion object { - private val fft48k = FloatFFT_1D(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1.toLong()) - private val fft300k = FloatFFT_1D(FmFullConstants.FFT_SIZE_48K_300K.toLong()) - private val fir48kLpf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) - - init { - fir48kLpf.put(0, FmFullConstants.FIR_LPF_48K_15K_3K1) - fft48k.realForward(fir48kLpf.array()) - } - } -} \ 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/opus/OpusDecoder.kt b/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt deleted file mode 100644 index 56fce2b..0000000 --- a/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt +++ /dev/null @@ -1,77 +0,0 @@ -package space.autistic.radio.opus - -import com.dylibso.chicory.runtime.ByteBufferMemory -import space.autistic.radio.reflection.getBuffer -import java.nio.ByteOrder - -class OpusDecoder(sampleRate: Int, private val channels: Int) { - private val instance = OpusFactory() - - init { - instance.export("_initialize").apply() - } - - private val errorPtr = instance.export("malloc").apply(4)[0] - - init { - if (errorPtr == 0L) { - throw IllegalStateException() - } - instance.memory().writeI32(errorPtr.toInt(), 0) - } - - private val decoder = - instance.export("opus_decoder_create").apply(sampleRate.toLong(), channels.toLong(), errorPtr)[0] - - init { - val error = instance.memory().readI32(errorPtr.toInt()) - if (error < 0) { - throw IllegalStateException( - instance.memory().readCString(instance.export("opus_strerror").apply(error)[0].toInt()) - ) - } - } - - private val opusDecodeFloat = instance.export("opus_decode_float") - - private val outBuf = instance.export("malloc").apply((4 * MAX_FRAME_SIZE * channels).toLong())[0] - - init { - if (outBuf == 0L) { - throw IllegalStateException() - } - } - - private val cbits = instance.export("malloc").apply(MAX_PACKET_SIZE.toLong())[0] - - init { - if (cbits == 0L) { - throw IllegalStateException() - } - } - - private val memory = instance.memory() as ByteBufferMemory - - fun decode(packet: ByteArray): FloatArray { - if (packet.size > MAX_PACKET_SIZE) { - throw IllegalArgumentException("packet too big") - } - memory.getBuffer().put(cbits.toInt(), packet) - val decoded = - opusDecodeFloat.apply(decoder, cbits, packet.size.toLong(), outBuf, MAX_FRAME_SIZE.toLong(), 0L)[0] - if (decoded < 0L) { - throw IllegalStateException( - instance.memory().readCString(instance.export("opus_strerror").apply(decoded)[0].toInt()) - ) - } - val out = FloatArray(decoded.toInt()) - memory.getBuffer().slice(outBuf.toInt(), outBuf.toInt() + 4 * channels * decoded.toInt()) - .order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(out) - return out - } - - companion object { - const val MAX_FRAME_SIZE = 6 * 960 - const val MAX_PACKET_SIZE = 3 * 1276 - } -} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt b/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt deleted file mode 100644 index 70e0c3c..0000000 --- a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package space.autistic.radio.opus - -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 java.io.InputStream - -object OpusFactory : () -> Instance { - private val defaultImports = ImportValues.builder().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("opus.wasm") } - .map<InputStream?> { it.toFile().inputStream() }.orElseGet { - this.javaClass.getResourceAsStream("/opus.wasm") - } - } -} \ 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() { +} |