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.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 {

View File

@@ -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<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() {
return mixinPlugins;
}
private static final List<AutoDiscoveryPlugin> mixinPlugins = new ArrayList<>();
private String mixinPackage;
public static List<AutoDiscoveryPlugin> getMixinPlugins() {
return mixinPlugins;
}
public void setMixinPackage(String mixinPackage) {
this.mixinPackage = mixinPackage;
mixinPlugins.add(this);
}
private String mixinPackage;
/**
* 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;
}
public void setMixinPackage(String mixinPackage) {
this.mixinPackage = mixinPackage;
mixinPlugins.add(this);
}
/**
* Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}.
*/
public String getMixinPackage() {
return mixinPackage;
}
/**
* 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 path inside the class root to the mixin package
*/
public String getMixinBaseDir() {
return mixinPackage.replace(".", "/");
}
/**
* Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}.
*/
public String getMixinPackage() {
return mixinPackage;
}
/**
* A list of all discovered mixins.
*/
private List<String> mixins = null;
/**
* Get the path inside the class root to the mixin package
*/
public String getMixinBaseDir() {
return mixinPackage.replace(".", "/");
}
/**
* 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));
}
}
/**
* A list of all discovered mixins.
*/
private List<String> mixins = null;
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);
/**
* 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);
}
}
/**
* 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;
}
/**
* 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);
}
}
/**
* 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);
}
}
}

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

View File

@@ -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
@@ -73,6 +96,26 @@ object PickaxeAbility : FirmamentFeature {
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
fun onSlotClick(it: SlotClickEvent) {
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
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,

View File

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

View File

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

View File

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

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

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/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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",