feat: Add pickobulus blocker on private island

This commit is contained in:
Linnea Gräf
2024-11-27 17:26:42 +01:00
parent ccb5c556de
commit 8df225399f
23 changed files with 609 additions and 250 deletions

View File

@@ -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.GuiOptionEditorAccordion
import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean
import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorButton 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.gui.editors.GuiOptionEditorText
import io.github.notenoughupdates.moulconfig.observer.GetSetter import io.github.notenoughupdates.moulconfig.observer.GetSetter
import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory
@@ -31,9 +32,11 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.StringIdentifiable
import net.minecraft.util.Util import net.minecraft.util.Util
import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.config.BooleanHandler 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.ClickHandler
import moe.nea.firmament.gui.config.DurationHandler import moe.nea.firmament.gui.config.DurationHandler
import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider 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 { init {
helpRegisterChoice<Nothing>()
register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject -> register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject ->
object : ProcessedEditableOptionFirm<Boolean>(option, categoryAccordionId, configObject) { object : ProcessedEditableOptionFirm<Boolean>(option, categoryAccordionId, configObject) {
override fun createEditor(): GuiOptionEditor { override fun createEditor(): GuiOptionEditor {

View File

@@ -16,160 +16,168 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
public class AutoDiscoveryPlugin { public class AutoDiscoveryPlugin {
private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>(); 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();
}
public static List<AutoDiscoveryPlugin> getMixinPlugins() { private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>();
return mixinPlugins;
}
private String mixinPackage; public static List<AutoDiscoveryPlugin> getMixinPlugins() {
return mixinPlugins;
}
public void setMixinPackage(String mixinPackage) { private String mixinPackage;
this.mixinPackage = mixinPackage;
mixinPlugins.add(this);
}
/** public void setMixinPackage(String mixinPackage) {
* Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. this.mixinPackage = mixinPackage;
* In either case the return value of this + the class name will resolve back to the original class url, or to other mixinPlugins.add(this);
* 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}. * 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
public String getMixinPackage() { * class urls for other classes.
return mixinPackage; */
} 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 path inside the class root to the mixin package * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}.
*/ */
public String getMixinBaseDir() { public String getMixinPackage() {
return mixinPackage.replace(".", "/"); return mixinPackage;
} }
/** /**
* A list of all discovered mixins. * Get the path inside the class root to the mixin package
*/ */
private List<String> mixins = null; public String getMixinBaseDir() {
return mixinPackage.replace(".", "/");
}
/** /**
* Try to add mixin class ot the mixins based on the filepath inside of the class root. * A list of all discovered mixins.
* Removes the {@code .class} file suffix, as well as the base mixin package. */
* <p><b>This method cannot be called after mixin initialization.</p> private List<String> mixins = null;
*
* @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 to add mixin class ot the mixins based on the filepath inside of the class root.
try { * Removes the {@code .class} file suffix, as well as the base mixin package.
file = Paths.get(getBaseUrlForClassUrl(url).toURI()); * <p><b>This method cannot be called after mixin initialization.</p>
} catch (URISyntaxException e) { *
throw new RuntimeException(e); * @param className the name or path of a class to be registered as a mixin.
} */
System.out.println("Base directory found at " + file); public void tryAddMixinClass(String className) {
if (!Files.exists(file)) { if (!className.endsWith(".class")) return;
System.out.println("Skipping non-existing mixin root: " + file); String norm = (className.substring(0, className.length() - ".class".length()))
return; .replace("\\", "/")
} .replace("/", ".");
if (Files.isDirectory(file)) { if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
walkDir(file); mixins.add(norm.substring(getMixinPackage().length() + 1));
} else { }
walkJar(file); }
}
System.out.println("Found mixins: " + mixins);
} 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}. * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()}
* */
* @param classRoot The root directory in which classes are stored for the default package. public List<String> getMixins() {
*/ if (mixins != null) return mixins;
private void walkDir(Path classRoot) { System.out.println("Trying to discover mixins");
System.out.println("Trying to find mixins from directory"); mixins = new ArrayList<>();
var path = classRoot.resolve(getMixinBaseDir()); URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
if (!Files.exists(path)) return; System.out.println("Found classes at " + classUrl);
try (Stream<Path> classes = Files.walk(path)) { tryDiscoverFromContentFile(classUrl);
classes.map(it -> classRoot.relativize(it).toString()) var classRoots = System.getProperty("firmament.classroots");
.forEach(this::tryAddMixinClass); if (classRoots != null && !classRoots.isBlank()) {
} catch (IOException e) { System.out.println("Found firmament class roots: " + classRoots);
throw new RuntimeException(e); 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;
}
/** /**
* Read through a JAR file, trying to find all mixins inside. * Search through directory for mixin classes based on {@link #getMixinBaseDir}.
*/ *
private void walkJar(Path file) { * @param classRoot The root directory in which classes are stored for the default package.
System.out.println("Trying to find mixins from jar file"); */
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { private void walkDir(Path classRoot) {
ZipEntry next; System.out.println("Trying to find mixins from directory");
while ((next = zis.getNextEntry()) != null) { var path = classRoot.resolve(getMixinBaseDir());
tryAddMixinClass(next.getName()); if (!Files.exists(path)) return;
zis.closeEntry(); try (Stream<Path> classes = Files.walk(path)) {
} classes.map(it -> classRoot.relativize(it).toString())
} catch (IOException e) { .forEach(this::tryAddMixinClass);
throw new RuntimeException(e); } 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);
}
}
} }

View File

@@ -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)
}

View File

@@ -1,10 +1,9 @@
package moe.nea.firmament.events.registration package moe.nea.firmament.events.registration
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents
import net.fabricmc.fabric.api.event.player.AttackBlockCallback import net.fabricmc.fabric.api.event.player.AttackBlockCallback
import net.fabricmc.fabric.api.event.player.UseBlockCallback import net.fabricmc.fabric.api.event.player.UseBlockCallback
import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.ActionResult import net.minecraft.util.ActionResult
import moe.nea.firmament.events.AllowChatEvent 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.ModifyChatEvent
import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.UseBlockEvent import moe.nea.firmament.events.UseBlockEvent
import moe.nea.firmament.events.UseItemEvent
private var lastReceivedMessage: Text? = null private var lastReceivedMessage: Text? = null
fun registerFirmamentEvents() { fun registerFirmamentEvents() {
ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp -> ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
lastReceivedMessage = message lastReceivedMessage = message
!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
&& !AllowChatEvent.publish(AllowChatEvent(message)).cancelled && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
}) })
ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay -> ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
lastReceivedMessage = message lastReceivedMessage = message
overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled && overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
!AllowChatEvent.publish(AllowChatEvent(message)).cancelled) !AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
}) })
ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay -> ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
if (overlay) message if (overlay) message
else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
}) })
ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay -> ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
if (!overlay && lastReceivedMessage !== message) { if (!overlay && lastReceivedMessage !== message) {
ProcessChatEvent.publish(ProcessChatEvent(message, true)) ProcessChatEvent.publish(ProcessChatEvent(message, true))
} }
}) })
ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp -> ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
if (lastReceivedMessage !== message) { if (lastReceivedMessage !== message) {
ProcessChatEvent.publish(ProcessChatEvent(message, true)) ProcessChatEvent.publish(ProcessChatEvent(message, true))
} }
}) })
AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction -> AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction ->
if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled) if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled)
ActionResult.CONSUME ActionResult.CONSUME
else ActionResult.PASS else ActionResult.PASS
}) })
UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled) if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled)
ActionResult.CONSUME ActionResult.CONSUME
else ActionResult.PASS 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
})
} }

View File

@@ -7,11 +7,13 @@ import net.minecraft.item.ItemStack
import net.minecraft.util.DyeColor import net.minecraft.util.DyeColor
import net.minecraft.util.Hand import net.minecraft.util.Hand
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.ProcessChatEvent import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.ProfileSwitchEvent import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.SlotClickEvent import moe.nea.firmament.events.SlotClickEvent
import moe.nea.firmament.events.UseItemEvent
import moe.nea.firmament.events.WorldReadyEvent import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig 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.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern 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.RenderCircleProgress
import moe.nea.firmament.util.render.lerp import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.skyblock.AbilityUtils 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.toShedaniel
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch import moe.nea.firmament.util.useMatch
@@ -43,6 +48,22 @@ object PickaxeAbility : FirmamentFeature {
val cooldownEnabled by toggle("ability-cooldown") { false } val cooldownEnabled by toggle("ability-cooldown") { false }
val cooldownScale by integer("ability-scale", 16, 64) { 16 } val cooldownScale by integer("ability-scale", 16, 64) { 16 }
val drillFuelBar by toggle("fuel-bar") { true } 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() var lobbyJoinTime = TimeMark.farPast()
@@ -56,6 +77,8 @@ object PickaxeAbility : FirmamentFeature {
"Maniac Miner" to 59.seconds, "Maniac Miner" to 59.seconds,
"Vein Seeker" to 60.seconds "Vein Seeker" to 60.seconds
) )
val destructiveAbilities = setOf("Pickobulus")
val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET)
override val config: ManagedConfig override val config: ManagedConfig
get() = TConfig get() = TConfig
@@ -73,6 +96,26 @@ object PickaxeAbility : FirmamentFeature {
return 1.0 return 1.0
} }
@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 @Subscribe
fun onSlotClick(it: SlotClickEvent) { fun onSlotClick(it: SlotClickEvent) {
if (MC.screen?.title?.unformattedString == "Heart of the Mountain") { if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {

View File

@@ -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
}
}

View File

@@ -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)
))
}
}
}

View File

@@ -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())
}
}
}
}

View File

@@ -1,5 +1,6 @@
package moe.nea.firmament.gui.config package moe.nea.firmament.gui.config
import com.mojang.serialization.Codec
import io.github.notenoughupdates.moulconfig.gui.CloseEventListener import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import io.github.notenoughupdates.moulconfig.gui.GuiContext import io.github.notenoughupdates.moulconfig.gui.GuiContext
@@ -20,6 +21,7 @@ import kotlin.io.path.writeText
import kotlin.time.Duration import kotlin.time.Duration
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.FirmButtonComponent import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.SavedKeyBinding import moe.nea.firmament.keybindings.SavedKeyBinding
@@ -113,6 +115,28 @@ abstract class ManagedConfig(
return option(propertyName, default, BooleanHandler(this)) 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( protected fun duration(
propertyName: String, propertyName: String,
min: Duration, min: Duration,

View File

@@ -1,26 +1,25 @@
package moe.nea.firmament.keybindings package moe.nea.firmament.keybindings
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
import net.minecraft.client.option.KeyBinding import net.minecraft.client.option.KeyBinding
import net.minecraft.client.util.InputUtil import net.minecraft.client.util.InputUtil
import moe.nea.firmament.gui.config.KeyBindingHandler
import moe.nea.firmament.gui.config.ManagedOption import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.util.TestUtil
object FirmamentKeyBindings { object FirmamentKeyBindings {
fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) { fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) {
val vanillaKeyBinding = KeyBindingHelper.registerKeyBinding( val vanillaKeyBinding = KeyBinding(
KeyBinding( name,
name, InputUtil.Type.KEYSYM,
InputUtil.Type.KEYSYM, -1,
-1, "firmament.key.category"
"firmament.key.category" )
) if (!TestUtil.isInTest) {
) KeyBindingHelper.registerKeyBinding(vanillaKeyBinding)
keyBindings[vanillaKeyBinding] = config }
} keyBindings[vanillaKeyBinding] = config
}
val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>() val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>()
} }

View File

@@ -92,12 +92,12 @@ object MC {
inline val inGameHud: InGameHud get() = instance.inGameHud inline val inGameHud: InGameHud get() = instance.inGameHud
inline val font get() = instance.textRenderer inline val font get() = instance.textRenderer
inline val soundManager get() = instance.soundManager 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 camera: Entity? get() = instance.cameraEntity
inline val guiAtlasManager get() = instance.guiAtlasManager 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? inline var screen: Screen?
get() = instance.currentScreen get() = TestUtil.unlessTesting{ instance.currentScreen }
set(value) = instance.setScreen(value) set(value) = instance.setScreen(value)
val screenName get() = screen?.title?.unformattedString?.trim() val screenName get() = screen?.title?.unformattedString?.trim()
inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>

View File

@@ -1,6 +1,7 @@
package moe.nea.firmament.util package moe.nea.firmament.util
object TestUtil { object TestUtil {
inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block()
val isInTest = val isInTest =
Thread.currentThread().stackTrace.any { Thread.currentThread().stackTrace.any {
it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.") it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.")

View File

@@ -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")
}
}

View File

@@ -32,10 +32,15 @@ value class ItemType private constructor(val name: String) {
val SWORD = ofName("SWORD") val SWORD = ofName("SWORD")
val DRILL = ofName("DRILL") val DRILL = ofName("DRILL")
val PICKAXE = ofName("PICKAXE") val PICKAXE = ofName("PICKAXE")
val GAUNTLET = ofName("GAUNTLET")
/** /**
* This one is not really official (it never shows up in game). * This one is not really official (it never shows up in game).
*/ */
val PET = ofName("PET") val PET = ofName("PET")
} }
override fun toString(): String {
return name
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -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/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/RegistryEntry$Reference setTags (Ljava/util/Collection;)V
accessible method net/minecraft/registry/entry/RegistryEntryList$Named setEntries (Ljava/util/List;)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;

View File

@@ -2,11 +2,14 @@ package moe.nea.firmament.test.testutil
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.StringNbtReader import net.minecraft.nbt.StringNbtReader
import net.minecraft.registry.RegistryOps
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.text.TextCodecs import net.minecraft.text.TextCodecs
import moe.nea.firmament.test.FirmTestBootstrap import moe.nea.firmament.test.FirmTestBootstrap
import moe.nea.firmament.util.MC
object ItemResources { object ItemResources {
init { init {
@@ -23,15 +26,16 @@ object ItemResources {
fun loadSNbt(path: String): NbtCompound { fun loadSNbt(path: String): NbtCompound {
return StringNbtReader.parse(loadString(path)) return StringNbtReader.parse(loadString(path))
} }
fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE)
fun loadText(name: String): Text { 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") } .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") }
} }
fun loadItem(name: String): ItemStack { fun loadItem(name: String): ItemStack {
// TODO: make the load work with enchantments // 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") } .getOrThrow { IllegalStateException("Could not load test item '$name': $it") }
} }
} }

View File

@@ -2,7 +2,6 @@ package moe.nea.firmament.test.util
import io.kotest.core.spec.style.AnnotationSpec import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.getLegacyFormatString import moe.nea.firmament.util.getLegacyFormatString

View File

@@ -2,7 +2,6 @@ package moe.nea.firmament.test.util.skyblock
import io.kotest.core.spec.style.AnnotationSpec import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text import net.minecraft.text.Text

View File

@@ -1,53 +1,26 @@
package moe.nea.firmament.test.util.skyblock package moe.nea.firmament.test.util.skyblock
import io.kotest.core.spec.style.AnnotationSpec import io.kotest.core.spec.style.ShouldSpec
import org.junit.jupiter.api.Assertions import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.skyblock.ItemType
class ItemTypeTest : AnnotationSpec() { class ItemTypeTest
@Test : ShouldSpec(
fun testPetItem() { {
Assertions.assertEquals( context("ItemType.fromItemstack") {
ItemType.PET, listOf(
ItemType.fromItemStack(ItemResources.loadItem("pets/lion-item")) "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,
@Test "titanium-drill" to ItemType.DRILL,
fun testPetInUI() { "diamond-pickaxe" to ItemType.PICKAXE,
Assertions.assertEquals( "gemstone-gauntlet" to ItemType.GAUNTLET,
ItemType.PET, ).forEach { (name, typ) ->
ItemType.fromItemStack(ItemResources.loadItem("pets/rabbit-selected")) should("return $typ for $name") {
) ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ
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"))
)
}
}

View File

@@ -2,7 +2,6 @@ package moe.nea.firmament.test.util.skyblock
import io.kotest.core.spec.style.AnnotationSpec import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import moe.nea.firmament.test.testutil.ItemResources import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.SackUtil import moe.nea.firmament.util.skyblock.SackUtil
import moe.nea.firmament.util.skyblock.SkyBlockItems import moe.nea.firmament.util.skyblock.SkyBlockItems

View File

@@ -130,11 +130,16 @@
"firmament.config.pets": "Pets", "firmament.config.pets": "Pets",
"firmament.config.pets.highlight-pet": "Highlight active pet", "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.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": "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-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": "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.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": "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.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", "firmament.config.power-user": "Power Users",