diff options
author | Linnea Gräf <nea@nea.moe> | 2024-11-27 17:26:42 +0100 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-11-27 17:26:42 +0100 |
commit | 8df225399f1932b8824d2fc44f4c964bc47fc6aa (patch) | |
tree | a2f1d7f64f68242aaaa5b97df2c15665eb7d12ce | |
parent | ccb5c556def69ea16a52c00b3fbfe3a224f51ac2 (diff) | |
download | Firmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.tar.gz Firmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.tar.bz2 Firmament-8df225399f1932b8824d2fc44f4c964bc47fc6aa.zip |
feat: Add pickobulus blocker on private island
23 files changed, 622 insertions, 263 deletions
diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt index 20a79a8..dec2559 100644 --- a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt +++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt @@ -20,6 +20,7 @@ import io.github.notenoughupdates.moulconfig.gui.editors.ComponentEditor import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorButton +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorDropdown import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorText import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory @@ -31,9 +32,11 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import net.minecraft.client.gui.screen.Screen import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable import net.minecraft.util.Util import moe.nea.firmament.Firmament import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ChoiceHandler import moe.nea.firmament.gui.config.ClickHandler import moe.nea.firmament.gui.config.DurationHandler import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider @@ -115,7 +118,33 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider { } } + fun <T> helpRegisterChoice() where T : Enum<T>, T : StringIdentifiable { + register(ChoiceHandler::class.java as Class<ChoiceHandler<T>>) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm<T>(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorDropdown( + this, + handler.universe.map { handler.renderer.getName(option, it).string }.toTypedArray() + ) + } + + override fun toT(any: Any?): T? { + return handler.universe[any as Int] + } + + override fun getType(): Type { + return Int::class.java + } + + override fun fromT(t: T): Any { + return t.ordinal + } + } + } + } + init { + helpRegisterChoice<Nothing>() register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject -> object : ProcessedEditableOptionFirm<Boolean>(option, categoryAccordionId, configObject) { override fun createEditor(): GuiOptionEditor { 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 Binary files differnew file mode 100644 index 0000000..1b87c55 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png 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 Binary files differnew file mode 100644 index 0000000..dcd9aa4 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png 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; diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt index ee2a322..107b565 100644 --- a/src/test/kotlin/testutil/ItemResources.kt +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -2,11 +2,14 @@ package moe.nea.firmament.test.testutil import net.minecraft.item.ItemStack import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement import net.minecraft.nbt.NbtOps import net.minecraft.nbt.StringNbtReader +import net.minecraft.registry.RegistryOps import net.minecraft.text.Text import net.minecraft.text.TextCodecs import moe.nea.firmament.test.FirmTestBootstrap +import moe.nea.firmament.util.MC object ItemResources { init { @@ -23,15 +26,16 @@ object ItemResources { fun loadSNbt(path: String): NbtCompound { return StringNbtReader.parse(loadString(path)) } + fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE) fun loadText(name: String): Text { - return TextCodecs.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/chat/$name.snbt")) + return TextCodecs.CODEC.parse(getNbtOps(), loadSNbt("testdata/chat/$name.snbt")) .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } } fun loadItem(name: String): ItemStack { // TODO: make the load work with enchantments - return ItemStack.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/items/$name.snbt")) + return ItemStack.CODEC.parse(getNbtOps(), loadSNbt("testdata/items/$name.snbt")) .getOrThrow { IllegalStateException("Could not load test item '$name': $it") } } } diff --git a/src/test/kotlin/util/TextUtilText.kt b/src/test/kotlin/util/TextUtilText.kt index e676d63..46ed3b4 100644 --- a/src/test/kotlin/util/TextUtilText.kt +++ b/src/test/kotlin/util/TextUtilText.kt @@ -2,7 +2,6 @@ package moe.nea.firmament.test.util import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.getLegacyFormatString diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt index dbce762..206a357 100644 --- a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -2,7 +2,6 @@ package moe.nea.firmament.test.util.skyblock import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import net.minecraft.text.Text diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt index c89e57d..cca3d13 100644 --- a/src/test/kotlin/util/skyblock/ItemTypeTest.kt +++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt @@ -1,53 +1,26 @@ package moe.nea.firmament.test.util.skyblock -import io.kotest.core.spec.style.AnnotationSpec -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.shouldBe import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.ItemType -class ItemTypeTest : AnnotationSpec() { - @Test - fun testPetItem() { - Assertions.assertEquals( - ItemType.PET, - ItemType.fromItemStack(ItemResources.loadItem("pets/lion-item")) - ) - } - - @Test - fun testPetInUI() { - Assertions.assertEquals( - ItemType.PET, - ItemType.fromItemStack(ItemResources.loadItem("pets/rabbit-selected")) - ) - Assertions.assertEquals( - ItemType.PET, - ItemType.fromItemStack(ItemResources.loadItem("pets/mithril-golem-not-selected")) - ) - } - - @Test - fun testAOTV() { - Assertions.assertEquals( - ItemType.SWORD, - ItemType.fromItemStack(ItemResources.loadItem("aspect-of-the-void")) - ) - } - - @Test - fun testDrill() { - Assertions.assertEquals( - ItemType.DRILL, - ItemType.fromItemStack(ItemResources.loadItem("titanium-drill")) - ) - } - - @Test - fun testPickaxe() { - Assertions.assertEquals( - ItemType.PICKAXE, - ItemType.fromItemStack(ItemResources.loadItem("diamond-pickaxe")) - ) - } -} +class ItemTypeTest + : ShouldSpec( + { + context("ItemType.fromItemstack") { + listOf( + "pets/lion-item" to ItemType.PET, + "pets/rabbit-selected" to ItemType.PET, + "pets/mithril-golem-not-selected" to ItemType.PET, + "aspect-of-the-void" to ItemType.SWORD, + "titanium-drill" to ItemType.DRILL, + "diamond-pickaxe" to ItemType.PICKAXE, + "gemstone-gauntlet" to ItemType.GAUNTLET, + ).forEach { (name, typ) -> + should("return $typ for $name") { + ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ + } + } + } + }) diff --git a/src/test/kotlin/util/skyblock/SackUtilTest.kt b/src/test/kotlin/util/skyblock/SackUtilTest.kt index e1b106e..f93cd2b 100644 --- a/src/test/kotlin/util/skyblock/SackUtilTest.kt +++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt @@ -2,7 +2,6 @@ package moe.nea.firmament.test.util.skyblock import io.kotest.core.spec.style.AnnotationSpec import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.util.skyblock.SackUtil import moe.nea.firmament.util.skyblock.SkyBlockItems diff --git a/translations/en_us.json b/translations/en_us.json index 35c7350..d191c84 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -130,11 +130,16 @@ "firmament.config.pets": "Pets", "firmament.config.pets.highlight-pet": "Highlight active pet", "firmament.config.pets.highlight-pet.description": "Highlight your currently selected pet in the /pets menu.", - "firmament.config.pickaxe-info": "Pickaxes", + "firmament.config.pickaxe-info": "Pickaxes & Drills", "firmament.config.pickaxe-info.ability-cooldown": "Pickaxe Ability Cooldown", "firmament.config.pickaxe-info.ability-cooldown.description": "Show a cooldown on your cross-hair for your pickaxe ability.", "firmament.config.pickaxe-info.ability-scale": "Ability Cooldown Scale", "firmament.config.pickaxe-info.ability-scale.description": "Resize the cooldown around your cross-hair for your pickaxe ability.", + "firmament.config.pickaxe-info.block-on-dynamic": "Block on Private Island", + "firmament.config.pickaxe-info.block-on-dynamic.choice.always": "Always Block", + "firmament.config.pickaxe-info.block-on-dynamic.choice.never": "Never Block", + "firmament.config.pickaxe-info.block-on-dynamic.choice.only_destructive": "Only with dangerous", + "firmament.config.pickaxe-info.block-on-dynamic.description": "Block pickaxe abilities on private islands by preventing you from right clicking.", "firmament.config.pickaxe-info.fuel-bar": "Drill Fuel Durability Bar", "firmament.config.pickaxe-info.fuel-bar.description": "Replace the item durability bar of your drills with one that shows the remaining fuel.", "firmament.config.power-user": "Power Users", |