Merge branch 'mc-1.21.3'

This commit is contained in:
Linnea Gräf
2024-12-31 16:52:29 +01:00
53 changed files with 2182 additions and 370 deletions

View File

@@ -0,0 +1,18 @@
package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ClientCommandInternals.class)
public class AlwaysDisplayFirmamentClientCommandErrors {
@ModifyExpressionValue(method = "executeCommand", at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/command/client/ClientCommandInternals;isIgnoredException(Lcom/mojang/brigadier/exceptions/CommandExceptionType;)Z"))
private static boolean markFirmamentExceptionsAsNotIgnores(boolean original, @Local(argsOnly = true) String command) {
if (command.startsWith("firm ") || command.equals("firm") || command.startsWith("firmament ") || command.equals("firmament")) {
return false;
}
return original;
}
}

View File

@@ -51,7 +51,7 @@ public class FirmKeybindsInVanillaControlsPatch {
var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding);
if (config == null) return;
resetButton.active = false;
editButton.setMessage(Text.translatable("firmament.keybinding.external", config.value.format()));
editButton.setMessage(Text.translatable("firmament.keybinding.external", config.getValue().format()));
ci.cancel();
}

View File

@@ -0,0 +1,29 @@
package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.features.fixes.Fixes;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.ingame.StatusEffectsDisplay;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(InventoryScreen.class)
public abstract class HideStatusEffectsPatch {
@Shadow
public abstract boolean shouldHideStatusEffectHud();
@Inject(method = "shouldHideStatusEffectHud", at = @At("HEAD"), cancellable = true)
private void hideStatusEffects(CallbackInfoReturnable<Boolean> cir) {
cir.setReturnValue(!Fixes.TConfig.INSTANCE.getHidePotionEffects());
}
@WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/StatusEffectsDisplay;drawStatusEffects(Lnet/minecraft/client/gui/DrawContext;IIF)V"))
private boolean conditionalRenderStatuses(StatusEffectsDisplay instance, DrawContext context, int mouseX, int mouseY, float tickDelta) {
return shouldHideStatusEffectHud() || !Fixes.TConfig.INSTANCE.getHidePotionEffects();
}
}

View File

@@ -0,0 +1,17 @@
package moe.nea.firmament.mixins;
import moe.nea.firmament.features.chat.QuickCommands;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public class SaveOriginalCommandTreePacket {
@Inject(method = "onCommandTree", at = @At(value = "RETURN"))
private void saveUnmodifiedCommandTree(CommandTreeS2CPacket packet, CallbackInfo ci) {
QuickCommands.INSTANCE.setLastReceivedTreePacket(packet);
}
}

View File

@@ -0,0 +1,18 @@
package moe.nea.firmament.mixins;
import moe.nea.firmament.util.mc.TolerantRegistriesOps;
import net.minecraft.registry.entry.RegistryEntryOwner;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(RegistryEntryOwner.class)
public interface TolerateFirmamentTolerateRegistryOwners<T> {
@Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true)
private void equalTolerantRegistryOwners(RegistryEntryOwner<T> other, CallbackInfoReturnable<Boolean> cir) {
if (other instanceof TolerantRegistriesOps.TolerantOwner<?>) {
cir.setReturnValue(true);
}
}
}

View File

@@ -0,0 +1,75 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.StringReader
import com.mojang.brigadier.arguments.ArgumentType
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType
import com.mojang.brigadier.suggestion.Suggestions
import com.mojang.brigadier.suggestion.SuggestionsBuilder
import java.util.concurrent.CompletableFuture
import java.util.function.Function
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import moe.nea.firmament.util.tr
object DurationArgumentType : ArgumentType<Duration> {
val unknownTimeCode = DynamicCommandExceptionType { timeCode ->
tr("firmament.command-argument.duration.error",
"Unknown time code '$timeCode'")
}
override fun parse(reader: StringReader): Duration {
val start = reader.cursor
val string = reader.readUnquotedString()
val matcher = regex.matcher(string)
var s = 0
var time = 0.seconds
fun createError(till: Int) {
throw unknownTimeCode.createWithContext(
reader.also { it.cursor = start + s },
string.substring(s, till))
}
while (matcher.find()) {
if (matcher.start() != s) {
createError(matcher.start())
}
s = matcher.end()
val amount = matcher.group("count").toDouble()
val what = timeSuffixes[matcher.group("what").single()]!!
time += amount.toDuration(what)
}
if (string.length != s) {
createError(string.length)
}
return time
}
override fun <S : Any?> listSuggestions(
context: CommandContext<S>,
builder: SuggestionsBuilder
): CompletableFuture<Suggestions> {
val remaining = builder.remainingLowerCase.substringBefore(' ')
if (remaining.isEmpty()) return super.listSuggestions(context, builder)
if (remaining.last().isDigit()) {
for (timeSuffix in timeSuffixes.keys) {
builder.suggest(remaining + timeSuffix)
}
}
return builder.buildFuture()
}
val timeSuffixes = mapOf(
'm' to DurationUnit.MINUTES,
's' to DurationUnit.SECONDS,
'h' to DurationUnit.HOURS,
)
val regex = "(?<count>[0-9]+)(?<what>[${timeSuffixes.keys.joinToString("")}])".toPattern()
override fun getExamples(): Collection<String> {
return listOf("3m", "20s", "1h45m")
}
}

View File

@@ -0,0 +1,9 @@
package moe.nea.firmament.events
data class PartyMessageReceivedEvent(
val from: ProcessChatEvent,
val message: String,
val name: String,
) : FirmamentEvent() {
companion object : FirmamentEventBus<PartyMessageReceivedEvent>()
}

View File

@@ -0,0 +1,134 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.StringReader
import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.mojang.brigadier.tree.LiteralCommandNode
import kotlin.time.Duration.Companion.seconds
import net.minecraft.util.math.BlockPos
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.PartyMessageReceivedEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.useMatch
object PartyCommands {
val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern()
@Subscribe
fun onChat(event: ProcessChatEvent) {
messageInChannel.useMatch(event.unformattedString) {
val channel = group("channel")
val message = group("message")
val name = group("name")
if (channel == "Party") {
PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent(
event, message, name
))
}
}
}
val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet()
data class PartyCommandContext(
val name: String
)
val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch ->
fun register(
name: String,
vararg alias: String,
block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {},
): LiteralCommandNode<PartyCommandContext> {
val node =
dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block))
alias.forEach { register(it) { redirect(node) } }
return node
}
register("warp", "pw", "pwarp", "partywarp") {
executes {
// TODO: add check if you are the party leader
MC.sendCommand("p warp")
0
}
}
register("transfer", "pt", "ptme") {
executes {
MC.sendCommand("p transfer ${it.source.name}")
0
}
}
register("allinvite", "allinv") {
executes {
MC.sendCommand("p settings allinvite")
0
}
}
register("coords") {
executes {
val p = MC.player?.blockPos ?: BlockPos.ORIGIN
MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
0
}
}
// TODO: downtime tracker (display message again at end of dungeon)
// instance ends: kuudra, dungeons, bacte
// TODO: at TPS command
}
object TConfig : ManagedConfig("party-commands", Category.CHAT) {
val enable by toggle("enable") { false }
val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds }
val ignoreOwnCommands by toggle("ignore-own") { false }
}
var lastCommand = TimeMark.farPast()
@Subscribe
fun listPartyCommands(event: CommandEvent.SubCommand) {
event.subcommand("partycommands") {
thenExecute {
// TODO: Better help, including descriptions and redirect detection
MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes"))
}
}
}
@Subscribe
fun onPartyMessage(event: PartyMessageReceivedEvent) {
if (!TConfig.enable) return
if (event.message.firstOrNull() !in commandPrefixes) return
if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return
if (lastCommand.passedTime() < TConfig.cooldown) {
MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed."))
return
}
// TODO: add trust levels
val commandLine = event.message.substring(1)
try {
dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name))
} catch (ex: Exception) {
if (ex is CommandSyntaxException) {
MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command."))
return
} else {
MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution."))
ErrorUtil.softError("Unknown error during command execution.", ex)
}
}
lastCommand = TimeMark.now()
}
}

View File

@@ -1,8 +1,12 @@
package moe.nea.firmament.features.chat
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.fabricmc.fabric.impl.command.client.ClientCommandInternals
import net.minecraft.command.CommandRegistryAccess
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
@@ -12,89 +16,139 @@ import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.tr
object QuickCommands : FirmamentFeature {
override val identifier: String
get() = "quick-commands"
override val identifier: String
get() = "quick-commands"
fun removePartialPrefix(text: String, prefix: String): String? {
var lf: String? = null
for (i in 1..prefix.length) {
if (text.startsWith(prefix.substring(0, i))) {
lf = text.substring(i)
}
}
return lf
}
object TConfig : ManagedConfig("quick-commands", Category.CHAT) {
val enableJoin by toggle("join") { true }
val enableDh by toggle("dh") { true }
override fun onChange(option: ManagedOption<*>) {
reloadCommands()
}
}
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
fun reloadCommands() {
val lastPacket = lastReceivedTreePacket ?: return
val network = MC.networkHandler ?: return
val fallback = ClientCommandInternals.getActiveDispatcher()
try {
val dispatcher = CommandDispatcher<FabricClientCommandSource>()
ClientCommandInternals.setActiveDispatcher(dispatcher)
ClientCommandRegistrationCallback.EVENT.invoker()
.register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries,
network.enabledFeatures))
ClientCommandInternals.finalizeInit()
network.onCommandTree(lastPacket)
} catch (ex: Exception) {
ClientCommandInternals.setActiveDispatcher(fallback)
throw ex
}
}
@Subscribe
fun onCommands(it: CommandEvent) {
it.register("join") {
thenArgument("what", RestArgumentType) { what ->
thenExecute {
val what = this[what]
if (!SBData.isOnSkyblock) {
MC.sendCommand("join $what")
return@thenExecute
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
} else {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
joinName))
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
}
}
}
fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
if (kuudraLevel != null) {
val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
it.startsWith(
kuudraLevel,
true
)
}
if (l !in kuudraLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
kuudraLevel))
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
}
val masterLevel = removePartialPrefix(w, "master")
val normalLevel =
removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
val dungeonLevel = masterLevel ?: normalLevel
if (dungeonLevel != null) {
val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
it.startsWith(
dungeonLevel,
true
)
}
if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
kuudraLevel))
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
}
return null
}
fun removePartialPrefix(text: String, prefix: String): String? {
var lf: String? = null
for (i in 1..prefix.length) {
if (text.startsWith(prefix.substring(0, i))) {
lf = text.substring(i)
}
}
return lf
}
var lastReceivedTreePacket: CommandTreeS2CPacket? = null
val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
@Subscribe
fun registerDh(event: CommandEvent) {
if (!TConfig.enableDh) return
event.register("dh") {
thenExecute {
MC.sendCommand("warp dhub")
}
}
event.register("dn") {
thenExecute {
MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey())
MC.sendCommand("warp dhub")
}
}
}
@Subscribe
fun registerJoin(it: CommandEvent) {
if (!TConfig.enableJoin) return
it.register("join") {
thenArgument("what", RestArgumentType) { what ->
thenExecute {
val what = this[what]
if (!SBData.isOnSkyblock) {
MC.sendCommand("join $what")
return@thenExecute
}
val joinName = getNameForFloor(what.replace(" ", "").lowercase())
if (joinName == null) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
} else {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
joinName))
MC.sendCommand("joininstance $joinName")
}
}
}
thenExecute {
source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
}
}
}
fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
if (kuudraLevel != null) {
val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
it.startsWith(
kuudraLevel,
true
)
}
if (l !in kuudraLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
kuudraLevel))
return null
}
return "KUUDRA_${kuudraLevelNames[l]}"
}
val masterLevel = removePartialPrefix(w, "master")
val normalLevel =
removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
val dungeonLevel = masterLevel ?: normalLevel
if (dungeonLevel != null) {
val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
it.startsWith(
dungeonLevel,
true
)
}
if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
return "CATACOMBS_ENTRANCE"
}
if (l !in dungeonLevelNames.indices) {
source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
kuudraLevel))
return null
}
return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
}
return null
}
}

View File

@@ -1,71 +1,67 @@
package moe.nea.firmament.features.fixes
import moe.nea.jarvis.api.Point
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Arm
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.errorBoundary
object Fixes : FirmamentFeature {
override val identifier: String
get() = "fixes"
override val identifier: String
get() = "fixes"
object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
}
object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
val hidePotionEffects by toggle("hide-mob-effects") { false }
}
override val config: ManagedConfig
get() = TConfig
override val config: ManagedConfig
get() = TConfig
fun handleIsPressed(
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
cir.returnValue = true
}
fun handleIsPressed(
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
cir.returnValue = true
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
if (!TConfig.autoSprintKeyBinding.isBound) return
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
MC.font, Text.translatable(
if (TConfig.autoSprint)
"firmament.fixes.auto-sprint.on"
else if (MC.player?.isSprinting == true)
"firmament.fixes.auto-sprint.sprinting"
else
"firmament.fixes.auto-sprint.not-sprinting"
), 0, 0, -1, false
)
it.context.matrices.pop()
}
@Subscribe
fun onRenderHud(it: HudRenderEvent) {
if (!TConfig.autoSprintKeyBinding.isBound) return
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
MC.font, Text.translatable(
if (TConfig.autoSprint)
"firmament.fixes.auto-sprint.on"
else if (MC.player?.isSprinting == true)
"firmament.fixes.auto-sprint.sprinting"
else
"firmament.fixes.auto-sprint.not-sprinting"
), 0, 0, -1, false
)
it.context.matrices.pop()
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (it.matches(TConfig.autoSprintKeyBinding)) {
TConfig.autoSprint = !TConfig.autoSprint
}
}
@Subscribe
fun onWorldKeyboard(it: WorldKeyboardEvent) {
if (it.matches(TConfig.autoSprintKeyBinding)) {
TConfig.autoSprint = !TConfig.autoSprint
}
}
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
fun shouldPeekChat(): Boolean {
return TConfig.peekChat.isPressed(atLeast = true)
}
}

View File

@@ -29,18 +29,7 @@ object ItemRarityCosmetics : FirmamentFeature {
override val config: ManagedConfig
get() = TConfig
private val rarityToColor = mapOf(
Rarity.COMMON to Formatting.WHITE,
Rarity.UNCOMMON to Formatting.GREEN,
Rarity.RARE to Formatting.BLUE,
Rarity.EPIC to Formatting.DARK_PURPLE,
Rarity.LEGENDARY to Formatting.GOLD,
Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
Rarity.DIVINE to Formatting.AQUA,
Rarity.SPECIAL to Formatting.RED,
Rarity.VERY_SPECIAL to Formatting.RED,
Rarity.SUPREME to Formatting.DARK_RED,
).mapValues {
private val rarityToColor = Rarity.colourMap.mapValues {
val c = Color(it.value.colorValue!!)
c.rgb
}

View File

@@ -2,7 +2,6 @@
package moe.nea.firmament.features.inventory
import com.mojang.blaze3d.systems.RenderSystem
import java.util.UUID
import org.lwjgl.glfw.GLFW
import kotlinx.serialization.Serializable
@@ -14,6 +13,7 @@ import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.util.Identifier
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
@@ -59,6 +59,17 @@ object SlotLocking : FirmamentFeature {
}
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
}
enum class SlotRenderLinesMode : StringIdentifiable {
EVERYTHING,
ONLY_BOXES,
NOTHING;
override fun asString(): String {
return name
}
}
override val config: TConfig
@@ -95,7 +106,7 @@ object SlotLocking : FirmamentFeature {
if (handler.inventory.size() < 9) return false
val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
if (sellItem == null) return false
if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true
if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!"
}
@@ -104,7 +115,7 @@ object SlotLocking : FirmamentFeature {
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
if (!event.slot.hasStack()) return
if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return
if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
val inv = event.slot.inventory
var anyBlocked = false
for (i in 0 until event.slot.index) {
@@ -227,23 +238,32 @@ object SlotLocking : FirmamentFeature {
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
boundSlots.entries.forEach {
val hotbarSlot = findByIndex(it.key) ?: return@forEach
val inventorySlot = findByIndex(it.value) ?: return@forEach
for (it in boundSlots.entries) {
val hotbarSlot = findByIndex(it.key) ?: continue
val inventorySlot = findByIndex(it.value) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
|| accScreen.focusedSlot_Firmament === inventorySlot
if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
continue
val color = if (anyHovered)
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
else
me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
event.context.drawLine(
invX + sx, invY + sy,
hotX + sx, hotY + sy,
color
)
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
16, 16, color.color)
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
16, 16, 0xFF00FF00u.toInt())
16, 16, color.color)
}
}

View File

@@ -0,0 +1,130 @@
package moe.nea.firmament.features.inventory
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
import java.time.format.FormatStyle
import java.time.format.TextStyle
import java.time.temporal.ChronoField
import net.minecraft.text.Text
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.aqua
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
object TimerInLore {
object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
val showTimers by toggle("show") { true }
val timerFormat by choice("format") { TimerFormat.SOCIALIST }
}
enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable {
RFC(DateTimeFormatter.RFC_1123_DATE_TIME),
LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)),
SOCIALIST(
{
appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT)
appendLiteral(" ")
appendValue(ChronoField.DAY_OF_MONTH, 2)
appendLiteral(".")
appendValue(ChronoField.MONTH_OF_YEAR, 2)
appendLiteral(".")
appendValue(ChronoField.YEAR, 4)
appendLiteral(" ")
appendValue(ChronoField.HOUR_OF_DAY, 2)
appendLiteral(":")
appendValue(ChronoField.MINUTE_OF_HOUR, 2)
appendLiteral(":")
appendValue(ChronoField.SECOND_OF_MINUTE, 2)
}),
AMERICAN("EEEE, MMM d h:mm a yyyy"),
;
constructor(block: DateTimeFormatterBuilder.() -> Unit)
: this(DateTimeFormatterBuilder().also(block).toFormatter())
constructor(format: String) : this(DateTimeFormatter.ofPattern(format))
override fun asString(): String {
return name
}
}
enum class CountdownTypes(
val match: String,
val label: String, // TODO: convert to a string
val isRelative: Boolean = false,
) {
STARTING("Starting in:", "Starts at"),
STARTS("Starts in:", "Starts at"),
INTEREST("Interest in:", "Interest at"),
UNTILINTEREST("Until interest:", "Interest at"),
ENDS("Ends in:", "Ends at"),
REMAINING("Remaining:", "Ends at"),
DURATION("Duration:", "Finishes at"),
TIMELEFT("Time left:", "Ends at"),
EVENTTIMELEFT("Event lasts for", "Ends at", isRelative = true),
SHENSUCKS("Auction ends in:", "Auction ends at"),
ENDS_PET_LEVELING(
"Ends:",
"Finishes at"
),
CALENDARDETAILS(" (§e", "Starts at"),
COMMUNITYPROJECTS("Contribute again", "Come back at"),
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
LIZSTONKREDEMPTION("Resets in:", "Resets at");
}
val regex =
"(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex()
@Subscribe
fun modifyLore(event: ItemTooltipEvent) {
if (!TConfig.showTimers) return
var lastTimer: ZonedDateTime? = null
for (i in event.lines.indices) {
val line = event.lines[i].unformattedString
val countdownType = CountdownTypes.entries.find { it.match in line } ?: continue
if (countdownType == CountdownTypes.CALENDARDETAILS
&& !event.stack.displayNameAccordingToNbt.unformattedString.startsWith("Day ")
) continue
val countdownMatch = regex.findAll(line).filter { it.value.isNotBlank() }.lastOrNull() ?: continue
val (years, days, hours, minutes, seconds) =
listOf("years", "days", "hours", "minutes", "seconds")
.map {
countdownMatch.groups[it]?.value?.toLong() ?: 0L
}
if (years + days + hours + minutes + seconds == 0L) continue
var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone)
if (countdownType.isRelative) {
if (lastTimer == null) {
event.lines.add(i + 1,
tr("firmament.loretimer.missingrelative",
"Found a relative countdown with no baseline (Firmament)").grey())
continue
}
baseLine = lastTimer
}
val timer =
baseLine.plusYears(years).plusDays(days).plusHours(hours).plusMinutes(minutes).plusSeconds(seconds)
lastTimer = timer
val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault())
// TODO: install approximate time stabilization algorithm
event.lines.add(i + 1,
Text.literal("${countdownType.label}: ")
.grey()
.append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
)
}
}
}

View File

@@ -1,5 +1,3 @@
package moe.nea.firmament.features.inventory.storageoverlay
import io.ktor.util.decodeBase64Bytes
@@ -19,47 +17,59 @@ import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
import net.minecraft.registry.RegistryOps
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.mc.TolerantRegistriesOps
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
val stacks: List<ItemStack>
val stacks: List<ItemStack>
) {
val rows = stacks.size / 9
val rows = stacks.size / 9
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
init {
assert(stacks.size % 9 == 0)
assert(stacks.size / 9 in 1..5)
}
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
object Serializer : KSerializer<VirtualInventory> {
const val INVENTORY = "INVENTORY"
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
return VirtualInventory(items.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else runCatching {
ItemStack.CODEC.parse(NbtOps.INSTANCE, it).orThrow
}.getOrElse { ItemStack.EMPTY }
})
}
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
val ops = getOps()
return VirtualInventory(items.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else ErrorUtil.catch("Could not deserialize item") {
ItemStack.CODEC.parse(ops, it).orThrow
}.or { ItemStack.EMPTY }
})
}
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
value.stacks.forEach {
if (it.isEmpty) list.add(NbtCompound())
else list.add(runCatching { ItemStack.CODEC.encode(it, NbtOps.INSTANCE, NbtCompound()).orThrow }
.getOrElse { NbtCompound() })
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
override fun serialize(encoder: Encoder, value: VirtualInventory) {
val list = NbtList()
val ops = getOps()
value.stacks.forEach {
if (it.isEmpty) list.add(NbtCompound())
else list.add(ErrorUtil.catch("Could not serialize item") {
ItemStack.CODEC.encode(it,
ops,
NbtCompound()).orThrow
}
.or { NbtCompound() })
}
val baos = ByteArrayOutputStream()
NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos)
encoder.encodeString(baos.toByteArray().encodeBase64())
}
}
}

View File

@@ -50,7 +50,6 @@ object PickaxeAbility : FirmamentFeature {
val drillFuelBar by toggle("fuel-bar") { true }
val blockOnPrivateIsland by choice(
"block-on-dynamic",
BlockPickaxeAbility.entries,
) {
BlockPickaxeAbility.ONLY_DESTRUCTIVE
}
@@ -99,6 +98,7 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun onPickaxeRightClick(event: UseItemEvent) {
if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return
if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return
val itemType = ItemType.fromItemStack(event.item)
if (itemType !in pickaxeTypes) return
val ability = AbilityUtils.getAbilities(event.item)

View File

@@ -0,0 +1,124 @@
package moe.nea.firmament.features.misc
import com.mojang.brigadier.arguments.IntegerArgumentType
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DurationArgumentType
import moe.nea.firmament.commands.RestArgumentType
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MinecraftDispatcher
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.clickCommand
import moe.nea.firmament.util.lime
import moe.nea.firmament.util.red
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.yellow
object TimerFeature {
data class Timer(
val start: TimeMark,
val duration: Duration,
val message: String,
val timerId: Int,
) {
fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds)
fun isDone() = start.passedTime() >= duration
}
// Theoretically for optimal performance this could be a treeset keyed to the end time
val timers = mutableListOf<Timer>()
@Subscribe
fun tick(event: TickEvent) {
timers.removeAll {
if (it.isDone()) {
MC.sendChat(tr("firmament.timer.finished",
"The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}")
.yellow())
Firmament.coroutineScope.launch {
withContext(MinecraftDispatcher) {
repeat(5) {
CommonSoundEffects.playSuccess()
delay(0.2.seconds)
}
}
}
true
} else {
false
}
}
}
fun startTimer(duration: Duration, message: String) {
val timerId = createTimerId++
timers.add(Timer(TimeMark.now(), duration, message, timerId))
MC.sendChat(
tr("firmament.timer.start",
"Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime()
.append(" ")
.append(
tr("firmament.timer.cancelbutton",
"Click here to cancel the timer."
).clickCommand("/firm timer clear $timerId").red()
)
)
}
fun clearTimer(timerId: Int) {
val timer = timers.indexOfFirst { it.timerId == timerId }
if (timer < 0) {
MC.sendChat(tr("firmament.timer.cancel.fail",
"Could not cancel that timer. Maybe it was already cancelled?").red())
} else {
val timerData = timers[timer]
timers.removeAt(timer)
MC.sendChat(tr("firmament.timer.cancel.done",
"Cancelled timer ${timerData.message}. It would have been done in ${
FirmFormatters.formatTimespan(timerData.timeLeft())
}.").lime())
}
}
var createTimerId = 0
@Subscribe
fun onCommands(event: CommandEvent.SubCommand) {
event.subcommand("cleartimer") {
thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId ->
thenExecute {
clearTimer(this[timerId])
}
}
thenExecute {
timers.map { it.timerId }.forEach {
clearTimer(it)
}
}
}
event.subcommand("timer") {
thenArgument("time", DurationArgumentType) { duration ->
thenExecute {
startTimer(this[duration], "no message")
}
thenArgument("message", RestArgumentType) { message ->
thenExecute {
startTimer(this[duration], this[message])
}
}
}
}
}
}

View File

@@ -13,6 +13,7 @@ import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.json.KJsonOps
class ChoiceHandler<E>(
val enumClass: Class<E>,
val universe: List<E>,
) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable {
val codec = StringIdentifiable.createCodec {

View File

@@ -117,13 +117,24 @@ abstract class ManagedConfig(
protected fun <E> choice(
propertyName: String,
universe: List<E>,
enumClass: Class<E>,
default: () -> E
): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
return option(propertyName, default, ChoiceHandler(universe))
return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList()))
}
// 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, E::class.java, default)
}
private fun <E> createStringIdentifiable(x: () -> Array<out E>): Codec<E> where E : Enum<E>, E : StringIdentifiable {
return StringIdentifiable.createCodec { x() }
}
// TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434
// protected inline fun <reified E> choice(
// propertyName: String,
// noinline default: () -> E
@@ -136,6 +147,8 @@ abstract class ManagedConfig(
// default
// )
// }
open fun onChange(option: ManagedOption<*>) {
}
protected fun duration(
propertyName: String,

View File

@@ -6,7 +6,6 @@ import kotlinx.serialization.json.JsonObject
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ErrorUtil
class ManagedOption<T : Any>(
@@ -28,7 +27,13 @@ class ManagedOption<T : Any>(
val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description"
val labelDescription: Text = Text.translatable(descriptionTranslationKey)
lateinit var value: T
private var actualValue: T? = null
var value: T
get() = actualValue ?: error("Lateinit variable not initialized")
set(value) {
actualValue = value
element.onChange(this)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value

View File

@@ -111,8 +111,9 @@ object EntityRenderer {
renderContext: DrawContext,
posX: Int,
posY: Int,
mouseX: Float,
mouseY: Float
// TODO: Add width, height properties here
mouseX: Double,
mouseY: Double
) {
var bottomOffset = 0.0F
var currentEntity = entity
@@ -148,15 +149,15 @@ object EntityRenderer {
y2: Int,
size: Float,
bottomOffset: Float,
mouseX: Float,
mouseY: Float,
mouseX: Double,
mouseY: Double,
entity: LivingEntity
) {
context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat())
val centerX = (x1 + x2) / 2f
val centerY = (y1 + y2) / 2f
val targetYaw = atan(((centerX - mouseX) / 40.0f).toDouble()).toFloat()
val targetPitch = atan(((centerY - mouseY) / 40.0f).toDouble()).toFloat()
val targetYaw = atan(((centerX - mouseX) / 40.0f)).toFloat()
val targetPitch = atan(((centerY - mouseY) / 40.0f)).toFloat()
val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat())
val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180))
rotateToFaceTheFront.mul(rotateToFaceTheCamera)

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -6,23 +5,22 @@ import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) : IReloadable {
var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
override fun reload(repository: NEURepository) {
val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
val baseRecipes = repository.items.items.values
.asSequence()
.flatMap { it.recipes }
val extraRecipes = essenceRecipeProvider.recipes
(baseRecipes + extraRecipes)
.forEach { recipe ->
recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
}
this.usages = usages
this.recipes = recipes
}
override fun reload(repository: NEURepository) {
val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
val baseRecipes = repository.items.items.values
.asSequence()
.flatMap { it.recipes }
(baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() })
.forEach { recipe ->
recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
}
this.usages = usages
this.recipes = recipes
}
}

View File

@@ -1,4 +1,3 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
@@ -7,44 +6,46 @@ import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
class EssenceRecipeProvider : IReloadable {
data class EssenceUpgradeRecipe(
val itemId: SkyblockId,
val starCountAfter: Int,
val essenceCost: Int,
val essenceType: String, // TODO: replace with proper type
val extraItems: List<NEUIngredient>,
) : NEURecipe {
val essenceIngredient= NEUIngredient.fromString("${essenceType}:$essenceCost")
val allUpgradeComponents = listOf(essenceIngredient) + extraItems
class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider {
data class EssenceUpgradeRecipe(
val itemId: SkyblockId,
val starCountAfter: Int,
val essenceCost: Int,
val essenceType: String, // TODO: replace with proper type
val extraItems: List<NEUIngredient>,
) : NEURecipe {
val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost")
val allUpgradeComponents = listOf(essenceIngredient) + extraItems
override fun getAllInputs(): Collection<NEUIngredient> {
return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
}
override fun getAllInputs(): Collection<NEUIngredient> {
return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
}
override fun getAllOutputs(): Collection<NEUIngredient> {
return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
}
}
override fun getAllOutputs(): Collection<NEUIngredient> {
return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
}
}
var recipes = listOf<EssenceUpgradeRecipe>()
private set
var recipes = listOf<EssenceUpgradeRecipe>()
private set
override fun reload(repository: NEURepository) {
val recipes = mutableListOf<EssenceUpgradeRecipe>()
for ((neuId, costs) in repository.constants.essenceCost.costs) {
// TODO: add dungeonization costs. this is in repo, but not in the repo parser.
for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
val items = costs.itemCosts[starCountAfter] ?: emptyList()
recipes.add(
EssenceUpgradeRecipe(
SkyblockId(neuId),
starCountAfter,
essenceCost,
"ESSENCE_" + costs.type.uppercase(), // how flimsy
items.map { NEUIngredient.fromString(it) }))
}
}
this.recipes = recipes
}
override fun provideExtraRecipes(): Iterable<NEURecipe> = recipes
override fun reload(repository: NEURepository) {
val recipes = mutableListOf<EssenceUpgradeRecipe>()
for ((neuId, costs) in repository.constants.essenceCost.costs) {
// TODO: add dungeonization costs. this is in repo, but not in the repo parser.
for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
val items = costs.itemCosts[starCountAfter] ?: emptyList()
recipes.add(
EssenceUpgradeRecipe(
SkyblockId(neuId),
starCountAfter,
essenceCost,
"ESSENCE_" + costs.type.uppercase(), // how flimsy
items.map { NEUIngredient.fromString(it) }))
}
}
this.recipes = recipes
}
}

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.data.NEURecipe
interface ExtraRecipeProvider {
fun provideExtraRecipes(): Iterable<NEURecipe>
}

View File

@@ -24,21 +24,27 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtString
import net.minecraft.text.Style
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.config.HudMeta
import moe.nea.firmament.gui.config.HudPosition
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.repo.RepoManager.initialize
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.mc.modifyLore
import moe.nea.firmament.util.mc.setCustomName
import moe.nea.firmament.util.mc.setSkullOwner
import moe.nea.firmament.util.transformEachRecursively
object ItemCache : IReloadable {
private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
@@ -94,6 +100,35 @@ object ItemCache : IReloadable {
}
}
fun un189Lore(lore: String): Text {
val base = Text.literal("")
base.setStyle(Style.EMPTY.withItalic(false))
var lastColorCode = Style.EMPTY
var readOffset = 0
while (readOffset < lore.length) {
var nextCode = lore.indexOf('§', readOffset)
if (nextCode < 0) {
nextCode = lore.length
}
val text = lore.substring(readOffset, nextCode)
if (text.isNotEmpty()) {
base.append(Text.literal(text).setStyle(lastColorCode))
}
readOffset = nextCode + 2
if (nextCode + 1 < lore.length) {
val colorCode = lore[nextCode + 1]
val formatting = LegacyFormattingCode.byCode[colorCode.lowercaseChar()] ?: LegacyFormattingCode.RESET
val modernFormatting = formatting.modern
if (modernFormatting.isColor) {
lastColorCode = Style.EMPTY.withColor(modernFormatting)
} else {
lastColorCode = lastColorCode.withFormatting(modernFormatting)
}
}
}
return base
}
private fun NEUItem.asItemStackNow(): ItemStack {
try {
val oldItemTag = get10809CompoundTag()
@@ -101,6 +136,8 @@ object ItemCache : IReloadable {
?: return brokenItemStack(this)
val itemInstance =
ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) }
itemInstance.displayNameAccordingToNbt = un189Lore(displayName)
val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
if (extraAttributes != null)
itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
@@ -135,12 +172,13 @@ object ItemCache : IReloadable {
}
fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text {
assert(this.siblings.isEmpty())
var string = this.string
loreReplacements.forEach { (find, replace) ->
string = string.replace("{$find}", replace)
return this.transformEachRecursively {
var string = it.directLiteralStringContent ?: return@transformEachRecursively it
loreReplacements.forEach { (find, replace) ->
string = string.replace("{$find}", replace)
}
Text.literal(string).setStyle(it.style)
}
return Text.literal(string).styled { this.style }
}
var job: Job? = null

View File

@@ -0,0 +1,160 @@
package moe.nea.firmament.repo
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.serializer
import net.minecraft.item.Item
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.util.Identifier
import moe.nea.firmament.util.ReforgeId
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.skyblock.Rarity
@Serializable
data class Reforge(
val reforgeName: String,
@SerialName("internalName") val reforgeStone: SkyblockId? = null,
val nbtModifier: ReforgeId? = null,
val requiredRarities: List<Rarity>? = null,
val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List<ReforgeEligibilityFilter>? = null,
val allowOn: List<ReforgeEligibilityFilter>? = null,
val reforgeCosts: RarityMapped<Double>? = null,
val reforgeAbility: RarityMapped<String>? = null,
val reforgeStats: RarityMapped<Map<String, Double>>? = null,
) {
val eligibleItems get() = allowOn ?: itemTypes ?: listOf()
val statUniverse: Set<String> = Rarity.entries.flatMapTo(mutableSetOf()) {
reforgeStats?.get(it)?.keys ?: emptySet()
}
@Serializable(with = ReforgeEligibilityFilter.Serializer::class)
sealed interface ReforgeEligibilityFilter {
object ItemTypesSerializer : KSerializer<List<ReforgeEligibilityFilter>> {
override val descriptor: SerialDescriptor
get() = JsonElement.serializer().descriptor
override fun deserialize(decoder: Decoder): List<ReforgeEligibilityFilter> {
decoder as JsonDecoder
val jsonElement = decoder.decodeJsonElement()
if (jsonElement is JsonPrimitive && jsonElement.isString) {
return jsonElement.content.split("/").map { AllowsItemType(ItemType.ofName(it)) }
}
if (jsonElement is JsonArray) {
return decoder.json.decodeFromJsonElement(serializer<List<ReforgeEligibilityFilter>>(), jsonElement)
}
jsonElement as JsonObject
val filters = mutableListOf<ReforgeEligibilityFilter>()
jsonElement["internalName"]?.let {
decoder.json.decodeFromJsonElement(serializer<List<SkyblockId>>(), it).forEach {
filters.add(AllowsInternalName(it))
}
}
jsonElement["itemId"]?.let {
decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach {
val ident = Identifier.tryParse(it)
if (ident != null)
filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident)))
}
}
return filters
}
override fun serialize(encoder: Encoder, value: List<ReforgeEligibilityFilter>) {
TODO("Not yet implemented")
}
}
object Serializer : KSerializer<ReforgeEligibilityFilter> {
override val descriptor: SerialDescriptor
get() = serializer<JsonElement>().descriptor
override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter {
val jsonObject = serializer<JsonObject>().deserialize(decoder)
jsonObject["internalName"]?.let {
return AllowsInternalName(SkyblockId((it as JsonPrimitive).content))
}
jsonObject["itemType"]?.let {
return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content))
}
jsonObject["minecraftId"]?.let {
return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM,
Identifier.of((it as JsonPrimitive).content)))
}
error("Unknown item type")
}
override fun serialize(encoder: Encoder, value: ReforgeEligibilityFilter) {
TODO("Not yet implemented")
}
}
data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter
data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter
data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter
}
val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase())
@Serializable(with = RarityMapped.Serializer::class)
sealed interface RarityMapped<T> {
fun get(rarity: Rarity?): T?
class Serializer<T>(
val values: KSerializer<T>
) : KSerializer<RarityMapped<T>> {
override val descriptor: SerialDescriptor
get() = JsonElement.serializer().descriptor
val indirect = MapSerializer(Rarity.serializer(), values)
override fun deserialize(decoder: Decoder): RarityMapped<T> {
decoder as JsonDecoder
val element = decoder.decodeJsonElement()
if (element is JsonObject) {
return PerRarity(decoder.json.decodeFromJsonElement(indirect, element))
} else {
return Direct(decoder.json.decodeFromJsonElement(values, element))
}
}
override fun serialize(encoder: Encoder, value: RarityMapped<T>) {
when (value) {
is Direct<T> ->
values.serialize(encoder, value.value)
is PerRarity<T> ->
indirect.serialize(encoder, value.values)
}
}
}
@Serializable
data class Direct<T>(val value: T) : RarityMapped<T> {
override fun get(rarity: Rarity?): T {
return value
}
}
@Serializable
data class PerRarity<T>(val values: Map<Rarity, T>) : RarityMapped<T> {
override fun get(rarity: Rarity?): T? {
return values[rarity]
}
}
}
}

View File

@@ -0,0 +1,124 @@
package moe.nea.firmament.repo
import com.google.gson.JsonElement
import com.mojang.serialization.JsonOps
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepoFile
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.NEURepositoryException
import io.github.moulberry.repo.data.NEURecipe
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import net.minecraft.item.Item
import net.minecraft.registry.RegistryKey
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ReforgeId
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.json.KJsonOps
import moe.nea.firmament.util.skyblock.ItemType
object ReforgeStore : ExtraRecipeProvider, IReloadable {
override fun provideExtraRecipes(): Iterable<NEURecipe> {
return emptyList()
}
var byType: Map<ItemType, List<Reforge>> = mapOf()
var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf()
var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf()
var modifierLut = mapOf<ReforgeId, Reforge>()
var byReforgeStone = mapOf<SkyblockId, Reforge>()
var allReforges = listOf<Reforge>()
fun findEligibleForItem(itemType: ItemType): List<Reforge> {
return byType[itemType] ?: listOf()
}
fun findEligibleForInternalName(internalName: SkyblockId): List<Reforge> {
return byInternalName[internalName] ?: listOf()
}
//TODO: return byVanillla
override fun reload(repo: NEURepository) {
val basicReforges =
repo.file("constants/reforges.json")
?.kJson(serializer<Map<String, Reforge>>())
?.values ?: emptyList()
val advancedReforges =
repo.file("constants/reforgestones.json")
?.kJson(serializer<Map<String, Reforge>>())
?.values ?: emptyList()
val allReforges = (basicReforges + advancedReforges)
modifierLut = allReforges.associateBy { it.reforgeId }
byReforgeStone = allReforges.filter { it.reforgeStone != null }
.associateBy { it.reforgeStone!! }
val byType = mutableMapOf<ItemType, MutableList<Reforge>>()
val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>()
val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>()
this.byType = byType
this.byVanilla = byVanilla
this.byInternalName = byInternalName
for (reforge in allReforges) {
for (eligibleItem in reforge.eligibleItems) {
when (eligibleItem) {
is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> {
byInternalName.getOrPut(eligibleItem.internalName, ::mutableListOf).add(reforge)
}
is Reforge.ReforgeEligibilityFilter.AllowsItemType -> {
val actualItemTypes = resolveItemType(eligibleItem.itemType)
for (itemType in actualItemTypes) {
byType.getOrPut(itemType, ::mutableListOf).add(reforge)
}
}
is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
byVanilla.getOrPut(eligibleItem.minecraftId, ::mutableListOf).add(reforge)
}
}
}
}
this.allReforges = allReforges
}
fun resolveItemType(itemType: ItemType): List<ItemType> {
if (ItemType.SWORD == itemType) {
return listOf(
ItemType.SWORD,
ItemType.GAUNTLET,
ItemType.LONGSWORD,// TODO: check name
ItemType.FISHING_WEAPON,// TODO: check name
)
}
if (itemType == ItemType.ofName("ARMOR")) {
return listOf(
ItemType.CHESTPLATE,
ItemType.LEGGINGS,
ItemType.HELMET,
ItemType.BOOTS,
)
}
if (itemType == ItemType.EQUIPMENT) {
return listOf(
ItemType.CLOAK,
ItemType.BRACELET,
ItemType.NECKLACE,
ItemType.BELT,
ItemType.GLOVES,
)
}
if (itemType == ItemType.ROD) {
return listOf(ItemType.FISHING_ROD, ItemType.FISHING_WEAPON)
}
return listOf(itemType)
}
fun <T> NEURepoFile.kJson(serializer: KSerializer<T>): T {
val rawJson = json(JsonElement::class.java)
try {
val kJsonElement = JsonOps.INSTANCE.convertTo(KJsonOps.INSTANCE, rawJson)
return Firmament.json.decodeFromJsonElement(serializer, kJsonElement)
} catch (ex: Exception) {
throw NEURepositoryException(path, "Could not decode kotlin JSON element", ex)
}
}
}

View File

@@ -0,0 +1,15 @@
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEUItem
import moe.nea.firmament.util.skyblock.ItemType
object RepoItemTypeCache : IReloadable {
var byItemType: Map<ItemType?, List<NEUItem>> = mapOf()
override fun reload(repository: NEURepository) {
byItemType = repository.items.items.values.groupBy { ItemType.fromEscapeCodeLore(it.lore.lastOrNull() ?: "") }
}
}

View File

@@ -53,13 +53,16 @@ object RepoManager {
var recentlyFailedToUpdateItemList = false
val essenceRecipeProvider = EssenceRecipeProvider()
val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider)
val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore)
fun makeNEURepository(path: Path): NEURepository {
return NEURepository.of(path).apply {
registerReloadListener(ItemCache)
registerReloadListener(RepoItemTypeCache)
registerReloadListener(ExpLadders)
registerReloadListener(ItemNameLookup)
registerReloadListener(ReforgeStore)
registerReloadListener(essenceRecipeProvider)
ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this))
registerReloadListener {
if (TestUtil.isInTest) return@registerReloadListener
@@ -70,7 +73,6 @@ object RepoManager {
}
}
}
registerReloadListener(essenceRecipeProvider)
registerReloadListener(recipeCache)
}
}

View File

@@ -9,18 +9,31 @@ import net.minecraft.item.ItemStack
import net.minecraft.network.RegistryByteBuf
import net.minecraft.network.codec.PacketCodec
import net.minecraft.network.codec.PacketCodecs
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.text.TextColor
import net.minecraft.util.Formatting
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.withFallback
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.ReforgeId
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.getReforgeId
import moe.nea.firmament.util.getUpgradeStars
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.appendLore
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.prepend
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.skyblock.Rarity
import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.useMatch
import moe.nea.firmament.util.withColor
data class SBItemStack constructor(
@@ -29,9 +42,9 @@ data class SBItemStack constructor(
private var stackSize: Int,
private var petData: PetData?,
val extraLore: List<Text> = emptyList(),
// TODO: grab this star data from nbt if possible
val stars: Int = 0,
val fallback: ItemStack? = null,
val reforge: ReforgeId? = null,
) {
fun getStackSize() = stackSize
@@ -68,7 +81,9 @@ data class SBItemStack constructor(
skyblockId,
RepoManager.getNEUItem(skyblockId),
itemStack.count,
petData = itemStack.petData?.let { PetData.fromHypixel(it) }
petData = itemStack.petData?.let { PetData.fromHypixel(it) },
stars = itemStack.getUpgradeStars(),
reforge = itemStack.getReforgeId()
)
}
@@ -83,6 +98,153 @@ data class SBItemStack constructor(
fun passthrough(itemStack: ItemStack): SBItemStack {
return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack)
}
fun appendEnhancedStats(
itemStack: ItemStack,
reforgeStats: Map<String, Double>,
buffKind: BuffKind,
) {
val namedReforgeStats = reforgeStats
.mapKeysTo(mutableMapOf()) { statIdToName(it.key) }
val loreMut = itemStack.loreAccordingToNbt.toMutableList()
var statBlockLastIndex = -1
for (i in loreMut.indices) {
val statLine = parseStatLine(loreMut[i])
if (statLine == null && statBlockLastIndex >= 0) {
break
}
if (statLine == null) {
continue
}
statBlockLastIndex = i
val statBuff = namedReforgeStats.remove(statLine.statName) ?: continue
loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute()
}
if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) {
loreMut.add(0, Text.literal(""))
}
// If there is no stat block the statBlockLastIndex falls through to -1
// TODO: this is good enough for some items. some other items might have their stats at a different place.
for ((statName, statBuff) in namedReforgeStats) {
val statLine = StatLine(statName, null).addStat(statBuff, buffKind)
loreMut.add(statBlockLastIndex + 1, statLine.reconstitute())
}
itemStack.loreAccordingToNbt = loreMut
}
data class StatFormatting(
val postFix: String,
val color: Formatting,
)
val formattingOverrides = mapOf(
"Sea Creature Chance" to StatFormatting("%", Formatting.RED),
"Strength" to StatFormatting("", Formatting.RED),
"Damage" to StatFormatting("", Formatting.RED),
"Bonus Attack Speed" to StatFormatting("%", Formatting.RED),
"Shot Cooldown" to StatFormatting("s", Formatting.RED),
"Ability Damage" to StatFormatting("%", Formatting.RED),
"Crit Damage" to StatFormatting("%", Formatting.RED),
"Crit Chance" to StatFormatting("%", Formatting.RED),
"Ability Damage" to StatFormatting("%", Formatting.RED),
"Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN),
"Health" to StatFormatting("", Formatting.GREEN),
"Defense" to StatFormatting("", Formatting.GREEN),
"Fishing Speed" to StatFormatting("", Formatting.GREEN),
"Double Hook Chance" to StatFormatting("%", Formatting.GREEN),
"Mining Speed" to StatFormatting("", Formatting.GREEN),
"Mining Fortune" to StatFormatting("", Formatting.GREEN),
"Heat Resistance" to StatFormatting("", Formatting.GREEN),
"Swing Range" to StatFormatting("", Formatting.GREEN),
"Rift Time" to StatFormatting("", Formatting.GREEN),
"Speed" to StatFormatting("", Formatting.GREEN),
"Farming Fortune" to StatFormatting("", Formatting.GREEN),
"True Defense" to StatFormatting("", Formatting.GREEN),
"Mending" to StatFormatting("", Formatting.GREEN),
"Foraging Wisdom" to StatFormatting("", Formatting.GREEN),
"Farming Wisdom" to StatFormatting("", Formatting.GREEN),
"Foraging Fortune" to StatFormatting("", Formatting.GREEN),
"Magic Find" to StatFormatting("", Formatting.GREEN),
"Ferocity" to StatFormatting("", Formatting.GREEN),
"Bonus Pest Chance" to StatFormatting("%", Formatting.GREEN),
"Cold Resistance" to StatFormatting("", Formatting.GREEN),
"Pet Luck" to StatFormatting("", Formatting.GREEN),
"Fear" to StatFormatting("", Formatting.GREEN),
"Mana Regen" to StatFormatting("%", Formatting.GREEN),
"Rift Damage" to StatFormatting("", Formatting.GREEN),
"Hearts" to StatFormatting("", Formatting.GREEN),
"Vitality" to StatFormatting("", Formatting.GREEN),
// TODO: make this a repo json
)
private val statLabelRegex = "(?<statName>.*): ".toPattern()
enum class BuffKind(
val color: Formatting,
val prefix: String,
val postFix: String,
) {
REFORGE(Formatting.BLUE, "(", ")"),
;
}
data class StatLine(
val statName: String,
val value: Text?,
val rest: List<Text> = listOf(),
val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull()
) {
fun addStat(amount: Double, buffKind: BuffKind): StatLine {
val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true)
return copy(
valueNum = (valueNum ?: 0.0) + amount,
value = null,
rest = rest +
listOf(
Text.literal(
buffKind.prefix + formattedAmount +
statFormatting.postFix +
buffKind.postFix + " ")
.withColor(buffKind.color)))
}
fun formatValue() =
Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0,
1,
includeSign = true) + statFormatting.postFix + " ")
.setStyle(Style.EMPTY.withColor(statFormatting.color))
val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN)
private fun abbreviate(abbreviateTo: Int): String {
if (abbreviateTo >= statName.length) return statName
val segments = statName.split(" ")
return segments.joinToString(" ") {
it.substring(0, maxOf(1, abbreviateTo / segments.size))
}
}
fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text =
Text.literal("").setStyle(Style.EMPTY.withItalic(false))
.append(Text.literal("${abbreviate(abbreviateTo)}: ").grey())
.append(value ?: formatValue())
.also { rest.forEach(it::append) }
}
fun statIdToName(statId: String): String {
val segments = statId.split("_")
return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } }
}
private fun parseStatLine(line: Text): StatLine? {
val sibs = line.siblings
val stat = sibs.firstOrNull() ?: return null
if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null
val statLabel = stat.directLiteralStringContent ?: return null
val statName = statLabelRegex.useMatch(statLabel) { group("statName") } ?: return null
return StatLine(statName, sibs[1], sibs.subList(2, sibs.size))
}
}
constructor(skyblockId: SkyblockId, petData: PetData) : this(
@@ -133,6 +295,25 @@ data class SBItemStack constructor(
}
private fun appendReforgeInfo(
itemStack: ItemStack,
) {
val rarity = Rarity.fromItem(itemStack) ?: return
val reforgeId = this.reforge ?: return
val reforge = ReforgeStore.modifierLut[reforgeId] ?: return
val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf()
itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy()
.prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE))
val data = itemStack.extraAttributes.copy()
data.putString("modifier", reforgeId.id)
itemStack.extraAttributes = data
appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE)
}
// TODO: avoid instantiating the item stack here
val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack())
val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack())
private var itemStack_: ItemStack? = null
private val itemStack: ItemStack
@@ -147,6 +328,7 @@ data class SBItemStack constructor(
return@run neuItem.asItemStack(idHint = skyblockId, replacementData)
.withFallback(fallback)
.copyWithCount(stackSize)
.also { appendReforgeInfo(it) }
.also { it.appendLore(extraLore) }
.also { enhanceStatsByStars(it, stars) }
}

View File

@@ -0,0 +1,10 @@
package moe.nea.firmament.util
import java.time.LocalDateTime
import java.time.Month
object AprilFoolsUtil {
val isAprilFoolsDay = LocalDateTime.now().let {
it.dayOfMonth == 1 && it.month == Month.APRIL
}
}

View File

@@ -15,21 +15,25 @@ import net.minecraft.text.Text
object FirmFormatters {
fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments)
fun formatCommas(long: Long, segments: Int = 3): String {
fun formatCommas(long: Long, segments: Int = 3, includeSign: Boolean = false): String {
if (long < 0 && long != Long.MIN_VALUE) {
return "-" + formatCommas(-long, segments, false)
}
val prefix = if (includeSign) "+" else ""
val α = long / 1000
if (α != 0L) {
return formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0')
return prefix + formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0')
}
return long.toString()
return prefix + long.toString()
}
fun formatCommas(float: Float, fractionalDigits: Int): String = formatCommas(float.toDouble(), fractionalDigits)
fun formatCommas(double: Double, fractionalDigits: Int): String {
fun formatCommas(double: Double, fractionalDigits: Int, includeSign: Boolean = false): String {
val long = double.toLong()
val δ = (double - long).absoluteValue
val μ = pow(10, fractionalDigits)
val digits = (μ * δ).toInt().toString().padStart(fractionalDigits, '0').trimEnd('0')
return formatCommas(long) + (if (digits.isEmpty()) "" else ".$digits")
return formatCommas(long, includeSign = includeSign) + (if (digits.isEmpty()) "" else ".$digits")
}
fun formatDistance(distance: Double): String {

View File

@@ -1,35 +1,37 @@
package moe.nea.firmament.util
import net.minecraft.util.Formatting
enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) {
BLACK("BLACK", '0', 0),
DARK_BLUE("DARK_BLUE", '1', 1),
DARK_GREEN("DARK_GREEN", '2', 2),
DARK_AQUA("DARK_AQUA", '3', 3),
DARK_RED("DARK_RED", '4', 4),
DARK_PURPLE("DARK_PURPLE", '5', 5),
GOLD("GOLD", '6', 6),
GRAY("GRAY", '7', 7),
DARK_GRAY("DARK_GRAY", '8', 8),
BLUE("BLUE", '9', 9),
GREEN("GREEN", 'a', 10),
AQUA("AQUA", 'b', 11),
RED("RED", 'c', 12),
LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
YELLOW("YELLOW", 'e', 14),
WHITE("WHITE", 'f', 15),
OBFUSCATED("OBFUSCATED", 'k', -1),
BOLD("BOLD", 'l', -1),
STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
UNDERLINE("UNDERLINE", 'n', -1),
ITALIC("ITALIC", 'o', -1),
RESET("RESET", 'r', -1);
BLACK("BLACK", '0', 0),
DARK_BLUE("DARK_BLUE", '1', 1),
DARK_GREEN("DARK_GREEN", '2', 2),
DARK_AQUA("DARK_AQUA", '3', 3),
DARK_RED("DARK_RED", '4', 4),
DARK_PURPLE("DARK_PURPLE", '5', 5),
GOLD("GOLD", '6', 6),
GRAY("GRAY", '7', 7),
DARK_GRAY("DARK_GRAY", '8', 8),
BLUE("BLUE", '9', 9),
GREEN("GREEN", 'a', 10),
AQUA("AQUA", 'b', 11),
RED("RED", 'c', 12),
LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13),
YELLOW("YELLOW", 'e', 14),
WHITE("WHITE", 'f', 15),
OBFUSCATED("OBFUSCATED", 'k', -1),
BOLD("BOLD", 'l', -1),
STRIKETHROUGH("STRIKETHROUGH", 'm', -1),
UNDERLINE("UNDERLINE", 'n', -1),
ITALIC("ITALIC", 'o', -1),
RESET("RESET", 'r', -1);
val modern = Formatting.byCode(char)!!
companion object {
val byCode = entries.associateBy { it.char }
}
val formattingCode = "§$char"
val modern = Formatting.byCode(char)!!
val formattingCode = "§$char"
}

View File

@@ -64,6 +64,8 @@ object MC {
}
fun sendCommand(command: String) {
// TODO: add a queue to this and sendServerChat
ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/"))
player?.networkHandler?.sendCommand(command)
}
@@ -96,8 +98,9 @@ object MC {
inline val camera: Entity? get() = instance.cameraEntity
inline val guiAtlasManager get() = instance.guiAtlasManager
inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
inline val playerName: String? get() = player?.name?.unformattedString
inline var screen: Screen?
get() = TestUtil.unlessTesting{ 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,5 +1,6 @@
package moe.nea.firmament.util
import java.time.ZoneId
import java.util.UUID
import net.hypixel.modapi.HypixelModAPI
import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket
@@ -10,63 +11,66 @@ import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.ServerConnectedEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldReadyEvent
object SBData {
private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
val profileSuggestTexts = listOf(
"CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
"CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
)
var profileId: UUID? = null
private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex()
val profileSuggestTexts = listOf(
"CLICK THIS TO SUGGEST IT IN CHAT [DASHES]",
"CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
)
var profileId: UUID? = null
private var hasReceivedProfile = false
var locraw: Locraw? = null
val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
var profileIdCommandDebounce = TimeMark.farPast()
fun init() {
ServerConnectedEvent.subscribe("SBData:onServerConnected") {
HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
}
HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
MC.onMainThread {
val lastLocraw = locraw
locraw = Locraw(it.serverName,
it.serverType.getOrNull()?.name?.uppercase(),
it.mode.getOrNull(),
it.map.getOrNull())
SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
profileIdCommandDebounce = TimeMark.now()
}
}
SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
profileIdCommandDebounce = TimeMark.now()
MC.sendServerCommand("profileid")
}
}
AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
event.cancel()
}
}
ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
val profileMatch = profileRegex.matchEntire(event.unformattedString)
if (profileMatch != null) {
val oldProfile = profileId
try {
profileId = UUID.fromString(profileMatch.groupValues[1])
hasReceivedProfile = true
} catch (e: IllegalArgumentException) {
profileId = null
e.printStackTrace()
}
if (oldProfile != profileId) {
ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
}
}
}
}
/**
* Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems
*/
val hypixelTimeZone = ZoneId.of("US/Eastern")
private var hasReceivedProfile = false
var locraw: Locraw? = null
val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation
val hasValidLocraw get() = locraw?.server !in listOf("limbo", null)
val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK"
var profileIdCommandDebounce = TimeMark.farPast()
fun init() {
ServerConnectedEvent.subscribe("SBData:onServerConnected") {
HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java)
}
HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) {
MC.onMainThread {
val lastLocraw = locraw
locraw = Locraw(it.serverName,
it.serverType.getOrNull()?.name?.uppercase(),
it.mode.getOrNull(),
it.map.getOrNull())
SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
profileIdCommandDebounce = TimeMark.now()
}
}
SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") {
if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) {
profileIdCommandDebounce = TimeMark.now()
MC.sendServerCommand("profileid")
}
}
AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event ->
if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) {
event.cancel()
}
}
ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event ->
val profileMatch = profileRegex.matchEntire(event.unformattedString)
if (profileMatch != null) {
val oldProfile = profileId
try {
profileId = UUID.fromString(profileMatch.groupValues[1])
hasReceivedProfile = true
} catch (e: IllegalArgumentException) {
profileId = null
e.printStackTrace()
}
if (oldProfile != profileId) {
ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId))
}
}
}
}
}

View File

@@ -35,6 +35,7 @@ private constructor(
val PRIVATE_ISLAND = forMode("dynamic")
val RIFT = forMode("rift")
val MINESHAFT = forMode("mineshaft")
val GARDEN = forMode("garden")
}
val userFriendlyName

View File

@@ -106,7 +106,10 @@ data class HypixelPetInfo(
private val jsonparser = Json { ignoreUnknownKeys = true }
val ItemStack.extraAttributes: NbtCompound
var ItemStack.extraAttributes: NbtCompound
set(value) {
set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(value))
}
get() {
val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run {
val component = NbtComponent.of(NbtCompound())
@@ -125,10 +128,26 @@ val ItemStack.skyblockUUID: UUID?
private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") {
val jsonString = it.extraAttributes.getString("petInfo")
if (jsonString.isNullOrBlank()) return@memoize Optional.empty()
ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") {
jsonparser.decodeFromString<HypixelPetInfo>(jsonString)
}
.or { null }.intoOptional()
}
fun ItemStack.getUpgradeStars(): Int {
return extraAttributes.getInt("upgrade_level").takeIf { it > 0 }
?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 }
?: 0
}
@Serializable
@JvmInline
value class ReforgeId(val id: String)
fun ItemStack.getReforgeId(): ReforgeId? {
return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId)
}
val ItemStack.petData: HypixelPetInfo?
get() = petDataCache(this).getOrNull()

View File

@@ -5,8 +5,8 @@ import net.minecraft.component.type.LoreComponent
import net.minecraft.item.ItemStack
import net.minecraft.text.Text
var ItemStack.loreAccordingToNbt
get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
var ItemStack.loreAccordingToNbt: List<Text>
get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
set(value) {
set(DataComponentTypes.LORE, LoreComponent(value))
}

View File

@@ -0,0 +1,29 @@
package moe.nea.firmament.util.mc
import com.mojang.serialization.DynamicOps
import java.util.Optional
import net.minecraft.registry.Registry
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryOps
import net.minecraft.registry.RegistryWrapper
import net.minecraft.registry.entry.RegistryEntryOwner
class TolerantRegistriesOps<T>(
delegate: DynamicOps<T>,
registryInfoGetter: RegistryInfoGetter
) : RegistryOps<T>(delegate, registryInfoGetter) {
constructor(delegate: DynamicOps<T>, registry: RegistryWrapper.WrapperLookup) :
this(delegate, CachedRegistryInfoGetter(registry))
class TolerantOwner<E> : RegistryEntryOwner<E> {
override fun ownerEquals(other: RegistryEntryOwner<E>?): Boolean {
return true
}
}
override fun <E : Any?> getOwner(registryRef: RegistryKey<out Registry<out E>>?): Optional<RegistryEntryOwner<E>> {
return super.getOwner(registryRef).map {
TolerantOwner()
}
}
}

View File

@@ -13,6 +13,13 @@ value class ItemType private constructor(val name: String) {
return ItemType(name)
}
private val obfuscatedRegex = "§[kK].*?(§[0-9a-fA-FrR]|$)".toRegex()
fun fromEscapeCodeLore(lore: String): ItemType? {
return lore.replace(obfuscatedRegex, "").trim().substringAfter(" ", "")
.takeIf { it.isNotEmpty() }
?.let(::ofName)
}
fun fromItemStack(itemStack: ItemStack): ItemType? {
if (itemStack.petData != null)
return PET
@@ -26,13 +33,31 @@ value class ItemType private constructor(val name: String) {
if (type.isEmpty()) return null
return ofName(type)
}
return null
return itemStack.loreAccordingToNbt.lastOrNull()?.directLiteralStringContent?.let(::fromEscapeCodeLore)
}
// TODO: some of those are not actual in game item types, but rather ones included in the repository to splat to multiple in game types. codify those somehow
val SWORD = ofName("SWORD")
val DRILL = ofName("DRILL")
val PICKAXE = ofName("PICKAXE")
val GAUNTLET = ofName("GAUNTLET")
val LONGSWORD = ofName("LONG SWORD")
val EQUIPMENT = ofName("EQUIPMENT")
val FISHING_WEAPON = ofName("FISHING WEAPON")
val CLOAK = ofName("CLOAK")
val BELT = ofName("BELT")
val NECKLACE = ofName("NECKLACE")
val BRACELET = ofName("BRACELET")
val GLOVES = ofName("GLOVES")
val ROD = ofName("ROD")
val FISHING_ROD = ofName("FISHING ROD")
val VACUUM = ofName("VACUUM")
val CHESTPLATE = ofName("CHESTPLATE")
val LEGGINGS = ofName("LEGGINGS")
val HELMET = ofName("HELMET")
val BOOTS = ofName("BOOTS")
val NIL = ofName("__NIL")
/**
* This one is not really official (it never shows up in game).

View File

@@ -1,7 +1,16 @@
package moe.nea.firmament.util.skyblock
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.util.Formatting
import moe.nea.firmament.util.StringUtil.words
import moe.nea.firmament.util.collections.lastNotNullOfOrNull
import moe.nea.firmament.util.mc.loreAccordingToNbt
@@ -10,6 +19,7 @@ import moe.nea.firmament.util.unformattedString
typealias RepoRarity = io.github.moulberry.repo.data.Rarity
@Serializable(with = Rarity.Serializer::class)
enum class Rarity(vararg altNames: String) {
COMMON,
UNCOMMON,
@@ -24,11 +34,37 @@ enum class Rarity(vararg altNames: String) {
UNKNOWN
;
val names = setOf(name) + altNames
object Serializer : KSerializer<Rarity> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor(Rarity::class.java.name, PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Rarity {
return valueOf(decoder.decodeString().replace(" ", "_"))
}
override fun serialize(encoder: Encoder, value: Rarity) {
encoder.encodeString(value.name)
}
}
val names = setOf(name) + altNames
val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this]))
val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name }
companion object {
// TODO: inline those formattings as fields
val colourMap = mapOf(
Rarity.COMMON to Formatting.WHITE,
Rarity.UNCOMMON to Formatting.GREEN,
Rarity.RARE to Formatting.BLUE,
Rarity.EPIC to Formatting.DARK_PURPLE,
Rarity.LEGENDARY to Formatting.GOLD,
Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
Rarity.DIVINE to Formatting.AQUA,
Rarity.SPECIAL to Formatting.RED,
Rarity.VERY_SPECIAL to Formatting.RED,
Rarity.SUPREME to Formatting.DARK_RED,
)
val byName = entries.flatMap { en -> en.names.map { it to en } }.toMap()
val fromNeuRepo = entries.associateBy { it.neuRepoRarity }

View File

@@ -7,4 +7,5 @@ object SkyBlockItems {
val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND")
val DIAMOND = SkyblockId("DIAMOND")
val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE")
val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL")
}

View File

@@ -133,6 +133,7 @@ fun MutableText.darkGreen() = withColor(Formatting.DARK_GREEN)
fun MutableText.purple() = withColor(Formatting.DARK_PURPLE)
fun MutableText.pink() = withColor(Formatting.LIGHT_PURPLE)
fun MutableText.yellow() = withColor(Formatting.YELLOW)
fun MutableText.gold() = withColor(Formatting.GOLD)
fun MutableText.grey() = withColor(Formatting.GRAY)
fun MutableText.red() = withColor(Formatting.RED)
fun MutableText.white() = withColor(Formatting.WHITE)
@@ -142,11 +143,15 @@ fun MutableText.bold(): MutableText = styled { it.withBold(true) }
fun MutableText.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND,
"/firm disablereiwarning"))
it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
}
}
fun MutableText.prepend(text: Text): MutableText {
siblings.addFirst(text)
return this
}
fun Text.transformEachRecursively(function: (Text) -> Text): Text {
val c = this.content
if (c is TranslatableTextContent) {

View File

@@ -3,6 +3,9 @@ accessible class net/minecraft/client/render/RenderLayer$MultiPhase
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/font/TextRenderer$Drawer
accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator;
accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable;
accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V
accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter
accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V