aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-07-27 05:07:58 +0200
committerLinnea Gräf <nea@nea.moe>2024-07-27 05:07:58 +0200
commit60994a08677d864f79de58d8912b02d2d7077289 (patch)
tree70850c6acb5d6fa58d8df9cc08e06a7d92606f2a
parent564f81e1896ea36b9ad165f29f49ce990912c388 (diff)
downloadfirmament-60994a08677d864f79de58d8912b02d2d7077289.tar.gz
firmament-60994a08677d864f79de58d8912b02d2d7077289.tar.bz2
firmament-60994a08677d864f79de58d8912b02d2d7077289.zip
Add some carnival features
-rw-r--r--gradle/libs.versions.toml2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java47
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java58
-rw-r--r--src/main/kotlin/moe/nea/firmament/commands/rome.kt13
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/EntityUpdateEvent.kt36
-rw-r--r--src/main/kotlin/moe/nea/firmament/events/PlayerInventoryUpdate.kt16
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/debug/DebugLogger.kt18
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/events/carnival/CarnivalFeatures.kt22
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/events/carnival/MinesweeperHelper.kt281
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/LegacyFormattingCode.kt3
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt29
-rw-r--r--src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt3
-rw-r--r--src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml56
-rw-r--r--src/main/resources/assets/firmament/lang/en_us.json5
17 files changed, 579 insertions, 14 deletions
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 89263b0..99c6150 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,7 +28,7 @@ hotswap_agent = "1.4.2-SNAPSHOT"
mixinextras = "0.3.5"
jarvis = "1.1.3"
nealisp = "1.0.0"
-moulconfig = "3.0.0-beta.13"
+moulconfig = "3.0.0-beta.14"
manninghamMills = "2.4.1"
[libraries]
diff --git a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java
new file mode 100644
index 0000000..b7476af
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java
@@ -0,0 +1,47 @@
+/*
+ * 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.sugar.Local;
+import moe.nea.firmament.events.EntityUpdateEvent;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.client.network.ClientConnectionState;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket;
+import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket;
+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;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public abstract class EntityUpdateEventListener extends ClientCommonNetworkHandler {
+
+ @Shadow
+ private ClientWorld world;
+
+ protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
+ super(client, connection, connectionState);
+ }
+
+ @Inject(method = "onEntityAttributes", at = @At("TAIL"))
+ private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) {
+ EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate(
+ (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries()));
+ }
+
+ @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER))
+ private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
+ EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues()));
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
new file mode 100644
index 0000000..ad2a919
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
@@ -0,0 +1,58 @@
+/*
+ * 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.sugar.Local;
+import moe.nea.firmament.events.PlayerInventoryUpdate;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientCommonNetworkHandler;
+import net.minecraft.client.network.ClientConnectionState;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.packet.s2c.play.InventoryS2CPacket;
+import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
+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 abstract class SlotUpdateListener extends ClientCommonNetworkHandler {
+ protected SlotUpdateListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
+ super(client, connection, connectionState);
+ }
+
+ @Inject(
+ method = "onScreenHandlerSlotUpdate",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/tutorial/TutorialManager;onSlotUpdate(Lnet/minecraft/item/ItemStack;)V"))
+ private void onSingleSlotUpdate(
+ ScreenHandlerSlotUpdateS2CPacket packet,
+ CallbackInfo ci) {
+ var player = this.client.player;
+ assert player != null;
+ if (packet.getSyncId() == ScreenHandlerSlotUpdateS2CPacket.UPDATE_PLAYER_INVENTORY_SYNC_ID
+ || packet.getSyncId() == 0) {
+ PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Single(packet.getSlot(), packet.getStack()));
+ } else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
+ // TODO: dispatch single chest slot
+ }
+ }
+
+ @Inject(method = "onInventory",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V",
+ shift = At.Shift.AFTER))
+ private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) {
+ var player = this.client.player;
+ assert player != null;
+ if (packet.getSyncId() == 0) {
+ PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents()));
+ } else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
+ // TODO: dispatch multi chest
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
index a50cc75..ace15c1 100644
--- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt
+++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt
@@ -14,10 +14,10 @@ import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen
-import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.gui.config.BooleanHandler
import moe.nea.firmament.gui.config.ManagedOption
@@ -182,11 +182,6 @@ fun firmamentCommand() = literal("firmament") {
}
}
thenLiteral("dev") {
- thenLiteral("config") {
- thenExecute {
- FairySouls.TConfig.showConfigEditor()
- }
- }
thenLiteral("simulate") {
thenArgument("message", RestArgumentType) { message ->
thenExecute {
@@ -208,6 +203,12 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
+ thenLiteral("copyEntities") {
+ thenExecute {
+ val player = MC.player ?: return@thenExecute
+ player.world.getOtherEntities(player, player.boundingBox.expand(12.0)).forEach(PowerUserTools::showEntity)
+ }
+ }
thenLiteral("callUrsa") {
thenArgument("path", string()) { path ->
thenExecute {
diff --git a/src/main/kotlin/moe/nea/firmament/events/EntityUpdateEvent.kt b/src/main/kotlin/moe/nea/firmament/events/EntityUpdateEvent.kt
new file mode 100644
index 0000000..e0d6b8c
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/EntityUpdateEvent.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.events
+
+import net.minecraft.entity.Entity
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.data.DataTracker
+import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
+
+/**
+ * This event is fired when some entity properties are updated.
+ * It is not fired for common changes like position, but is for less common ones,
+ * like health, tracked data, names, equipment. It is always fired
+ * *after* the values have been applied to the entity.
+ */
+sealed class EntityUpdateEvent : FirmamentEvent() {
+ companion object : FirmamentEventBus<EntityUpdateEvent>()
+
+ abstract val entity: Entity
+
+ data class AttributeUpdate(
+ override val entity: LivingEntity,
+ val attributes: List<EntityAttributesS2CPacket.Entry>,
+ ) : EntityUpdateEvent()
+
+ data class TrackedDataUpdate(
+ override val entity: Entity,
+ val trackedValues: List<DataTracker.SerializedEntry<*>>,
+ ) : EntityUpdateEvent()
+
+// TODO: onEntityPassengersSet, onEntityAttach?, onEntityEquipmentUpdate, onEntityStatusEffect
+}
diff --git a/src/main/kotlin/moe/nea/firmament/events/PlayerInventoryUpdate.kt b/src/main/kotlin/moe/nea/firmament/events/PlayerInventoryUpdate.kt
new file mode 100644
index 0000000..3afdbe3
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/events/PlayerInventoryUpdate.kt
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.events
+
+import net.minecraft.item.ItemStack
+
+sealed class PlayerInventoryUpdate : FirmamentEvent() {
+ companion object : FirmamentEventBus<PlayerInventoryUpdate>()
+ data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate()
+ data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate()
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
index f047ad3..d127381 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt
@@ -23,6 +23,7 @@ import moe.nea.firmament.features.debug.MinorTrolling
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.diana.DianaWaypoints
import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures
+import moe.nea.firmament.features.events.carnival.CarnivalFeatures
import moe.nea.firmament.features.fixes.CompatibliltyFeatures
import moe.nea.firmament.features.fixes.Fixes
import moe.nea.firmament.features.inventory.CraftingOverlay
@@ -80,6 +81,7 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(DianaWaypoints)
loadFeature(ItemRarityCosmetics)
loadFeature(PickaxeAbility)
+ loadFeature(CarnivalFeatures)
if (Firmament.DEBUG) {
loadFeature(DeveloperFeatures)
loadFeature(DebugView)
diff --git a/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt b/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt
index 4b7ba9e..f9ed5dc 100644
--- a/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt
@@ -10,6 +10,7 @@ package moe.nea.firmament.features
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.gui.config.ManagedConfig
+// TODO: remove this entire feature system and revamp config
interface FirmamentFeature : SubscriptionOwner {
val identifier: String
val defaultEnabled: Boolean
diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/DebugLogger.kt b/src/main/kotlin/moe/nea/firmament/features/debug/DebugLogger.kt
new file mode 100644
index 0000000..72a641a
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/debug/DebugLogger.kt
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.debug
+
+import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
+
+class DebugLogger(val tag: String) {
+ fun isEnabled() = DeveloperFeatures.isEnabled // TODO: allow filtering by tag
+ fun log(text: () -> String) {
+ if (!isEnabled()) return
+ MC.sendChat(Text.literal(text()))
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
index ec565aa..95ed72d 100644
--- a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt
@@ -88,6 +88,7 @@ object PowerUserTools : FirmamentFeature {
fun showEntity(target: Entity) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type))
MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name))
+ MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos))
if (target is LivingEntity) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.armor"))
for (armorItem in target.armorItems) {
diff --git a/src/main/kotlin/moe/nea/firmament/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/moe/nea/firmament/features/events/carnival/CarnivalFeatures.kt
new file mode 100644
index 0000000..0593678
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/events/carnival/CarnivalFeatures.kt
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.events.carnival
+
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+
+object CarnivalFeatures : FirmamentFeature {
+ object TConfig : ManagedConfig(identifier) {
+ val enableBombSolver by toggle("bombs-solver") { true }
+ val displayTutorials by toggle("tutorials") { true }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+ override val identifier: String
+ get() = "carnival"
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/moe/nea/firmament/features/events/carnival/MinesweeperHelper.kt
new file mode 100644
index 0000000..1df6234
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/events/carnival/MinesweeperHelper.kt
@@ -0,0 +1,281 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+package moe.nea.firmament.features.events.carnival
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import java.util.UUID
+import net.minecraft.block.Blocks
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.text.ClickEvent
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import net.minecraft.world.WorldAccess
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.AttackBlockEvent
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.item.createSkullItem
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.setSkyBlockFirmamentUiId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.useMatch
+
+object MinesweeperHelper {
+ val sandBoxLow = BlockPos(-112, 72, -11)
+ val sandBoxHigh = BlockPos(-106, 72, -5)
+ val boardSize = Pair(sandBoxHigh.x - sandBoxLow.x, sandBoxHigh.z - sandBoxLow.z)
+
+ val gameStartMessage = "[NPC] Carnival Pirateman: Good luck, matey!"
+ val gameEndMessage = "Fruit Digging"
+ val bombPattern = "MINES! There (are|is) (?<bombCount>[0-8]) bombs? hidden nearby\\.".toPattern()
+ val startGameQuestion = "[NPC] Carnival Pirateman: Would ye like to do some Fruit Digging?"
+
+
+ enum class Piece(
+ @get:Bind("fruitName")
+ val fruitName: String,
+ val points: Int,
+ val specialAbility: String,
+ val totalPerBoard: Int,
+ val textureHash: String,
+ val fruitColor: LegacyFormattingCode,
+ ) {
+ COCONUT("Coconut",
+ 200,
+ "Prevents a bomb from exploding next turn",
+ 3,
+ "10ceb1455b471d016a9f06d25f6e468df9fcf223e2c1e4795b16e84fcca264ee",
+ LegacyFormattingCode.DARK_PURPLE),
+ APPLE("Apple",
+ 100,
+ "Gains 100 points for each apple dug up",
+ 8,
+ "17ea278d6225c447c5943d652798d0bbbd1418434ce8c54c54fdac79994ddd6c",
+ LegacyFormattingCode.GREEN),
+ WATERMELON("Watermelon",
+ 100,
+ "Blows up an adjacent fruit for half the points",
+ 4,
+ "efe4ef83baf105e8dee6cf03dfe7407f1911b3b9952c891ae34139560f2931d6",
+ LegacyFormattingCode.DARK_BLUE),
+ DURIAN("Durian",
+ 800,
+ "Halves the points earned in the next turn",
+ 2,
+ "ac268d36c2c6047ffeec00124096376b56dbb4d756a55329363a1b27fcd659cd",
+ LegacyFormattingCode.DARK_PURPLE),
+ MANGO("Mango",
+ 300,
+ "Just an ordinary fruit",
+ 10,
+ "f363a62126a35537f8189343a22660de75e810c6ac004a7d3da65f1c040a839",
+ LegacyFormattingCode.GREEN),
+ DRAGON_FRUIT("Dragonfruit",
+ 1200,
+ "Halves the points earned in the next turn",
+ 1,
+ "3cc761bcb0579763d9b8ab6b7b96fa77eb6d9605a804d838fec39e7b25f95591",
+ LegacyFormattingCode.LIGHT_PURPLE),
+ POMEGRANATE("Pomegranate",
+ 200,
+ "Grants an extra 50% more points in the next turn",
+ 4,
+ "40824d18079042d5769f264f44394b95b9b99ce689688cc10c9eec3f882ccc08",
+ LegacyFormattingCode.DARK_BLUE),
+ CHERRY("Cherry",
+ 200,
+ "The second cherry grants 300 bonus points",
+ 2,
+ "c92b099a62cd2fbf8ada09dec145c75d7fda4dc57b968bea3a8fa11e37aa48b2",
+ LegacyFormattingCode.DARK_PURPLE),
+ BOMB("Bomb",
+ -1,
+ "Destroys nearby fruit",
+ 15,
+ "a76a2811d1e176a07b6d0a657b910f134896ce30850f6e80c7c83732d85381ea",
+ LegacyFormattingCode.DARK_RED),
+ RUM("Rum",
+ -1,
+ "Stops your dowsing ability for one turn",
+ 5,
+ "407b275d28b927b1bf7f6dd9f45fbdad2af8571c54c8f027d1bff6956fbf3c16",
+ LegacyFormattingCode.YELLOW),
+ ;
+
+ val textureUrl = "http://textures.minecraft.net/texture/$textureHash"
+ val itemStack = createSkullItem(UUID.randomUUID(), textureUrl)
+ .setSkyBlockFirmamentUiId("MINESWEEPER_$name")
+
+ @Bind
+ fun getIcon() = ModernItemStack.of(itemStack)
+
+ @Bind
+ fun pieceLabel() = fruitColor.formattingCode + fruitName
+
+ @Bind
+ fun boardLabel() = "§a$totalPerBoard§7/§rboard"
+
+ @Bind("description")
+ fun getDescription() = buildString {
+ append(specialAbility)
+ if (points >= 0) {
+ append(" Default points: $points.")
+ }
+ }
+ }
+
+ object TutorialScreen {
+ @get:Bind("pieces")
+ val pieces = ObservableList(Piece.entries.toList().reversed())
+
+ @get:Bind("modes")
+ val modes = ObservableList(DowsingMode.entries.toList())
+ }
+
+ enum class DowsingMode(
+ val itemType: Item,
+ @get:Bind("feature")
+ val feature: String,
+ @get:Bind("description")
+ val description: String,
+ ) {
+ MINES(Items.IRON_SHOVEL, "Bomb detection", "Tells you how many bombs are near the block"),
+ ANCHOR(Items.DIAMOND_SHOVEL, "Lowest fruit", "Shows you which block nearby contains the lowest scoring fruit"),
+ TREASURE(Items.GOLDEN_SHOVEL, "Highest fruit", "Tells you which kind of fruit is the highest scoring nearby"),
+ ;
+
+ @Bind("itemType")
+ fun getItemStack() = ModernItemStack.of(ItemStack(itemType))
+
+ companion object {
+ val id = SkyblockId("CARNIVAL_SHOVEL")
+ fun fromItem(itemStack: ItemStack): DowsingMode? {
+ if (itemStack.skyBlockId != id) return null
+ return DowsingMode.entries.find { it.itemType == itemStack.item }
+ }
+ }
+ }
+
+ data class BoardPosition(
+ val x: Int,
+ val y: Int
+ ) {
+ fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y)
+
+ fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block
+ fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND
+ fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE
+ fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS
+
+ companion object {
+ fun fromBlockPos(blockPos: BlockPos): BoardPosition? {
+ if (blockPos.y != sandBoxLow.y) return null
+ val x = blockPos.x - sandBoxLow.x
+ val y = blockPos.z - sandBoxLow.z
+ if (x < 0 || x >= boardSize.first) return null
+ if (y < 0 || y >= boardSize.second) return null
+ return BoardPosition(x, y)
+ }
+ }
+ }
+
+ data class GameState(
+ val nearbyBombs: MutableMap<BoardPosition, Int> = mutableMapOf(),
+ val knownBombPositions: MutableSet<BoardPosition> = mutableSetOf(),
+ var lastClickedPosition: BoardPosition? = null,
+ var lastDowsingMode: DowsingMode? = null,
+ )
+
+ var gameState: GameState? = null
+ val log = DebugLogger("minesweeper")
+
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("minesweepertutorial") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("carnival/minesweeper_tutorial",
+ TutorialScreen,
+ null))
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldChange(event: WorldReadyEvent) {
+ gameState = null
+ }
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
+ MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
+ it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
+ })
+ }
+ if (!CarnivalFeatures.TConfig.enableBombSolver) {
+ gameState = null // TODO: replace this which a watchable property
+ return
+ }
+ if (event.unformattedString == gameStartMessage) {
+ gameState = GameState()
+ log.log { "Game started" }
+ }
+ if (event.unformattedString.trim() == gameEndMessage) {
+ gameState = null // TODO: add a loot tracker maybe? probably not, i dont think people care
+ log.log { "Finished game" }
+ }
+ val gs = gameState ?: return
+ bombPattern.useMatch(event.unformattedString) {
+ val bombCount = group("bombCount").toInt()
+ log.log { "Marking ${gs.lastClickedPosition} as having $bombCount nearby" }
+ val pos = gs.lastClickedPosition ?: return
+ gs.nearbyBombs[pos] = bombCount
+ }
+ }
+
+ @Subscribe
+ fun onMobChange(event: EntityUpdateEvent) {
+ val gs = gameState ?: return
+ if (event !is EntityUpdateEvent.TrackedDataUpdate) return
+ // TODO: listen to state
+ }
+
+ @Subscribe
+ fun onBlockClick(event: AttackBlockEvent) {
+ val gs = gameState ?: return
+ val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
+ log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
+ gs.lastClickedPosition = boardPosition
+ gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
+ }
+
+ @Subscribe
+ fun onRender(event: WorldRenderLastEvent) {
+ val gs = gameState ?: return
+ RenderInWorldContext.renderInWorld(event) {
+ for ((pos, bombCount) in gs.nearbyBombs) {
+ this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3"))
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/moe/nea/firmament/util/LegacyFormattingCode.kt b/src/main/kotlin/moe/nea/firmament/util/LegacyFormattingCode.kt
index 1dcd08e..ff4d85b 100644
--- a/src/main/kotlin/moe/nea/firmament/util/LegacyFormattingCode.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/LegacyFormattingCode.kt
@@ -1,5 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+ * SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
@@ -34,4 +35,6 @@ enum class LegacyFormattingCode(val label: String, val char: Char, val index: In
val modern = Formatting.byCode(char)!!
+ val formattingCode = "§$char"
+
}
diff --git a/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt b/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
index 040e2e9..3ac1463 100644
--- a/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/SkyblockId.kt
@@ -16,9 +16,11 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.json.Json
import net.minecraft.component.DataComponentTypes
+import net.minecraft.component.type.NbtComponent
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.set
import moe.nea.firmament.util.json.DashlessUUIDSerializer
/**
@@ -32,12 +34,12 @@ import moe.nea.firmament.util.json.DashlessUUIDSerializer
value class SkyblockId(val neuItem: String) {
val identifier
get() = Identifier.of("skyblockitem",
- neuItem.lowercase().replace(";", "__")
- .replace(":", "___")
- .replace(illlegalPathRegex) {
- it.value.toCharArray()
- .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
- })
+ neuItem.lowercase().replace(";", "__")
+ .replace(":", "___")
+ .replace(illlegalPathRegex) {
+ it.value.toCharArray()
+ .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
+ })
override fun toString(): String {
return neuItem
@@ -85,7 +87,14 @@ data class HypixelPetInfo(
private val jsonparser = Json { ignoreUnknownKeys = true }
val ItemStack.extraAttributes: NbtCompound
- get() = get(DataComponentTypes.CUSTOM_DATA)?.nbt ?: NbtCompound()
+ get() {
+ val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run {
+ val component = NbtComponent.of(NbtCompound())
+ set(DataComponentTypes.CUSTOM_DATA, component)
+ component
+ }
+ return customData.nbt
+ }
val ItemStack.skyblockUUIDString: String?
get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() }
@@ -101,6 +110,12 @@ val ItemStack.petData: HypixelPetInfo?
.getOrElse { return null }
}
+fun ItemStack.setSkyBlockFirmamentUiId(uiId: String) = setSkyBlockId(SkyblockId("FIRMAMENT_UI_$uiId"))
+fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack {
+ this.extraAttributes["id"] = skyblockId.neuItem
+ return this
+}
+
val ItemStack.skyBlockId: SkyblockId?
get() {
return when (val id = extraAttributes.getString("id")) {
diff --git a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
index 291abed..c22b987 100644
--- a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
+++ b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt
@@ -59,6 +59,9 @@ fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) {
}
val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
+fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD)
+ .also { it.setSkullOwner(uuid, url) }
+
fun ItemStack.setSkullOwner(uuid: UUID, url: String) {
assert(this.item == Items.PLAYER_HEAD)
val gameProfile = GameProfile(uuid, "nea89")
diff --git a/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml b/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml
new file mode 100644
index 0000000..408094b
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<!--
+SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA" insets="10">
+ <Column>
+ <Scale scale="2">
+ <Text text="§aFruit§7 Digging"/>
+ </Scale>
+ <Text
+ text="The goal of the fruit digging minigame is to find as many§a fruits§r as possible on a §b7x7§e sand§r grid."
+ width="300"/>
+ <Text
+ text="To do so, you break§e sand blocks§r to reveal what is hidden underneath: a §afruit§r, a §cbomb, or §erum§r."
+ width="300"/>
+ <Text
+ text="When you break a block, you can also get some extra information based on your §7dowsing mode§r."
+ width="300"/>
+ <Scale scale="1.5">
+ <Text text="§7Dowsing Modes"/>
+ </Scale>
+ <Array data="@modes">
+ <Row>
+ <ItemStack value="@itemType"/>
+ <Text text="@feature" width="80"/>
+ <Text text="@description" width="220"/>
+ </Row>
+ </Array>
+
+ <Scale scale="1.5">
+ <Text text="§aTiles"/>
+ </Scale>
+ <ScrollPanel width="300" height="120">
+ <Array data="@pieces">
+ <Row>
+ <Center>
+ <ItemStack value="@getIcon"/>
+ </Center>
+ <Text text="@pieceLabel" width="80"/>
+ <Text text="@description" width="145"/>
+ <Spacer width="5"/>
+ <Text text="@boardLabel" width="50"/>
+ </Row>
+ </Array>
+ </ScrollPanel>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json
index adba8e7..05b10d5 100644
--- a/src/main/resources/assets/firmament/lang/en_us.json
+++ b/src/main/resources/assets/firmament/lang/en_us.json
@@ -16,6 +16,7 @@
"firmament.poweruser.entity.fail": "No entity found under cursor",
"firmament.poweruser.entity.type": "Entity Type: %s",
"firmament.poweruser.entity.name": "Entity Name: %s",
+ "firmament.poweruser.entity.position": "Position: %s",
"firmament.poweruser.entity.armor": "Entity Armor:",
"firmament.poweruser.entity.armor.item": " - %s",
"firmament.poweruser.entity.passengers": "%s Passengers",
@@ -57,6 +58,10 @@
"firmament.config.auto-completions.warp-complete": "Auto Complete /warp",
"firmament.config.auto-completions.warp-is": "Redirect /warp is to /warp island",
"firmanent.config.edit": "Edit",
+ "firmament.config.carnival": "Carnival Features",
+ "firmament.config.carnival.bombs-solver": "Bombs Solver",
+ "firmament.config.carnival.tutorials": "Tutorial Reminder",
+ "firmament.carnival.tutorial.minesweeper": "§eClick here to check out Firmaments Tutorial for this minigame!",
"firmament.config.repo": "Firmament Repo Settings",
"firmament.config.repo.autoUpdate": "Auto Update",
"firmament.config.repo.username": "Repo Username",