diff options
| author | Linnea Gräf <nea@nea.moe> | 2025-08-24 14:52:07 +0200 |
|---|---|---|
| committer | Linnea Gräf <nea@nea.moe> | 2025-08-24 14:52:07 +0200 |
| commit | d186e876bb330230c5d869f754649764e7420163 (patch) | |
| tree | 55c93e28f3afabb2ef29f56f4a5f2944101674ce /src | |
| parent | d4b02f2a0c24220a3eb6d25775f53c8f6c796b47 (diff) | |
| download | Firmament-d186e876bb330230c5d869f754649764e7420163.tar.gz Firmament-d186e876bb330230c5d869f754649764e7420163.tar.bz2 Firmament-d186e876bb330230c5d869f754649764e7420163.zip | |
feat: add party API
Diffstat (limited to 'src')
| -rw-r--r-- | src/main/kotlin/events/JoinServerEvent.kt | 11 | ||||
| -rw-r--r-- | src/main/kotlin/events/registration/ChatEvents.kt | 5 | ||||
| -rw-r--r-- | src/main/kotlin/features/misc/ModAnnouncer.kt | 77 | ||||
| -rw-r--r-- | src/main/kotlin/util/MC.kt | 2 | ||||
| -rw-r--r-- | src/main/kotlin/util/skyblock/PartyUtil.kt | 210 | ||||
| -rw-r--r-- | src/main/kotlin/util/textutil.kt | 7 | ||||
| -rw-r--r-- | src/main/resources/fabric.mod.json | 3 |
7 files changed, 312 insertions, 3 deletions
diff --git a/src/main/kotlin/events/JoinServerEvent.kt b/src/main/kotlin/events/JoinServerEvent.kt new file mode 100644 index 0000000..225a2c1 --- /dev/null +++ b/src/main/kotlin/events/JoinServerEvent.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.events + +import net.fabricmc.fabric.api.networking.v1.PacketSender +import net.minecraft.client.network.ClientPlayNetworkHandler + +data class JoinServerEvent( + val networkHandler: ClientPlayNetworkHandler, + val packetSender: PacketSender, +) : FirmamentEvent() { + companion object : FirmamentEventBus<JoinServerEvent>() +} diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt index 1dcc91a..aadf498 100644 --- a/src/main/kotlin/events/registration/ChatEvents.kt +++ b/src/main/kotlin/events/registration/ChatEvents.kt @@ -1,6 +1,7 @@ package moe.nea.firmament.events.registration import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents import net.fabricmc.fabric.api.event.player.AttackBlockCallback import net.fabricmc.fabric.api.event.player.UseBlockCallback import net.fabricmc.fabric.api.event.player.UseItemCallback @@ -8,6 +9,7 @@ import net.minecraft.text.Text import net.minecraft.util.ActionResult import moe.nea.firmament.events.AllowChatEvent import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.JoinServerEvent import moe.nea.firmament.events.ModifyChatEvent import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.UseBlockEvent @@ -60,4 +62,7 @@ fun registerFirmamentEvents() { if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.CONSUME else ActionResult.PASS }) + ClientPlayConnectionEvents.JOIN.register { networkHandler, packetSender, _ -> + JoinServerEvent.publish(JoinServerEvent(networkHandler, packetSender)) + } } diff --git a/src/main/kotlin/features/misc/ModAnnouncer.kt b/src/main/kotlin/features/misc/ModAnnouncer.kt new file mode 100644 index 0000000..a99c6db --- /dev/null +++ b/src/main/kotlin/features/misc/ModAnnouncer.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.features.misc + +import io.netty.buffer.ByteBuf +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.network.codec.PacketCodec +import net.minecraft.network.codec.PacketCodecs +import net.minecraft.network.packet.CustomPayload +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.JoinServerEvent + +/** + * This is a class that announces all mods to hypixel (or any other server you connect to). + * + * I don't mind cheat mods, i just think they should be honest about themselves. + * + * If you are a cheat mod (or think you could possibly be seen as one), you have two options: + * + * - risk getting your users banned from hypixel by exposing your modid + version number in a packet sent on first connect + * - hide yourself from this list by either a mixin, or (more easily) by specifying a tag in your fabric.mod.json: + * + * ```json + * { + * "schemaVersion": 1, + * "id": "my-cheat-mod", + * "custom": { "firmament:hide_from_modlist": true } + * } + * ``` + */ +object ModAnnouncer { + + data class ModEntry( + val modid: String, + val modVersion: String, + ) { + companion object { + val CODEC: PacketCodec<ByteBuf, ModEntry> = PacketCodec.tuple( + PacketCodecs.STRING, ModEntry::modid, + PacketCodecs.STRING, ModEntry::modVersion, + ::ModEntry + ) + } + } + + data class ModPacket( + val mods: List<ModEntry>, + ) : CustomPayload { + override fun getId(): CustomPayload.Id<out ModPacket> { + return ID + } + + companion object { + val ID = CustomPayload.Id<ModPacket>(Firmament.identifier("mod_list")) + val CODEC: PacketCodec<ByteBuf, ModPacket> = ModEntry.CODEC.collect(PacketCodecs.toList()) + .xmap(::ModPacket, ModPacket::mods) + } + } + + @Subscribe + fun onServerJoin(event: JoinServerEvent) { + event.networkHandler.sendPacket( + event.packetSender.createPacket( + ModPacket( + FabricLoader.getInstance() + .allMods + .filter { !it.metadata.containsCustomValue("firmament:hide_from_modlist") } + .map { ModEntry(it.metadata.id, it.metadata.version.friendlyString) }) + ) + ) + } + + init { + PayloadTypeRegistry.playC2S().register(ModPacket.ID, ModPacket.CODEC) + } +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index 7ab0cbb..a6e3205 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -110,7 +110,7 @@ object MC { inline val stackInHand: ItemStack get() = player?.mainHandStack ?: ItemStack.EMPTY inline val guiAtlasManager get() = instance.guiAtlasManager inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } - inline val playerName: String? get() = player?.name?.unformattedString + inline val playerName: String get() = player?.name?.unformattedString ?: MC.instance.session.username inline var screen: Screen? get() = TestUtil.unlessTesting { instance.currentScreen } set(value) = instance.setScreen(value) diff --git a/src/main/kotlin/util/skyblock/PartyUtil.kt b/src/main/kotlin/util/skyblock/PartyUtil.kt new file mode 100644 index 0000000..7d28868 --- /dev/null +++ b/src/main/kotlin/util/skyblock/PartyUtil.kt @@ -0,0 +1,210 @@ +package moe.nea.firmament.util.skyblock + +import java.util.UUID +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket +import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket.PartyRole +import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket +import org.intellij.lang.annotations.Language +import kotlinx.coroutines.launch +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.apis.Routes +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.boolColour +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.useMatch + +object PartyUtil { + object Internal { + val hma = HypixelModAPI.getInstance() + + val handler = hma.createHandler(ClientboundPartyInfoPacket::class.java) { clientboundPartyInfoPacket -> + Firmament.coroutineScope.launch { + party = Party(clientboundPartyInfoPacket.memberMap.values.map { + PartyMember.fromUuid(it.uuid, it.role) + }) + } + } + + fun sendSyncPacket() { + hma.sendPacket(ServerboundPartyInfoPacket()) + } + + @Subscribe + fun onDevCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("party") { + thenLiteral("refresh") { + thenExecute { + sendSyncPacket() + source.sendFeedback(tr("firmament.dev.partyinfo.refresh", "Refreshing party info")) + } + } + thenExecute { + val p = party + val text = Text.empty() + text.append( + tr("firmament.dev.partyinfo", "Party Info: ") + .boolColour(p != null) + ) + if (p == null) { + text.append(tr("firmament.dev.partyinfo.empty", "Empty Party").grey()) + } else { + text.append(tr("firmament.dev.partyinfo.count", "${p.members.size} members").grey()) + p.members.forEach { + text.append("\n") + .append(Text.literal(" - ${it.name}")) + .append(" (") + .append( + when (it.role) { + PartyRole.LEADER -> tr("firmament.dev.partyinfo.leader", "Leader").bold() + PartyRole.MOD -> tr("firmament.dev.partyinfo.mod", "Moderator") + PartyRole.MEMBER -> tr("firmament.dev.partyinfo.member", "Member") + } + ) + .append(")") + } + } + source.sendFeedback(text) + } + } + } + } + + object Regexes { + @Language("RegExp") + val NAME = "(\\[[^\\]]+\\] )?(?<name>[a-zA-Z0-9_]{2,16})" + val NAME_SECONDARY = NAME.replace("name", "name2") + val joinSelf = "You have joined $NAME's? party!".toPattern() + val joinOther = "$NAME joined the party\\.".toPattern() + val leaveSelf = "You left the party\\.".toPattern() + val disbandedEmpty = + "The party was disbanded because all invites expired and the party was empty\\.".toPattern() + val leaveOther = "$NAME has left the party\\.".toPattern() + val kickedOther = "$NAME has been removed from the party\\.".toPattern() + val kickedOtherOffline = "Kicked $NAME because they were offline\\.".toPattern() + val disconnectedOther = "$NAME was removed from your party because they disconnected\\.".toPattern() + val transferLeave = "The party was transferred to $NAME because $NAME_SECONDARY left\\.?".toPattern() + val transferVoluntary = "The party was transferred to $NAME by $NAME_SECONDARY\\.?".toPattern() + val disbanded = "$NAME has disbanded the party!".toPattern() + val kickedSelf = "You have been kicked from the party by $NAME ?\\.?".toPattern() + val partyFinderJoin = "Party Finder > $NAME joined the .* group!.*".toPattern() + } + + fun modifyParty( + allowEmpty: Boolean = false, + modifier: (MutableList<PartyMember>) -> Unit + ) { + val oldList = party?.members ?: emptyList() + if (oldList.isEmpty() && !allowEmpty) return + party = Party(oldList.toMutableList().also(modifier)) + } + + fun MutableList<PartyMember>.modifyMember(name: String, mod: (PartyMember) -> PartyMember) { + val idx = indexOfFirst { it.name == name } + val member = if (idx < 0) { + PartyMember(name, PartyRole.MEMBER) + } else { + removeAt(idx) + } + add(mod(member)) + } + + fun addMemberToParty(name: String) { + modifyParty(true) { + if (it.isEmpty()) + it.add(PartyMember(MC.playerName, PartyRole.LEADER)) + it.add(PartyMember(name, PartyRole.MEMBER)) + } + } + + @Subscribe + fun onJoinServer(event: WorldReadyEvent) { // This event isn't perfect... Hypixel isn't ready yet when we join the server. We should probably just listen to the mod api hello packet and go from there, but this works (since you join and leave servers quite often). + if (party == null) + sendSyncPacket() + } + + @Subscribe + fun onPartyRelatedMessage(event: ProcessChatEvent) { + Regexes.joinSelf.useMatch(event.unformattedString) { + sendSyncPacket() + } + Regexes.joinOther.useMatch(event.unformattedString) { + addMemberToParty(group("name")) + } + Regexes.leaveOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.leaveSelf.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.disbandedEmpty.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.kickedOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.kickedOtherOffline.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.disconnectedOther.useMatch(event.unformattedString) { + modifyParty { it.removeIf { it.name == group("name") } } + } + Regexes.transferLeave.useMatch(event.unformattedString) { + modifyParty { + it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) } + it.removeIf { it.name == group("name2") } + } + } + Regexes.transferVoluntary.useMatch(event.unformattedString) { + modifyParty { + it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) } + it.modifyMember(group("name2")) { it.copy(role = PartyRole.MOD) } + } + } + Regexes.disbanded.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.kickedSelf.useMatch(event.unformattedString) { + modifyParty { it.clear() } + } + Regexes.partyFinderJoin.useMatch(event.unformattedString) { + addMemberToParty(group("name")) + } + } + } + + data class Party( + val members: List<PartyMember> + ) + + data class PartyMember( + val name: String, + val role: PartyRole + ) { + companion object { + suspend fun fromUuid(uuid: UUID, role: PartyRole = PartyRole.MEMBER): PartyMember { + return PartyMember( + ErrorUtil.notNullOr( + Routes.getPlayerNameForUUID(uuid), + "Could not find username for player $uuid" + ) { "Ghost" }, + role + ) + } + } + } + + var party: Party? = null +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 177b0af..f7c7d1c 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -161,7 +161,12 @@ fun MutableText.red() = withColor(Formatting.RED) fun MutableText.white() = withColor(Formatting.WHITE) fun MutableText.bold(): MutableText = styled { it.withBold(true) } fun MutableText.hover(text: Text): MutableText = styled { it.withHoverEvent(HoverEvent.ShowText(text)) } - +fun MutableText.boolColour( + bool: Boolean, + ifTrue: Formatting = Formatting.GREEN, + ifFalse: Formatting = Formatting.DARK_RED +) = + if (bool) withColor(ifTrue) else withColor(ifFalse) fun MutableText.clickCommand(command: String): MutableText { require(command.startsWith("/")) diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 115778f..437d5dc 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -70,6 +70,7 @@ "dependencies": [ "roughlyenoughitems(recommended){modrinth:rei}" ] - } + }, + "firmament:hide_from_modlist": true } } |
