From ba79b46f64d541504b391dae17efb485a08e7745 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 14:33:43 +0100 Subject: fix: Limit dangerous pickaxe abilities on garden as well --- src/main/kotlin/util/SkyBlockIsland.kt | 1 + 1 file changed, 1 insertion(+) (limited to 'src/main/kotlin/util') diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt index c42a55c..f15cadb 100644 --- a/src/main/kotlin/util/SkyBlockIsland.kt +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -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 -- cgit From ebcb06df4092500a38e9a1a8d56d249b5ff37c47 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 22 Dec 2024 21:17:11 +0100 Subject: feat: Add party commands --- .../kotlin/events/PartyMessageReceivedEvent.kt | 9 ++ src/main/kotlin/features/chat/PartyCommands.kt | 134 +++++++++++++++++++++ src/main/kotlin/util/MC.kt | 5 +- translations/en_us.json | 7 ++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/events/PartyMessageReceivedEvent.kt create mode 100644 src/main/kotlin/features/chat/PartyCommands.kt (limited to 'src/main/kotlin/util') diff --git a/src/main/kotlin/events/PartyMessageReceivedEvent.kt b/src/main/kotlin/events/PartyMessageReceivedEvent.kt new file mode 100644 index 0000000..4688dfe --- /dev/null +++ b/src/main/kotlin/events/PartyMessageReceivedEvent.kt @@ -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() +} diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt new file mode 100644 index 0000000..de3a0d9 --- /dev/null +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -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 = "(?Party|Guild) >([^:]+?)? (?[^: ]+): (?.+)".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().also { dispatch -> + fun register( + name: String, + vararg alias: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit = {}, + ): LiteralCommandNode { + val node = + dispatch.register(CaseInsensitiveLiteralCommandNode.Builder(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() + } +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index 294334a..215d2a8 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -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<*> diff --git a/translations/en_us.json b/translations/en_us.json index 4d1801a..d1af11e 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -129,6 +129,13 @@ "firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity", "firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.", "firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.", + "firmament.config.party-commands": "Party Commands", + "firmament.config.party-commands.cooldown": "Cooldown", + "firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.", + "firmament.config.party-commands.enable": "Enable Party Commands", + "firmament.config.party-commands.enable.description": "Allow people in your party to use commands like !warp, !coords, !ptme and so on. See /firm partycommands for a list", + "firmament.config.party-commands.ignore-own": "Ignore Own Messages", + "firmament.config.party-commands.ignore-own.description": "Prevent your own messages from triggering party commands", "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.", -- cgit From 39d35afb702cf017569ef9594774561848db7494 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Mon, 23 Dec 2024 23:53:27 +0100 Subject: fix: Some items not being saved in /firm stoarge --- .../TolerateFirmamentTolerateRegistryOwners.java | 18 +++++ .../inventory/storageoverlay/VirtualInventory.kt | 82 ++++++++++++---------- src/main/kotlin/util/mc/TolerantRegistriesOps.kt | 29 ++++++++ src/main/resources/firmament.accesswidener | 2 + 4 files changed, 95 insertions(+), 36 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java create mode 100644 src/main/kotlin/util/mc/TolerantRegistriesOps.kt (limited to 'src/main/kotlin/util') diff --git a/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java new file mode 100644 index 0000000..ac6f614 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java @@ -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 { + @Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true) + private void equalTolerantRegistryOwners(RegistryEntryOwner other, CallbackInfoReturnable cir) { + if (other instanceof TolerantRegistriesOps.TolerantOwner) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt index e07df8a..3b86184 100644 --- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -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 + val stacks: List ) { - 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 { + const val INVENTORY = "INVENTORY" + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) - object Serializer : KSerializer { - 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()) + 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 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 } - }) - } + fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) - 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()) - } - } + 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()) + } + } } diff --git a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt new file mode 100644 index 0000000..ce596a0 --- /dev/null +++ b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt @@ -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( + delegate: DynamicOps, + registryInfoGetter: RegistryInfoGetter +) : RegistryOps(delegate, registryInfoGetter) { + constructor(delegate: DynamicOps, registry: RegistryWrapper.WrapperLookup) : + this(delegate, CachedRegistryInfoGetter(registry)) + + class TolerantOwner : RegistryEntryOwner { + override fun ownerEquals(other: RegistryEntryOwner?): Boolean { + return true + } + } + + override fun getOwner(registryRef: RegistryKey>?): Optional> { + return super.getOwner(registryRef).map { + TolerantOwner() + } + } +} diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index cfa8e90..b280087 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -4,6 +4,8 @@ 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 (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V +accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter accessible field net/minecraft/client/render/item/HeldItemRenderer itemRenderer Lnet/minecraft/client/render/item/ItemRenderer; accessible field net/minecraft/client/render/item/ItemModels missingModelSupplier Ljava/util/function/Supplier; -- cgit From 24110c24af3ed14c0da050a2cdb1053b183b30b6 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Dec 2024 01:01:10 +0100 Subject: feat: Add /firm timer command --- .../AlwaysDisplayFirmamentClientCommandErrors.java | 18 +++ src/main/kotlin/commands/Duration.kt | 75 +++++++++++++ src/main/kotlin/features/misc/TimerFeature.kt | 124 +++++++++++++++++++++ src/main/kotlin/util/textutil.kt | 3 +- 4 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java create mode 100644 src/main/kotlin/commands/Duration.kt create mode 100644 src/main/kotlin/features/misc/TimerFeature.kt (limited to 'src/main/kotlin/util') diff --git a/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java new file mode 100644 index 0000000..59769c6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java @@ -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; + } +} diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt new file mode 100644 index 0000000..42f143d --- /dev/null +++ b/src/main/kotlin/commands/Duration.kt @@ -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 { + 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 listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder + ): CompletableFuture { + 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 = "(?[0-9]+)(?[${timeSuffixes.keys.joinToString("")}])".toPattern() + + override fun getExamples(): Collection { + return listOf("3m", "20s", "1h45m") + } +} diff --git a/src/main/kotlin/features/misc/TimerFeature.kt b/src/main/kotlin/features/misc/TimerFeature.kt new file mode 100644 index 0000000..7c4833d --- /dev/null +++ b/src/main/kotlin/features/misc/TimerFeature.kt @@ -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() + + @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]) + } + } + } + } + } +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 5d95d7a..06ed8c8 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -142,8 +142,7 @@ 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)) } } -- cgit From fbab19b40f72574b7930ddd2981998b2d2845471 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Dec 2024 01:58:46 +0100 Subject: feat: Add lore timers --- src/main/kotlin/features/inventory/TimerInLore.kt | 130 ++++++++++++++++++++++ src/main/kotlin/util/SBData.kt | 116 +++++++++---------- translations/en_us.json | 9 ++ 3 files changed, 199 insertions(+), 56 deletions(-) create mode 100644 src/main/kotlin/features/inventory/TimerInLore.kt (limited to 'src/main/kotlin/util') diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt new file mode 100644 index 0000000..f1b77c6 --- /dev/null +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -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)(?:(?[0-9]+) ?(y|years?) )?(?:(?[0-9]+) ?(d|days?))? ?(?:(?[0-9]+) ?(h|hours?))? ?(?:(?[0-9]+) ?(m|minutes?))? ?(?:(?[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()) + ) + } + } + +} diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt index 051d070..e785ff6 100644 --- a/src/main/kotlin/util/SBData.kt +++ b/src/main/kotlin/util/SBData.kt @@ -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)) + } + } + } + } } diff --git a/translations/en_us.json b/translations/en_us.json index a157432..3d8de96 100644 --- a/translations/en_us.json +++ b/translations/en_us.json @@ -129,6 +129,15 @@ "firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity", "firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.", "firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.", + "firmament.config.lore-timers": "Lore Timers", + "firmament.config.lore-timers.format": "Time Format", + "firmament.config.lore-timers.format.choice.american": "§9Ame§cri§fcan", + "firmament.config.lore-timers.format.choice.local": "System Time Format", + "firmament.config.lore-timers.format.choice.rfc": "RFC", + "firmament.config.lore-timers.format.choice.socialist": "European-ish", + "firmament.config.lore-timers.format.description": "Choose the time format in which resolved timers are displayed.", + "firmament.config.lore-timers.show": "Show Lore Timers", + "firmament.config.lore-timers.show.description": "Shows when a timer in a lore (such as interest, auction duration) would end.", "firmament.config.party-commands": "Party Commands", "firmament.config.party-commands.cooldown": "Cooldown", "firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.", -- cgit From e16c60169bf192b79991176b5f9cee66b5b16e7d Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 24 Dec 2024 03:58:43 +0100 Subject: WIP: Reforge recipes --- .../nea/firmament/compat/rei/FirmamentReiPlugin.kt | 6 + .../compat/rei/recipes/SBReforgeRecipe.kt | 147 +++++++++++++++++++++ src/main/kotlin/repo/BetterRepoRecipeCache.kt | 36 +++-- src/main/kotlin/repo/EssenceRecipeProvider.kt | 75 +++++------ src/main/kotlin/repo/ExtraRecipeProvider.kt | 7 + src/main/kotlin/repo/ItemCache.kt | 47 ++++++- src/main/kotlin/repo/Reforge.kt | 146 ++++++++++++++++++++ src/main/kotlin/repo/ReforgeStore.kt | 124 +++++++++++++++++ src/main/kotlin/repo/RepoItemTypeCache.kt | 15 +++ src/main/kotlin/repo/RepoManager.kt | 6 +- src/main/kotlin/repo/SBItemStack.kt | 21 ++- src/main/kotlin/util/LegacyFormattingCode.kt | 54 ++++---- src/main/kotlin/util/SkyblockId.kt | 18 ++- src/main/kotlin/util/skyblock/ItemType.kt | 27 +++- src/main/kotlin/util/skyblock/Rarity.kt | 21 +++ src/test/resources/testdata/items/hyperion.snbt | 96 ++++++++++++++ .../resources/testdata/items/implosion-belt.snbt | 105 +++++++++++++++ 17 files changed, 858 insertions(+), 93 deletions(-) create mode 100644 src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt create mode 100644 src/main/kotlin/repo/ExtraRecipeProvider.kt create mode 100644 src/main/kotlin/repo/Reforge.kt create mode 100644 src/main/kotlin/repo/ReforgeStore.kt create mode 100644 src/main/kotlin/repo/RepoItemTypeCache.kt create mode 100644 src/test/resources/testdata/items/hyperion.snbt create mode 100644 src/test/resources/testdata/items/implosion-belt.snbt (limited to 'src/main/kotlin/util') diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt index f576eda..92f2cfc 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt @@ -24,6 +24,7 @@ import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe import moe.nea.firmament.compat.rei.recipes.SBKatRecipe import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe +import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe import moe.nea.firmament.events.HandledScreenPushREIEvent import moe.nea.firmament.features.inventory.CraftingOverlay import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen @@ -78,6 +79,7 @@ class FirmamentReiPlugin : REIClientPlugin { registry.add(SBForgeRecipe.Category) registry.add(SBMobDropRecipe.Category) registry.add(SBKatRecipe.Category) + registry.add(SBReforgeRecipe.Category) registry.add(SBEssenceUpgradeRecipe.Category) } @@ -90,6 +92,10 @@ class FirmamentReiPlugin : REIClientPlugin { registry.registerDisplayGenerator( SBCraftingRecipe.Category.catIdentifier, SkyblockCraftingRecipeDynamicGenerator) + registry.registerDisplayGenerator( + SBReforgeRecipe.catIdentifier, + SBReforgeRecipe.DynamicGenerator + ) registry.registerDisplayGenerator( SBForgeRecipe.Category.categoryIdentifier, SkyblockForgeRecipeDynamicGenerator) diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt new file mode 100644 index 0000000..2805132 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -0,0 +1,147 @@ +package moe.nea.firmament.compat.rei.recipes + +import java.util.Optional +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator +import me.shedaniel.rei.api.client.view.ViewSearchBuilder +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.display.Display +import me.shedaniel.rei.api.common.display.DisplaySerializer +import me.shedaniel.rei.api.common.entry.EntryIngredient +import me.shedaniel.rei.api.common.entry.EntryStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.repo.Reforge +import moe.nea.firmament.repo.ReforgeStore +import moe.nea.firmament.repo.RepoItemTypeCache +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.tr + +class SBReforgeRecipe( + val reforge: Reforge, + val limitToItem: SkyblockId?, +) : Display { + companion object { + val catIdentifier = CategoryIdentifier.of(Firmament.MOD_ID, "reforge_recipe") + } + + object Category : DisplayCategory { + override fun getCategoryIdentifier(): CategoryIdentifier { + return catIdentifier + } + + override fun getTitle(): Text { + return tr("firmament.recipecategory.reforge", "Reforge") + } + + override fun getIcon(): Renderer { + return SBItemEntryDefinition.getEntry(SkyblockId("REFORGE_ANVIL")) + } + + override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList { + val list = mutableListOf() + list.add(Widgets.createRecipeBase(bounds)) + // TODO: actual layout after christmas, probably + list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY)) + .markInput().entries(display.inputItems)) + val stoneSlot = Widgets.createSlot(Point(bounds.minX + 38, bounds.centerY)) + .markInput() + if (display.reforgeStone != null) + stoneSlot.entry(display.reforgeStone) + list.add(stoneSlot) + list.add(Widgets.createSlot(Point(bounds.minX + 38 + 18, bounds.centerY)) + .markInput().entries(display.outputItems)) + return list + } + } + + object DynamicGenerator : DynamicDisplayGenerator { + fun getRecipesForSBItemStack(item: SBItemStack): Optional> { + val reforgeRecipes = mutableListOf() + for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) { + reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId)) + } + for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) { + reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId)) + } + if (reforgeRecipes.isEmpty()) return Optional.empty() + return Optional.of(reforgeRecipes) + } + + override fun getRecipeFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + return getRecipesForSBItemStack(item) + } + + override fun getUsageFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + ReforgeStore.byReforgeStone[item.skyblockId]?.let { stoneReforge -> + return Optional.of(listOf(SBReforgeRecipe(stoneReforge, null))) + } + return getRecipesForSBItemStack(item) + } + + override fun generate(builder: ViewSearchBuilder): Optional> { + // TODO: check builder.recipesFor and such and optionally return all reforge recipes + return Optional.empty() + } + } + + private val eligibleItems = + if (limitToItem != null) listOfNotNull(RepoManager.getNEUItem(limitToItem)) + else reforge.eligibleItems.flatMap { + when (it) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> + listOfNotNull(RepoManager.getNEUItem(it.internalName)) + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> + ReforgeStore.resolveItemType(it.itemType) + .flatMap { + RepoItemTypeCache.byItemType[it] ?: listOf() + } + + is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> { + listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type + } + } + } + private val inputItems = eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) } + private val outputItems = + inputItems.map { SBItemEntryDefinition.getEntry(it.value.copy(reforge = reforge.reforgeId)) } + private val reforgeStone = reforge.reforgeStone?.let(SBItemEntryDefinition::getEntry) + private val inputEntries = + listOf(EntryIngredient.of(inputItems)) + listOfNotNull(reforgeStone?.let(EntryIngredient::of)) + private val outputEntries = listOf(EntryIngredient.of(outputItems)) + + override fun getInputEntries(): List { + return inputEntries + } + + override fun getOutputEntries(): List { + return outputEntries + } + + override fun getCategoryIdentifier(): CategoryIdentifier<*> { + return catIdentifier + } + + override fun getDisplayLocation(): Optional { + return Optional.empty() + } + + override fun getSerializer(): DisplaySerializer? { + return null + } +} diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt index 91a6b50..4b32e57 100644 --- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -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> = mapOf() - var recipes: Map> = mapOf() +class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable { + var usages: Map> = mapOf() + var recipes: Map> = mapOf() - override fun reload(repository: NEURepository) { - val usages = mutableMapOf>() - val recipes = mutableMapOf>() - 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>() + val recipes = mutableMapOf>() + 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 + } } diff --git a/src/main/kotlin/repo/EssenceRecipeProvider.kt b/src/main/kotlin/repo/EssenceRecipeProvider.kt index 1833258..38559d5 100644 --- a/src/main/kotlin/repo/EssenceRecipeProvider.kt +++ b/src/main/kotlin/repo/EssenceRecipeProvider.kt @@ -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, - ) : 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, + ) : NEURecipe { + val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost") + val allUpgradeComponents = listOf(essenceIngredient) + extraItems + + override fun getAllInputs(): Collection { + return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents + } - override fun getAllInputs(): Collection { - return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents - } + override fun getAllOutputs(): Collection { + return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + } + } - override fun getAllOutputs(): Collection { - return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) - } - } + var recipes = listOf() + private set - var recipes = listOf() - private set + override fun provideExtraRecipes(): Iterable = recipes - override fun reload(repository: NEURepository) { - val recipes = mutableListOf() - 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 reload(repository: NEURepository) { + val recipes = mutableListOf() + 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 + } } diff --git a/src/main/kotlin/repo/ExtraRecipeProvider.kt b/src/main/kotlin/repo/ExtraRecipeProvider.kt new file mode 100644 index 0000000..9d3b5a0 --- /dev/null +++ b/src/main/kotlin/repo/ExtraRecipeProvider.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.data.NEURecipe + +interface ExtraRecipeProvider { + fun provideExtraRecipes(): Iterable +} diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index 014de7d..f88dd48 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -24,21 +24,26 @@ 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.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 = ConcurrentHashMap() @@ -70,6 +75,7 @@ object ItemCache : IReloadable { val ItemStack.isBroken get() = get(FirmamentDataComponentTypes.IS_BROKEN) ?: false + fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack { return ItemStack(Items.PAINTING).apply { setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) @@ -88,6 +94,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() @@ -95,6 +130,7 @@ object ItemCache : IReloadable { ?: return brokenItemStack(this) val itemInstance = ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this) + itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes") if (extraAttributes != null) itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) @@ -129,12 +165,13 @@ object ItemCache : IReloadable { } fun Text.applyLoreReplacements(loreReplacements: Map): 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 diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt new file mode 100644 index 0000000..ea01818 --- /dev/null +++ b/src/main/kotlin/repo/Reforge.kt @@ -0,0 +1,146 @@ +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? = null, + val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List? = null, + val allowOn: List? = null, + val reforgeCosts: RarityMapped? = null, + val reforgeAbility: RarityMapped? = null, + val reforgeStats: RarityMapped>? = null, +) { + val eligibleItems get() = allowOn ?: itemTypes ?: listOf() + + @Serializable(with = ReforgeEligibilityFilter.Serializer::class) + sealed interface ReforgeEligibilityFilter { + object ItemTypesSerializer : KSerializer> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + override fun deserialize(decoder: Decoder): List { + 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>(), jsonElement) + } + jsonElement as JsonObject + val filters = mutableListOf() + jsonElement["internalName"]?.let { + decoder.json.decodeFromJsonElement(serializer>(), it).forEach { + filters.add(AllowsInternalName(it)) + } + } + jsonElement["itemId"]?.let { + decoder.json.decodeFromJsonElement(serializer>(), 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) { + TODO("Not yet implemented") + } + } + + object Serializer : KSerializer { + override val descriptor: SerialDescriptor + get() = serializer().descriptor + + override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter { + val jsonObject = serializer().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) : ReforgeEligibilityFilter + } + + + val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase()) + + @Serializable(with = RarityMapped.Serializer::class) + sealed interface RarityMapped { + class Serializer( + val values: KSerializer + ) : KSerializer> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + val indirect = MapSerializer(Rarity.serializer(), values) + override fun deserialize(decoder: Decoder): RarityMapped { + 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) { + when (value) { + is Direct -> + values.serialize(encoder, value.value) + + is PerRarity -> + indirect.serialize(encoder, value.values) + } + } + } + + @Serializable + data class Direct(val value: T) : RarityMapped + + @Serializable + data class PerRarity(val values: Map) : RarityMapped + } + +} diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt new file mode 100644 index 0000000..f03903b --- /dev/null +++ b/src/main/kotlin/repo/ReforgeStore.kt @@ -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 { + return emptyList() + } + + var byType: Map> = mapOf() + var byVanilla: Map, List> = mapOf() + var byInternalName: Map> = mapOf() + var modifierLut = mapOf() + var byReforgeStone = mapOf() + var allReforges = listOf() + + fun findEligibleForItem(itemType: ItemType): List { + return byType[itemType] ?: listOf() + } + + fun findEligibleForInternalName(internalName: SkyblockId): List { + return byInternalName[internalName] ?: listOf() + } + + //TODO: return byVanillla + override fun reload(repo: NEURepository) { + val basicReforges = + repo.file("constants/reforges.json") + ?.kJson(serializer>()) + ?.values ?: emptyList() + val advancedReforges = + repo.file("constants/reforgestones.json") + ?.kJson(serializer>()) + ?.values ?: emptyList() + val allReforges = (basicReforges + advancedReforges) + modifierLut = allReforges.associateBy { it.reforgeId } + byReforgeStone = allReforges.filter { it.reforgeStone != null } + .associateBy { it.reforgeStone!! } + val byType = mutableMapOf>() + val byVanilla = mutableMapOf, MutableList>() + val byInternalName = mutableMapOf>() + 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 { + 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 NEURepoFile.kJson(serializer: KSerializer): 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) + } + } +} diff --git a/src/main/kotlin/repo/RepoItemTypeCache.kt b/src/main/kotlin/repo/RepoItemTypeCache.kt new file mode 100644 index 0000000..414ec09 --- /dev/null +++ b/src/main/kotlin/repo/RepoItemTypeCache.kt @@ -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> = mapOf() + + override fun reload(repository: NEURepository) { + byItemType = repository.items.items.values.groupBy { ItemType.fromEscapeCodeLore(it.lore.lastOrNull() ?: "") } + } +} diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt index 667ab73..6d9ba14 100644 --- a/src/main/kotlin/repo/RepoManager.kt +++ b/src/main/kotlin/repo/RepoManager.kt @@ -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) } } diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index 18126ee..e5cacaa 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -14,11 +14,16 @@ import net.minecraft.util.Formatting import moe.nea.firmament.repo.ItemCache.asItemStack 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.getReforgeId +import moe.nea.firmament.util.getUpgradeStars 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.skyBlockId +import moe.nea.firmament.util.skyblock.ItemType import moe.nea.firmament.util.skyblockId import moe.nea.firmament.util.withColor @@ -28,8 +33,8 @@ data class SBItemStack constructor( private var stackSize: Int, private var petData: PetData?, val extraLore: List = emptyList(), - // TODO: grab this star data from nbt if possible val stars: Int = 0, + val reforge: ReforgeId? = null, ) { fun getStackSize() = stackSize @@ -66,7 +71,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() ) } @@ -127,6 +134,15 @@ data class SBItemStack constructor( } + private fun appendReforgeStatsToLore( + itemStack: ItemStack, + ) { + val rarity = itemStack.rarity + val lore = itemStack.loreAccordingToNbt + } + + // TODO: avoid instantiating the item stack here + val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack()) private var itemStack_: ItemStack? = null private val itemStack: ItemStack @@ -141,6 +157,7 @@ data class SBItemStack constructor( return@run neuItem.asItemStack(idHint = skyblockId, replacementData) .copyWithCount(stackSize) .also { it.appendLore(extraLore) } + .also { if (reforge != null) it.appendLore(listOf(Text.literal("Reforge: $reforge"))) } // TODO: use this for proper rendering .also { enhanceStatsByStars(it, stars) } } if (itemStack_ == null) diff --git a/src/main/kotlin/util/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt index 44bacfc..1a5d1dd 100644 --- a/src/main/kotlin/util/LegacyFormattingCode.kt +++ b/src/main/kotlin/util/LegacyFormattingCode.kt @@ -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); + + companion object { + val byCode = entries.associateBy { it.char } + } - val modern = Formatting.byCode(char)!! + val modern = Formatting.byCode(char)!! - val formattingCode = "§$char" + val formattingCode = "§$char" } diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index 1c1aa77..497a2d2 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -125,10 +125,26 @@ val ItemStack.skyblockUUID: UUID? private val petDataCache = WeakCache.memoize>("PetInfo") { val jsonString = it.extraAttributes.getString("petInfo") if (jsonString.isNullOrBlank()) return@memoize Optional.empty() - ErrorUtil.catch("Could not decode hypixel pet info") { jsonparser.decodeFromString(jsonString) } + ErrorUtil.catch("Could not decode hypixel pet info") { + jsonparser.decodeFromString(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() diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt index 6ddb077..7149379 100644 --- a/src/main/kotlin/util/skyblock/ItemType.kt +++ b/src/main/kotlin/util/skyblock/ItemType.kt @@ -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). diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt index f26cefe..0244630 100644 --- a/src/main/kotlin/util/skyblock/Rarity.kt +++ b/src/main/kotlin/util/skyblock/Rarity.kt @@ -1,5 +1,12 @@ 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.Text import moe.nea.firmament.util.StringUtil.words @@ -10,6 +17,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,6 +32,19 @@ enum class Rarity(vararg altNames: String) { UNKNOWN ; + object Serializer : KSerializer { + 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 neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name } diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt new file mode 100644 index 0000000..c57d457 --- /dev/null +++ b/src/test/resources/testdata/items/hyperion.snbt @@ -0,0 +1,96 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + ability_scroll: [ + "IMPLOSION_SCROLL", + "WITHER_SHIELD_SCROLL", + "SHADOW_WARP_SCROLL" + ], + art_of_war_count: 1, + champion_combat_xp: 1.3556020889209766E7d, + donated_museum: 1b, + enchantments: { + champion: 10, + cleave: 5, + critical: 6, + cubism: 5, + ender_slayer: 6, + execute: 5, + experience: 3, + fire_aspect: 2, + first_strike: 4, + giant_killer: 6, + impaling: 3, + lethality: 5, + looting: 4, + luck: 6, + scavenger: 4, + smite: 7, + syphon: 4, + thunderlord: 6, + ultimate_wise: 5, + vampirism: 5, + venomous: 5 + }, + hot_potato_count: 15, + id: "HYPERION", + modifier: "heroic", + rarity_upgrades: 1, + stats_book: 65934, + timestamp: 1658091600000L, + upgrade_level: 5, + uuid: "a45337aa-9eaa-4e6f-aa27-26a42f8eca95" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Heroic Hyperion "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"1145 "},{"color":"dark_gray","text":"(4271)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+355 "},{"color":"yellow","text":"(+30) "},{"color":"dark_gray","text":"(+1,490.37)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+250 "},{"color":"yellow","text":"(+30) "},{"color":"gold","text":"[+5] "},{"color":"blue","text":"(+50) "},{"color":"dark_gray","text":"(+1,064.55)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+70% "},{"color":"dark_gray","text":"(+317.1%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+7% "},{"color":"blue","text":"(+7%) "},{"color":"dark_gray","text":"(+10.5%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+588 "},{"color":"blue","text":"(+125) "},{"color":"dark_gray","text":"(+2,505.09)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+33 "},{"color":"dark_gray","text":"(+45)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"✎"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"},{"color":"blue","text":", "},{"color":"blue","text":"Champion X"},{"color":"blue","text":", "},{"color":"blue","text":"Cleave V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Critical VI"},{"color":"blue","text":", "},{"color":"blue","text":"Cubism V"},{"color":"blue","text":", "},{"color":"blue","text":"Ender Slayer VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Execute V"},{"color":"blue","text":", "},{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"Fire Aspect II"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Giant Killer VI"},{"color":"blue","text":", "},{"color":"blue","text":"Impaling III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Lethality V"},{"color":"blue","text":", "},{"color":"blue","text":"Looting IV"},{"color":"blue","text":", "},{"color":"blue","text":"Luck VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Scavenger IV"},{"color":"blue","text":", "},{"color":"blue","text":"Smite VII"},{"color":"blue","text":", "},{"color":"blue","text":"Syphon IV"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Thunderlord VI"},{"color":"blue","text":", "},{"color":"blue","text":"Vampirism V"},{"color":"blue","text":", "},{"color":"blue","text":"Venomous V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Deals "},{"color":"red","text":"+50% "},{"color":"gray","text":"damage to Withers."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"red","text":"+1 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"green","text":"+2 "},{"color":"aqua","text":"✎"}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Intelligence "},{"color":"gray","text":"per "},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"green","text":"Scroll Abilities:"}],"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Wither Impact "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"10 blocks"},{"color":"gray","text":" ahead of you."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Then implode dealing "},{"color":"red","text":"21,658 "},{"color":"gray","text":"damage"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"to nearby enemies. Also applies the"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"wither shield scroll ability reducing"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"damage taken and granting an"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"absorption shield for "},{"color":"yellow","text":"5 "},{"color":"gray","text":"seconds."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"150"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"white","text":"Kills: "},{"color":"gold","text":"65,934"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON SWORD "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:iron_sword" +} diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt new file mode 100644 index 0000000..b73542d --- /dev/null +++ b/src/test/resources/testdata/items/implosion-belt.snbt @@ -0,0 +1,105 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + attributes: { + dominance: 1, + experience: 1 + }, + id: "IMPLOSION_BELT", + timestamp: "12/5/22 5:17 PM", + uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -896440193, + -59755884, + -1280665573, + -1297214643 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +}{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + attributes: { + dominance: 1, + experience: 1 + }, + id: "IMPLOSION_BELT", + timestamp: "12/5/22 5:17 PM", + uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -896440193, + -59755884, + -1280665573, + -1297214643 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" + } -- cgit From ddebaf47900dfab41590c97c202984142ae5b9f6 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 25 Dec 2024 16:15:20 +0100 Subject: WIP: Reforge Recipes --- .../moe/nea/firmament/compat/rei/EntityWidget.kt | 34 ++++-- .../compat/rei/recipes/SBReforgeRecipe.kt | 52 ++++++-- .../features/inventory/ItemRarityCosmetics.kt | 13 +- src/main/kotlin/gui/entity/EntityRenderer.kt | 13 +- src/main/kotlin/repo/ItemCache.kt | 2 + src/main/kotlin/repo/Reforge.kt | 14 ++- src/main/kotlin/repo/SBItemStack.kt | 136 ++++++++++++++++++++- src/main/kotlin/util/AprilFoolsUtil.kt | 10 ++ src/main/kotlin/util/FirmFormatters.kt | 14 ++- src/main/kotlin/util/SkyblockId.kt | 5 +- src/main/kotlin/util/mc/NbtItemData.kt | 4 +- src/main/kotlin/util/skyblock/Rarity.kt | 17 ++- src/main/kotlin/util/skyblock/SkyBlockItems.kt | 1 + src/main/kotlin/util/textutil.kt | 6 + 14 files changed, 273 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/util/AprilFoolsUtil.kt (limited to 'src/main/kotlin/util') diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt index d8238be..2b9e4bf 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt @@ -1,19 +1,22 @@ package moe.nea.firmament.compat.rei import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.Drawable import net.minecraft.client.gui.Element -import net.minecraft.client.gui.ParentElement import net.minecraft.entity.LivingEntity import moe.nea.firmament.gui.entity.EntityRenderer import moe.nea.firmament.util.ErrorUtil -class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBounds() { +class EntityWidget( + val entity: LivingEntity?, + val point: Point, + val size: FloatingDimension = FloatingDimension(defaultSize) +) : WidgetWithBounds() { override fun children(): List { return emptyList() } @@ -22,18 +25,35 @@ class EntityWidget(val entity: LivingEntity?, val point: Point) : WidgetWithBoun override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { try { - if (!hasErrored) - EntityRenderer.renderEntity(entity!!, context, point.x, point.y, mouseX.toFloat(), mouseY.toFloat()) + context.matrices.push() + if (!hasErrored) { + context.matrices.translate(point.x.toDouble(), point.y.toDouble(), 0.0) + val xScale = size.width / defaultSize.width.toDouble() + val yScale = size.height / defaultSize.height.toDouble() + context.matrices.scale(xScale.toFloat(), yScale.toFloat(), 1.0F) + EntityRenderer.renderEntity( + entity!!, + context, + 0, 0, + (mouseX - point.x) * xScale, + (mouseY - point.y) * yScale) + } } catch (ex: Exception) { ErrorUtil.softError("Failed to render constructed entity: $entity", ex) hasErrored = true + } finally { + context.matrices.pop() } if (hasErrored) { - context.fill(point.x, point.y, point.x + 50, point.y + 80, 0xFFAA2222.toInt()) + context.fill(point.x, point.y, point.x + size.width.toInt(), point.y + size.height.toInt(), 0xFFAA2222.toInt()) } } + companion object { + val defaultSize = Dimension(50, 80) + } + override fun getBounds(): Rectangle { - return Rectangle(point, Dimension(50, 80)) + return Rectangle(point, size) } } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt index 2805132..232f04f 100644 --- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -1,6 +1,8 @@ package moe.nea.firmament.compat.rei.recipes import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension import me.shedaniel.math.Point import me.shedaniel.math.Rectangle import me.shedaniel.rei.api.client.gui.Renderer @@ -14,17 +16,27 @@ import me.shedaniel.rei.api.common.display.Display import me.shedaniel.rei.api.common.display.DisplaySerializer import me.shedaniel.rei.api.common.entry.EntryIngredient import me.shedaniel.rei.api.common.entry.EntryStack +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnReason import net.minecraft.text.Text import net.minecraft.util.Identifier +import net.minecraft.village.VillagerProfession import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.EntityWidget import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.gui.entity.EntityRenderer import moe.nea.firmament.repo.Reforge import moe.nea.firmament.repo.ReforgeStore import moe.nea.firmament.repo.RepoItemTypeCache import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.AprilFoolsUtil +import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.gold import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblock.SkyBlockItems import moe.nea.firmament.util.skyblockId import moe.nea.firmament.util.tr @@ -46,21 +58,44 @@ class SBReforgeRecipe( } override fun getIcon(): Renderer { - return SBItemEntryDefinition.getEntry(SkyblockId("REFORGE_ANVIL")) + return SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL) } override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList { val list = mutableListOf() list.add(Widgets.createRecipeBase(bounds)) // TODO: actual layout after christmas, probably - list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY)) + list.add(Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY - 9)) .markInput().entries(display.inputItems)) - val stoneSlot = Widgets.createSlot(Point(bounds.minX + 38, bounds.centerY)) - .markInput() - if (display.reforgeStone != null) - stoneSlot.entry(display.reforgeStone) - list.add(stoneSlot) - list.add(Widgets.createSlot(Point(bounds.minX + 38 + 18, bounds.centerY)) + if (display.reforgeStone != null) { + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24, bounds.centerY - 9 - 10)) + .markInput().entry(display.reforgeStone)) + list.add(Widgets.withTooltip( + Widgets.withTranslate(Widgets.wrapRenderer( + Rectangle(Point(bounds.minX + 10 + 24, bounds.centerY - 9 + 10), Dimension(18, 18)), + SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)), 0.0, 0.0, 150.0), + Rarity.entries.mapNotNull { rarity -> + display.reforge.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Text.literal("") + .append(rarity.text) + .append(": ") + .append(Text.literal("${FirmFormatters.formatCommas(cost, 0)} Coins").gold()) + } + )) + } else { + val size = if (AprilFoolsUtil.isAprilFoolsDay) 1.2 else 0.6 + val dimension = + FloatingDimension(EntityWidget.defaultSize.width * size, EntityWidget.defaultSize.height * size) + list.add(EntityWidget( + EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND) + ?.also { it.villagerData = it.villagerData.withProfession(VillagerProfession.WEAPONSMITH) }, + Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2), + dimension + )) +// TODO: render a blacksmith entity or smth + } + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24 + 24, bounds.centerY - 9)) .markInput().entries(display.outputItems)) return list } @@ -106,6 +141,7 @@ class SBReforgeRecipe( when (it) { is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> listOfNotNull(RepoManager.getNEUItem(it.internalName)) + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> ReforgeStore.resolveItemType(it.itemType) .flatMap { diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt index d2c555b..fdc378a 100644 --- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -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 } diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt index 022b9a3..9a09fc6 100644 --- a/src/main/kotlin/gui/entity/EntityRenderer.kt +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -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) diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt index f88dd48..9f1d45c 100644 --- a/src/main/kotlin/repo/ItemCache.kt +++ b/src/main/kotlin/repo/ItemCache.kt @@ -39,6 +39,7 @@ 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 @@ -131,6 +132,7 @@ object ItemCache : IReloadable { 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)) diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt index ea01818..b52adc6 100644 --- a/src/main/kotlin/repo/Reforge.kt +++ b/src/main/kotlin/repo/Reforge.kt @@ -108,6 +108,8 @@ data class Reforge( @Serializable(with = RarityMapped.Serializer::class) sealed interface RarityMapped { + fun get(rarity: Rarity): T? + class Serializer( val values: KSerializer ) : KSerializer> { @@ -137,10 +139,18 @@ data class Reforge( } @Serializable - data class Direct(val value: T) : RarityMapped + data class Direct(val value: T) : RarityMapped { + override fun get(rarity: Rarity): T { + return value + } + } @Serializable - data class PerRarity(val values: Map) : RarityMapped + data class PerRarity(val values: Map) : RarityMapped { + override fun get(rarity: Rarity): T? { + return values[rarity] + } + } } } diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt index e5cacaa..a5f54ae 100644 --- a/src/main/kotlin/repo/SBItemStack.kt +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -9,22 +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.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.blue +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( @@ -84,6 +93,117 @@ data class SBItemStack constructor( } return SBItemStack(neuIngredient.skyblockId, neuIngredient.amount.toInt()) } + + fun appendEnhancedStats( + itemStack: ItemStack, + reforgeStats: Map, + 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), + "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN), + // TODO: add other types and make this a repo json + ) + + + private val statLabelRegex = "(?.*): ".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 = 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) + fun reconstitute(): Text = + Text.literal("").setStyle(Style.EMPTY.withItalic(false)) + .append(Text.literal("$statName: ").grey()) + .append(value ?: formatValue()) + .also { rest.forEach(it::append) } + } + + private fun statIdToName(statId: String): String { + return statId.split("_").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( @@ -134,11 +254,19 @@ data class SBItemStack constructor( } - private fun appendReforgeStatsToLore( + private fun appendReforgeInfo( itemStack: ItemStack, ) { - val rarity = itemStack.rarity - val lore = itemStack.loreAccordingToNbt + 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 @@ -156,8 +284,8 @@ data class SBItemStack constructor( injectReplacementDataForPets(replacementData) return@run neuItem.asItemStack(idHint = skyblockId, replacementData) .copyWithCount(stackSize) + .also { appendReforgeInfo(it) } .also { it.appendLore(extraLore) } - .also { if (reforge != null) it.appendLore(listOf(Text.literal("Reforge: $reforge"))) } // TODO: use this for proper rendering .also { enhanceStatsByStars(it, stars) } } if (itemStack_ == null) diff --git a/src/main/kotlin/util/AprilFoolsUtil.kt b/src/main/kotlin/util/AprilFoolsUtil.kt new file mode 100644 index 0000000..a940fa1 --- /dev/null +++ b/src/main/kotlin/util/AprilFoolsUtil.kt @@ -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 + } +} diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt index 92fb9e5..4b32c2a 100644 --- a/src/main/kotlin/util/FirmFormatters.kt +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -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 { diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt index 497a2d2..631b444 100644 --- a/src/main/kotlin/util/SkyblockId.kt +++ b/src/main/kotlin/util/SkyblockId.kt @@ -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()) diff --git a/src/main/kotlin/util/mc/NbtItemData.kt b/src/main/kotlin/util/mc/NbtItemData.kt index e8a908f..0c49862 100644 --- a/src/main/kotlin/util/mc/NbtItemData.kt +++ b/src/main/kotlin/util/mc/NbtItemData.kt @@ -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 + get() = get(DataComponentTypes.LORE)?.lines ?: listOf() set(value) { set(DataComponentTypes.LORE, LoreComponent(value)) } diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt index 0244630..b19f371 100644 --- a/src/main/kotlin/util/skyblock/Rarity.kt +++ b/src/main/kotlin/util/skyblock/Rarity.kt @@ -8,7 +8,9 @@ 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 @@ -46,10 +48,23 @@ enum class Rarity(vararg altNames: String) { } 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 } diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt index c94ebfe..cfd8429 100644 --- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -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") } diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 06ed8c8..ab3de43 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -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) @@ -146,6 +147,11 @@ fun MutableText.clickCommand(command: String): MutableText { } } +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) { -- cgit