summary refs log tree commit diff stats
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b4
-rw-r--r--src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c723
-rw-r--r--src/main/generated/assets/pirate-radio/models/item/fm-receiver.json6
-rw-r--r--src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json3
-rw-r--r--src/main/kotlin/space/autistic/radio/CommonProxy.kt10
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadio.kt59
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadioComponents.kt30
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt26
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadioItems.kt23
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadioRegistries.kt12
-rw-r--r--src/main/kotlin/space/autistic/radio/PirateRadioRegistryKeys.kt13
-rw-r--r--src/main/kotlin/space/autistic/radio/SidedProxy.kt9
-rw-r--r--src/main/kotlin/space/autistic/radio/antenna/Antenna.kt9
-rw-r--r--src/main/kotlin/space/autistic/radio/antenna/AntennaSerializer.kt8
-rw-r--r--src/main/kotlin/space/autistic/radio/antenna/ConstAntenna.kt7
-rw-r--r--src/main/kotlin/space/autistic/radio/antenna/PirateRadioAntennaSerializers.kt18
-rw-r--r--src/main/kotlin/space/autistic/radio/antenna/WasmAntenna.kt7
-rw-r--r--src/main/kotlin/space/autistic/radio/entity/DisposableTransmitterEntity.kt178
-rw-r--r--src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt4
-rw-r--r--src/main/kotlin/space/autistic/radio/item/DisposableTransmitterItem.kt56
-rw-r--r--src/main/kotlin/space/autistic/radio/item/StorageCardItem.kt20
-rw-r--r--src/main/kotlin/space/autistic/radio/network/StorageCardUpdateC2SPayload.kt27
-rw-r--r--src/main/kotlin/space/autistic/radio/wasm/Bindings.kt173
-rw-r--r--src/main/resources/assets/pirate-radio/blockstates/disposable-transmitter.json10
-rw-r--r--src/main/resources/assets/pirate-radio/lang/en_us.json22
-rw-r--r--src/main/resources/assets/pirate-radio/models/block/disposable-transmitter-vertical.json29
-rw-r--r--src/main/resources/assets/pirate-radio/models/block/disposable-transmitter.json30
-rw-r--r--src/main/resources/assets/pirate-radio/textures/entity/electronics-trader.pngbin0 -> 1059 bytes
-rw-r--r--src/main/resources/assets/pirate-radio/textures/gui/radio-receiver.pngbin0 -> 1059 bytes
-rw-r--r--src/main/resources/assets/pirate-radio/textures/item/disposable-transmitter.pngbin0 -> 881 bytes
-rw-r--r--src/main/resources/data/pirate-radio/pirate-radio/antenna/const.json4
-rw-r--r--src/main/resources/data/pirate-radio/pirate-radio/antenna/null.json4
-rw-r--r--src/main/resources/fabric.mod.json6
-rw-r--r--src/main/resources/pirate-radio.client-mixins.json14
34 files changed, 790 insertions, 34 deletions
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..d463e41 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(1080, 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..fe26a86
--- /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/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..cc123a1
--- /dev/null
+++ b/src/main/kotlin/space/autistic/radio/wasm/Bindings.kt
@@ -0,0 +1,173 @@
+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
+                }
+
+                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/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..81145f0 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,25 @@
   "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"
 }
\ 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..c7d859a 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -34,6 +34,12 @@
 			}
 		]
 	},
+	"mixins": [
+		{
+			"config": "pirate-radio.client-mixins.json",
+			"environment": "client"
+		}
+	],
 	"depends": {
 		"fabricloader": ">=0.16.10",
 		"minecraft": "~1.21.1",
diff --git a/src/main/resources/pirate-radio.client-mixins.json b/src/main/resources/pirate-radio.client-mixins.json
new file mode 100644
index 0000000..27a5861
--- /dev/null
+++ b/src/main/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