aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-11-27 17:26:42 +0100
committerLinnea Gräf <nea@nea.moe>2024-11-27 17:26:42 +0100
commit8df225399f1932b8824d2fc44f4c964bc47fc6aa (patch)
treea2f1d7f64f68242aaaa5b97df2c15665eb7d12ce /src/main
parentccb5c556def69ea16a52c00b3fbfe3a224f51ac2 (diff)
downloadFirmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.tar.gz
Firmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.tar.bz2
Firmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.zip
feat: Add pickobulus blocker on private islandHEADmaster
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java318
-rw-r--r--src/main/kotlin/events/UseItemEvent.kt11
-rw-r--r--src/main/kotlin/events/registration/ChatEvents.kt81
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt43
-rw-r--r--src/main/kotlin/gui/CheckboxComponent.kt56
-rw-r--r--src/main/kotlin/gui/config/ChoiceHandler.kt47
-rw-r--r--src/main/kotlin/gui/config/EnumRenderer.kt15
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt24
-rw-r--r--src/main/kotlin/keybindings/FirmamentKeyBindings.kt29
-rw-r--r--src/main/kotlin/util/MC.kt6
-rw-r--r--src/main/kotlin/util/TestUtil.kt1
-rw-r--r--src/main/kotlin/util/json/KJsonOps.kt131
-rw-r--r--src/main/kotlin/util/skyblock/ItemType.kt5
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.pngbin0 -> 683 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.pngbin0 -> 614 bytes
-rw-r--r--src/main/resources/firmament.accesswidener2
16 files changed, 560 insertions, 209 deletions
diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
index 9b891a4..0713068 100644
--- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
+++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
@@ -16,160 +16,168 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class AutoDiscoveryPlugin {
- private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>();
-
- public static List<AutoDiscoveryPlugin> getMixinPlugins() {
- return mixinPlugins;
- }
-
- private String mixinPackage;
-
- public void setMixinPackage(String mixinPackage) {
- this.mixinPackage = mixinPackage;
- mixinPlugins.add(this);
- }
-
- /**
- * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root.
- * In either case the return value of this + the class name will resolve back to the original class url, or to other
- * class urls for other classes.
- */
- public URL getBaseUrlForClassUrl(URL classUrl) {
- String string = classUrl.toString();
- if (classUrl.getProtocol().equals("jar")) {
- try {
- return new URL(string.substring(4).split("!")[0]);
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
- if (string.endsWith(".class")) {
- try {
- return new URL(string.replace("\\", "/")
- .replace(getClass().getCanonicalName()
- .replace(".", "/") + ".class", ""));
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
- return classUrl;
- }
-
- /**
- * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}.
- */
- public String getMixinPackage() {
- return mixinPackage;
- }
-
- /**
- * Get the path inside the class root to the mixin package
- */
- public String getMixinBaseDir() {
- return mixinPackage.replace(".", "/");
- }
-
- /**
- * A list of all discovered mixins.
- */
- private List<String> mixins = null;
-
- /**
- * Try to add mixin class ot the mixins based on the filepath inside of the class root.
- * Removes the {@code .class} file suffix, as well as the base mixin package.
- * <p><b>This method cannot be called after mixin initialization.</p>
- *
- * @param className the name or path of a class to be registered as a mixin.
- */
- public void tryAddMixinClass(String className) {
- if (!className.endsWith(".class")) return;
- String norm = (className.substring(0, className.length() - ".class".length()))
- .replace("\\", "/")
- .replace("/", ".");
- if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
- mixins.add(norm.substring(getMixinPackage().length() + 1));
- }
- }
-
- private void tryDiscoverFromContentFile(URL url) {
- Path file;
- try {
- file = Paths.get(getBaseUrlForClassUrl(url).toURI());
- } catch (URISyntaxException e) {
- throw new RuntimeException(e);
- }
- System.out.println("Base directory found at " + file);
- if (!Files.exists(file)) {
- System.out.println("Skipping non-existing mixin root: " + file);
- return;
- }
- if (Files.isDirectory(file)) {
- walkDir(file);
- } else {
- walkJar(file);
- }
- System.out.println("Found mixins: " + mixins);
-
- }
-
- /**
- * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()}
- */
- public List<String> getMixins() {
- if (mixins != null) return mixins;
- System.out.println("Trying to discover mixins");
- mixins = new ArrayList<>();
- URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
- System.out.println("Found classes at " + classUrl);
- tryDiscoverFromContentFile(classUrl);
- var classRoots = System.getProperty("firmament.classroots");
- if (classRoots != null && !classRoots.isBlank()) {
- System.out.println("Found firmament class roots: " + classRoots);
- for (String s : classRoots.split(File.pathSeparator)) {
- if (s.isBlank()) {
- continue;
- }
- try {
- tryDiscoverFromContentFile(new File(s).toURI().toURL());
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
- }
- return mixins;
- }
-
- /**
- * Search through directory for mixin classes based on {@link #getMixinBaseDir}.
- *
- * @param classRoot The root directory in which classes are stored for the default package.
- */
- private void walkDir(Path classRoot) {
- System.out.println("Trying to find mixins from directory");
- var path = classRoot.resolve(getMixinBaseDir());
- if (!Files.exists(path)) return;
- try (Stream<Path> classes = Files.walk(path)) {
- classes.map(it -> classRoot.relativize(it).toString())
- .forEach(this::tryAddMixinClass);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Read through a JAR file, trying to find all mixins inside.
- */
- private void walkJar(Path file) {
- System.out.println("Trying to find mixins from jar file");
- try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) {
- ZipEntry next;
- while ((next = zis.getNextEntry()) != null) {
- tryAddMixinClass(next.getName());
- zis.closeEntry();
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
+ public static List<String> getDefaultAllMixinClassesFQNs() {
+ var defaultName = "moe.nea.firmament.mixins";
+ var plugin = new AutoDiscoveryPlugin();
+ plugin.setMixinPackage(defaultName);
+ var mixins = plugin.getMixins();
+ return mixins.stream().map(it -> defaultName + "." + it).toList();
+ }
+
+ private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>();
+
+ public static List<AutoDiscoveryPlugin> getMixinPlugins() {
+ return mixinPlugins;
+ }
+
+ private String mixinPackage;
+
+ public void setMixinPackage(String mixinPackage) {
+ this.mixinPackage = mixinPackage;
+ mixinPlugins.add(this);
+ }
+
+ /**
+ * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root.
+ * In either case the return value of this + the class name will resolve back to the original class url, or to other
+ * class urls for other classes.
+ */
+ public URL getBaseUrlForClassUrl(URL classUrl) {
+ String string = classUrl.toString();
+ if (classUrl.getProtocol().equals("jar")) {
+ try {
+ return new URL(string.substring(4).split("!")[0]);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (string.endsWith(".class")) {
+ try {
+ return new URL(string.replace("\\", "/")
+ .replace(getClass().getCanonicalName()
+ .replace(".", "/") + ".class", ""));
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return classUrl;
+ }
+
+ /**
+ * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}.
+ */
+ public String getMixinPackage() {
+ return mixinPackage;
+ }
+
+ /**
+ * Get the path inside the class root to the mixin package
+ */
+ public String getMixinBaseDir() {
+ return mixinPackage.replace(".", "/");
+ }
+
+ /**
+ * A list of all discovered mixins.
+ */
+ private List<String> mixins = null;
+
+ /**
+ * Try to add mixin class ot the mixins based on the filepath inside of the class root.
+ * Removes the {@code .class} file suffix, as well as the base mixin package.
+ * <p><b>This method cannot be called after mixin initialization.</p>
+ *
+ * @param className the name or path of a class to be registered as a mixin.
+ */
+ public void tryAddMixinClass(String className) {
+ if (!className.endsWith(".class")) return;
+ String norm = (className.substring(0, className.length() - ".class".length()))
+ .replace("\\", "/")
+ .replace("/", ".");
+ if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
+ mixins.add(norm.substring(getMixinPackage().length() + 1));
+ }
+ }
+
+ private void tryDiscoverFromContentFile(URL url) {
+ Path file;
+ try {
+ file = Paths.get(getBaseUrlForClassUrl(url).toURI());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ System.out.println("Base directory found at " + file);
+ if (!Files.exists(file)) {
+ System.out.println("Skipping non-existing mixin root: " + file);
+ return;
+ }
+ if (Files.isDirectory(file)) {
+ walkDir(file);
+ } else {
+ walkJar(file);
+ }
+ System.out.println("Found mixins: " + mixins);
+
+ }
+
+ /**
+ * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()}
+ */
+ public List<String> getMixins() {
+ if (mixins != null) return mixins;
+ System.out.println("Trying to discover mixins");
+ mixins = new ArrayList<>();
+ URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
+ System.out.println("Found classes at " + classUrl);
+ tryDiscoverFromContentFile(classUrl);
+ var classRoots = System.getProperty("firmament.classroots");
+ if (classRoots != null && !classRoots.isBlank()) {
+ System.out.println("Found firmament class roots: " + classRoots);
+ for (String s : classRoots.split(File.pathSeparator)) {
+ if (s.isBlank()) {
+ continue;
+ }
+ try {
+ tryDiscoverFromContentFile(new File(s).toURI().toURL());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ return mixins;
+ }
+
+ /**
+ * Search through directory for mixin classes based on {@link #getMixinBaseDir}.
+ *
+ * @param classRoot The root directory in which classes are stored for the default package.
+ */
+ private void walkDir(Path classRoot) {
+ System.out.println("Trying to find mixins from directory");
+ var path = classRoot.resolve(getMixinBaseDir());
+ if (!Files.exists(path)) return;
+ try (Stream<Path> classes = Files.walk(path)) {
+ classes.map(it -> classRoot.relativize(it).toString())
+ .forEach(this::tryAddMixinClass);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Read through a JAR file, trying to find all mixins inside.
+ */
+ private void walkJar(Path file) {
+ System.out.println("Trying to find mixins from jar file");
+ try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) {
+ ZipEntry next;
+ while ((next = zis.getNextEntry()) != null) {
+ tryAddMixinClass(next.getName());
+ zis.closeEntry();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/src/main/kotlin/events/UseItemEvent.kt b/src/main/kotlin/events/UseItemEvent.kt
new file mode 100644
index 0000000..e294bb1
--- /dev/null
+++ b/src/main/kotlin/events/UseItemEvent.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.events
+
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Hand
+import net.minecraft.world.World
+
+data class UseItemEvent(val playerEntity: PlayerEntity, val world: World, val hand: Hand) : FirmamentEvent.Cancellable() {
+ companion object : FirmamentEventBus<UseItemEvent>()
+ val item: ItemStack = playerEntity.getStackInHand(hand)
+}
diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt
index 4c1c63f..1dcc91a 100644
--- a/src/main/kotlin/events/registration/ChatEvents.kt
+++ b/src/main/kotlin/events/registration/ChatEvents.kt
@@ -1,10 +1,9 @@
-
-
package moe.nea.firmament.events.registration
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents
import net.fabricmc.fabric.api.event.player.AttackBlockCallback
import net.fabricmc.fabric.api.event.player.UseBlockCallback
+import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.minecraft.text.Text
import net.minecraft.util.ActionResult
import moe.nea.firmament.events.AllowChatEvent
@@ -12,43 +11,53 @@ import moe.nea.firmament.events.AttackBlockEvent
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.UseBlockEvent
+import moe.nea.firmament.events.UseItemEvent
private var lastReceivedMessage: Text? = null
fun registerFirmamentEvents() {
- ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
- lastReceivedMessage = message
- !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
- && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
- })
- ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
- lastReceivedMessage = message
- overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
- !AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
- })
- ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
- if (overlay) message
- else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
- })
- ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
- if (!overlay && lastReceivedMessage !== message) {
- ProcessChatEvent.publish(ProcessChatEvent(message, true))
- }
- })
- ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
- if (lastReceivedMessage !== message) {
- ProcessChatEvent.publish(ProcessChatEvent(message, true))
- }
- })
+ ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
+ lastReceivedMessage = message
+ !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
+ && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
+ })
+ ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
+ lastReceivedMessage = message
+ overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
+ !AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
+ })
+ ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
+ if (overlay) message
+ else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
+ })
+ ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
+ if (!overlay && lastReceivedMessage !== message) {
+ ProcessChatEvent.publish(ProcessChatEvent(message, true))
+ }
+ })
+ ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
+ if (lastReceivedMessage !== message) {
+ ProcessChatEvent.publish(ProcessChatEvent(message, true))
+ }
+ })
- AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction ->
- if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled)
- ActionResult.CONSUME
- else ActionResult.PASS
- })
- UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
- if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled)
- ActionResult.CONSUME
- else ActionResult.PASS
- })
+ AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction ->
+ if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled)
+ ActionResult.CONSUME
+ else ActionResult.PASS
+ })
+ UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
+ if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled)
+ ActionResult.CONSUME
+ else ActionResult.PASS
+ })
+ UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
+ if (UseItemEvent.publish(UseItemEvent(player, world, hand)).cancelled)
+ ActionResult.CONSUME
+ else ActionResult.PASS
+ })
+ UseItemCallback.EVENT.register(UseItemCallback { playerEntity, world, hand ->
+ if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.CONSUME
+ else ActionResult.PASS
+ })
}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 4fcf8a7..2d6c3ee 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -7,11 +7,13 @@ import net.minecraft.item.ItemStack
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.SlotClickEvent
+import moe.nea.firmament.events.UseItemEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
@@ -27,10 +29,13 @@ import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
+import moe.nea.firmament.util.red
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.skyblock.AbilityUtils
+import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.toShedaniel
+import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
@@ -43,6 +48,22 @@ object PickaxeAbility : FirmamentFeature {
val cooldownEnabled by toggle("ability-cooldown") { false }
val cooldownScale by integer("ability-scale", 16, 64) { 16 }
val drillFuelBar by toggle("fuel-bar") { true }
+ val blockOnPrivateIsland by choice(
+ "block-on-dynamic",
+ BlockPickaxeAbility.entries,
+ ) {
+ BlockPickaxeAbility.ONLY_DESTRUCTIVE
+ }
+ }
+
+ enum class BlockPickaxeAbility : StringIdentifiable {
+ NEVER,
+ ALWAYS,
+ ONLY_DESTRUCTIVE;
+
+ override fun asString(): String {
+ return name
+ }
}
var lobbyJoinTime = TimeMark.farPast()
@@ -56,6 +77,8 @@ object PickaxeAbility : FirmamentFeature {
"Maniac Miner" to 59.seconds,
"Vein Seeker" to 60.seconds
)
+ val destructiveAbilities = setOf("Pickobulus")
+ val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET)
override val config: ManagedConfig
get() = TConfig
@@ -74,6 +97,26 @@ object PickaxeAbility : FirmamentFeature {
}
@Subscribe
+ fun onPickaxeRightClick(event: UseItemEvent) {
+ if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return
+ val itemType = ItemType.fromItemStack(event.item)
+ if (itemType !in pickaxeTypes) return
+ val ability = AbilityUtils.getAbilities(event.item)
+ val shouldBlock = when (TConfig.blockOnPrivateIsland) {
+ BlockPickaxeAbility.NEVER -> false
+ BlockPickaxeAbility.ALWAYS -> ability.any()
+ BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities }
+ }
+ if (shouldBlock) {
+ MC.sendChat(tr("firmament.pickaxe.blocked",
+ "Firmament blocked a pickaxe ability from being used on a private island.")
+ .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic")
+ )
+ event.cancel()
+ }
+ }
+
+ @Subscribe
fun onSlotClick(it: SlotClickEvent) {
if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
val name = it.stack.displayNameAccordingToNbt.unformattedString
diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt
new file mode 100644
index 0000000..761c086
--- /dev/null
+++ b/src/main/kotlin/gui/CheckboxComponent.kt
@@ -0,0 +1,56 @@
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
+import net.minecraft.client.render.RenderLayer
+import moe.nea.firmament.Firmament
+
+class CheckboxComponent<T>(
+ val state: GetSetter<T>,
+ val value: T,
+) : GuiComponent() {
+ override fun getWidth(): Int {
+ return 16
+ }
+
+ override fun getHeight(): Int {
+ return 16
+ }
+
+ fun isEnabled(): Boolean {
+ return state.get() == value
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ val ctx = (context.renderContext as ModernRenderContext).drawContext
+ ctx.drawGuiTexture(
+ RenderLayer::getGuiTextured,
+ if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked")
+ else Firmament.identifier("firmament:widget/checkbox_unchecked"),
+ 0, 0,
+ 16, 16
+ )
+ }
+
+ var isClicking = false
+
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (mouseEvent is MouseEvent.Click) {
+ if (isClicking && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
+ isClicking = false
+ if (context.isHovered)
+ state.set(value)
+ return true
+ }
+ if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) {
+ requestFocus()
+ isClicking = true
+ return true
+ }
+ }
+ return false
+ }
+}
diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt
new file mode 100644
index 0000000..25e885a
--- /dev/null
+++ b/src/main/kotlin/gui/config/ChoiceHandler.kt
@@ -0,0 +1,47 @@
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign
+import io.github.notenoughupdates.moulconfig.gui.VerticalAlign
+import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import kotlinx.serialization.json.JsonElement
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.util.StringIdentifiable
+import moe.nea.firmament.gui.CheckboxComponent
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.json.KJsonOps
+
+class ChoiceHandler<E>(
+ val universe: List<E>,
+) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable {
+ val codec = StringIdentifiable.createCodec {
+ @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ (universe as java.util.List<*>).toArray(arrayOfNulls<Enum<E>>(0)) as Array<E>
+ }
+ val renderer = EnumRenderer.default<E>()
+
+ override fun toJson(element: E): JsonElement? {
+ return codec.encodeStart(KJsonOps.INSTANCE, element)
+ .promotePartial { ErrorUtil.softError("Failed to encode json element '$element': $it") }.result()
+ .getOrNull()
+ }
+
+ override fun fromJson(element: JsonElement): E {
+ return codec.decode(KJsonOps.INSTANCE, element)
+ .promotePartial { ErrorUtil.softError("Failed to decode json element '$element': $it") }
+ .result()
+ .get()
+ .first
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<E>, guiAppender: GuiAppender) {
+ guiAppender.appendFullRow(TextComponent(opt.labelText.string))
+ for (e in universe) {
+ guiAppender.appendFullRow(RowComponent(
+ AlignComponent(CheckboxComponent(opt, e), { HorizontalAlign.LEFT }, { VerticalAlign.CENTER }),
+ TextComponent(renderer.getName(opt, e).string)
+ ))
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt
new file mode 100644
index 0000000..3b80b7e
--- /dev/null
+++ b/src/main/kotlin/gui/config/EnumRenderer.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.gui.config
+
+import net.minecraft.text.Text
+
+interface EnumRenderer<E : Any> {
+ fun getName(option: ManagedOption<E>, value: E): Text
+
+ companion object {
+ fun <E : Enum<E>> default() = object : EnumRenderer<E> {
+ override fun getName(option: ManagedOption<E>, value: E): Text {
+ return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase())
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
index 8222a46..47a9c92 100644
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.gui.config
+import com.mojang.serialization.Codec
import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import io.github.notenoughupdates.moulconfig.gui.GuiContext
@@ -20,6 +21,7 @@ import kotlin.io.path.writeText
import kotlin.time.Duration
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.SavedKeyBinding
@@ -113,6 +115,28 @@ abstract class ManagedConfig(
return option(propertyName, default, BooleanHandler(this))
}
+ protected fun <E> choice(
+ propertyName: String,
+ universe: List<E>,
+ default: () -> E
+ ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
+ return option(propertyName, default, ChoiceHandler(universe))
+ }
+
+// TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434
+// protected inline fun <reified E> choice(
+// propertyName: String,
+// noinline default: () -> E
+// ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
+// return choice(
+// propertyName,
+// enumEntries<E>().toList(),
+// StringIdentifiable.createCodec { enumValues<E>() },
+// EnumRenderer.default(),
+// default
+// )
+// }
+
protected fun duration(
propertyName: String,
min: Duration,
diff --git a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
index e2bed8d..59b131a 100644
--- a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
+++ b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
@@ -1,26 +1,25 @@
-
-
package moe.nea.firmament.keybindings
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
import net.minecraft.client.option.KeyBinding
import net.minecraft.client.util.InputUtil
-import moe.nea.firmament.gui.config.KeyBindingHandler
import moe.nea.firmament.gui.config.ManagedOption
+import moe.nea.firmament.util.TestUtil
object FirmamentKeyBindings {
- fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) {
- val vanillaKeyBinding = KeyBindingHelper.registerKeyBinding(
- KeyBinding(
- name,
- InputUtil.Type.KEYSYM,
- -1,
- "firmament.key.category"
- )
- )
- keyBindings[vanillaKeyBinding] = config
- }
+ fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) {
+ val vanillaKeyBinding = KeyBinding(
+ name,
+ InputUtil.Type.KEYSYM,
+ -1,
+ "firmament.key.category"
+ )
+ if (!TestUtil.isInTest) {
+ KeyBindingHelper.registerKeyBinding(vanillaKeyBinding)
+ }
+ keyBindings[vanillaKeyBinding] = config
+ }
- val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>()
+ val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>()
}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index a60d5c4..294334a 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -92,12 +92,12 @@ object MC {
inline val inGameHud: InGameHud get() = instance.inGameHud
inline val font get() = instance.textRenderer
inline val soundManager get() = instance.soundManager
- inline val player: ClientPlayerEntity? get() = instance.player
+ inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player }
inline val camera: Entity? get() = instance.cameraEntity
inline val guiAtlasManager get() = instance.guiAtlasManager
- inline val world: ClientWorld? get() = instance.world
+ inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
inline var screen: Screen?
- get() = instance.currentScreen
+ get() = TestUtil.unlessTesting{ instance.currentScreen }
set(value) = instance.setScreen(value)
val screenName get() = screen?.title?.unformattedString?.trim()
inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt
index 2d38f35..45e3dde 100644
--- a/src/main/kotlin/util/TestUtil.kt
+++ b/src/main/kotlin/util/TestUtil.kt
@@ -1,6 +1,7 @@
package moe.nea.firmament.util
object TestUtil {
+ inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block()
val isInTest =
Thread.currentThread().stackTrace.any {
it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.")
diff --git a/src/main/kotlin/util/json/KJsonOps.kt b/src/main/kotlin/util/json/KJsonOps.kt
new file mode 100644
index 0000000..404ea5e
--- /dev/null
+++ b/src/main/kotlin/util/json/KJsonOps.kt
@@ -0,0 +1,131 @@
+package moe.nea.firmament.util.json
+
+import com.google.gson.internal.LazilyParsedNumber
+import com.mojang.datafixers.util.Pair
+import com.mojang.serialization.DataResult
+import com.mojang.serialization.DynamicOps
+import java.util.stream.Stream
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.booleanOrNull
+import kotlin.streams.asSequence
+
+class KJsonOps : DynamicOps<JsonElement> {
+ companion object {
+ val INSTANCE = KJsonOps()
+ }
+
+ override fun empty(): JsonElement {
+ return JsonNull
+ }
+
+ override fun createNumeric(num: Number): JsonElement {
+ return JsonPrimitive(num)
+ }
+
+ override fun createString(str: String): JsonElement {
+ return JsonPrimitive(str)
+ }
+
+ override fun remove(input: JsonElement, key: String): JsonElement {
+ if (input is JsonObject) {
+ return JsonObject(input.filter { it.key != key })
+ } else {
+ return input
+ }
+ }
+
+ override fun createList(stream: Stream<JsonElement>): JsonElement {
+ return JsonArray(stream.toList())
+ }
+
+ override fun getStream(input: JsonElement): DataResult<Stream<JsonElement>> {
+ if (input is JsonArray)
+ return DataResult.success(input.stream())
+ return DataResult.error { "Not a json array: $input" }
+ }
+
+ override fun createMap(map: Stream<Pair<JsonElement, JsonElement>>): JsonElement {
+ return JsonObject(map.asSequence()
+ .map { ((it.first as JsonPrimitive).content) to it.second }
+ .toMap())
+ }
+
+ override fun getMapValues(input: JsonElement): DataResult<Stream<Pair<JsonElement, JsonElement>>> {
+ if (input is JsonObject) {
+ return DataResult.success(input.entries.stream().map { Pair.of(createString(it.key), it.value) })
+ }
+ return DataResult.error { "Not a JSON object: $input" }
+ }
+
+ override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult<JsonElement> {
+ if (key !is JsonPrimitive || key.isString) {
+ return DataResult.error { "key is not a string: $key" }
+ }
+ val jKey = key.content
+ val extra = mapOf(jKey to value)
+ if (map == empty()) {
+ return DataResult.success(JsonObject(extra))
+ }
+ if (map is JsonObject) {
+ return DataResult.success(JsonObject(map + extra))
+ }
+ return DataResult.error { "mergeToMap called with not a map: $map" }
+ }
+
+ override fun mergeToList(list: JsonElement, value: JsonElement): DataResult<JsonElement> {
+ if (list == empty())
+ return DataResult.success(JsonArray(listOf(value)))
+ if (list is JsonArray) {
+ return DataResult.success(JsonArray(list + value))
+ }
+ return DataResult.error { "mergeToList called with not a list: $list" }
+ }
+
+ override fun getStringValue(input: JsonElement): DataResult<String> {
+ if (input is JsonPrimitive && input.isString) {
+ return DataResult.success(input.content)
+ }
+ return DataResult.error { "Not a string: $input" }
+ }
+
+ override fun getNumberValue(input: JsonElement): DataResult<Number> {
+ if (input is JsonPrimitive && !input.isString && input.booleanOrNull == null)
+ return DataResult.success(LazilyParsedNumber(input.content))
+ return DataResult.error { "not a number: $input" }
+ }
+
+ override fun createBoolean(value: Boolean): JsonElement {
+ return JsonPrimitive(value)
+ }
+
+ override fun getBooleanValue(input: JsonElement): DataResult<Boolean> {
+ if (input is JsonPrimitive) {
+ if (input.booleanOrNull != null)
+ return DataResult.success(input.boolean)
+ return super.getBooleanValue(input)
+ }
+ return DataResult.error { "Not a boolean: $input" }
+ }
+
+ override fun <U : Any?> convertTo(output: DynamicOps<U>, input: JsonElement): U {
+ if (input is JsonObject)
+ return output.createMap(
+ input.entries.stream().map { Pair.of(output.createString(it.key), convertTo(output, it.value)) })
+ if (input is JsonArray)
+ return output.createList(input.stream().map { convertTo(output, it) })
+ if (input is JsonNull)
+ return output.empty()
+ if (input is JsonPrimitive) {
+ if (input.isString)
+ return output.createString(input.content)
+ if (input.booleanOrNull != null)
+ return output.createBoolean(input.boolean)
+ }
+ error("Unknown json value: $input")
+ }
+}
diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt
index b031b69..6ddb077 100644
--- a/src/main/kotlin/util/skyblock/ItemType.kt
+++ b/src/main/kotlin/util/skyblock/ItemType.kt
@@ -32,10 +32,15 @@ value class ItemType private constructor(val name: String) {
val SWORD = ofName("SWORD")
val DRILL = ofName("DRILL")
val PICKAXE = ofName("PICKAXE")
+ val GAUNTLET = ofName("GAUNTLET")
/**
* This one is not really official (it never shows up in game).
*/
val PET = ofName("PET")
}
+
+ override fun toString(): String {
+ return name
+ }
}
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png
new file mode 100644
index 0000000..1b87c55
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png
new file mode 100644
index 0000000..dcd9aa4
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png
Binary files differ
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index c542fc8..8e7dbab 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -34,3 +34,5 @@ accessible method net/minecraft/entity/passive/TameableEntity isInSameTeam (Lnet
accessible method net/minecraft/entity/Entity isInSameTeam (Lnet/minecraft/entity/Entity;)Z
accessible method net/minecraft/registry/entry/RegistryEntry$Reference setTags (Ljava/util/Collection;)V
accessible method net/minecraft/registry/entry/RegistryEntryList$Named setEntries (Ljava/util/List;)V
+accessible method net/minecraft/world/biome/source/util/VanillaBiomeParameters writeOverworldBiomeParameters (Ljava/util/function/Consumer;)V
+accessible method net/minecraft/world/gen/densityfunction/DensityFunctions createSurfaceNoiseRouter (Lnet/minecraft/registry/RegistryEntryLookup;Lnet/minecraft/registry/RegistryEntryLookup;ZZ)Lnet/minecraft/world/gen/noise/NoiseRouter;