aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/moe/nea/firmament/mixins/CustomPayloadEventDispatcher.java27
-rw-r--r--src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadC2SPacketCodec.java34
-rw-r--r--src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadS2CPacketCodec.java55
-rw-r--r--src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java23
-rw-r--r--src/main/kotlin/moe/nea/firmament/apis/ingame/FirmamentCustomPayload.kt36
-rw-r--r--src/main/kotlin/moe/nea/firmament/apis/ingame/HypixelModAPI.kt56
-rw-r--r--src/main/kotlin/moe/nea/firmament/apis/ingame/InGameCodecWrapper.kt51
-rw-r--r--src/main/kotlin/moe/nea/firmament/apis/ingame/JoinedCustomPayload.kt25
-rw-r--r--src/main/kotlin/moe/nea/firmament/apis/ingame/packets/PartyInfoRequest.kt134
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/FirmamentCustomPayloadEvent.kt15
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json1
11 files changed, 457 insertions, 0 deletions
diff --git a/src/main/java/moe/nea/firmament/mixins/CustomPayloadEventDispatcher.java b/src/main/java/moe/nea/firmament/mixins/CustomPayloadEventDispatcher.java
new file mode 100644
index 0000000..66710eb
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/CustomPayloadEventDispatcher.java
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.apis.ingame.FirmamentCustomPayload;
+import moe.nea.firmament.events.FirmamentCustomPayloadEvent;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.packet.CustomPayload;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public class CustomPayloadEventDispatcher {
+ @Inject(method = "onCustomPayload", at = @At("HEAD"), cancellable = true)
+ private void handleFirmamentParsedPayload(CustomPayload payload, CallbackInfo ci) {
+ if (payload instanceof FirmamentCustomPayload customPayload) {
+ FirmamentCustomPayloadEvent.Companion.publish(new FirmamentCustomPayloadEvent(customPayload));
+ ci.cancel();
+ }
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadC2SPacketCodec.java b/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadC2SPacketCodec.java
new file mode 100644
index 0000000..150611e
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadC2SPacketCodec.java
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.apis.ingame.InGameCodecWrapper;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.List;
+
+@Mixin(priority = 1001, value = CustomPayloadC2SPacket.class)
+public class WrapCustomPayloadC2SPacketCodec {
+
+ @WrapOperation(method = "<clinit>", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/CustomPayload;createCodec(Lnet/minecraft/network/packet/CustomPayload$CodecFactory;Ljava/util/List;)Lnet/minecraft/network/codec/PacketCodec;"))
+ private static PacketCodec<PacketByteBuf, CustomPayload> wrapFactory(
+ CustomPayload.CodecFactory<PacketByteBuf> unknownCodecFactory,
+ List<CustomPayload.Type<PacketByteBuf, ?>> types,
+ Operation<PacketCodec<PacketByteBuf, CustomPayload>> original) {
+
+ var originalCodec = original.call(unknownCodecFactory, types);
+
+ return new InGameCodecWrapper(originalCodec, InGameCodecWrapper.Direction.C2S);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadS2CPacketCodec.java b/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadS2CPacketCodec.java
new file mode 100644
index 0000000..7cb8f47
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/WrapCustomPayloadS2CPacketCodec.java
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.apis.ingame.InGameCodecWrapper;
+import moe.nea.firmament.apis.ingame.JoinedCustomPayload;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.codec.PacketCodec;
+import net.minecraft.network.listener.ClientCommonPacketListener;
+import net.minecraft.network.packet.CustomPayload;
+import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+
+@Mixin(priority = 1001, value = CustomPayloadS2CPacket.class)
+public abstract class WrapCustomPayloadS2CPacketCodec {
+
+ @Shadow
+ public abstract CustomPayload payload();
+
+ @WrapOperation(method = "<clinit>", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/CustomPayload;createCodec(Lnet/minecraft/network/packet/CustomPayload$CodecFactory;Ljava/util/List;)Lnet/minecraft/network/codec/PacketCodec;"))
+ private static PacketCodec<PacketByteBuf, CustomPayload> wrapFactory(
+ CustomPayload.CodecFactory<PacketByteBuf> unknownCodecFactory,
+ List<CustomPayload.Type<PacketByteBuf, ?>> types,
+ Operation<PacketCodec<PacketByteBuf, CustomPayload>> original) {
+
+ var originalCodec = original.call(unknownCodecFactory, types);
+
+ return new InGameCodecWrapper(originalCodec, InGameCodecWrapper.Direction.S2C);
+ }
+
+
+ // TODO: move to own class
+ @Inject(method = "apply(Lnet/minecraft/network/listener/ClientCommonPacketListener;)V", at = @At("HEAD"), cancellable = true)
+ private void onApply(ClientCommonPacketListener clientCommonPacketListener, CallbackInfo ci) {
+ if (payload() instanceof JoinedCustomPayload joinedCustomPayload) {
+ new CustomPayloadS2CPacket(joinedCustomPayload.getOriginal()).apply(clientCommonPacketListener);
+ new CustomPayloadS2CPacket(joinedCustomPayload.getSmuggled()).apply(clientCommonPacketListener);
+ ci.cancel();
+ }
+ }
+
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java b/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java
new file mode 100644
index 0000000..1e99285
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.mixins.devenv;
+
+import moe.nea.firmament.Firmament;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.network.packet.UnknownCustomPayload;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(UnknownCustomPayload.class)
+public class WarnForUnknownCustomPayloadSends {
+ @Inject(method = "method_56493", at = @At("HEAD"))
+ private static void warn(UnknownCustomPayload value, PacketByteBuf buf, CallbackInfo ci) {
+ Firmament.INSTANCE.getLogger().warn("Unknown custom payload is being sent: {}", value);
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/apis/ingame/FirmamentCustomPayload.kt b/src/main/kotlin/moe/nea/firmament/apis/ingame/FirmamentCustomPayload.kt
new file mode 100644
index 0000000..34e39ab
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/apis/ingame/FirmamentCustomPayload.kt
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.apis.ingame
+
+import io.netty.buffer.ByteBuf
+import net.minecraft.network.codec.PacketCodec
+import net.minecraft.network.packet.CustomPayload
+import net.minecraft.util.Identifier
+
+interface FirmamentCustomPayload : CustomPayload {
+
+ class Unhandled private constructor(val identifier: Identifier) : FirmamentCustomPayload {
+ override fun getId(): CustomPayload.Id<out CustomPayload> {
+ return CustomPayload.id(identifier.toString())
+ }
+
+ companion object {
+ fun <B : ByteBuf> createCodec(identifier: Identifier): PacketCodec<B, Unhandled> {
+ return object : PacketCodec<B, Unhandled> {
+ override fun decode(buf: B): Unhandled {
+ return Unhandled(identifier)
+ }
+
+ override fun encode(buf: B, value: Unhandled) {
+ // we will never send an unhandled packet stealthy
+ }
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/apis/ingame/HypixelModAPI.kt b/src/main/kotlin/moe/nea/firmament/apis/ingame/HypixelModAPI.kt
new file mode 100644
index 0000000..460df91
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/apis/ingame/HypixelModAPI.kt
@@ -0,0 +1,56 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.apis.ingame
+
+import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.apis.ingame.packets.PartyInfoRequest
+import moe.nea.firmament.apis.ingame.packets.PartyInfoResponse
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.FirmamentCustomPayloadEvent
+import moe.nea.firmament.events.subscription.SubscriptionOwner
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.util.MC
+
+
+object HypixelModAPI : SubscriptionOwner {
+ init {
+ InGameCodecWrapper.Direction.C2S.customCodec =
+ InGameCodecWrapper.createStealthyCodec(
+ PartyInfoRequest.intoType()
+ )
+ InGameCodecWrapper.Direction.S2C.customCodec =
+ InGameCodecWrapper.createStealthyCodec(
+ PartyInfoResponse.intoType()
+ )
+ }
+
+ @JvmStatic
+ fun sendRequest(packet: FirmamentCustomPayload) {
+ MC.networkHandler?.sendPacket(CustomPayloadC2SPacket(packet))
+ }
+
+ @Subscribe
+ fun testCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("sendpartyrequest") {
+ thenExecute {
+ sendRequest(PartyInfoRequest(1))
+ }
+ }
+ }
+
+ @Subscribe
+ fun logEvents(event: FirmamentCustomPayloadEvent) {
+ MC.sendChat(Text.stringifiedTranslatable("firmament.modapi.event", event.toString()))
+ }
+
+ override val delegateFeature: FirmamentFeature
+ get() = DeveloperFeatures
+}
diff --git a/src/main/kotlin/moe/nea/firmament/apis/ingame/InGameCodecWrapper.kt b/src/main/kotlin/moe/nea/firmament/apis/ingame/InGameCodecWrapper.kt
new file mode 100644
index 0000000..0720dbf
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/apis/ingame/InGameCodecWrapper.kt
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.apis.ingame
+
+import net.minecraft.network.PacketByteBuf
+import net.minecraft.network.codec.PacketCodec
+import net.minecraft.network.packet.CustomPayload
+
+class InGameCodecWrapper(
+ val wrapped: PacketCodec<PacketByteBuf, CustomPayload>,
+ val direction: Direction,
+) : PacketCodec<PacketByteBuf, CustomPayload> {
+ enum class Direction {
+ S2C,
+ C2S,
+ ;
+
+ var customCodec: PacketCodec<PacketByteBuf, FirmamentCustomPayload> = createStealthyCodec()
+ }
+
+ companion object {
+ fun createStealthyCodec(vararg codecs: CustomPayload.Type<PacketByteBuf, out FirmamentCustomPayload>): PacketCodec<PacketByteBuf, FirmamentCustomPayload> {
+ return CustomPayload.createCodec(
+ { FirmamentCustomPayload.Unhandled.createCodec(it) },
+ codecs.toList()
+ ) as PacketCodec<PacketByteBuf, FirmamentCustomPayload>
+ }
+
+ }
+
+ override fun decode(buf: PacketByteBuf): CustomPayload {
+ val duplicateBuffer = PacketByteBuf(buf.slice())
+ val original = wrapped.decode(buf)
+ val duplicate = direction.customCodec.decode(duplicateBuffer)
+ if (duplicate is FirmamentCustomPayload.Unhandled)
+ return original
+ return JoinedCustomPayload(original, duplicate)
+ }
+
+ override fun encode(buf: PacketByteBuf, value: CustomPayload) {
+ if (value is FirmamentCustomPayload) {
+ direction.customCodec.encode(buf, value)
+ } else {
+ wrapped.encode(buf, value)
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/apis/ingame/JoinedCustomPayload.kt b/src/main/kotlin/moe/nea/firmament/apis/ingame/JoinedCustomPayload.kt
new file mode 100644
index 0000000..c673264
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/apis/ingame/JoinedCustomPayload.kt
@@ -0,0 +1,25 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.apis.ingame
+
+import net.minecraft.network.packet.CustomPayload
+
+/**
+ * A class to smuggle two parsed instances of the same custom payload packet.
+ */
+class JoinedCustomPayload(
+ val original: CustomPayload,
+ val smuggled: FirmamentCustomPayload
+) : CustomPayload {
+ companion object {
+ val joinedId = CustomPayload.id<JoinedCustomPayload>("firmament:joined")
+ }
+
+ override fun getId(): CustomPayload.Id<out JoinedCustomPayload> {
+ return joinedId
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/apis/ingame/packets/PartyInfoRequest.kt b/src/main/kotlin/moe/nea/firmament/apis/ingame/packets/PartyInfoRequest.kt
new file mode 100644
index 0000000..2b3d234
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/apis/ingame/packets/PartyInfoRequest.kt
@@ -0,0 +1,134 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.apis.ingame.packets
+
+import io.netty.buffer.ByteBuf
+import java.util.UUID
+import net.minecraft.network.PacketByteBuf
+import net.minecraft.network.codec.PacketCodec
+import net.minecraft.network.codec.PacketCodecs
+import net.minecraft.network.packet.CustomPayload
+import net.minecraft.util.Uuids
+import moe.nea.firmament.apis.ingame.FirmamentCustomPayload
+
+interface FirmamentCustomPayloadMeta<T : FirmamentCustomPayload> {
+ val ID: CustomPayload.Id<T>
+ val CODEC: PacketCodec<PacketByteBuf, T>
+
+ fun intoType(): CustomPayload.Type<PacketByteBuf, T> {
+ return CustomPayload.Type(ID, CODEC)
+ }
+}
+
+data class PartyInfoRequest(val version: Int) : FirmamentCustomPayload {
+ companion object : FirmamentCustomPayloadMeta<PartyInfoRequest> {
+ override val ID = CustomPayload.id<PartyInfoRequest>("hypixel:party_info")
+ override val CODEC =
+ PacketCodecs.VAR_INT.cast<PacketByteBuf>()
+ .xmap(::PartyInfoRequest, PartyInfoRequest::version)
+ }
+
+ override fun getId(): CustomPayload.Id<out CustomPayload> {
+ return ID
+ }
+}
+
+sealed interface PartyInfoResponseV
+sealed interface HypixelVersionedPacketData<out T>
+data class HypixelSuccessfulResponse<T>(val data: T) : HypixelVersionedPacketData<T>
+data class HypixelUnknownVersion(val version: Int) : HypixelVersionedPacketData<Nothing>
+data class HypixelApiError(val label: String, val errorId: Int) : HypixelVersionedPacketData<Nothing> {
+ companion object {
+ fun <B : ByteBuf> createCodec(label: String): PacketCodec<B, HypixelApiError> {
+ return PacketCodecs.VAR_INT
+ .cast<B>()
+ .xmap({ HypixelApiError(label, it) }, HypixelApiError::errorId)
+ }
+ }
+}
+
+object CodecUtils {
+ fun <B : PacketByteBuf, T> dispatchVersioned(
+ versions: Map<Int, PacketCodec<B, out T>>,
+ errorCodec: PacketCodec<B, HypixelApiError>
+ ): PacketCodec<B, HypixelVersionedPacketData<T>> {
+ return object : PacketCodec<B, HypixelVersionedPacketData<T>> {
+ override fun decode(buf: B): HypixelVersionedPacketData<T> {
+ if (!buf.readBoolean()) {
+ return errorCodec.decode(buf)
+ }
+ val version = buf.readVarInt()
+ val versionCodec = versions[version]
+ ?: return HypixelUnknownVersion(version)
+ return HypixelSuccessfulResponse(versionCodec.decode(buf))
+ }
+
+ override fun encode(buf: B, value: HypixelVersionedPacketData<T>?) {
+ error("Cannot encode a hypixel packet")
+ }
+ }
+ }
+
+ fun <B : PacketByteBuf, T> dispatchS2CBoolean(
+ ifTrue: PacketCodec<B, out T>,
+ ifFalse: PacketCodec<B, out T>
+ ): PacketCodec<B, T> {
+ return object : PacketCodec<B, T> {
+ override fun decode(buf: B): T {
+ return if (buf.readBoolean()) {
+ ifTrue.decode(buf)
+ } else {
+ ifFalse.decode(buf)
+ }
+ }
+
+ override fun encode(buf: B, value: T) {
+ error("Cannot reverse dispatch boolean")
+ }
+ }
+ }
+
+}
+
+
+data object PartyInfoResponseVUnknown : PartyInfoResponseV
+data class PartyInfoResponseV1(
+ val leader: UUID?,
+ val members: Set<UUID>,
+) : PartyInfoResponseV {
+ data object PartyMember
+ companion object {
+ val CODEC: PacketCodec<PacketByteBuf, PartyInfoResponseV1> =
+ CodecUtils.dispatchS2CBoolean(
+ PacketCodec.tuple(
+ Uuids.PACKET_CODEC, PartyInfoResponseV1::leader,
+ Uuids.PACKET_CODEC.collect(PacketCodecs.toCollection(::HashSet)), PartyInfoResponseV1::members,
+ ::PartyInfoResponseV1
+ ),
+ PacketCodec.unit(PartyInfoResponseV1(null, setOf())))
+ }
+}
+
+
+data class PartyInfoResponse(val data: HypixelVersionedPacketData<PartyInfoResponseV>) : FirmamentCustomPayload {
+ companion object : FirmamentCustomPayloadMeta<PartyInfoResponse> {
+ override val ID: CustomPayload.Id<PartyInfoResponse> = CustomPayload.id("hypixel:party_info")
+ override val CODEC =
+ CodecUtils
+ .dispatchVersioned<PacketByteBuf, PartyInfoResponseV>(
+ mapOf(
+ 1 to PartyInfoResponseV1.CODEC,
+ ),
+ HypixelApiError.createCodec("PartyInfoResponse"))
+ .xmap(::PartyInfoResponse, PartyInfoResponse::data)
+
+ }
+
+ override fun getId(): CustomPayload.Id<out CustomPayload> {
+ return ID
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/events/FirmamentCustomPayloadEvent.kt b/src/main/kotlin/moe/nea/firmament/events/FirmamentCustomPayloadEvent.kt
new file mode 100644
index 0000000..057fcef
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/FirmamentCustomPayloadEvent.kt
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.events
+
+import moe.nea.firmament.apis.ingame.FirmamentCustomPayload
+
+data class FirmamentCustomPayloadEvent(
+ val payload: FirmamentCustomPayload
+) : FirmamentEvent() {
+ companion object : FirmamentEventBus<FirmamentCustomPayloadEvent>()
+}
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index d20fd8e..089a223 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -153,6 +153,7 @@
"firmament.config.fixes.player-skins": "Fix unsigned Player Skins",
"firmament.config.power-user.show-item-id": "Show SkyBlock Ids",
"firmament.config.power-user.copy-item-id": "Copy SkyBlock Id",
+ "firmament.modapi.event": "Received mod API event: %s",
"firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id",
"firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id",
"firmament.config.power-user.copy-nbt-data": "Copy NBT data",